@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 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
- - **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 `allowWarnings` is set. For `MISSING_REQUIRED_SECRET`, set the affected keys with `r.secrets.set`, then retry.
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. Each retry emits `deploy.retry`; exhausted retries keep the last `Run402DeployError` and add `attempts`, `maxRetries`, and `lastRetryCode`.
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. Retry with `allowWarnings` only after review. |
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 confirm it is read-only. |
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":"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"}
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.endsWith("/*"))
1881
+ if (!isFinalWildcardRoutePattern(route.pattern))
1486
1882
  return false;
1487
1883
  if (!route.methods)
1488
1884
  return false;
1489
- const methods = new Set(route.methods);
1490
- return (methods.size > 0 &&
1491
- [...methods].every((method) => method === "GET" || method === "HEAD"));
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 abortOnConfirmationWarnings(plan, opts) {
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 missing = blocking.find((w) => w.code === "MISSING_REQUIRED_SECRET");
1519
- const first = missing ?? blocking[0];
1520
- throw new Run402DeployError(`Deploy plan returned warning ${first.code} that requires confirmation; resolve it or retry with allowWarnings after explicit review.`, {
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: { warnings: blocking },
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
  }