@podosoft/podokit 0.16.3 → 0.17.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.
Files changed (84) hide show
  1. package/dist/deploy-compose-exec.d.ts +93 -0
  2. package/dist/deploy-compose-exec.js +198 -0
  3. package/dist/deploy-compose-profile.d.ts +107 -0
  4. package/dist/deploy-compose-profile.js +361 -0
  5. package/dist/deploy-compose-render.d.ts +48 -0
  6. package/dist/deploy-compose-render.js +333 -0
  7. package/dist/deploy-compose-sync.d.ts +144 -0
  8. package/dist/deploy-compose-sync.js +412 -0
  9. package/dist/deploy-compose.d.ts +98 -0
  10. package/dist/deploy-compose.js +667 -0
  11. package/dist/deploy-dispatch.d.ts +33 -0
  12. package/dist/deploy-dispatch.js +58 -0
  13. package/dist/deploy-driver.d.ts +25 -0
  14. package/dist/deploy-driver.js +78 -0
  15. package/dist/deploy-profile.d.ts +4 -7
  16. package/dist/deploy-profile.js +86 -274
  17. package/dist/deploy-render.js +10 -1
  18. package/dist/deploy-schema.d.ts +56 -0
  19. package/dist/deploy-schema.js +261 -0
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.js +178 -6
  22. package/dist/lib.d.ts +9 -0
  23. package/dist/lib.js +30 -1
  24. package/dist/templates/fullstack-nest-svelte/AGENTS.md +2 -1
  25. package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +34 -3
  26. package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +68 -7
  27. package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +60 -8
  28. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +2 -1
  29. package/dist/templates/fullstack-nest-svelte/apps/web/server.js +31 -0
  30. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/api.ts +2 -2
  31. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
  32. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +24 -1
  33. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.js +267 -0
  34. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
  35. package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/[...path]/+server.ts +6 -2
  36. package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/auth/[...all]/+server.ts +6 -2
  37. package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +17 -7
  38. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
  39. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +50 -1
  40. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/release.md +64 -0
  41. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
  42. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
  43. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
  44. package/dist/templates/fullstack-nest-svelte/dot-dockerignore +7 -0
  45. package/dist/templates/fullstack-nest-svelte/dot-env.example +7 -0
  46. package/dist/templates/fullstack-nest-svelte/dot-github/workflows/release.yml +142 -0
  47. package/dist/templates/fullstack-nest-svelte/infra/k3s/README.md +18 -0
  48. package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +6 -1
  49. package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +8 -0
  50. package/dist/templates/fullstack-nest-svelte/infra/k3s/web-deployment.yaml +11 -1
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +3 -2
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.test.ts +62 -0
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.ts +50 -0
  54. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +3 -2
  55. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +3 -2
  56. package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +2 -1
  57. package/dist/templates/todo/AGENTS.md +2 -1
  58. package/dist/templates/todo/Dockerfile.dev +34 -3
  59. package/dist/templates/todo/apps/api/Dockerfile +68 -7
  60. package/dist/templates/todo/apps/web/Dockerfile +60 -8
  61. package/dist/templates/todo/apps/web/package.json +2 -1
  62. package/dist/templates/todo/apps/web/server.js +31 -0
  63. package/dist/templates/todo/apps/web/src/lib/server/api.ts +2 -2
  64. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
  65. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +24 -1
  66. package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.js +267 -0
  67. package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
  68. package/dist/templates/todo/apps/web/src/routes/api/[...path]/+server.ts +6 -2
  69. package/dist/templates/todo/apps/web/src/routes/api/auth/[...all]/+server.ts +6 -2
  70. package/dist/templates/todo/apps/web/vite.config.ts +17 -7
  71. package/dist/templates/todo/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
  72. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/profile.md +47 -0
  73. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/release.md +64 -0
  74. package/dist/templates/todo/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
  75. package/dist/templates/todo/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
  76. package/dist/templates/todo/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
  77. package/dist/templates/todo/dot-dockerignore +7 -0
  78. package/dist/templates/todo/dot-env.example +7 -0
  79. package/dist/templates/todo/dot-github/workflows/release.yml +142 -0
  80. package/dist/templates/todo/infra/k3s/README.md +18 -0
  81. package/dist/templates/todo/infra/k3s/api-deployment.yaml +6 -1
  82. package/dist/templates/todo/infra/k3s/ingress.yaml +8 -0
  83. package/dist/templates/todo/infra/k3s/web-deployment.yaml +11 -1
  84. package/package.json +1 -1
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.profileDirectory = profileDirectory;
4
- exports.profilePath = profilePath;
3
+ exports.profilePath = exports.profileDirectory = void 0;
5
4
  exports.loadDeploymentProfile = loadDeploymentProfile;
6
5
  exports.listDeploymentProfiles = listDeploymentProfiles;
7
6
  exports.initializeDeploymentProfile = initializeDeploymentProfile;
