diff --git a/SKILL.md b/SKILL.md index 811bf6d3..2e9813b9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -106,17 +106,35 @@ Upload a blob (any size, up to 5 TiB) to project storage via direct-to-S3 presig - `content` (optional) — Inline content, ≤ 1 MB. Use only for small text blobs. - `content_type` (optional) — MIME type - `visibility` (optional, default: `"public"`) — `"public"` (bypasses auth) or `"private"` (requires apikey) -- `immutable` (optional, default: `false`) — If true with `sha256`, also produces a content-addressed URL that gets `Cache-Control: immutable`. -- `sha256` (optional) — Required when `immutable: true`. Client-asserted hash; gateway verifies if S3 returns one. +- `immutable` (optional, **default `true` since v1.45**) — Produces a content-addressed URL that pairs with SRI and never needs cache invalidation. Pass `{ immutable: false }` only when you specifically want to skip the SHA-256 pass on a very large upload (storage cost is identical; immutable just adds the content-hashed URL). +- `sha256` (optional) — Computed automatically by the SDK when `immutable: true` (the default). You don't need to set this. -**Returns:** an `AssetRef`: `{ key, size_bytes, sha256, visibility, url, immutable_url, size, contentSha256, contentType, immutableUrl, etag, sri, contentDigest, cacheKind, cdn: { version, invalidationId, invalidationStatus, ready, hint } }`. The legacy snake_case fields (`size_bytes`, `sha256`, `immutable_url`) are kept for back-compat; the camelCase fields are the v1.45 agent-DX surface. +**Returns:** an `AssetRef`. The agent-DX way to use it: **call `result.scriptTag()`, `result.linkTag()`, or `result.imgTag()` and paste the output directly into your generated HTML.** The tag emitters return ready-to-use HTML with `src` / `href` set to a content-hashed URL on `pr-.run402.com`, `integrity` set to the SRI hash, and `crossorigin` set so browsers verify the bytes. They also bake in modern best-practice attributes — `defer` on ` + +const styles = await client.blobs.put(projectId, "styles.css", { content }); +html += styles.linkTag(); +// → +``` + +**Why this is the recommended path.** The URL the emitters use (`cdnUrl`) is content-addressed (no cache invalidation, no `wait_for_cdn_freshness`), served from the auto-subdomain `pr-.run402.com` (the host that's guaranteed to work through the v1.33 CDN), and stable across re-deploys of the same content. SRI guarantees the browser refuses execution on byte mismatch. There are no decisions for the agent to make. + +**Other AssetRef fields** for advanced use: +- `cdnUrl` — the content-addressed auto-subdomain URL the tag emitters use. Use this directly if you're not generating HTML (e.g. CSS `url()` references, JSON data URLs). +- `cdnMutableUrl` — mutable form of the auto-subdomain URL. Eventual-consistency caveats apply; prefer `cdnUrl` for generated code. +- `url` / `immutableUrl` (preferred-host forms) — the URL on the project's pretty host (claimed subdomain or custom domain when configured). Currently NOT served through the CDN — keep using these for direct-API consumers (e.g. `client.blobs.get`), not in ` +html += asset.linkTag(); // +html += asset.imgTag("Company logo"); // Company logo +``` + +`immutable: true` is the default since v1.45 — the SDK computes the SHA-256 client-side and the gateway returns a content-addressed URL paired with SRI. The resulting URL never needs cache invalidation, never breaks on re-deploys, never needs `cdn wait-fresh` follow-up. Pass `{ immutable: false }` explicitly only when you want to skip the SHA pass on a very large upload (in that case the tag emitters throw because there's no SHA to bind for SRI). The tag emitters also bake in modern best-practice attributes — `defer` on ``; + }, + + linkTag(opts) { + // Always emit crossorigin — required for SRI to actually be + // enforced. Without crossorigin the browser silently ignores the + // integrity attribute (HTML spec). This applies to rel="preload" + // too: matching crossorigin on the preload + the eventual fetch is + // what lets the browser dedupe instead of double-fetching. + const { url, sri } = requireImmutable("linkTag"); + const rel = opts?.rel ?? "stylesheet"; + const attrs: string[] = [`rel="${escapeHtmlAttr(rel)}"`]; + attrs.push(`href="${escapeHtmlAttr(url)}"`); + if (opts?.as) attrs.push(`as="${escapeHtmlAttr(opts.as)}"`); + attrs.push(`integrity="${escapeHtmlAttr(sri)}"`); + attrs.push("crossorigin"); + return ``; + }, + + imgTag(alt) { + // Defaults: loading="lazy" + decoding="async" — modern best + // practice. 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. + // doesn't accept SRI per HTML5; the URL is content-hashed + // so it's still stable across re-deploys. Agents who need + // byte-level integrity for images should verify Content-Digest + // server-side. + const { url } = requireImmutable("imgTag"); + const a = alt ?? ""; + return `${escapeHtmlAttr(a)}`; + }, }; } @@ -204,7 +289,13 @@ export class Blobs { } const contentType = opts.contentType ?? guessContentType(key); - const sha256 = opts.immutable ? await sha256Hex(bytes) : undefined; + // v1.45 default: `immutable: true`. The agent-DX surface (cdnUrl, sri, + // scriptTag/linkTag/imgTag) only works for content-addressed uploads, + // so the default reaches for the best path. Pass `{ immutable: false }` + // explicitly when you specifically want a non-content-hashed URL + // (e.g. very large file where you want to skip the SHA pass). + const immutable = opts.immutable ?? true; + const sha256 = immutable ? await sha256Hex(bytes) : undefined; // 1. Init upload — gateway returns presigned S3 URLs for each part. const init = await this.client.request("/storage/v1/uploads", { @@ -218,7 +309,7 @@ export class Blobs { size_bytes: sizeBytes, content_type: contentType, visibility: opts.visibility ?? "public", - immutable: opts.immutable ?? false, + immutable, sha256, }, context: "initializing upload", diff --git a/sdk/src/namespaces/blobs.types.ts b/sdk/src/namespaces/blobs.types.ts index 24515ad3..29aa3e4b 100644 --- a/sdk/src/namespaces/blobs.types.ts +++ b/sdk/src/namespaces/blobs.types.ts @@ -19,7 +19,20 @@ export interface BlobPutOptions { contentType?: string; /** Default: `"public"`. Public blobs get a CDN URL; private requires auth. */ visibility?: BlobVisibility; - /** When true, the returned URL includes a content-hash suffix — overwrites produce distinct URLs. Forces sha256 computation. */ + /** + * Default (v1.45+): `true`. Returns an `AssetRef` with `cdnUrl` populated + * and the `scriptTag()` / `linkTag()` / `imgTag()` emitters working — + * this is the agent-DX flow (paste-and-go HTML with SRI baked in). + * + * Cost: one SHA-256 pass over the bytes on the client side. For small + * assets (the typical case — images, JS, CSS, fonts, JSON < 1 MB) it's + * a few ms dominated by network. Pass `false` to skip the SHA pass for + * very large uploads where you specifically don't need a content-hashed + * URL or SRI. + * + * When `false`, the returned `AssetRef` has `cdnUrl: null`, `sri: null`, + * and the tag emitters throw with an "immutable: true required" hint. + */ immutable?: boolean; } @@ -50,21 +63,32 @@ export interface BlobCdnEnvelope { } /** - * Stable URL reference returned by `client.blobs.put`. Includes both the - * legacy snake_case fields (back-compat with pre-v1.45 SDK) and the v1.45+ - * agent-DX fields: + * Stable URL reference returned by `client.blobs.put`. * - * - `immutableUrl` — content-addressed URL; correct from upload time, no - * wait. Prefer this in generated HTML/CSS/JS code. - * - `etag`, `sri`, `contentDigest` — strong integrity headers derived from - * the SHA-256, suitable for ` + * + * asset.scriptTag({ type: "module" }); + * // → + */ + scriptTag(opts?: { type?: "module" | "text/javascript"; defer?: boolean; async?: boolean }): string; + + /** + * Returns a ready-to-paste `` tag (default `rel="stylesheet"`) + * with content-addressed URL + SRI + `crossorigin`. `crossorigin` is + * always emitted — required for SRI to actually be enforced (also + * required for rel="preload" to dedupe with the matching fetch). + * + * @example + * asset.linkTag(); // stylesheet + * asset.linkTag({ rel: "preload", as: "font" }); + * asset.linkTag({ rel: "modulepreload" }); + */ + linkTag(opts?: { rel?: string; as?: string }): string; + + /** + * Returns a ready-to-paste `` tag with the content-addressed URL. + * + * **Defaults:** `loading="lazy"` + `decoding="async"`. Modern best + * practice — lazy loads below-fold images on demand, async decoding + * moves the decode off the main thread. Both are baseline-supported + * in all major browsers. Agents who specifically need an above-fold + * eager image can wrap the result and override. + * + * Browsers don't support SRI on ``, so no `integrity` attribute + * is emitted. The URL is content-hashed so it's still stable across + * re-deploys; for byte-level verification, read `Content-Digest` + * server-side. + * + * `alt` is the image's accessibility text (default `""` for decorative + * images). Pass a description when the image conveys information. + * + * @example + * asset.imgTag("Company logo") + * // → Company logo + */ + imgTag(alt?: string): string; } /**