Skip to content

feat(sdk): paste-and-go AssetRef with scriptTag/linkTag/imgTag emitters#118

Merged
MajorTal merged 2 commits into
mainfrom
paste-and-go-asset-ref
Apr 27, 2026
Merged

feat(sdk): paste-and-go AssetRef with scriptTag/linkTag/imgTag emitters#118
MajorTal merged 2 commits into
mainfrom
paste-and-go-asset-ref

Conversation

@MajorTal
Copy link
Copy Markdown
Collaborator

Summary

The agent-DX rewrite of client.blobs.put return shape. The agent's flow becomes one line:

const asset = await client.blobs.put(p, "logo.png", { bytes }, { immutable: true });
html += asset.imgTag("Company logo");
// → <img src="https://pr-abc.run402.com/_blob/logo-3a7fc02e.png" alt="Company logo">

const styles = await client.blobs.put(p, "styles.css", { content }, { immutable: true });
html += styles.linkTag();
// → <link rel="stylesheet" href="https://pr-abc.run402.com/_blob/styles-aabbccdd.css" integrity="sha256-…" crossorigin>

const app = await client.blobs.put(p, "app.js", { content }, { immutable: true });
html += app.scriptTag({ type: "module" });
// → <script src="https://pr-abc.run402.com/_blob/app-deadbeef.js" type="module" integrity="sha256-…" crossorigin></script>

No URL-form decision (just paste it), no SRI plumbing (baked in), no wait_for_cdn_freshness follow-up (URL is content-addressed → always served correctly first try).

Changes