@@ -11,134 +10,24 @@ const node_crypto_1 = require("node:crypto");
11
10
  const node_fs_1 = require("node:fs");
12
11
  const node_path_1 = require("node:path");
13
12
  const lockfile_1 = require("./lockfile");
14
- const PROFILE_NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
15
- const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
16
- const IMAGE_NAME = /^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[1-9][0-9]{0,4})?\/)?(?:[a-z0-9]+(?:[._-][a-z0-9]+)*\/)*[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
17
- const IMAGE_TAG = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
13
+ const deploy_schema_1 = require("./deploy-schema");
14
+ var deploy_schema_2 = require("./deploy-schema");
15
+ Object.defineProperty(exports, "profileDirectory", { enumerable: true, get: function () { return deploy_schema_2.profileDirectory; } });
16
+ Object.defineProperty(exports, "profilePath", { enumerable: true, get: function () { return deploy_schema_2.profilePath; } });
18
17
  const RESOURCE_VALUE = /^[1-9][0-9]*(?:m|Mi|Gi)?$/;
19
18
  const STORAGE_VALUE = /^[1-9][0-9]*(?:Mi|Gi|Ti)$/;
20
- const SENSITIVE_KEY = /(password|passphrase|secret|token|private.?key|credential|access.?key|api.?keys?|signing.?key|encryption.?key)/i;
21
- const STABLE_SEMVER_TAG_PATTERN = "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$";
22
- function containsUrlCredentials(value) {
23
- if (!value.includes("://"))
24
- return false;
25
- try {
26
- const parsed = new URL(value);
27
- return parsed.username !== "" || parsed.password !== "";
28
- }
29
- catch {
30
- return false;
31
- }
32
- }
33
- function isRecord(value) {
34
- return typeof value === "object" && value !== null && !Array.isArray(value);
35
- }
36
- function assertOnlyKeys(value, allowed, field) {
37
- const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
38
- if (unexpected.length) {
39
- throw new Error(`Deployment profile field "${field}" contains unknown key(s): ${unexpected.join(", ")}.`);
40
- }
41
- }
42
- function requiredRecord(parent, key) {
43
- const value = parent[key];
44
- if (!isRecord(value))
45
- throw new Error(`Deployment profile field "${key}" must be an object.`);
46
- return value;
47
- }
48
- function requiredString(parent, key) {
49
- const value = parent[key];
50
- if (typeof value !== "string" || value.trim() === "") {
51
- throw new Error(`Deployment profile field "${key}" must be a non-empty string.`);
52
- }
53
- return value;
54
- }
55
- function optionalString(parent, key) {
56
- const value = parent[key];
57
- if (value === null || value === undefined)
58
- return null;
59
- if (typeof value !== "string" || value.trim() === "") {
60
- throw new Error(`Deployment profile field "${key}" must be a string or null.`);
61
- }
62
- return value;
63
- }
64
- function requiredNumber(parent, key) {
65
- const value = parent[key];
66
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
67
- throw new Error(`Deployment profile field "${key}" must be a positive integer.`);
68
- }
69
- return value;
70
- }
71
- function parseRequiredKeys(value, field) {
72
- if (!Array.isArray(value)) {
73
- throw new Error(`Deployment profile field "${field}" must be a string array.`);
74
- }
75
- const keys = value.map((entry) => {
76
- if (typeof entry !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(entry)) {
77
- throw new Error(`Deployment profile field "${field}" contains an invalid environment key.`);
78
- }
79
- return entry;
80
- });
81
- if (new Set(keys).size !== keys.length) {
82
- throw new Error(`Deployment profile field "${field}" contains duplicate keys.`);
83
- }
84
- return keys.sort();
85
- }
86
19
  function parseNamedSecret(value, field) {
87
- if (!isRecord(value)) {
20
+ if (!(0, deploy_schema_1.isRecord)(value)) {
88
21
  throw new Error(`Deployment profile field "${field}" must be an object.`);
89
22
  }
90
- assertOnlyKeys(value, ["name", "requiredKeys"], field);
23
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["name", "requiredKeys"], field);
91
24
  const parsed = {
92
- name: requiredString(value, "name"),
93
- requiredKeys: parseRequiredKeys(value.requiredKeys, `${field}.requiredKeys`),
25
+ name: (0, deploy_schema_1.requiredString)(value, "name"),
26
+ requiredKeys: (0, deploy_schema_1.parseRequiredKeys)(value.requiredKeys, `${field}.requiredKeys`),
94
27
  };
95
- validateDnsLabel(parsed.name, `${field}.name`);
28
+ (0, deploy_schema_1.validateDnsLabel)(parsed.name, `${field}.name`);
96
29
  return parsed;
97
30
  }
