@run402/sdk 1.69.8 → 1.70.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 +5 -4
- package/dist/namespaces/deploy.d.ts.map +1 -1
- package/dist/namespaces/deploy.js +452 -12
- package/dist/namespaces/deploy.js.map +1 -1
- package/dist/namespaces/deploy.types.d.ts +11 -0
- package/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/dist/namespaces/deploy.types.js.map +1 -1
- package/dist/namespaces/tier.d.ts +16 -0
- package/dist/namespaces/tier.d.ts.map +1 -1
- package/dist/namespaces/tier.js.map +1 -1
- package/dist/node/deploy-manifest.d.ts +2 -0
- package/dist/node/deploy-manifest.d.ts.map +1 -1
- package/dist/node/deploy-manifest.js +24 -1
- package/dist/node/deploy-manifest.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,9 +192,10 @@ const resumed = await r.deploy.resume("op_...");
|
|
|
192
192
|
- **Per-resource semantics on the spec.** `site.replace` = "this is the whole site" (files absent are removed). `site.patch.put` / `patch.delete` are surgical updates. `site.public_paths` controls browser-visible static paths separately from backing release asset paths: explicit mode uses a complete map such as `{ "/events": { asset: "events.html", cache_class: "html" } }`, so `/events` serves `events.html` while `/events.html` is not public unless separately declared. Implicit mode restores filename-derived reachability and can widen access. A public-path-only site spec is deployable. `functions.replace` / `functions.patch.set` / `functions.patch.delete` mirror that. Secrets are value-free: set values first with `r.secrets.set(project, key, value)`, then deploy with `secrets.require` and/or `secrets.delete`. `subdomains.set` / `subdomains.add` / `subdomains.remove` use their own shape. Top-level absence = leave untouched.
|
|
193
193
|
- **Same-origin web routes.** `routes` is `undefined | null | { replace: RouteSpec[] }`. Omit it or pass `null` to carry forward base routes, pass `{ replace: [] }` to clear routes, or pass route entries to replace the table. Function targets use `{ type: "function", name }`; exact static route targets use `{ type: "static", file }` with methods `["GET"]` or `["GET","HEAD"]`, no wildcard pattern, and a relative deployed asset path with no leading slash. `file` is not a public path, URL, CAS hash, rewrite, or redirect. Prefer `site.public_paths` for ordinary clean static URLs like `/events -> events.html`; use static route targets for method-aware aliases such as static `GET /login` plus function `POST /login`. Routed browser ingress invokes Node 22 Fetch Request -> Response handlers; `req.url` is the full public URL on managed subdomains, deployment hosts, and verified custom domains. Direct `/functions/v1/:name` invocation remains API-key protected. Runtime route failure codes include `ROUTE_MANIFEST_LOAD_FAILED`, `ROUTED_INVOKE_WORKER_SECRET_MISSING`, `ROUTED_INVOKE_AUTH_FAILED`, `ROUTED_ROUTE_STALE`, `ROUTE_METHOD_NOT_ALLOWED`, and `ROUTED_RESPONSE_TOO_LARGE`.
|
|
194
194
|
- **Strict spec validation happens before network calls.** Raw `ReleaseSpec` objects reject unknown fields (for example `project_id` or `subdomain`) instead of silently dropping them during normalization, and project/base-only or empty nested specs fail with `Run402DeployError.code === "MANIFEST_EMPTY"`. Use the Node manifest helpers when starting from CLI/MCP-style JSON.
|
|
195
|
-
- **
|
|
195
|
+
- **Tier preflight happens before deploy side effects.** After normalization and before manifest CAS upload or `/deploy/v2/plans`, deploy checks literal function timeout, memory, cron minimum interval, and scheduled-function count when known. Violations throw `Run402DeployError.code === "BAD_FIELD"` with `details.field`, `details.value`, `details.tier`, the relevant cap, and `details.limit_source`; gateway validation remains authoritative.
|
|
196
|
+
- **Warnings are structured.** `DeployResult.warnings` contains `WarningEntry[]` (`code`, `severity`, `requires_confirmation`, `message`, optional `affected`/`details`/`confidence`); the type preserves legacy low/medium/high plan warnings and modern deploy-observability info/warn/high warnings. `apply()` emits `plan.warnings` and stops before upload/commit on confirmation-required warnings unless broad `allowWarnings` is set or every blocking code is listed in `allowWarningCodes`. For `MISSING_REQUIRED_SECRET`, set the affected keys with `r.secrets.set`, then retry.
|
|
196
197
|
- **Deploy summaries are SDK-owned convenience.** `summarizeDeployResult(result)` returns `DeploySummary` (`schema_version: "deploy-summary.v1"`) with a headline plus reliable current buckets for site path counts, CAS new/reused bytes, functions, migrations, routes, secrets, subdomains, and warning counts. It is derived from `DeployResult.diff` / `DeployResult.warnings`; it makes no extra gateway calls, omits sections the gateway did not return, and intentionally excludes timings, client-side duration estimates, and function old/new code hashes.
|
|
197
|
-
- **Safe release-race retries are SDK-owned.** `deploy.apply()` automatically re-plans and retries omitted/current-base specs when the gateway returns `BASE_RELEASE_CONFLICT` with `safe_to_retry: true`. The default budget is two retries after the initial attempt; pass `{ maxRetries: 0 }` to opt out.
|
|
198
|
+
- **Safe release-race retries are SDK-owned.** `deploy.apply()` automatically re-plans and retries omitted/current-base specs when the gateway returns `BASE_RELEASE_CONFLICT` with `safe_to_retry: true`. Static activation/config failures reported from `activation_pending` throw immediately with gateway metadata preserved. The default retry budget is two retries after the initial attempt; pass `{ maxRetries: 0 }` to opt out.
|
|
198
199
|
- **Planning supports dry-runs.** `r.deploy.plan(spec, { dryRun: true })` calls the server-authoritative dry-run route and returns the normalized v2 plan envelope without uploading bytes or creating plan/operation rows (`plan_id` and `operation_id` are `null`).
|
|
199
200
|
- **Release observability is typed.** Use `r.deploy.getRelease({ project, releaseId, siteLimit? })`, `r.deploy.getActiveRelease({ project, siteLimit? })`, and `r.deploy.diff({ project, from, to, limit? })` to inspect release inventory and release-to-release diffs. Inventories include `release_generation`, `static_manifest_sha256`, nullable `static_manifest_metadata` (`file_count`, `total_bytes`, `cache_classes`, `cache_class_sources`, `spa_fallback`), and `static_public_paths[]` when returned. `site.paths` lists release static assets; `static_public_paths[]` lists browser reachability with `public_path`, `asset_path`, `reachability_authority`, `direct`, cache class, and content type. `diff` returns `ReleaseToReleaseDiff` with `migrations.applied_between_releases`; secret diffs expose keys only; `static_assets` exposes unchanged/changed/added/removed files, CAS byte reuse, eliminated deployment-copy bytes, and immutable/CAS warning counts.
|
|
200
201
|
- **Server-authoritative manifest digest** — no byte-for-byte canonicalize requirement on the client.
|
|
@@ -281,11 +282,11 @@ const resumed = await r.deploy.resume("op_...");
|
|
|
281
282
|
|
|
282
283
|
| Code | Why it matters | Recovery |
|
|
283
284
|
|------|----------------|----------|
|
|
284
|
-
| `PUBLIC_ROUTED_FUNCTION` | Function becomes public same-origin browser ingress. | Review app auth, CSRF, CORS/`OPTIONS`, and cookies; direct `/functions/v1/:name` remains API-key protected.
|
|
285
|
+
| `PUBLIC_ROUTED_FUNCTION` | Function becomes public same-origin browser ingress. | Review app auth, CSRF, CORS/`OPTIONS`, and cookies; direct `/functions/v1/:name` remains API-key protected. Prefer `allowWarningCodes` after review; broad `allowWarnings` only after every warning was reviewed. |
|
|
285
286
|
| `ROUTE_TARGET_CARRIED_FORWARD` | Carried-forward route still targets a base-release function. | Inspect active routes and deploy `routes.replace` if the target should change. |
|
|
286
287
|
| `ROUTE_SHADOWS_STATIC_PATH` / `WILDCARD_ROUTE_SHADOWS_STATIC_PATHS` | Dynamic route shadows direct public static content. | Inspect warning details, active routes, `static_public_paths`, and resolve diagnostics; confirm only when intentional. |
|
|
287
288
|
| `METHOD_SPECIFIC_ROUTE_ALLOWS_GET_STATIC_FALLBACK` | Unmatched methods can serve static content. | Confirm fallback is intended or add method coverage. |
|
|
288
|
-
| `WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS` | Wildcard function route only allows `GET`/`HEAD`. | Add mutation methods such as `POST`, omit methods for an API prefix, or
|
|
289
|
+
| `WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS` | Wildcard function route only allows `GET`/`HEAD`. | Add mutation methods such as `POST`, omit methods for an API prefix, or set `acknowledge_readonly: true` on an intentionally read-only GET/HEAD final-wildcard function route. |
|
|
289
290
|
| `ROUTE_TABLE_NEAR_LIMIT` | Route table is near a limit. | Consolidate or remove routes. |
|
|
290
291
|
| `ROUTES_NOT_ENABLED` | Routes are disabled for the project/environment. | Deploy without `routes` or request enablement; direct function invoke is not a browser-route substitute. |
|
|
291
292
|
| `STATIC_ALIAS_SHADOWS_STATIC_PATH` / `STATIC_ALIAS_RELATIVE_ASSET_RISK` | Route-only static alias conflicts with a direct public static path or has relative-asset risk. | Inspect active routes, `static_public_paths`, and the backing `asset_path`; prefer `site.public_paths` for ordinary clean URLs and confirm only when intentional. |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAiB3C,OAAO,KAAK,EACV,YAAY,EACZ,sBAAsB,EAKtB,WAAW,EACX,oBAAoB,EAEpB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,EAWrB,iBAAiB,EAGjB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,2BAA2B,EAC3B,uBAAuB,EACvB,WAAW,EACX,oBAAoB,EACpB,YAAY,EAEb,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,6 +37,38 @@ const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
|
37
37
|
const APPLY_SAFE_RETRY_CODES = new Set([
|
|
38
38
|
"BASE_RELEASE_CONFLICT",
|
|
39
39
|
]);
|
|
40
|
+
const STATIC_ACTIVATION_FAILURE_CODES = new Set([
|
|
41
|
+
"BAD_FIELD",
|
|
42
|
+
"INVALID_SPEC",
|
|
43
|
+
"FUNCTION_ACTIVATE_FAILED",
|
|
44
|
+
"FUNCTION_CONFIG_INVALID",
|
|
45
|
+
"FUNCTION_TIMEOUT_EXCEEDS_TIER",
|
|
46
|
+
"FUNCTION_MEMORY_EXCEEDS_TIER",
|
|
47
|
+
"FUNCTION_SCHEDULE_EXCEEDS_TIER",
|
|
48
|
+
"FUNCTION_SCHEDULE_INTERVAL_TOO_SHORT",
|
|
49
|
+
"FUNCTION_SCHEDULE_LIMIT_EXCEEDED",
|
|
50
|
+
"TIER_LIMIT_EXCEEDED",
|
|
51
|
+
]);
|
|
52
|
+
const STATIC_FUNCTION_LIMITS_BY_TIER = {
|
|
53
|
+
prototype: {
|
|
54
|
+
maxTimeoutSeconds: 10,
|
|
55
|
+
maxMemoryMb: 128,
|
|
56
|
+
maxScheduledFunctions: 1,
|
|
57
|
+
minCronIntervalMinutes: 15,
|
|
58
|
+
},
|
|
59
|
+
hobby: {
|
|
60
|
+
maxTimeoutSeconds: 30,
|
|
61
|
+
maxMemoryMb: 256,
|
|
62
|
+
maxScheduledFunctions: 3,
|
|
63
|
+
minCronIntervalMinutes: 5,
|
|
64
|
+
},
|
|
65
|
+
team: {
|
|
66
|
+
maxTimeoutSeconds: 60,
|
|
67
|
+
maxMemoryMb: 512,
|
|
68
|
+
maxScheduledFunctions: 10,
|
|
69
|
+
minCronIntervalMinutes: 1,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
40
72
|
const MANIFEST_CONTENT_TYPE = "application/vnd.run402.deploy-manifest+json";
|
|
41
73
|
const TERMINAL_STATUSES = [
|
|
42
74
|
"ready",
|
|
@@ -331,11 +363,12 @@ function requireNonEmptyStringQueryOption(value, label, context) {
|
|
|
331
363
|
}
|
|
332
364
|
// ─── Internal pipeline ───────────────────────────────────────────────────────
|
|
333
365
|
async function applyOnce(client, spec, opts, emit) {
|
|
366
|
+
const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
|
|
334
367
|
emit({ type: "plan.started" });
|
|
335
368
|
const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey);
|
|
336
369
|
emit({ type: "plan.diff", diff: plan.diff });
|
|
337
370
|
emitPlanWarnings(plan, emit);
|
|
338
|
-
abortOnConfirmationWarnings(plan, opts);
|
|
371
|
+
abortOnConfirmationWarnings(plan, opts, allowWarningCodes);
|
|
339
372
|
if (plan.payment_required) {
|
|
340
373
|
emit({
|
|
341
374
|
type: "payment.required",
|
|
@@ -420,6 +453,7 @@ async function planInternal(client, spec, idempotencyKey, dryRun = false) {
|
|
|
420
453
|
if (ciCredentials)
|
|
421
454
|
assertCiDeployableSpec(spec);
|
|
422
455
|
const { normalized, byteReaders } = await normalizeReleaseSpec(client, spec);
|
|
456
|
+
await preflightTierFunctionLimits(client, normalized, ciCredentials);
|
|
423
457
|
// The gateway expects { spec, manifest_ref?, idempotency_key? } with
|
|
424
458
|
// ReleaseSpec.project (singular). For oversized specs the SDK uploads
|
|
425
459
|
// the manifest JSON to CAS first and references it; the gateway still
|
|
@@ -474,6 +508,338 @@ async function planInternal(client, spec, idempotencyKey, dryRun = false) {
|
|
|
474
508
|
}
|
|
475
509
|
return { plan, byteReaders };
|
|
476
510
|
}
|
|
511
|
+
async function preflightTierFunctionLimits(client, spec, ciCredentials) {
|
|
512
|
+
if (ciCredentials)
|
|
513
|
+
return;
|
|
514
|
+
if (!hasFunctionTierPreflightInputs(spec.functions))
|
|
515
|
+
return;
|
|
516
|
+
const limits = await readTierFunctionLimits(client);
|
|
517
|
+
if (!limits)
|
|
518
|
+
return;
|
|
519
|
+
const entries = collectFunctionPreflightEntries(spec.functions);
|
|
520
|
+
for (const entry of entries) {
|
|
521
|
+
const timeout = entry.fn.config?.timeoutSeconds;
|
|
522
|
+
if (timeout !== undefined &&
|
|
523
|
+
limits.maxTimeoutSeconds &&
|
|
524
|
+
timeout > limits.maxTimeoutSeconds.value) {
|
|
525
|
+
throw tierLimitError(`Function ${entry.name} timeoutSeconds ${timeout} exceeds the ${limits.tier} tier maximum of ${limits.maxTimeoutSeconds.value}.`, `${entry.fieldPrefix}.config.timeoutSeconds`, timeout, limits, limits.maxTimeoutSeconds, {
|
|
526
|
+
tier_max: limits.maxTimeoutSeconds.value,
|
|
527
|
+
max_function_timeout_seconds: limits.maxTimeoutSeconds.value,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const memory = entry.fn.config?.memoryMb;
|
|
531
|
+
if (memory !== undefined &&
|
|
532
|
+
limits.maxMemoryMb &&
|
|
533
|
+
memory > limits.maxMemoryMb.value) {
|
|
534
|
+
throw tierLimitError(`Function ${entry.name} memoryMb ${memory} exceeds the ${limits.tier} tier maximum of ${limits.maxMemoryMb.value}.`, `${entry.fieldPrefix}.config.memoryMb`, memory, limits, limits.maxMemoryMb, {
|
|
535
|
+
tier_max: limits.maxMemoryMb.value,
|
|
536
|
+
max_function_memory_mb: limits.maxMemoryMb.value,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (isScheduledCron(entry.fn.schedule) && limits.minCronIntervalMinutes) {
|
|
540
|
+
const intervalMinutes = estimateCronMinimumIntervalMinutes(entry.fn.schedule);
|
|
541
|
+
if (intervalMinutes !== null &&
|
|
542
|
+
intervalMinutes < limits.minCronIntervalMinutes.value) {
|
|
543
|
+
throw tierLimitError(`Function ${entry.name} schedule runs every ${intervalMinutes} minute(s), below the ${limits.tier} tier minimum interval of ${limits.minCronIntervalMinutes.value} minutes.`, `${entry.fieldPrefix}.schedule`, entry.fn.schedule, limits, limits.minCronIntervalMinutes, {
|
|
544
|
+
interval_minutes: intervalMinutes,
|
|
545
|
+
min_interval_minutes: limits.minCronIntervalMinutes.value,
|
|
546
|
+
min_cron_interval_minutes: limits.minCronIntervalMinutes.value,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (limits.maxScheduledFunctions) {
|
|
552
|
+
const lowerBound = countScheduledFunctionsInSetEntries(spec.functions);
|
|
553
|
+
if (lowerBound > limits.maxScheduledFunctions.value) {
|
|
554
|
+
throw scheduledCountTierLimitError(lowerBound, limits, limits.maxScheduledFunctions, "manifest");
|
|
555
|
+
}
|
|
556
|
+
const desired = await computeDesiredScheduledFunctionCount(client, spec);
|
|
557
|
+
if (desired && desired.count > limits.maxScheduledFunctions.value) {
|
|
558
|
+
throw scheduledCountTierLimitError(desired.count, limits, limits.maxScheduledFunctions, desired.source);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function hasFunctionTierPreflightInputs(functions) {
|
|
563
|
+
if (!functions)
|
|
564
|
+
return false;
|
|
565
|
+
return collectFunctionPreflightEntries(functions).some((entry) => (entry.fn.config?.timeoutSeconds !== undefined ||
|
|
566
|
+
entry.fn.config?.memoryMb !== undefined ||
|
|
567
|
+
isScheduledCron(entry.fn.schedule)));
|
|
568
|
+
}
|
|
569
|
+
function collectFunctionPreflightEntries(functions) {
|
|
570
|
+
if (!functions)
|
|
571
|
+
return [];
|
|
572
|
+
const entries = [];
|
|
573
|
+
for (const [name, fn] of Object.entries(functions.replace ?? {})) {
|
|
574
|
+
entries.push({ name, fn, fieldPrefix: `functions.${name}` });
|
|
575
|
+
}
|
|
576
|
+
for (const [name, fn] of Object.entries(functions.patch?.set ?? {})) {
|
|
577
|
+
entries.push({ name, fn, fieldPrefix: `functions.${name}` });
|
|
578
|
+
}
|
|
579
|
+
return entries;
|
|
580
|
+
}
|
|
581
|
+
async function readTierFunctionLimits(client) {
|
|
582
|
+
let status;
|
|
583
|
+
try {
|
|
584
|
+
status = await client.request("/tiers/v1/status", {
|
|
585
|
+
context: "checking tier status for deploy preflight",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
if (typeof status.tier !== "string" || status.tier.length === 0) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const tierKey = status.tier.toLowerCase();
|
|
595
|
+
const fallback = STATIC_FUNCTION_LIMITS_BY_TIER[tierKey];
|
|
596
|
+
const limits = { tier: status.tier };
|
|
597
|
+
limits.maxTimeoutSeconds = tierStatusLimitOrFallback(status, [
|
|
598
|
+
"max_function_timeout_seconds",
|
|
599
|
+
"max_timeout_seconds",
|
|
600
|
+
"timeout_seconds_max",
|
|
601
|
+
"function_timeout_seconds_max",
|
|
602
|
+
], fallback?.maxTimeoutSeconds);
|
|
603
|
+
limits.maxMemoryMb = tierStatusLimitOrFallback(status, [
|
|
604
|
+
"max_function_memory_mb",
|
|
605
|
+
"max_memory_mb",
|
|
606
|
+
"memory_mb_max",
|
|
607
|
+
"function_memory_mb_max",
|
|
608
|
+
], fallback?.maxMemoryMb);
|
|
609
|
+
limits.maxScheduledFunctions = tierStatusLimitOrFallback(status, [
|
|
610
|
+
"max_scheduled_functions",
|
|
611
|
+
"scheduled_functions_limit",
|
|
612
|
+
"scheduled_function_limit",
|
|
613
|
+
"max_function_schedules",
|
|
614
|
+
], fallback?.maxScheduledFunctions);
|
|
615
|
+
limits.minCronIntervalMinutes = tierStatusLimitOrFallback(status, [
|
|
616
|
+
"min_cron_interval_minutes",
|
|
617
|
+
"minimum_cron_interval_minutes",
|
|
618
|
+
"min_schedule_interval_minutes",
|
|
619
|
+
"min_scheduled_function_interval_minutes",
|
|
620
|
+
], fallback?.minCronIntervalMinutes);
|
|
621
|
+
limits.currentScheduledFunctions = tierStatusLimit(status, [
|
|
622
|
+
"current_scheduled_functions",
|
|
623
|
+
"current_scheduled_function_count",
|
|
624
|
+
"scheduled_function_count",
|
|
625
|
+
"scheduled_functions",
|
|
626
|
+
]);
|
|
627
|
+
return hasAnyTierFunctionLimit(limits) ? limits : null;
|
|
628
|
+
}
|
|
629
|
+
function tierStatusLimitOrFallback(status, keys, fallback) {
|
|
630
|
+
return tierStatusLimit(status, keys) ?? (fallback === undefined
|
|
631
|
+
? undefined
|
|
632
|
+
: { value: fallback, source: "local_static_fallback" });
|
|
633
|
+
}
|
|
634
|
+
function tierStatusLimit(status, keys) {
|
|
635
|
+
const limits = objectField(status, "limits");
|
|
636
|
+
const containers = [
|
|
637
|
+
objectField(status, "function_limits"),
|
|
638
|
+
objectField(limits, "functions"),
|
|
639
|
+
objectField(limits, "function_limits"),
|
|
640
|
+
objectField(status, "pool_usage"),
|
|
641
|
+
status,
|
|
642
|
+
];
|
|
643
|
+
for (const container of containers) {
|
|
644
|
+
if (!container || typeof container !== "object" || Array.isArray(container))
|
|
645
|
+
continue;
|
|
646
|
+
const obj = container;
|
|
647
|
+
for (const key of keys) {
|
|
648
|
+
const value = obj[key];
|
|
649
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
650
|
+
return { value, source: "tier_status" };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
function hasAnyTierFunctionLimit(limits) {
|
|
657
|
+
return Boolean(limits.maxTimeoutSeconds ||
|
|
658
|
+
limits.maxMemoryMb ||
|
|
659
|
+
limits.maxScheduledFunctions ||
|
|
660
|
+
limits.minCronIntervalMinutes ||
|
|
661
|
+
limits.currentScheduledFunctions);
|
|
662
|
+
}
|
|
663
|
+
async function computeDesiredScheduledFunctionCount(client, spec) {
|
|
664
|
+
const functions = spec.functions;
|
|
665
|
+
if (!functions)
|
|
666
|
+
return null;
|
|
667
|
+
const replaceScheduled = scheduledFunctionNames(functions.replace);
|
|
668
|
+
if (replaceScheduled) {
|
|
669
|
+
applyScheduledFunctionPatch(replaceScheduled, functions.patch);
|
|
670
|
+
return { count: replaceScheduled.size, source: "manifest" };
|
|
671
|
+
}
|
|
672
|
+
if (!functions.patch)
|
|
673
|
+
return null;
|
|
674
|
+
const activeScheduled = await readActiveScheduledFunctionNames(client, spec.project);
|
|
675
|
+
if (!activeScheduled)
|
|
676
|
+
return null;
|
|
677
|
+
applyScheduledFunctionPatch(activeScheduled, functions.patch);
|
|
678
|
+
return { count: activeScheduled.size, source: "active_release_inventory" };
|
|
679
|
+
}
|
|
680
|
+
function countScheduledFunctionsInSetEntries(functions) {
|
|
681
|
+
if (!functions)
|
|
682
|
+
return 0;
|
|
683
|
+
const names = new Set();
|
|
684
|
+
for (const [name, fn] of Object.entries(functions.replace ?? {})) {
|
|
685
|
+
if (isScheduledCron(fn.schedule))
|
|
686
|
+
names.add(name);
|
|
687
|
+
}
|
|
688
|
+
for (const [name, fn] of Object.entries(functions.patch?.set ?? {})) {
|
|
689
|
+
if (isScheduledCron(fn.schedule))
|
|
690
|
+
names.add(name);
|
|
691
|
+
}
|
|
692
|
+
return names.size;
|
|
693
|
+
}
|
|
694
|
+
function scheduledFunctionNames(functions) {
|
|
695
|
+
if (!functions)
|
|
696
|
+
return null;
|
|
697
|
+
const scheduled = new Set();
|
|
698
|
+
for (const [name, fn] of Object.entries(functions)) {
|
|
699
|
+
if (isScheduledCron(fn.schedule))
|
|
700
|
+
scheduled.add(name);
|
|
701
|
+
}
|
|
702
|
+
return scheduled;
|
|
703
|
+
}
|
|
704
|
+
function applyScheduledFunctionPatch(scheduled, patch) {
|
|
705
|
+
for (const name of patch?.delete ?? []) {
|
|
706
|
+
scheduled.delete(name);
|
|
707
|
+
}
|
|
708
|
+
for (const [name, fn] of Object.entries(patch?.set ?? {})) {
|
|
709
|
+
if (fn.schedule === null)
|
|
710
|
+
scheduled.delete(name);
|
|
711
|
+
else if (isScheduledCron(fn.schedule))
|
|
712
|
+
scheduled.add(name);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async function readActiveScheduledFunctionNames(client, projectId) {
|
|
716
|
+
let inventory;
|
|
717
|
+
try {
|
|
718
|
+
inventory = await client.request(appendQuery("/deploy/v2/releases/active", { site_limit: 1 }), {
|
|
719
|
+
headers: await apikeyHeaders(client, projectId),
|
|
720
|
+
context: "fetching active release inventory for deploy preflight",
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const scheduled = new Set();
|
|
727
|
+
for (const fn of inventory.functions ?? []) {
|
|
728
|
+
if (isScheduledCron(fn.schedule))
|
|
729
|
+
scheduled.add(fn.name);
|
|
730
|
+
}
|
|
731
|
+
return scheduled;
|
|
732
|
+
}
|
|
733
|
+
function scheduledCountTierLimitError(count, limits, limit, countSource) {
|
|
734
|
+
return tierLimitError(`Deploy would have ${count} scheduled function(s), exceeding the ${limits.tier} tier maximum of ${limit.value}.`, "functions.scheduled_count", count, limits, limit, {
|
|
735
|
+
tier_max: limit.value,
|
|
736
|
+
max_scheduled_functions: limit.value,
|
|
737
|
+
count_source: countSource,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
function tierLimitError(message, field, value, limits, limit, extraDetails) {
|
|
741
|
+
const hint = limit.source === "local_static_fallback"
|
|
742
|
+
? "Tier limits came from the SDK's static fallback because /tiers/v1/status did not expose function caps. Run `run402 tier status` to refresh, lower the function setting, upgrade the tier, or retry and let gateway validation decide if this seems stale."
|
|
743
|
+
: "Lower the function setting or upgrade the tier before deploying.";
|
|
744
|
+
const details = {
|
|
745
|
+
field,
|
|
746
|
+
value,
|
|
747
|
+
tier: limits.tier,
|
|
748
|
+
limit_source: limit.source,
|
|
749
|
+
...extraDetails,
|
|
750
|
+
};
|
|
751
|
+
const body = {
|
|
752
|
+
code: "BAD_FIELD",
|
|
753
|
+
category: "deploy",
|
|
754
|
+
message,
|
|
755
|
+
retryable: false,
|
|
756
|
+
details,
|
|
757
|
+
hint,
|
|
758
|
+
};
|
|
759
|
+
return new Run402DeployError(message, {
|
|
760
|
+
code: "BAD_FIELD",
|
|
761
|
+
phase: "validate",
|
|
762
|
+
resource: field,
|
|
763
|
+
retryable: false,
|
|
764
|
+
body,
|
|
765
|
+
context: "validating deploy tier limits",
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function isScheduledCron(value) {
|
|
769
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
770
|
+
}
|
|
771
|
+
function estimateCronMinimumIntervalMinutes(expression) {
|
|
772
|
+
const parts = expression.trim().split(/\s+/);
|
|
773
|
+
if (parts.length !== 5)
|
|
774
|
+
return null;
|
|
775
|
+
const minutes = expandCronNumberField(parts[0], 0, 59);
|
|
776
|
+
const hours = expandCronNumberField(parts[1], 0, 23);
|
|
777
|
+
if (!minutes || !hours || minutes.length === 0 || hours.length === 0)
|
|
778
|
+
return null;
|
|
779
|
+
const occurrences = [];
|
|
780
|
+
for (const hour of hours) {
|
|
781
|
+
for (const minute of minutes) {
|
|
782
|
+
occurrences.push(hour * 60 + minute);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
occurrences.sort((a, b) => a - b);
|
|
786
|
+
if (occurrences.length <= 1)
|
|
787
|
+
return 24 * 60;
|
|
788
|
+
let minGap = Number.POSITIVE_INFINITY;
|
|
789
|
+
for (let i = 1; i < occurrences.length; i += 1) {
|
|
790
|
+
minGap = Math.min(minGap, occurrences[i] - occurrences[i - 1]);
|
|
791
|
+
}
|
|
792
|
+
minGap = Math.min(minGap, 24 * 60 - occurrences[occurrences.length - 1] + occurrences[0]);
|
|
793
|
+
return Number.isFinite(minGap) ? minGap : null;
|
|
794
|
+
}
|
|
795
|
+
function expandCronNumberField(field, min, max) {
|
|
796
|
+
const values = new Set();
|
|
797
|
+
for (const part of field.split(",")) {
|
|
798
|
+
const expanded = expandCronNumberPart(part.trim(), min, max);
|
|
799
|
+
if (!expanded)
|
|
800
|
+
return null;
|
|
801
|
+
for (const value of expanded)
|
|
802
|
+
values.add(value);
|
|
803
|
+
}
|
|
804
|
+
return [...values].sort((a, b) => a - b);
|
|
805
|
+
}
|
|
806
|
+
function expandCronNumberPart(part, min, max) {
|
|
807
|
+
if (!part)
|
|
808
|
+
return null;
|
|
809
|
+
const [rangePart, stepPart] = part.split("/");
|
|
810
|
+
if (part.split("/").length > 2)
|
|
811
|
+
return null;
|
|
812
|
+
const step = stepPart === undefined ? 1 : Number(stepPart);
|
|
813
|
+
if (!Number.isSafeInteger(step) || step < 1)
|
|
814
|
+
return null;
|
|
815
|
+
let start;
|
|
816
|
+
let end;
|
|
817
|
+
if (rangePart === "*") {
|
|
818
|
+
start = min;
|
|
819
|
+
end = max;
|
|
820
|
+
}
|
|
821
|
+
else if (rangePart?.includes("-")) {
|
|
822
|
+
const [rawStart, rawEnd] = rangePart.split("-");
|
|
823
|
+
start = Number(rawStart);
|
|
824
|
+
end = Number(rawEnd);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
start = Number(rangePart);
|
|
828
|
+
end = stepPart === undefined ? start : max;
|
|
829
|
+
}
|
|
830
|
+
if (!Number.isSafeInteger(start) ||
|
|
831
|
+
!Number.isSafeInteger(end) ||
|
|
832
|
+
start < min ||
|
|
833
|
+
end > max ||
|
|
834
|
+
start > end) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
const values = [];
|
|
838
|
+
for (let value = start; value <= end; value += step) {
|
|
839
|
+
values.push(value);
|
|
840
|
+
}
|
|
841
|
+
return values;
|
|
842
|
+
}
|
|
477
843
|
async function commitInternal(client, planId, idempotencyKey) {
|
|
478
844
|
try {
|
|
479
845
|
return await client.request(`/deploy/v2/plans/${encodeURIComponent(planId)}/commit`, {
|
|
@@ -781,6 +1147,11 @@ async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, pro
|
|
|
781
1147
|
closePreviousPhase(undefined, "failed");
|
|
782
1148
|
throw translateGatewayError(snapshot.error, snapshot.status, snapshot.plan_id, snapshot.operation_id);
|
|
783
1149
|
}
|
|
1150
|
+
if (snapshot.status === "activation_pending" &&
|
|
1151
|
+
isTerminalStaticActivationError(snapshot.error)) {
|
|
1152
|
+
closePreviousPhase(undefined, "failed");
|
|
1153
|
+
throw translateGatewayError(snapshot.error, "activate", snapshot.plan_id, snapshot.operation_id);
|
|
1154
|
+
}
|
|
784
1155
|
if (Date.now() - start > COMMIT_POLL_TIMEOUT_MS) {
|
|
785
1156
|
throw new Run402DeployError(`Timed out waiting for operation ${snapshot.operation_id} to reach ready`, {
|
|
786
1157
|
code: "INTERNAL_ERROR",
|
|
@@ -798,8 +1169,18 @@ async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, pro
|
|
|
798
1169
|
snapshot = await client.request(`/deploy/v2/operations/${encodeURIComponent(snapshot.operation_id)}`, { headers: opHeaders, context: "polling deploy operation" });
|
|
799
1170
|
}
|
|
800
1171
|
}
|
|
1172
|
+
function isTerminalStaticActivationError(error) {
|
|
1173
|
+
if (!error?.code)
|
|
1174
|
+
return false;
|
|
1175
|
+
if (error.retryable === false)
|
|
1176
|
+
return true;
|
|
1177
|
+
if (error.safe_to_retry === false)
|
|
1178
|
+
return true;
|
|
1179
|
+
return STATIC_ACTIVATION_FAILURE_CODES.has(error.code.toUpperCase());
|
|
1180
|
+
}
|
|
801
1181
|
// ─── start() implementation ──────────────────────────────────────────────────
|
|
802
1182
|
async function startInternal(client, spec, opts) {
|
|
1183
|
+
const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
|
|
803
1184
|
const buffered = [];
|
|
804
1185
|
const subscribers = [];
|
|
805
1186
|
const emit = (event) => {
|
|
@@ -825,7 +1206,7 @@ async function startInternal(client, spec, opts) {
|
|
|
825
1206
|
const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey);
|
|
826
1207
|
emit({ type: "plan.diff", diff: plan.diff });
|
|
827
1208
|
emitPlanWarnings(plan, emit);
|
|
828
|
-
abortOnConfirmationWarnings(plan, opts);
|
|
1209
|
+
abortOnConfirmationWarnings(plan, opts, allowWarningCodes);
|
|
829
1210
|
if (plan.payment_required) {
|
|
830
1211
|
emit({
|
|
831
1212
|
type: "payment.required",
|
|
@@ -937,6 +1318,7 @@ async function startInternal(client, spec, opts) {
|
|
|
937
1318
|
};
|
|
938
1319
|
}
|
|
939
1320
|
const RELEASE_SPEC_FIELDS = new Set([
|
|
1321
|
+
"$schema",
|
|
940
1322
|
"project",
|
|
941
1323
|
"base",
|
|
942
1324
|
"database",
|
|
@@ -976,7 +1358,7 @@ const SITE_PUBLIC_PATHS_FIELDS = new Set(["mode", "replace"]);
|
|
|
976
1358
|
const PUBLIC_STATIC_PATH_FIELDS = new Set(["asset", "cache_class"]);
|
|
977
1359
|
const SUBDOMAINS_SPEC_FIELDS = new Set(["set", "add", "remove"]);
|
|
978
1360
|
const ROUTES_SPEC_FIELDS = new Set(["replace"]);
|
|
979
|
-
const ROUTE_ENTRY_FIELDS = new Set(["pattern", "methods", "target"]);
|
|
1361
|
+
const ROUTE_ENTRY_FIELDS = new Set(["pattern", "methods", "target", "acknowledge_readonly"]);
|
|
980
1362
|
const FUNCTION_ROUTE_TARGET_FIELDS = new Set(["type", "name"]);
|
|
981
1363
|
const STATIC_ROUTE_TARGET_FIELDS = new Set(["type", "file"]);
|
|
982
1364
|
const ROUTE_METHOD_SET = new Set(ROUTE_HTTP_METHODS);
|
|
@@ -1223,10 +1605,24 @@ function validateRouteEntry(route, resource) {
|
|
|
1223
1605
|
}
|
|
1224
1606
|
}
|
|
1225
1607
|
const targetType = validateRouteTarget(entry.target, `${resource}.target`);
|
|
1608
|
+
validateRouteReadOnlyAcknowledgement(entry, targetType, resource);
|
|
1226
1609
|
if (targetType === "static") {
|
|
1227
1610
|
validateStaticRouteEntry(entry, resource);
|
|
1228
1611
|
}
|
|
1229
1612
|
}
|
|
1613
|
+
function validateRouteReadOnlyAcknowledgement(entry, targetType, resource) {
|
|
1614
|
+
if (entry.acknowledge_readonly === undefined)
|
|
1615
|
+
return;
|
|
1616
|
+
if (entry.acknowledge_readonly !== true) {
|
|
1617
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.acknowledge_readonly must be true when present`, `${resource}.acknowledge_readonly`);
|
|
1618
|
+
}
|
|
1619
|
+
if (targetType !== "function" ||
|
|
1620
|
+
typeof entry.pattern !== "string" ||
|
|
1621
|
+
!isFinalWildcardRoutePattern(entry.pattern) ||
|
|
1622
|
+
!isReadOnlyRouteMethods(entry.methods)) {
|
|
1623
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.acknowledge_readonly applies only to GET/HEAD final-wildcard function routes`, `${resource}.acknowledge_readonly`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1230
1626
|
function validateRouteTarget(target, resource) {
|
|
1231
1627
|
const obj = requireObject(target, resource);
|
|
1232
1628
|
if ((hasOwn(obj, "function") || hasOwn(obj, "static")) && !hasOwn(obj, "type")) {
|
|
@@ -1482,13 +1878,13 @@ function clientRoutePlanWarnings(spec) {
|
|
|
1482
1878
|
.filter((route) => {
|
|
1483
1879
|
if (route.target.type !== "function")
|
|
1484
1880
|
return false;
|
|
1485
|
-
if (!route.pattern
|
|
1881
|
+
if (!isFinalWildcardRoutePattern(route.pattern))
|
|
1486
1882
|
return false;
|
|
1487
1883
|
if (!route.methods)
|
|
1488
1884
|
return false;
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1885
|
+
if (route.acknowledge_readonly === true)
|
|
1886
|
+
return false;
|
|
1887
|
+
return isReadOnlyRouteMethods(route.methods);
|
|
1492
1888
|
})
|
|
1493
1889
|
.map((route) => route.pattern)
|
|
1494
1890
|
.sort();
|
|
@@ -1509,21 +1905,65 @@ function clientRoutePlanWarnings(spec) {
|
|
|
1509
1905
|
},
|
|
1510
1906
|
];
|
|
1511
1907
|
}
|
|
1512
|
-
function
|
|
1908
|
+
function isFinalWildcardRoutePattern(pattern) {
|
|
1909
|
+
return pattern.endsWith("/*");
|
|
1910
|
+
}
|
|
1911
|
+
function isReadOnlyRouteMethods(methods) {
|
|
1912
|
+
if (!Array.isArray(methods) || methods.length === 0)
|
|
1913
|
+
return false;
|
|
1914
|
+
return methods.every((method) => method === "GET" || method === "HEAD");
|
|
1915
|
+
}
|
|
1916
|
+
function normalizeAllowWarningCodes(value) {
|
|
1917
|
+
if (value === undefined)
|
|
1918
|
+
return new Set();
|
|
1919
|
+
if (!Array.isArray(value)) {
|
|
1920
|
+
throw new Run402DeployError("ApplyOptions.allowWarningCodes must be an array of warning-code strings", {
|
|
1921
|
+
code: "INVALID_SPEC",
|
|
1922
|
+
phase: "validate",
|
|
1923
|
+
resource: "allowWarningCodes",
|
|
1924
|
+
retryable: false,
|
|
1925
|
+
context: "validating deploy warning options",
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
const codes = new Set();
|
|
1929
|
+
for (const code of value) {
|
|
1930
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
1931
|
+
throw new Run402DeployError("ApplyOptions.allowWarningCodes entries must be non-empty strings", {
|
|
1932
|
+
code: "INVALID_SPEC",
|
|
1933
|
+
phase: "validate",
|
|
1934
|
+
resource: "allowWarningCodes",
|
|
1935
|
+
retryable: false,
|
|
1936
|
+
context: "validating deploy warning options",
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
codes.add(code);
|
|
1940
|
+
}
|
|
1941
|
+
return codes;
|
|
1942
|
+
}
|
|
1943
|
+
function abortOnConfirmationWarnings(plan, opts, allowWarningCodes) {
|
|
1513
1944
|
if (opts.allowWarnings)
|
|
1514
1945
|
return;
|
|
1515
1946
|
const blocking = plan.warnings.filter((w) => w.requires_confirmation || w.code === "MISSING_REQUIRED_SECRET");
|
|
1516
1947
|
if (blocking.length === 0)
|
|
1517
1948
|
return;
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1949
|
+
const unacknowledged = blocking.filter((w) => !allowWarningCodes.has(w.code));
|
|
1950
|
+
if (unacknowledged.length === 0)
|
|
1951
|
+
return;
|
|
1952
|
+
const missing = unacknowledged.find((w) => w.code === "MISSING_REQUIRED_SECRET");
|
|
1953
|
+
const first = missing ?? unacknowledged[0];
|
|
1954
|
+
const unacknowledgedCodes = Array.from(new Set(unacknowledged.map((w) => w.code))).sort();
|
|
1955
|
+
throw new Run402DeployError(`Deploy plan returned unacknowledged warning ${first.code}; resolve it, retry with allowWarningCodes for reviewed warning codes, or retry with allowWarnings after explicit review.`, {
|
|
1521
1956
|
code: first.code || "DEPLOY_WARNING_REQUIRES_CONFIRMATION",
|
|
1522
1957
|
phase: "plan",
|
|
1523
1958
|
resource: "warnings",
|
|
1524
1959
|
retryable: false,
|
|
1525
1960
|
fix: { action: "review_warnings", path: "warnings" },
|
|
1526
|
-
body: {
|
|
1961
|
+
body: {
|
|
1962
|
+
warnings: blocking,
|
|
1963
|
+
unacknowledged_warnings: unacknowledged,
|
|
1964
|
+
unacknowledged_warning_codes: unacknowledgedCodes,
|
|
1965
|
+
allowed_warning_codes: Array.from(allowWarningCodes).sort(),
|
|
1966
|
+
},
|
|
1527
1967
|
context: "planning deploy",
|
|
1528
1968
|
});
|
|
1529
1969
|
}
|