run402-mcp 2.0.0 → 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.
- package/README.md +9 -9
- package/dist/tools/assets-put.d.ts.map +1 -1
- package/dist/tools/assets-put.js +16 -112
- package/dist/tools/assets-put.js.map +1 -1
- package/dist/tools/deploy.d.ts +175 -0
- package/dist/tools/deploy.d.ts.map +1 -1
- package/dist/tools/deploy.js +38 -0
- package/dist/tools/deploy.js.map +1 -1
- package/package.json +1 -1
- package/schemas/release-spec.v1.json +70 -0
- package/sdk/README.md +3 -3
- package/sdk/dist/namespaces/assets.d.ts +13 -9
- package/sdk/dist/namespaces/assets.d.ts.map +1 -1
- package/sdk/dist/namespaces/assets.js +68 -89
- package/sdk/dist/namespaces/assets.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +195 -24
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +102 -1
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/node/assets-node.d.ts +12 -7
- package/sdk/dist/node/assets-node.d.ts.map +1 -1
- package/sdk/dist/node/assets-node.js +91 -143
- package/sdk/dist/node/assets-node.js.map +1 -1
- package/sdk/dist/node/deploy-manifest.d.ts +25 -2
- package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
- package/sdk/dist/node/deploy-manifest.js +116 -0
- package/sdk/dist/node/deploy-manifest.js.map +1 -1
|
@@ -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
|
-
|
|
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)
|
|
@@ -905,20 +913,11 @@ async function uploadMissing(client, projectId, presence, byteReaders, emit) {
|
|
|
905
913
|
});
|
|
906
914
|
}
|
|
907
915
|
const bytes = await reader();
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
// plan-level content commit performs the CAS promotion.
|
|
914
|
-
const completeBody = uploadCompleteBody(session, uploadedParts);
|
|
915
|
-
await client.request(`/storage/v1/uploads/${encodeURIComponent(session.upload_id)}/complete`, {
|
|
916
|
-
method: "POST",
|
|
917
|
-
headers,
|
|
918
|
-
body: completeBody,
|
|
919
|
-
context: "completing content upload session",
|
|
920
|
-
});
|
|
921
|
-
}
|
|
916
|
+
await uploadOneWithRetry(client.fetch, session, bytes);
|
|
917
|
+
// v1.48 unified-apply: per-session completion via /storage/v1/uploads/:id/
|
|
918
|
+
// complete was removed (the route is now 404). All sessions are CAS-style
|
|
919
|
+
// staged-then-promote; the plan-level POST /content/v1/plans/:id/commit
|
|
920
|
+
// call below promotes every session for the plan in one shot.
|
|
922
921
|
done += 1;
|
|
923
922
|
emit({
|
|
924
923
|
type: "content.upload.progress",
|
|
@@ -1328,8 +1327,10 @@ const RELEASE_SPEC_FIELDS = new Set([
|
|
|
1328
1327
|
"subdomains",
|
|
1329
1328
|
"routes",
|
|
1330
1329
|
"checks",
|
|
1330
|
+
"assets", // v1.48 unified-apply
|
|
1331
1331
|
]);
|
|
1332
1332
|
const DEPLOYABLE_SPEC_FIELDS = [
|
|
1333
|
+
"assets", // v1.48 unified-apply
|
|
1333
1334
|
"database",
|
|
1334
1335
|
"site",
|
|
1335
1336
|
"functions",
|
|
@@ -1719,7 +1720,17 @@ function hasDeployableContent(spec) {
|
|
|
1719
1720
|
hasSecretsContent(spec.secrets) ||
|
|
1720
1721
|
hasSubdomainsContent(spec.subdomains) ||
|
|
1721
1722
|
hasRecordEntries(spec.routes) ||
|
|
1722
|
-
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);
|
|
1723
1734
|
}
|
|
1724
1735
|
function hasDatabaseContent(database) {
|
|
1725
1736
|
if (!isRecord(database))
|
|
@@ -2107,6 +2118,13 @@ async function normalizeReleaseSpec(client, spec) {
|
|
|
2107
2118
|
normalized.site = { public_paths: publicPaths };
|
|
2108
2119
|
}
|
|
2109
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
|
+
}
|
|
2110
2128
|
return { normalized, byteReaders };
|
|
2111
2129
|
}
|
|
2112
2130
|
async function normalizeFunctionMap(map, remember) {
|
|
@@ -2146,6 +2164,164 @@ async function normalizeFileSet(set, remember) {
|
|
|
2146
2164
|
}
|
|
2147
2165
|
return out;
|
|
2148
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
|
+
}
|
|
2149
2325
|
async function normalizeMigration(client, projectId, m, remember) {
|
|
2150
2326
|
if (!m.id) {
|
|
2151
2327
|
throw new Run402DeployError("MigrationSpec.id is required", {
|
|
@@ -2317,14 +2493,9 @@ async function uploadInlineCas(client, projectId, bytes, contentType) {
|
|
|
2317
2493
|
});
|
|
2318
2494
|
if (planRes.missing.length > 0) {
|
|
2319
2495
|
const session = planRes.missing[0];
|
|
2320
|
-
|
|
2321
|
-
//
|
|
2322
|
-
|
|
2323
|
-
method: "POST",
|
|
2324
|
-
headers,
|
|
2325
|
-
body: uploadCompleteBody(session, uploadedParts),
|
|
2326
|
-
context: "completing content upload session",
|
|
2327
|
-
});
|
|
2496
|
+
await uploadOne(client.fetch, session, bytes);
|
|
2497
|
+
// v1.48 unified-apply: per-session /storage/v1/uploads/:id/complete is
|
|
2498
|
+
// gone (404). The plan-level commit below promotes the session to CAS.
|
|
2328
2499
|
await client.request(`/content/v1/plans/${encodeURIComponent(planRes.plan_id)}/commit`, { method: "POST", headers, body: {}, context: "committing content upload" });
|
|
2329
2500
|
}
|
|
2330
2501
|
return { sha256, size: bytes.byteLength, contentType };
|