98
- function validateDnsLabel(value, field) {
99
- if (!DNS_LABEL.test(value)) {
100
- throw new Error(`Deployment profile field "${field}" must be a Kubernetes DNS label.`);
101
- }
102
- }
103
- function validateDnsSubdomain(value, field) {
104
- if (value.length > 253 ||
105
- !value.split(".").every((label) => DNS_LABEL.test(label))) {
106
- throw new Error(`Deployment profile field "${field}" must be a Kubernetes DNS subdomain.`);
107
- }
108
- }
109
- function parseImageReference(value) {
110
- if (value.length > 512 || value.includes("://") || /[\s\u0000-\u001f\u007f]/.test(value)) {
111
- return null;
112
- }
113
- const at = value.indexOf("@");
114
- if (at !== -1 && at !== value.lastIndexOf("@"))
115
- return null;
116
- const digest = at === -1 ? null : value.slice(at + 1);
117
- const tagged = at === -1 ? value : value.slice(0, at);
118
- const lastSlash = tagged.lastIndexOf("/");
119
- const lastColon = tagged.lastIndexOf(":");
120
- const hasTag = lastColon > lastSlash;
121
- const name = hasTag ? tagged.slice(0, lastColon) : tagged;
122
- const tag = hasTag ? tagged.slice(lastColon + 1) : null;
123
- if (!IMAGE_NAME.test(name) ||
124
- (tag !== null && !IMAGE_TAG.test(tag)) ||
125
- (digest !== null && !/^sha256:[a-f0-9]{64}$/.test(digest))) {
126
- return null;
127
- }
128
- return { name, tag, digest };
129
- }
130
- function validateImage(value, field) {
131
- const parsed = parseImageReference(value);
132
- if (!parsed || parsed.tag === "latest" || (!parsed.tag && !parsed.digest)) {
133
- throw new Error(`Deployment profile field "${field}" must use an explicit non-latest image tag or digest.`);
134
- }
135
- }
136
- function validateImageRepository(value, field) {
137
- const parsed = parseImageReference(value);
138
- if (!parsed || parsed.tag || parsed.digest) {
139
- throw new Error(`Deployment profile field "${field}" must be an image repository without a tag or digest.`);
140
- }
141
- }
142
31
  function validateResource(value, field) {
143
32
  if (!RESOURCE_VALUE.test(value)) {
144
33
  throw new Error(`Deployment profile field "${field}" is not a supported resource quantity.`);
@@ -150,22 +39,22 @@ function validateStorage(value, field) {
150
39
  }
151
40
  }
152
41
  function parseResources(value, field) {
153
- assertOnlyKeys(value, ["cpuRequest", "cpuLimit", "memoryRequest", "memoryLimit"], field);
42
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["cpuRequest", "cpuLimit", "memoryRequest", "memoryLimit"], field);
154
43
  const parsed = {
155
- cpuRequest: requiredString(value, "cpuRequest"),
156
- cpuLimit: requiredString(value, "cpuLimit"),
157
- memoryRequest: requiredString(value, "memoryRequest"),
158
- memoryLimit: requiredString(value, "memoryLimit"),
44
+ cpuRequest: (0, deploy_schema_1.requiredString)(value, "cpuRequest"),
45
+ cpuLimit: (0, deploy_schema_1.requiredString)(value, "cpuLimit"),
46
+ memoryRequest: (0, deploy_schema_1.requiredString)(value, "memoryRequest"),
47
+ memoryLimit: (0, deploy_schema_1.requiredString)(value, "memoryLimit"),
159
48
  };
160
49
  for (const [key, quantity] of Object.entries(parsed))
161
50
  validateResource(quantity, `${field}.${key}`);
162
51
  return parsed;
163
52
  }
164
53
  function parseWorkload(value, field) {
165
- assertOnlyKeys(value, ["replicas", "resources"], field);
54
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["replicas", "resources"], field);
166
55
  return {
167
- replicas: requiredNumber(value, "replicas"),
168
- resources: parseResources(requiredRecord(value, "resources"), `${field}.resources`),
56
+ replicas: (0, deploy_schema_1.requiredNumber)(value, "replicas"),
57
+ resources: parseResources((0, deploy_schema_1.requiredRecord)(value, "resources"), `${field}.resources`),
169
58
  };
170
59
  }