AssetRef

  • NEW cdnUrl — content-addressed URL on pr-<public_id>.run402.com, the host guaranteed to work through the v1.33 /_blob/* CDN behavior. Null on non-immutable or private uploads.
  • NEW cdnMutableUrl — mutable form on auto-subdomain. Eventual-consistency caveats; prefer cdnUrl for generated code.
  • NEW scriptTag(opts?) / linkTag(opts?) / imgTag(alt?) — paste-and-go HTML tag emitters. Throw with an actionable hint on non-immutable uploads.
  • All existing fields (url, immutable_url, immutableUrl, etag, sri, contentDigest, cdn) kept unchanged.

Tag emitters

  • scriptTag({ type?, defer?, async? }) — emits <script> with integrity + crossorigin.
  • linkTag({ rel = "stylesheet", as? }) — emits <link> with integrity + crossorigin. Supports preload / prefetch via rel.
  • imgTag(alt = "") — emits <img> with src + alt. No integrity (HTML5 doesn't support SRI on <img>), but the URL is content-hashed so it's still stable across re-deploys.
  • All inputs HTML-attribute-escaped.
  • Throw on non-immutable uploads with a hint pointing at { immutable: true }.

Docs

  • SKILL.mdblob_put rewritten to lead with the tag-emitter flow. AssetRef field reference reorganized so the agent-DX fields come first.
  • cli/llms-cli.txt — same agent-DX guidance for the CLI / SDK reference section.

Coordinated change

Pairs with the gateway PR run402-private#73 which emits cdn_url + cdn_immutable_url on the upload-completion response. The SDK falls back gracefully against older gateway versions: cdnUrl is null and the tag emitters throw with an actionable hint.

Test plan

  • npx tsc --noEmit clean (SDK + MCP + CLI)
  • SDK blob tests pass: 25 / 25 (4 new — cdnUrl widening, tag-emitter shape, HTML attribute escaping, non-immutable error)
  • Existing tests still pass: client.blobs.put, get, ls, rm, sign, diagnoseUrl, waitFresh
  • After merge: rebuild SDK + smoke-test against deployed gateway: full upload returns the new fields populated, tag emitters render valid HTML pointing at the auto-subdomain URL, browser fetch of that URL returns 200 with matching SHA

Followup (separate PR)

Make claimed subdomains and custom domains serve /_blob/* through the v1.33 CDN. Today they 404 from BlobRoutingV2 because the KVS values lack project_id. The SDK steers agents around this via the auto-subdomain URL — but it's worth fixing so result.url (preferred host) becomes embed-safe too.

🤖 Generated with Claude Code

MajorTal and others added 2 commits April 27, 2026 21:57
v1.45 best-DX: agents call blobs.put with immutable:true and paste asset.scriptTag() / asset.linkTag() / asset.imgTag() into generated HTML. Each emitter returns a ready-to-use tag with src/href set to a content-hashed URL on pr-<public_id>.run402.com (the host guaranteed to work through the v1.33 CDN), integrity set to the SRI hash, and crossorigin set so browsers verify the bytes.

AssetRef widening:

- cdnUrl: the immutable-form auto-subdomain URL — what scriptTag/linkTag/imgTag bind to. Null on non-immutable or private uploads.

- cdnMutableUrl: mutable-form auto-subdomain URL. Eventual-consistency caveats apply; prefer cdnUrl in code.

- url / immutable_url / immutableUrl (preferred-host forms) — kept for backward compat and direct-API consumers (e.g. blobs.get). Currently NOT served through the v1.33 CDN behavior on claimed subdomains / custom domains; followup tracked separately.

Tag emitter ergonomics:

- scriptTag({ type?, defer?, async? }): bakes integrity + crossorigin

- linkTag({ rel = 'stylesheet', as? }): bakes integrity + crossorigin

- imgTag(alt = ''): no SRI (HTML5 spec), but URL is content-hashed so still stable across re-deploys

- HTML attribute escaping for url + alt

- Throw with actionable hint when called on non-immutable uploads

Tests:

- 4 new test cases (immutable widening, tag emitter shapes, HTML escaping, non-immutable error)

- Existing 22 tests still green; total now 25

Coordinated with run402-private: the gateway emits the new cdn_url + cdn_immutable_url fields on the upload-completion response. Older gateway versions (without the new fields) still work — the SDK leaves cdnUrl/cdnMutableUrl null and the tag emitters throw with the immutable-only hint, which surfaces on the first call rather than during upload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…async-decode

Push the agent-DX defaults all the way: with these on, the agent's typical flow is one line and gets every modern best practice for free.

Defaults flipped:

1. BlobPutOptions.immutable defaults to true. The cdnUrl + sri + tag emitters all require an immutable upload, so the v1.45 default reaches for the best path. Cost: one client-side SHA-256 pass, dominated by network for typical asset sizes (< 1 MB). Storage cost: +128 bytes (one blob_url_refs row). Pass { immutable: false } to opt out for very large uploads where you specifically don't need a content-hashed URL.

2. scriptTag() defaults defer: true. Modern best practice — non-render-blocking when placed in <head>, no-op at end of <body>. async: true overrides defer (the two are mutually exclusive per HTML spec). Pass { defer: false } for the rare sync-required case.

3. imgTag() defaults loading="lazy" + decoding="async". Lazy is harmless for above-fold images (browsers handle the heuristic) and a flat win for the much more common below-fold case. Async decoding moves the decode off the main thread. Both are baseline-supported in all major browsers.

linkTag intentionally NOT changed — crossorigin always emitted because we always emit integrity (SRI requires CORS mode), AND for rel="preload" matching crossorigin is what lets the browser dedupe with the eventual fetch instead of double-fetching.

Tests:

- New: 'defaults to immutable: true' covers the v1.45 default path + cdnUrl/sri/scriptTag work without the agent passing { immutable: true }

- Updated: scriptTag/linkTag/imgTag emitter test asserts the new default attributes (defer, loading=lazy, decoding=async) and the explicit-opt-out paths (defer: false, async: true overriding defer)

- Updated: existing tests that checked sha256=undefined / immutable=false explicitly pass { immutable: false } to preserve their intent

- All 26 SDK tests green

Docs:

- SKILL.md and cli/llms-cli.txt updated to reflect immutable=true as the default and remove the 'always pass { immutable: true }' guidance (no longer needed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MajorTal MajorTal force-pushed the paste-and-go-asset-ref branch from e66b3cd to b5827a6 Compare April 27, 2026 19:57
@MajorTal MajorTal merged commit 47d6a06 into main Apr 27, 2026
4 checks passed
@MajorTal MajorTal deleted the paste-and-go-asset-ref branch April 27, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant