run402-mcp 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -386,7 +386,15 @@ async function applyOnce(client, spec, opts, emit) {
386
386
  emit({ type: "commit.phase", phase: "validate", status: "started" });
387
387
  const { planId } = requirePersistedPlan(plan, "applying deploy");
388
388
  const commit = await commitInternal(client, planId, opts.idempotencyKey);
389
- return await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project);
389
+ const result = await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project);
390
+ // v1.48 unified-apply: thread the plan response's `asset_entries[]` back
391
+ // into DeployResult.assets so callers reading `result.assets.byKey[key]`
392
+ // get the gateway-authoritative `AssetRef` envelope (URLs, SRI, etag).
393
+ // Release-only applies leave `result.assets` undefined.
394
+ if (plan.asset_entries && plan.asset_entries.length > 0) {
395
+ result.assets = buildAssetManifestFromPlanEntries(plan.asset_entries);
396
+ }
397
+ return result;
390
398
  }
391
399
  function normalizeApplyMaxRetries(value) {
392
400
  if (value === undefined)
@@ -1319,8 +1327,10 @@ const RELEASE_SPEC_FIELDS = new Set([
1319
1327
  "subdomains",
1320
1328
  "routes",
1321
1329
  "checks",
1330
+ "assets", // v1.48 unified-apply
1322
1331
  ]);
1323
1332
  const DEPLOYABLE_SPEC_FIELDS = [
1333
+ "assets", // v1.48 unified-apply
1324
1334
  "database",
1325
1335
  "site",
1326
1336
  "functions",
@@ -1710,7 +1720,17 @@ function hasDeployableContent(spec) {
1710
1720
  hasSecretsContent(spec.secrets) ||
1711
1721
  hasSubdomainsContent(spec.subdomains) ||
1712
1722
  hasRecordEntries(spec.routes) ||
1713
- hasArrayEntries(spec.checks));
1723
+ hasArrayEntries(spec.checks) ||
1724
+ hasAssetsContent(spec.assets));
1725
+ }
1726
+ function hasAssetsContent(assets) {
1727
+ if (!isRecord(assets))
1728
+ return false;
1729
+ if (hasArrayEntries(assets.put))
1730
+ return true;
1731
+ if (hasArrayEntries(assets.delete))
1732
+ return true;
1733
+ return isRecord(assets.sync);
1714
1734
  }
1715
1735
  function hasDatabaseContent(database) {
1716
1736
  if (!isRecord(database))
@@ -2098,6 +2118,13 @@ async function normalizeReleaseSpec(client, spec) {
2098
2118
  normalized.site = { public_paths: publicPaths };
2099
2119
  }
2100
2120
  }
2121
+ // v1.48 unified-apply: asset slice normalization. Mirrors the site
2122
+ // branch — for each `put` entry with a `source`, hash the bytes and
2123
+ // register a byte-reader; emit the wire-shaped `AssetPutEntry[]`.
2124
+ // Cross-kind SHA dedup is automatic via the shared `byteReaders` map.
2125
+ if (spec.assets) {
2126
+ normalized.assets = await normalizeAssetSlice(spec.assets, remember);
2127
+ }
2101
2128
  return { normalized, byteReaders };
2102
2129
  }
2103
2130
  async function normalizeFunctionMap(map, remember) {
@@ -2137,6 +2164,164 @@ async function normalizeFileSet(set, remember) {
2137
2164
  }
2138
2165
  return out;
2139
2166
  }
2167
+ // ─── Asset manifest assembly from plan response (v1.48 unified-apply) ───────
2168
+ /**
2169
+ * Build a {@link AssetManifest} from the plan response's `asset_entries[]`
2170
+ * array. Each entry's `asset_ref` carries gateway-authoritative URLs that
2171
+ * mirror the AssetRef envelope `Assets.put` returns for the single-entry
2172
+ * case. Keys are stored in null-prototype objects (design D9) so
2173
+ * attacker-controlled or filesystem-derived keys (`__proto__`,
2174
+ * `constructor`, `toString`) don't collide with prototype properties.
2175
+ *
2176
+ * Totals are placeholders here (`bytes_uploaded: 0`, `bytes_reused: 0`,
2177
+ * `duration_ms: 0`); upstream callers that need realised values back-fill
2178
+ * them from the realised `content.upload.*` event stream. The shape stays
2179
+ * stable so consumers can rely on the keys being present.
2180
+ */
2181
+ function buildAssetManifestFromPlanEntries(entries) {
2182
+ const list = [];
2183
+ const byKey = Object.create(null);
2184
+ const manifest = Object.create(null);
2185
+ for (const entry of entries) {
2186
+ const e = {
2187
+ key: entry.key,
2188
+ sha256: entry.sha256,
2189
+ size_bytes: entry.size_bytes,
2190
+ content_type: entry.content_type,
2191
+ visibility: entry.visibility,
2192
+ url: entry.asset_ref.url,
2193
+ immutable_url: entry.asset_ref.immutable_url,
2194
+ cdn_url: entry.asset_ref.cdn_url,
2195
+ cdn_immutable_url: entry.asset_ref.cdn_immutable_url,
2196
+ sri: entry.asset_ref.sri,
2197
+ etag: entry.asset_ref.etag,
2198
+ content_digest: entry.asset_ref.content_digest,
2199
+ };
2200
+ list.push(e);
2201
+ byKey[entry.key] = e;
2202
+ manifest[entry.key] = e;
2203
+ }
2204
+ return {
2205
+ list,
2206
+ byKey,
2207
+ manifest,
2208
+ totals: { files: entries.length, bytes_uploaded: 0, bytes_reused: 0, duration_ms: 0 },
2209
+ };
2210
+ }
2211
+ // ─── Asset slice normalization (v1.48 unified-apply) ─────────────────────────
2212
+ /**
2213
+ * Type guard: distinguish the SDK-input shape (`AssetPutEntryInput` with
2214
+ * `source: ContentSource`) from the wire shape (`AssetPutEntry` with
2215
+ * `sha256` already computed). The two forms can be mixed in the same
2216
+ * `assets.put` array — the normalizer handles both branches.
2217
+ */
2218
+ function isAssetPutEntryInput(entry) {
2219
+ return (typeof entry.source !== "undefined" &&
2220
+ typeof entry.sha256 === "undefined");
2221
+ }
2222
+ /**
2223
+ * Normalize the assets slice per design D3 (three-schema fidelity). For
2224
+ * each `put` entry:
2225
+ *
2226
+ * 1. If it's an `AssetPutEntryInput` (has `source`): call `resolveContent`
2227
+ * to hash the bytes, register a byte-reader via `remember()`, and emit
2228
+ * a wire-shaped `AssetPutEntry` (no `source` field).
2229
+ * 2. If it's already an `AssetPutEntry` (has `sha256`): pass through.
2230
+ *
2231
+ * Validates per-spec invariants before any network call: duplicate keys
2232
+ * in `put`, key in both `put` and `delete`, empty manifest (only the
2233
+ * assets slice was set and it had no `put`/`delete`/`sync` content).
2234
+ *
2235
+ * `spec.assets.delete` and `spec.assets.sync` pass through unchanged.
2236
+ */
2237
+ async function normalizeAssetSlice(slice, remember) {
2238
+ const out = {};
2239
+ if (slice.put && slice.put.length > 0) {
2240
+ const seenKeys = new Set();
2241
+ const put = [];
2242
+ for (let idx = 0; idx < slice.put.length; idx++) {
2243
+ const entry = slice.put[idx];
2244
+ if (!entry.key || typeof entry.key !== "string") {
2245
+ throw new Run402DeployError(`assets.put[${idx}] missing required \`key\``, {
2246
+ code: "INVALID_SPEC",
2247
+ phase: "validate",
2248
+ resource: `assets.put[${idx}]`,
2249
+ retryable: false,
2250
+ fix: { action: "set_field", path: `assets.put[${idx}].key` },
2251
+ context: "validating spec",
2252
+ });
2253
+ }
2254
+ if (seenKeys.has(entry.key)) {
2255
+ throw new Run402DeployError(`assets.put contains duplicate key \`${entry.key}\``, {
2256
+ code: "ASSET_DUPLICATE_KEY_IN_PUT",
2257
+ phase: "validate",
2258
+ resource: `assets.put[${idx}]`,
2259
+ retryable: false,
2260
+ fix: { action: "set_field", path: `assets.put[${idx}].key` },
2261
+ context: "validating spec",
2262
+ });
2263
+ }
2264
+ seenKeys.add(entry.key);
2265
+ if (isAssetPutEntryInput(entry)) {
2266
+ const label = `assets.put[${idx}] (${entry.key})`;
2267
+ const resolved = await resolveContent(entry.source, label);
2268
+ if (!resolved.ref.contentType) {
2269
+ resolved.ref.contentType = entry.content_type ?? guessContentType(entry.key);
2270
+ }
2271
+ const ref = remember(resolved);
2272
+ put.push({
2273
+ key: entry.key,
2274
+ sha256: ref.sha256,
2275
+ size_bytes: ref.size,
2276
+ content_type: entry.content_type ?? ref.contentType ?? "application/octet-stream",
2277
+ visibility: entry.visibility ?? "public",
2278
+ immutable: entry.immutable ?? true,
2279
+ });
2280
+ }
2281
+ else {
2282
+ // Wire-shaped entry — pass through verbatim. The caller is
2283
+ // responsible for ensuring the bytes are already in CAS (or will
2284
+ // be uploaded out-of-band).
2285
+ put.push({
2286
+ key: entry.key,
2287
+ sha256: entry.sha256,
2288
+ size_bytes: entry.size_bytes,
2289
+ content_type: entry.content_type ?? "application/octet-stream",
2290
+ visibility: entry.visibility ?? "public",
2291
+ immutable: entry.immutable ?? true,
2292
+ });
2293
+ }
2294
+ }
2295
+ out.put = put;
2296
+ }
2297
+ if (slice.delete && slice.delete.length > 0) {
2298
+ out.delete = [...slice.delete];
2299
+ }
2300
+ // Cross-slice invariant: a key may not be both `put` and `delete`-d.
2301
+ if (out.put && out.delete) {
2302
+ const putKeys = new Set(out.put.map((e) => e.key));
2303
+ for (const k of out.delete) {
2304
+ if (putKeys.has(k)) {
2305
+ throw new Run402DeployError(`assets.put and assets.delete both reference key \`${k}\``, {
2306
+ code: "ASSET_KEY_IN_PUT_AND_DELETE",
2307
+ phase: "validate",
2308
+ resource: `assets`,
2309
+ retryable: false,
2310
+ fix: { action: "remove_field", path: `assets.delete[\`${k}\`]` },
2311
+ context: "validating spec",
2312
+ });
2313
+ }
2314
+ }
2315
+ }
2316
+ if (slice.sync) {
2317
+ out.sync = {
2318
+ prefix: slice.sync.prefix,
2319
+ prune: slice.sync.prune,
2320
+ ...(slice.sync.confirm ? { confirm: slice.sync.confirm } : {}),
2321
+ };
2322
+ }
2323
+ return out;
2324
+ }
2140
2325
  async function normalizeMigration(client, projectId, m, remember) {
2141
2326
  if (!m.id) {
2142
2327
  throw new Run402DeployError("MigrationSpec.id is required", {