171
60
  function parseMode(value, field) {
@@ -175,16 +64,16 @@ function parseMode(value, field) {
175
64
  return value;
176
65
  }
177
66
  function parseStatefulDependency(value, field, extraKeys = []) {
178
- assertOnlyKeys(value, ["mode", "image", "secretName", "storageClassName", "storageSize", ...extraKeys], field);
67
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["mode", "image", "secretName", "storageClassName", "storageSize", ...extraKeys], field);
179
68
  const parsed = {
180
69
  mode: parseMode(value.mode, `${field}.mode`),
181
- image: requiredString(value, "image"),
182
- secretName: requiredString(value, "secretName"),
70
+ image: (0, deploy_schema_1.requiredString)(value, "image"),
71
+ secretName: (0, deploy_schema_1.requiredString)(value, "secretName"),
183
72
  storageClassName: typeof value.storageClassName === "string" ? value.storageClassName : "",
184
- storageSize: requiredString(value, "storageSize"),
73
+ storageSize: (0, deploy_schema_1.requiredString)(value, "storageSize"),
185
74
  };
186
- validateImage(parsed.image, `${field}.image`);
187
- validateDnsLabel(parsed.secretName, `${field}.secretName`);
75
+ (0, deploy_schema_1.validateImage)(parsed.image, `${field}.image`);
76
+ (0, deploy_schema_1.validateDnsLabel)(parsed.secretName, `${field}.secretName`);
188
77
  validateStorage(parsed.storageSize, `${field}.storageSize`);
189
78
  if (parsed.storageClassName && parsed.storageClassName.length > 253) {
190
79
  throw new Error(`Deployment profile field "${field}.storageClassName" is too long.`);
@@ -194,25 +83,10 @@ function parseStatefulDependency(value, field, extraKeys = []) {
194
83
  function parseRedisDependency(value, field) {
195
84
  return parseStatefulDependency(value, field);
196
85
  }
197
- function validateHttpsOrigin(value) {
198
- let parsed;
199
- try {
200
- parsed = new URL(value);
201
- }
202
- catch {
203
- throw new Error(`Deployment verification baseUrl "${value}" is invalid.`);
204
- }
205
- if (parsed.username || parsed.password) {
206
- throw new Error("Deployment verification baseUrl must not contain URL credentials; use a credential-free HTTPS origin.");
207
- }
208
- if (parsed.protocol !== "https:" || parsed.pathname !== "/" || parsed.search || parsed.hash) {
209
- throw new Error("Deployment verification baseUrl must be an HTTPS origin without a path.");
210
- }
211
- }
212
86
  function parseProfile(value) {
213
- if (!isRecord(value))
87
+ if (!(0, deploy_schema_1.isRecord)(value))
214
88
  throw new Error("Deployment profile must be a JSON object.");
215
- assertOnlyKeys(value, [
89
+ (0, deploy_schema_1.assertOnlyKeys)(value, [
216
90
  "schemaVersion",
217
91
  "driver",
218
92
  "target",
@@ -221,6 +95,7 @@ function parseProfile(value) {
221
95
  "workloads",
222
96
  "dependencies",
223
97
  "secrets",
98
+ "migration",
224
99
  "runtimeConfig",
225
100
  "verification",
226
101
  ], "root");
@@ -230,64 +105,67 @@ function parseProfile(value) {
230
105
  if (value.driver !== "kubernetes-helm") {
231
106
  throw new Error(`Unsupported deployment driver: ${String(value.driver)}`);
232
107
  }
233
- const target = requiredRecord(value, "target");
234
- const release = requiredRecord(value, "release");
235
- const exposure = requiredRecord(value, "exposure");
236
- const workloads = requiredRecord(value, "workloads");
237
- const dependencies = requiredRecord(value, "dependencies");
238
- const secrets = requiredRecord(value, "secrets");
239
- const verification = requiredRecord(value, "verification");
240
- const runtimeConfig = requiredRecord(value, "runtimeConfig");
241
- assertOnlyKeys(target, ["context", "clusterFingerprint", "namespace", "release", "createNamespace"], "target");
242
- assertOnlyKeys(release, ["strategy", "tagPattern", "apiRepository", "webRepository"], "release");
243
- assertOnlyKeys(exposure, ["mode", "host", "ingressClassName", "tlsSecretName"], "exposure");
244
- assertOnlyKeys(workloads, ["api", "web", "worker"], "workloads");
245
- assertOnlyKeys(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
246
- assertOnlyKeys(secrets, ["api", "web", "imagePull"], "secrets");
247
- assertOnlyKeys(verification, ["baseUrl", "checks"], "verification");
248
- const context = requiredString(target, "context");
249
- const clusterFingerprint = requiredString(target, "clusterFingerprint");
108
+ const target = (0, deploy_schema_1.requiredRecord)(value, "target");
109
+ const release = (0, deploy_schema_1.requiredRecord)(value, "release");
110
+ const exposure = (0, deploy_schema_1.requiredRecord)(value, "exposure");
111
+ const workloads = (0, deploy_schema_1.requiredRecord)(value, "workloads");
112
+ const dependencies = (0, deploy_schema_1.requiredRecord)(value, "dependencies");
113
+ const secrets = (0, deploy_schema_1.requiredRecord)(value, "secrets");
114
+ const migration = value.migration === undefined ? undefined : (0, deploy_schema_1.requiredRecord)(value, "migration");
115
+ const verification = (0, deploy_schema_1.requiredRecord)(value, "verification");
116
+ const runtimeConfig = (0, deploy_schema_1.requiredRecord)(value, "runtimeConfig");
117
+ (0, deploy_schema_1.assertOnlyKeys)(target, ["context", "clusterFingerprint", "namespace", "release", "createNamespace"], "target");
118
+ (0, deploy_schema_1.assertOnlyKeys)(release, ["strategy", "tagPattern", "apiRepository", "webRepository"], "release");
119
+ (0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "ingressClassName", "tlsSecretName"], "exposure");
120
+ (0, deploy_schema_1.assertOnlyKeys)(workloads, ["api", "web", "worker"], "workloads");
121
+ (0, deploy_schema_1.assertOnlyKeys)(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
122
+ (0, deploy_schema_1.assertOnlyKeys)(secrets, ["api", "web", "imagePull"], "secrets");
123
+ if (migration)
124
+ (0, deploy_schema_1.assertOnlyKeys)(migration, ["command"], "migration");
125
+ (0, deploy_schema_1.assertOnlyKeys)(verification, ["baseUrl", "checks"], "verification");
126
+ const context = (0, deploy_schema_1.requiredString)(target, "context");
127
+ const clusterFingerprint = (0, deploy_schema_1.requiredString)(target, "clusterFingerprint");
250
128
  if (!/^sha256:[a-f0-9]{64}$/.test(clusterFingerprint)) {
251
129
  throw new Error("Deployment target.clusterFingerprint must be a sha256 fingerprint.");
252
130
  }
253
- const namespace = requiredString(target, "namespace");
254
- const releaseName = requiredString(target, "release");
255
- validateDnsLabel(namespace, "target.namespace");
256
- validateDnsLabel(releaseName, "target.release");
131
+ const namespace = (0, deploy_schema_1.requiredString)(target, "namespace");
132
+ const releaseName = (0, deploy_schema_1.requiredString)(target, "release");
133
+ (0, deploy_schema_1.validateDnsLabel)(namespace, "target.namespace");
134
+ (0, deploy_schema_1.validateDnsLabel)(releaseName, "target.release");
257
135
  if (releaseName.length > 40) {
258
136
  throw new Error("Deployment target.release must be at most 40 characters.");
259
137
  }
260
138
  if (target.createNamespace !== false) {
261
139
  throw new Error("Deployment target.createNamespace must be false in schema version 1.");
262
140
  }
263
- const tagPattern = requiredString(release, "tagPattern");
264
- if (tagPattern !== STABLE_SEMVER_TAG_PATTERN) {
141
+ const tagPattern = (0, deploy_schema_1.requiredString)(release, "tagPattern");
142
+ if (tagPattern !== deploy_schema_1.STABLE_SEMVER_TAG_PATTERN) {
265
143
  throw new Error("Deployment release.tagPattern must require stable vMAJOR.MINOR.PATCH tags.");
266
144
  }
267
145
  if (release.strategy !== "shared-tag") {
268
146
  throw new Error("Deployment release.strategy must be shared-tag.");
269
147
  }
270
- const apiRepository = requiredString(release, "apiRepository");
271
- const webRepository = requiredString(release, "webRepository");
272
- validateImageRepository(apiRepository, "release.apiRepository");
273
- validateImageRepository(webRepository, "release.webRepository");
148
+ const apiRepository = (0, deploy_schema_1.requiredString)(release, "apiRepository");
149
+ const webRepository = (0, deploy_schema_1.requiredString)(release, "webRepository");
150
+ (0, deploy_schema_1.validateImageRepository)(apiRepository, "release.apiRepository");
151
+ (0, deploy_schema_1.validateImageRepository)(webRepository, "release.webRepository");
274
152
  const exposureMode = exposure.mode;
275
153
  if (exposureMode !== "ingress" && exposureMode !== "nodePort") {
276
154
  throw new Error("Deployment exposure.mode must be ingress or nodePort.");
277
155
  }
278
- const host = requiredString(exposure, "host");
156
+ const host = (0, deploy_schema_1.requiredString)(exposure, "host");
279
157
  if (exposureMode === "ingress") {
280
- validateDnsSubdomain(host, "exposure.host");
158
+ (0, deploy_schema_1.validateDnsSubdomain)(host, "exposure.host");
281
159
  }
282
- const ingressClassName = requiredString(exposure, "ingressClassName");
283
- validateDnsSubdomain(ingressClassName, "exposure.ingressClassName");
284
- const tlsSecretName = optionalString(exposure, "tlsSecretName");
160
+ const ingressClassName = (0, deploy_schema_1.requiredString)(exposure, "ingressClassName");
161
+ (0, deploy_schema_1.validateDnsSubdomain)(ingressClassName, "exposure.ingressClassName");
162
+ const tlsSecretName = (0, deploy_schema_1.optionalString)(exposure, "tlsSecretName");
285
163
  if (tlsSecretName)
286
- validateDnsSubdomain(tlsSecretName, "exposure.tlsSecretName");
287
- const postgres = parseStatefulDependency(requiredRecord(dependencies, "postgres"), "dependencies.postgres");
288
- const redis = parseRedisDependency(requiredRecord(dependencies, "redis"), "dependencies.redis");
289
- const objectStorageRecord = requiredRecord(dependencies, "objectStorage");
290
- assertOnlyKeys(objectStorageRecord, [
164
+ (0, deploy_schema_1.validateDnsSubdomain)(tlsSecretName, "exposure.tlsSecretName");
165
+ const postgres = parseStatefulDependency((0, deploy_schema_1.requiredRecord)(dependencies, "postgres"), "dependencies.postgres");
166
+ const redis = parseRedisDependency((0, deploy_schema_1.requiredRecord)(dependencies, "redis"), "dependencies.redis");
167
+ const objectStorageRecord = (0, deploy_schema_1.requiredRecord)(dependencies, "objectStorage");
168
+ (0, deploy_schema_1.assertOnlyKeys)(objectStorageRecord, [
291
169
  "mode",
292
170
  "image",
293
171
  "secretName",
@@ -298,73 +176,13 @@ function parseProfile(value) {
298
176
  ], "dependencies.objectStorage");
299
177
  const objectStorage = {
300
178
  ...parseStatefulDependency(objectStorageRecord, "dependencies.objectStorage", ["bucket", "clientImage"]),
301
- bucket: requiredString(objectStorageRecord, "bucket"),
302
- clientImage: requiredString(objectStorageRecord, "clientImage"),
179
+ bucket: (0, deploy_schema_1.requiredString)(objectStorageRecord, "bucket"),
180
+ clientImage: (0, deploy_schema_1.requiredString)(objectStorageRecord, "clientImage"),
303
181
  };
304
- validateDnsLabel(objectStorage.bucket, "dependencies.objectStorage.bucket");
305
- validateImage(objectStorage.clientImage, "dependencies.objectStorage.clientImage");
306
- const parsedConfig = {};
307
- for (const [key, entry] of Object.entries(runtimeConfig)) {
308
- if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
309
- throw new Error(`Deployment runtimeConfig key "${key}" must be an uppercase environment name.`);
310
- }
311
- if (SENSITIVE_KEY.test(key)) {
312
- throw new Error(`Deployment runtimeConfig key "${key}" may contain a secret; use a Kubernetes Secret.`);
313
- }
314
- if (["PORT", "BACKEND_INTERNAL_URL"].includes(key)) {
315
- throw new Error(`Deployment runtimeConfig key "${key}" is managed by PodoKit.`);
316
- }
317
- if (typeof entry !== "string") {
318
- throw new Error(`Deployment runtimeConfig value "${key}" must be a string.`);
319
- }
320
- if (containsUrlCredentials(entry)) {
321
- throw new Error(`Deployment runtimeConfig value "${key}" contains URL credentials; use a Kubernetes Secret.`);
322
- }
323
- parsedConfig[key] = entry;
324
- }
325
- const baseUrl = requiredString(verification, "baseUrl");
326
- validateHttpsOrigin(baseUrl);
327
- const baseOrigin = new URL(baseUrl).origin;
328
- const checksValue = verification.checks;
329
- if (!Array.isArray(checksValue) || checksValue.length === 0) {
330
- throw new Error("Deployment verification.checks must be a non-empty array.");
331
- }
332
- const checks = checksValue.map((entry, index) => {
333
- if (!isRecord(entry)) {
334
- throw new Error(`Deployment verification.checks[${index}] must be an object.`);
335
- }
336
- assertOnlyKeys(entry, ["path", "expectedStatus", "expectedJson"], `verification.checks[${index}]`);
337
- const path = requiredString(entry, "path");
338
- if (!path.startsWith("/") ||
339
- path.startsWith("//") ||
340
- path.includes("\\") ||
341
- /[\u0000-\u001f\u007f]/.test(path) ||
342
- new URL(path, baseUrl).origin !== baseOrigin) {
343
- throw new Error("Every deployment verification path must be an absolute URL path.");
344
- }
345
- const expectedStatus = requiredNumber(entry, "expectedStatus");
346
- if (expectedStatus < 100 || expectedStatus > 599) {
347
- throw new Error("Deployment verification expectedStatus must be an HTTP status.");
348
- }
349
- const expectedJsonValue = entry.expectedJson;
350
- let expectedJson;
351
- if (expectedJsonValue !== undefined) {
352
- if (!isRecord(expectedJsonValue)) {
353
- throw new Error("Deployment verification expectedJson must be an object.");
354
- }
355
- expectedJson = {};
356
- for (const [key, value] of Object.entries(expectedJsonValue)) {
357
- if (value !== null &&
358
- typeof value !== "string" &&
359
- typeof value !== "number" &&
360
- typeof value !== "boolean") {
361
- throw new Error("Deployment verification expectedJson values must be scalar.");
362
- }
363
- expectedJson[key] = value;
364
- }
365
- }
366
- return { path, expectedStatus, ...(expectedJson ? { expectedJson } : {}) };
367
- });
182
+ (0, deploy_schema_1.validateDnsLabel)(objectStorage.bucket, "dependencies.objectStorage.bucket");
183
+ (0, deploy_schema_1.validateImage)(objectStorage.clientImage, "dependencies.objectStorage.clientImage");
184
+ const parsedConfig = (0, deploy_schema_1.parseRuntimeConfig)(runtimeConfig);
185
+ const { baseUrl, checks } = (0, deploy_schema_1.parseVerification)(verification);
368
186
  const profile = {
369
187
  schemaVersion: 1,
370
188
  driver: "kubernetes-helm",
@@ -388,11 +206,11 @@ function parseProfile(value) {
388
206
  tlsSecretName,
389
207
  },
390
208
  workloads: {
391
- api: parseWorkload(requiredRecord(workloads, "api"), "workloads.api"),
392
- web: parseWorkload(requiredRecord(workloads, "web"), "workloads.web"),
209
+ api: parseWorkload((0, deploy_schema_1.requiredRecord)(workloads, "api"), "workloads.api"),
210
+ web: parseWorkload((0, deploy_schema_1.requiredRecord)(workloads, "web"), "workloads.web"),
393
211
  worker: workloads.worker === null
394
212
  ? null
395
- : parseWorkload(requiredRecord(workloads, "worker"), "workloads.worker"),
213
+ : parseWorkload((0, deploy_schema_1.requiredRecord)(workloads, "worker"), "workloads.worker"),
396
214
  },
397
215
  dependencies: { postgres, redis, objectStorage },
398
216
  secrets: {
@@ -400,25 +218,19 @@ function parseProfile(value) {
400
218
  web: secrets.web === null
401
219
  ? null
402
220
  : parseNamedSecret(secrets.web, "secrets.web"),
403
- imagePull: requiredString(secrets, "imagePull"),
221
+ imagePull: (0, deploy_schema_1.requiredString)(secrets, "imagePull"),
404
222
  },
223
+ ...(migration
224
+ ? { migration: { command: (0, deploy_schema_1.parseCommand)(migration.command, "migration.command") } }
225
+ : {}),
405
226
  runtimeConfig: parsedConfig,
406
227
  verification: { baseUrl, checks },
407
228
  };
408
- validateDnsLabel(profile.secrets.imagePull, "secrets.imagePull");
229
+ (0, deploy_schema_1.validateDnsLabel)(profile.secrets.imagePull, "secrets.imagePull");
409
230
  return profile;
410
231
  }
411
- function profileDirectory(projectRoot) {
412
- return (0, node_path_1.join)((0, node_path_1.resolve)(projectRoot), ".podokit", "deploy");
413
- }
414
- function profilePath(projectRoot, name) {
415
- if (!PROFILE_NAME.test(name)) {
416
- throw new Error(`Invalid deployment profile name "${name}".`);
417
- }
418
- return (0, node_path_1.join)(profileDirectory(projectRoot), `${name}.json`);
419
- }
420
232
  function loadDeploymentProfile(projectRoot, name) {
421
- const path = profilePath(projectRoot, name);
233
+ const path = (0, deploy_schema_1.profilePath)(projectRoot, name);
422
234
  if (!(0, node_fs_1.existsSync)(path))
423
235
  throw new Error(`Deployment profile "${name}" does not exist at ${path}.`);
424
236
  let parsed;
@@ -431,7 +243,7 @@ function loadDeploymentProfile(projectRoot, name) {
431
243
  return parseProfile(parsed);
432
244
  }
433
245
  function listDeploymentProfiles(projectRoot) {
434
- const directory = profileDirectory(projectRoot);
246
+ const directory = (0, deploy_schema_1.profileDirectory)(projectRoot);
435
247
  if (!(0, node_fs_1.existsSync)(directory))
436
248
  return [];
437
249
  return (0, node_fs_1.readdirSync)(directory)
@@ -449,7 +261,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
449
261
  const projectName = manifest.answers.projectName;
450
262
  if (!projectName)
451
263
  throw new Error("PodoKit manifest is missing answers.projectName.");
452
- const path = profilePath(projectRoot, name);
264
+ const path = (0, deploy_schema_1.profilePath)(projectRoot, name);
453
265
  if ((0, node_fs_1.existsSync)(path))
454
266
  throw new Error(`Deployment profile "${name}" already exists.`);
455
267
  const releaseName = projectName
@@ -480,7 +292,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
480
292
  },
481
293
  release: {
482
294
  strategy: "shared-tag",
483
- tagPattern: STABLE_SEMVER_TAG_PATTERN,
295
+ tagPattern: deploy_schema_1.STABLE_SEMVER_TAG_PATTERN,
484
296
  apiRepository: `ghcr.io/example/${releaseName}-api`,
485
297
  webRepository: `ghcr.io/example/${releaseName}-web`,
486
298
  },
@@ -589,7 +401,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
589
401
  },
590
402
  };
591
403
  const validated = parseProfile(defaultProfile);
592
- (0, node_fs_1.mkdirSync)(profileDirectory(projectRoot), { recursive: true });
404
+ (0, node_fs_1.mkdirSync)((0, deploy_schema_1.profileDirectory)(projectRoot), { recursive: true });
593
405
  (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(validated, null, 2)}\n`, { flag: "wx" });
594
406
  return { name, path, profile: validated };
595
407
  }
@@ -8,6 +8,14 @@ const deploy_profile_1 = require("./deploy-profile");
8
8
  function quote(value) {
9
9
  return JSON.stringify(value);
10
10
  }
11
+ function yamlScalar(value) {
12
+ return /^(?!null$|true$|false$|~$)[A-Za-z_][A-Za-z0-9_./:-]*$/i.test(value)
13
+ ? value
14
+ : quote(value);
15
+ }
16
+ function yamlStringSequence(values) {
17
+ return `[${values.map((value) => yamlScalar(value)).join(", ")}]`;
18
+ }
11
19
  function digest(parts) {
12
20
  const hash = (0, node_crypto_1.createHash)("sha256");
13
21
  for (const part of parts)
@@ -571,6 +579,7 @@ function migrationRuntimeEnv(profile) {
571
579
  }
572
580
  function migrationManifest(profile, release, images) {
573
581
  const releaseSlug = release.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
582
+ const command = profile.migration?.command ?? ["npm", "run", "migrate:all"];
574
583
  return `apiVersion: batch/v1
575
584
  kind: Job
576
585
  metadata:
@@ -596,7 +605,7 @@ ${labels(`${profile.target.release}-migration`, 8)} spec:
596
605
  - name: migrate
597
606
  image: ${quote(images.api)}
598
607
  imagePullPolicy: IfNotPresent
599
- command: [npm, run, migrate:all]
608
+ command: ${yamlStringSequence(command)}
600
609
  env:
601
610
  ${migrationRuntimeEnv(profile)}
602
611
  envFrom:
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Validation primitives shared by every deployment driver.
3
+ *
4
+ * A driver profile decides its own target, exposure, and dependency shape, but the
5
+ * pieces that describe *what is being released* — image repositories, the stable tag
6
+ * pattern, the migration command, non-secret runtime configuration, and the public
7
+ * verification contract — mean the same thing everywhere and are parsed here once.
8
+ */
9
+ export interface VerificationCheckProfile {
10
+ path: string;
11
+ expectedStatus: number;
12
+ expectedJson?: Record<string, string | number | boolean | null>;
13
+ }
14
+ export interface VerificationProfile {
15
+ baseUrl: string;
16
+ checks: VerificationCheckProfile[];
17
+ }
18
+ export interface MigrationProfile {
19
+ command: string[];
20
+ }
21
+ export interface ReleaseProfile {
22
+ strategy: "shared-tag";
23
+ tagPattern: string;
24
+ apiRepository: string;
25
+ webRepository: string;
26
+ }
27
+ export declare const PROFILE_NAME: RegExp;
28
+ export declare const DNS_LABEL: RegExp;
29
+ export declare const SENSITIVE_KEY: RegExp;
30
+ export declare const STABLE_SEMVER_TAG_PATTERN = "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$";
31
+ /** Keys PodoKit derives itself, so a profile may not set them. */
32
+ export declare const MANAGED_RUNTIME_CONFIG_KEYS: string[];
33
+ export declare function containsUrlCredentials(value: string): boolean;
34
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
35
+ export declare function assertOnlyKeys(value: Record<string, unknown>, allowed: string[], field: string): void;
36
+ export declare function requiredRecord(parent: Record<string, unknown>, key: string): Record<string, unknown>;
37
+ export declare function requiredString(parent: Record<string, unknown>, key: string): string;
38
+ export declare function optionalString(parent: Record<string, unknown>, key: string): string | null;
39
+ export declare function requiredNumber(parent: Record<string, unknown>, key: string): number;
40
+ export declare function parseRequiredKeys(value: unknown, field: string): string[];
41
+ export declare function parseCommand(value: unknown, field: string): string[];
42
+ export declare function validateDnsLabel(value: string, field: string): void;
43
+ export declare function validateDnsSubdomain(value: string, field: string): void;
44
+ export declare function parseImageReference(value: string): {
45
+ name: string;
46
+ tag: string | null;
47
+ digest: string | null;
48
+ } | null;
49
+ export declare function validateImage(value: string, field: string): void;
50
+ export declare function validateImageRepository(value: string, field: string): void;
51
+ export declare function parseReleaseProfile(release: Record<string, unknown>): ReleaseProfile;
52
+ export declare function parseRuntimeConfig(runtimeConfig: Record<string, unknown>): Record<string, string>;
53
+ export declare function validateHttpsOrigin(value: string): void;
54
+ export declare function parseVerification(verification: Record<string, unknown>): VerificationProfile;
55
+ export declare function profileDirectory(projectRoot: string): string;
56
+ export declare function profilePath(projectRoot: string, name: string): string;