run402 1.60.1 → 1.60.3

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/lib/deploy-v2.mjs CHANGED
@@ -24,8 +24,8 @@
24
24
  */
25
25
 
26
26
  import { readFileSync } from "node:fs";
27
- import { resolve, dirname, isAbsolute, join } from "node:path";
28
- import { githubActionsCredentials } from "#sdk/node";
27
+ import { resolve, dirname, isAbsolute } from "node:path";
28
+ import { githubActionsCredentials, normalizeDeployManifest } from "#sdk/node";
29
29
  import { getSdk } from "./sdk.mjs";
30
30
  import { reportSdkError, fail } from "./sdk-errors.mjs";
31
31
  import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId } from "./config.mjs";
@@ -206,11 +206,12 @@ async function applyCmd(args) {
206
206
  }
207
207
 
208
208
  let raw;
209
+ let manifestPath = null;
209
210
  if (opts.spec) {
210
211
  raw = opts.spec;
211
212
  } else if (opts.manifest) {
212
213
  try {
213
- const manifestPath = isAbsolute(opts.manifest) ? opts.manifest : resolve(process.cwd(), opts.manifest);
214
+ manifestPath = isAbsolute(opts.manifest) ? opts.manifest : resolve(process.cwd(), opts.manifest);
214
215
  raw = readFileSync(manifestPath, "utf-8");
215
216
  } catch (err) {
216
217
  fail({
@@ -235,7 +236,7 @@ async function applyCmd(args) {
235
236
  }
236
237
  rejectLegacySecretManifest(spec, {
237
238
  source: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin",
238
- ...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
239
+ ...(manifestPath ? { path: manifestPath } : {}),
239
240
  });
240
241
 
241
242
  // GH-232: Reject empty specs client-side. Without this guard,
@@ -269,38 +270,44 @@ async function applyCmd(args) {
269
270
  hint: "Did you mean to write a 'site.replace' or 'database.migrations' block? See https://run402.com/schemas/manifest.v1.json",
270
271
  details: {
271
272
  field: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin",
272
- ...(opts.manifest ? { path: resolve(opts.manifest) } : {}),
273
+ ...(manifestPath ? { path: manifestPath } : {}),
273
274
  meaningful_keys: meaningful,
274
275
  },
275
276
  });
276
277
  }
277
278
 
278
- if (opts.manifest) resolveFileDataPaths(spec, dirname(resolve(opts.manifest)));
279
-
280
- if (opts.project && spec.project_id && spec.project_id !== opts.project) {
279
+ const manifestProject = spec.project ?? spec.project_id;
280
+ if (opts.project && manifestProject && manifestProject !== opts.project) {
281
281
  fail({
282
282
  code: "BAD_USAGE",
283
- message: `project_id conflict: spec.project_id=${spec.project_id} but --project=${opts.project}`,
284
- details: { spec_project_id: spec.project_id, flag_project_id: opts.project },
283
+ message: `project_id conflict: manifest project=${manifestProject} but --project=${opts.project}`,
284
+ details: { spec_project_id: manifestProject, flag_project_id: opts.project },
285
285
  });
286
286
  }
287
- if (opts.project) spec.project_id = opts.project;
288
287
  const useGithubActionsOidc = hasGithubActionsOidcEnv();
289
- if (!spec.project_id) {
290
- spec.project_id = useGithubActionsOidc ? resolveCiProjectId() : resolveProjectId(null);
288
+ let defaultProject;
289
+ if (!opts.project && !manifestProject) {
290
+ defaultProject = useGithubActionsOidc ? resolveCiProjectId() : resolveProjectId(null);
291
+ }
292
+
293
+ let normalizedManifest;
294
+ try {
295
+ normalizedManifest = await normalizeDeployManifest(spec, {
296
+ baseDir: manifestPath ? dirname(manifestPath) : process.cwd(),
297
+ ...(opts.project ? { project: opts.project } : {}),
298
+ ...(defaultProject ? { defaultProject } : {}),
299
+ });
300
+ } catch (err) {
301
+ reportSdkError(err);
291
302
  }
292
303
 
293
- // Translate { project_id, ... } envelope → ReleaseSpec ({ project, ... })
294
- // The SDK ReleaseSpec uses `project` rather than `project_id`; both shapes
295
- // are accepted at the manifest layer (project_id is friendlier for agents
296
- // sharing JSON manifests with the MCP tool).
297
- const releaseSpec = mapManifestToReleaseSpec(spec);
298
- const idempotencyKey = spec.idempotency_key;
304
+ const releaseSpec = normalizedManifest.spec;
305
+ const idempotencyKey = normalizedManifest.idempotencyKey;
299
306
 
300
307
  let sdkOpts;
301
308
  if (useGithubActionsOidc) {
302
309
  sdkOpts = {
303
- credentials: githubActionsCredentials({ projectId: spec.project_id, apiBase: API }),
310
+ credentials: githubActionsCredentials({ projectId: releaseSpec.project, apiBase: API }),
304
311
  disablePaidFetch: true,
305
312
  };
306
313
  } else {
@@ -668,174 +675,3 @@ function parsePositiveInt(value, flag) {
668
675
  }
669
676
  return parsed;
670
677
  }
671
-
672
- // ─── Manifest → ReleaseSpec ──────────────────────────────────────────────────
673
-
674
- function mapManifestToReleaseSpec(spec) {
675
- const out = { project: spec.project_id };
676
- if (spec.base !== undefined) out.base = spec.base;
677
- if (spec.subdomains !== undefined) out.subdomains = spec.subdomains;
678
- if (spec.secrets !== undefined) out.secrets = spec.secrets;
679
- if (spec.routes !== undefined) out.routes = spec.routes;
680
- if (spec.checks !== undefined) out.checks = spec.checks;
681
-
682
- if (spec.database) {
683
- out.database = {};
684
- if (spec.database.expose !== undefined) out.database.expose = spec.database.expose;
685
- if (spec.database.zero_downtime !== undefined) out.database.zero_downtime = spec.database.zero_downtime;
686
- if (spec.database.migrations) {
687
- out.database.migrations = spec.database.migrations.map((m) => {
688
- const mm = { id: m.id };
689
- if (m.sql !== undefined) mm.sql = m.sql;
690
- if (m.sql_ref !== undefined) mm.sql_ref = m.sql_ref;
691
- if (m.checksum !== undefined) mm.checksum = m.checksum;
692
- if (m.transaction !== undefined) mm.transaction = m.transaction;
693
- return mm;
694
- });
695
- }
696
- }
697
-
698
- if (spec.functions) {
699
- out.functions = {};
700
- if (spec.functions.replace) out.functions.replace = mapFunctionMap(spec.functions.replace);
701
- if (spec.functions.patch) {
702
- out.functions.patch = {};
703
- if (spec.functions.patch.set) out.functions.patch.set = mapFunctionMap(spec.functions.patch.set);
704
- if (spec.functions.patch.delete) out.functions.patch.delete = spec.functions.patch.delete;
705
- }
706
- }
707
-
708
- if (spec.site) {
709
- if (spec.site.replace) {
710
- out.site = { replace: mapFileMap(spec.site.replace) };
711
- } else if (spec.site.patch) {
712
- const patch = {};
713
- if (spec.site.patch.put) patch.put = mapFileMap(spec.site.patch.put);
714
- if (spec.site.patch.delete) patch.delete = spec.site.patch.delete;
715
- out.site = { patch };
716
- }
717
- }
718
-
719
- return out;
720
- }
721
-
722
- function mapFunctionMap(map) {
723
- const out = {};
724
- for (const [name, fn] of Object.entries(map)) {
725
- const f = {};
726
- if (fn.runtime) f.runtime = fn.runtime;
727
- if (fn.source !== undefined) f.source = fileEntryToContentSource(fn.source);
728
- if (fn.files) f.files = mapFileMap(fn.files);
729
- if (fn.entrypoint !== undefined) f.entrypoint = fn.entrypoint;
730
- if (fn.config !== undefined) f.config = fn.config;
731
- if (fn.schedule !== undefined) f.schedule = fn.schedule;
732
- out[name] = f;
733
- }
734
- return out;
735
- }
736
-
737
- function mapFileMap(map) {
738
- const out = {};
739
- for (const [path, entry] of Object.entries(map)) {
740
- out[path] = fileEntryToContentSource(entry);
741
- }
742
- return out;
743
- }
744
-
745
- function fileEntryToContentSource(entry) {
746
- if (entry === null || entry === undefined) return entry;
747
- if (typeof entry === "string") return entry;
748
- if (entry instanceof Uint8Array) return entry;
749
- if (typeof entry === "object") {
750
- if (entry.encoding === "base64" && typeof entry.data === "string") {
751
- const bytes = Buffer.from(entry.data, "base64");
752
- const u8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
753
- return entry.contentType ? { data: u8, contentType: entry.contentType } : u8;
754
- }
755
- if (typeof entry.data === "string") {
756
- return entry.contentType ? { data: entry.data, contentType: entry.contentType } : entry.data;
757
- }
758
- // Pre-resolved ContentRef shape — pass through.
759
- if (typeof entry.sha256 === "string" && typeof entry.size === "number") {
760
- return entry;
761
- }
762
- }
763
- return entry;
764
- }
765
-
766
- /**
767
- * Resolve any `{ "path": "..." }` entries in the manifest to inline data.
768
- * Mirrors the legacy deploy.mjs behavior so `run402 deploy apply` accepts
769
- * the same files-with-paths shape that `run402 deploy` does today.
770
- */
771
- function resolveFileDataPaths(spec, baseDir) {
772
- // Site files
773
- if (spec.site?.replace) resolveMap(spec.site.replace, baseDir);
774
- if (spec.site?.patch?.put) resolveMap(spec.site.patch.put, baseDir);
775
- // Function files
776
- const visitFns = (fnMap) => {
777
- if (!fnMap) return;
778
- for (const fn of Object.values(fnMap)) {
779
- if (fn.source && typeof fn.source === "object" && fn.source.path) {
780
- const resolved = readFileEntry(fn.source, baseDir);
781
- if (resolved) fn.source = resolved;
782
- }
783
- if (fn.files) resolveMap(fn.files, baseDir);
784
- }
785
- };
786
- visitFns(spec.functions?.replace);
787
- visitFns(spec.functions?.patch?.set);
788
- // Migration sql_path / sql_file
789
- if (spec.database?.migrations) {
790
- for (const m of spec.database.migrations) {
791
- if (!m.sql && m.sql_path) {
792
- try {
793
- const p = isAbsolute(m.sql_path) ? m.sql_path : join(baseDir, m.sql_path);
794
- m.sql = readFileSync(p, "utf-8");
795
- delete m.sql_path;
796
- } catch (err) {
797
- fail({
798
- code: "BAD_USAGE",
799
- message: `Failed to read migration sql_path '${m.sql_path}': ${err.message}`,
800
- details: { migration_id: m.id, sql_path: m.sql_path },
801
- });
802
- }
803
- }
804
- }
805
- }
806
- }
807
-
808
- function resolveMap(map, baseDir) {
809
- for (const [key, entry] of Object.entries(map)) {
810
- if (entry && typeof entry === "object" && typeof entry.path === "string" && entry.data === undefined) {
811
- const resolved = readFileEntry(entry, baseDir);
812
- if (resolved) map[key] = resolved;
813
- }
814
- }
815
- }
816
-
817
- function readFileEntry(entry, baseDir) {
818
- try {
819
- const p = isAbsolute(entry.path) ? entry.path : join(baseDir, entry.path);
820
- const buf = readFileSync(p);
821
- const out = {};
822
- // Detect text vs binary via simple UTF-8 round-trip; mirrors the bundle
823
- // deploy behavior. Image/font types get base64; HTML/CSS/JS stay UTF-8.
824
- const looksTextual = !entry.contentType?.match(/^(image|font|application\/(pdf|wasm|octet-stream|zip))/);
825
- if (looksTextual) {
826
- out.data = buf.toString("utf-8");
827
- out.encoding = "utf-8";
828
- } else {
829
- out.data = buf.toString("base64");
830
- out.encoding = "base64";
831
- }
832
- if (entry.contentType) out.contentType = entry.contentType;
833
- return out;
834
- } catch (err) {
835
- fail({
836
- code: "BAD_USAGE",
837
- message: `Failed to read file '${entry.path}': ${err.message}`,
838
- details: { path: entry.path },
839
- });
840
- }
841
- }
package/lib/deploy.mjs CHANGED
@@ -4,6 +4,7 @@ import { resolveProjectId } from "./config.mjs";
4
4
  import { resolveFilePathsInManifest, resolveMigrationsFile } from "./manifest.mjs";
5
5
  import { getSdk } from "./sdk.mjs";
6
6
  import { reportSdkError, fail } from "./sdk-errors.mjs";
7
+ import { normalizeDeployManifest } from "#sdk/node";
7
8
 
8
9
  const HELP = `run402 deploy — Deploy to an existing project on Run402
9
10
 
@@ -347,10 +348,26 @@ export async function run(args) {
347
348
  manifest.project_id = resolveProjectId(null);
348
349
  }
349
350
 
350
- // Strip fields that aren't part of the bundleDeploy contract.
351
351
  const projectId = manifest.project_id;
352
- delete manifest.project_id;
353
352
  delete manifest.name;
353
+
354
+ if (isV2Manifest(manifest)) {
355
+ try {
356
+ const normalized = await normalizeDeployManifest(manifest, {
357
+ baseDir: opts.manifest ? dirname(resolve(opts.manifest)) : process.cwd(),
358
+ });
359
+ const result = await getSdk().deploy.apply(normalized.spec, {
360
+ idempotencyKey: normalized.idempotencyKey,
361
+ });
362
+ console.log(JSON.stringify({ project_id: projectId, ...result }, null, 2));
363
+ } catch (err) {
364
+ reportSdkError(err);
365
+ }
366
+ return;
367
+ }
368
+
369
+ // Strip fields that aren't part of the bundleDeploy contract.
370
+ delete manifest.project_id;
354
371
  delete manifest.migrations_file;
355
372
 
356
373
  try {
@@ -360,3 +377,19 @@ export async function run(args) {
360
377
  reportSdkError(err);
361
378
  }
362
379
  }
380
+
381
+ function isV2Manifest(manifest) {
382
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return false;
383
+ if (manifest.database !== undefined) return true;
384
+ if (manifest.site !== undefined) return true;
385
+ if (manifest.subdomains !== undefined) return true;
386
+ if (manifest.routes !== undefined) return true;
387
+ if (manifest.checks !== undefined) return true;
388
+ if (manifest.secrets && typeof manifest.secrets === "object" && !Array.isArray(manifest.secrets)) {
389
+ return true;
390
+ }
391
+ if (manifest.functions && typeof manifest.functions === "object" && !Array.isArray(manifest.functions)) {
392
+ return true;
393
+ }
394
+ return false;
395
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.60.1",
3
+ "version": "1.60.3",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -764,6 +764,43 @@ async function startInternal(client, spec, opts) {
764
764
  },
765
765
  };
766
766
  }
767
+ const RELEASE_SPEC_FIELDS = new Set([
768
+ "project",
769
+ "base",
770
+ "database",
771
+ "secrets",
772
+ "functions",
773
+ "site",
774
+ "subdomains",
775
+ "routes",
776
+ "checks",
777
+ ]);
778
+ const DEPLOYABLE_SPEC_FIELDS = [
779
+ "database",
780
+ "site",
781
+ "functions",
782
+ "secrets",
783
+ "subdomains",
784
+ "routes",
785
+ "checks",
786
+ ];
787
+ const BASE_SPEC_FIELDS = new Set(["release", "release_id"]);
788
+ const DATABASE_SPEC_FIELDS = new Set(["migrations", "expose", "zero_downtime"]);
789
+ const MIGRATION_SPEC_FIELDS = new Set(["id", "checksum", "sql", "sql_ref", "transaction"]);
790
+ const FUNCTIONS_SPEC_FIELDS = new Set(["replace", "patch"]);
791
+ const FUNCTIONS_PATCH_FIELDS = new Set(["set", "delete"]);
792
+ const FUNCTION_SPEC_FIELDS = new Set([
793
+ "runtime",
794
+ "source",
795
+ "files",
796
+ "entrypoint",
797
+ "config",
798
+ "schedule",
799
+ ]);
800
+ const FUNCTION_CONFIG_FIELDS = new Set(["timeoutSeconds", "memoryMb"]);
801
+ const SITE_SPEC_FIELDS = new Set(["replace", "patch"]);
802
+ const SITE_PATCH_FIELDS = new Set(["put", "delete"]);
803
+ const SUBDOMAINS_SPEC_FIELDS = new Set(["set", "add", "remove"]);
767
804
  function validateSpec(spec) {
768
805
  if (!spec || typeof spec !== "object") {
769
806
  throw new Run402DeployError("ReleaseSpec must be an object", {
@@ -775,6 +812,11 @@ function validateSpec(spec) {
775
812
  context: "validating spec",
776
813
  });
777
814
  }
815
+ const raw = spec;
816
+ validateKnownFields(raw, "spec", RELEASE_SPEC_FIELDS, {
817
+ project_id: "Use `project` in ReleaseSpec, or call `loadDeployManifest()` / `normalizeDeployManifest()` for MCP/CLI-style manifests.",
818
+ subdomain: "Use `subdomains: { set: [name] }`.",
819
+ });
778
820
  if (!spec.project || typeof spec.project !== "string") {
779
821
  throw new Run402DeployError("ReleaseSpec.project is required", {
780
822
  code: "INVALID_SPEC",
@@ -785,7 +827,17 @@ function validateSpec(spec) {
785
827
  context: "validating spec",
786
828
  });
787
829
  }
788
- if (spec.subdomains?.set && spec.subdomains.set.length > 1) {
830
+ validateBaseSpec(raw.base);
831
+ validateDatabaseSpec(raw.database);
832
+ validateFunctionsSpec(raw.functions);
833
+ validateSiteSpec(raw.site);
834
+ validateSubdomainsSpec(raw.subdomains);
835
+ validateRoutesSpec(raw.routes);
836
+ validateChecksSpec(raw.checks);
837
+ validateSecretsSpec(raw.secrets);
838
+ const subdomains = raw.subdomains;
839
+ const set = subdomains?.set;
840
+ if (set && set.length > 1) {
789
841
  throw new Run402DeployError("subdomains.set accepts at most one subdomain per project; multi-subdomain support is not yet available", {
790
842
  code: "SUBDOMAIN_MULTI_NOT_SUPPORTED",
791
843
  phase: "validate",
@@ -795,7 +847,211 @@ function validateSpec(spec) {
795
847
  context: "validating spec",
796
848
  });
797
849
  }
798
- validateSecretsSpec(spec.secrets);
850
+ if (!hasDeployableContent(raw)) {
851
+ throw new Run402DeployError(`ReleaseSpec contains no deployable sections. Expected at least one non-empty section: ${DEPLOYABLE_SPEC_FIELDS.join(", ")}`, {
852
+ code: "MANIFEST_EMPTY",
853
+ phase: "validate",
854
+ resource: "spec",
855
+ retryable: false,
856
+ fix: { action: "set_field", path: "site.replace" },
857
+ body: { deployable_fields: DEPLOYABLE_SPEC_FIELDS },
858
+ context: "validating spec",
859
+ });
860
+ }
861
+ }
862
+ function validateBaseSpec(base) {
863
+ if (base === undefined)
864
+ return;
865
+ const obj = requireObject(base, "base");
866
+ validateKnownFields(obj, "base", BASE_SPEC_FIELDS);
867
+ if (hasOwn(obj, "release") && hasOwn(obj, "release_id")) {
868
+ throw invalidSpec("ReleaseSpec.base must use either release or release_id, not both", "base");
869
+ }
870
+ }
871
+ function validateDatabaseSpec(database) {
872
+ if (database === undefined)
873
+ return;
874
+ const obj = requireObject(database, "database");
875
+ validateKnownFields(obj, "database", DATABASE_SPEC_FIELDS);
876
+ if (obj.migrations !== undefined) {
877
+ if (!Array.isArray(obj.migrations)) {
878
+ throw invalidSpec("ReleaseSpec.database.migrations must be an array", "database.migrations");
879
+ }
880
+ for (const [index, migration] of obj.migrations.entries()) {
881
+ const m = requireObject(migration, `database.migrations.${index}`);
882
+ validateKnownFields(m, `database.migrations.${index}`, MIGRATION_SPEC_FIELDS);
883
+ }
884
+ }
885
+ if (obj.expose !== undefined) {
886
+ requireObject(obj.expose, "database.expose");
887
+ }
888
+ }
889
+ function validateFunctionsSpec(functions) {
890
+ if (functions === undefined)
891
+ return;
892
+ const obj = requireObject(functions, "functions");
893
+ validateKnownFields(obj, "functions", FUNCTIONS_SPEC_FIELDS);
894
+ if (obj.replace !== undefined) {
895
+ validateFunctionMap(obj.replace, "functions.replace");
896
+ }
897
+ if (obj.patch !== undefined) {
898
+ const patch = requireObject(obj.patch, "functions.patch");
899
+ validateKnownFields(patch, "functions.patch", FUNCTIONS_PATCH_FIELDS);
900
+ if (patch.set !== undefined)
901
+ validateFunctionMap(patch.set, "functions.patch.set");
902
+ if (patch.delete !== undefined)
903
+ validateStringArray(patch.delete, "functions.patch.delete");
904
+ }
905
+ }
906
+ function validateFunctionMap(value, resource) {
907
+ const map = requireObject(value, resource);
908
+ for (const [name, fn] of Object.entries(map)) {
909
+ const entry = requireObject(fn, `${resource}.${name}`);
910
+ validateKnownFields(entry, `${resource}.${name}`, FUNCTION_SPEC_FIELDS);
911
+ if (entry.config !== undefined) {
912
+ const config = requireObject(entry.config, `${resource}.${name}.config`);
913
+ validateKnownFields(config, `${resource}.${name}.config`, FUNCTION_CONFIG_FIELDS);
914
+ }
915
+ if (entry.files !== undefined) {
916
+ requireObject(entry.files, `${resource}.${name}.files`);
917
+ }
918
+ }
919
+ }
920
+ function validateSiteSpec(site) {
921
+ if (site === undefined)
922
+ return;
923
+ const obj = requireObject(site, "site");
924
+ validateKnownFields(obj, "site", SITE_SPEC_FIELDS, {
925
+ file: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
926
+ files: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
927
+ });
928
+ if (hasOwn(obj, "replace") && hasOwn(obj, "patch")) {
929
+ throw invalidSpec("ReleaseSpec.site must use either replace or patch, not both", "site");
930
+ }
931
+ if (obj.replace !== undefined) {
932
+ requireObject(obj.replace, "site.replace");
933
+ }
934
+ if (obj.patch !== undefined) {
935
+ const patch = requireObject(obj.patch, "site.patch");
936
+ validateKnownFields(patch, "site.patch", SITE_PATCH_FIELDS);
937
+ if (patch.put !== undefined)
938
+ requireObject(patch.put, "site.patch.put");
939
+ if (patch.delete !== undefined)
940
+ validateStringArray(patch.delete, "site.patch.delete");
941
+ }
942
+ }
943
+ function validateSubdomainsSpec(subdomains) {
944
+ if (subdomains === undefined)
945
+ return;
946
+ const obj = requireObject(subdomains, "subdomains");
947
+ validateKnownFields(obj, "subdomains", SUBDOMAINS_SPEC_FIELDS);
948
+ if (obj.set !== undefined)
949
+ validateStringArray(obj.set, "subdomains.set");
950
+ if (obj.add !== undefined)
951
+ validateStringArray(obj.add, "subdomains.add");
952
+ if (obj.remove !== undefined)
953
+ validateStringArray(obj.remove, "subdomains.remove");
954
+ }
955
+ function validateRoutesSpec(routes) {
956
+ if (routes === undefined)
957
+ return;
958
+ requireObject(routes, "routes");
959
+ }
960
+ function validateChecksSpec(checks) {
961
+ if (checks === undefined)
962
+ return;
963
+ if (!Array.isArray(checks)) {
964
+ throw invalidSpec("ReleaseSpec.checks must be an array", "checks");
965
+ }
966
+ }
967
+ function validateKnownFields(obj, resource, allowed, hints = {}) {
968
+ for (const key of Object.keys(obj)) {
969
+ if (allowed.has(key))
970
+ continue;
971
+ const field = resource === "spec" ? `spec.${key}` : `${resource}.${key}`;
972
+ const hint = hints[key] ? ` ${hints[key]}` : "";
973
+ throw invalidSpec(`Unknown ReleaseSpec field: ${field}.${hint}`, field);
974
+ }
975
+ }
976
+ function requireObject(value, resource) {
977
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
978
+ throw invalidSpec(`ReleaseSpec.${resource} must be an object`, resource);
979
+ }
980
+ return value;
981
+ }
982
+ function validateStringArray(value, resource) {
983
+ if (!Array.isArray(value)) {
984
+ throw invalidSpec(`ReleaseSpec.${resource} must be an array`, resource);
985
+ }
986
+ if (value.some((entry) => typeof entry !== "string")) {
987
+ throw invalidSpec(`ReleaseSpec.${resource} entries must be strings`, resource);
988
+ }
989
+ }
990
+ function hasDeployableContent(spec) {
991
+ return (hasDatabaseContent(spec.database) ||
992
+ hasSiteContent(spec.site) ||
993
+ hasFunctionsContent(spec.functions) ||
994
+ hasSecretsContent(spec.secrets) ||
995
+ hasSubdomainsContent(spec.subdomains) ||
996
+ hasRecordEntries(spec.routes) ||
997
+ hasArrayEntries(spec.checks));
998
+ }
999
+ function hasDatabaseContent(database) {
1000
+ if (!isRecord(database))
1001
+ return false;
1002
+ return hasArrayEntries(database.migrations) || hasRecordEntries(database.expose);
1003
+ }
1004
+ function hasSiteContent(site) {
1005
+ if (!isRecord(site))
1006
+ return false;
1007
+ if (hasRecordEntries(site.replace))
1008
+ return true;
1009
+ if (!isRecord(site.patch))
1010
+ return false;
1011
+ return hasRecordEntries(site.patch.put) || hasArrayEntries(site.patch.delete);
1012
+ }
1013
+ function hasFunctionsContent(functions) {
1014
+ if (!isRecord(functions))
1015
+ return false;
1016
+ if (hasRecordEntries(functions.replace))
1017
+ return true;
1018
+ if (!isRecord(functions.patch))
1019
+ return false;
1020
+ return hasRecordEntries(functions.patch.set) || hasArrayEntries(functions.patch.delete);
1021
+ }
1022
+ function hasSecretsContent(secrets) {
1023
+ if (!isRecord(secrets))
1024
+ return false;
1025
+ return hasArrayEntries(secrets.require) || hasArrayEntries(secrets.delete);
1026
+ }
1027
+ function hasSubdomainsContent(subdomains) {
1028
+ if (!isRecord(subdomains))
1029
+ return false;
1030
+ return (hasArrayEntries(subdomains.set) ||
1031
+ hasArrayEntries(subdomains.add) ||
1032
+ hasArrayEntries(subdomains.remove));
1033
+ }
1034
+ function hasRecordEntries(value) {
1035
+ return isRecord(value) && Object.keys(value).length > 0;
1036
+ }
1037
+ function hasArrayEntries(value) {
1038
+ return Array.isArray(value) && value.length > 0;
1039
+ }
1040
+ function hasOwn(obj, key) {
1041
+ return Object.prototype.hasOwnProperty.call(obj, key);
1042
+ }
1043
+ function isRecord(value) {
1044
+ return !!value && typeof value === "object" && !Array.isArray(value);
1045
+ }
1046
+ function invalidSpec(message, resource) {
1047
+ return new Run402DeployError(message, {
1048
+ code: "INVALID_SPEC",
1049
+ phase: "validate",
1050
+ resource,
1051
+ retryable: false,
1052
+ fix: { action: "set_field", path: resource.replace(/^spec\./, "") },
1053
+ context: "validating spec",
1054
+ });
799
1055
  }
800
1056
  function normalizePlanResponse(plan) {
801
1057
  const raw = plan;