@podosoft/podokit 0.16.4 → 0.17.1

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 +3 -10
  16. package/dist/deploy-profile.js +82 -291
  17. package/dist/deploy-schema.d.ts +56 -0
  18. package/dist/deploy-schema.js +261 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.js +178 -6
  21. package/dist/lib.d.ts +9 -0
  22. package/dist/lib.js +30 -1
  23. package/dist/templates/fullstack-nest-svelte/AGENTS.md +2 -1
  24. package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +34 -3
  25. package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +68 -7
  26. package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +60 -8
  27. package/dist/templates/fullstack-nest-svelte/apps/web/server.js +31 -0
  28. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.test.ts +21 -0
  29. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +15 -1
  30. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.js +267 -0
  31. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
  32. package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +17 -7
  33. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +55 -10
  34. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +50 -1
  35. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/release.md +64 -0
  36. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy-fast/SKILL.md +92 -0
  37. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +1 -1
  38. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy-fast/SKILL.md +8 -0
  39. package/dist/templates/fullstack-nest-svelte/dot-dockerignore +7 -0
  40. package/dist/templates/fullstack-nest-svelte/dot-env.example +7 -0
  41. package/dist/templates/fullstack-nest-svelte/dot-github/workflows/release.yml +142 -0
  42. package/dist/templates/fullstack-nest-svelte/infra/k3s/README.md +18 -0
  43. package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +6 -1
  44. package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +8 -0
  45. package/dist/templates/fullstack-nest-svelte/infra/k3s/web-deployment.yaml +11 -1
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +15 -2
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.test.ts +62 -0
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/device-label.ts +50 -0
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +5 -0
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +5 -0
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +3 -2
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +3 -2
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/reset-password/+page.svelte +12 -1
  54. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +13 -1
  55. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +27 -2
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +20 -1
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +26 -1
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +10 -1
  59. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +3 -3
  60. package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +2 -1
  61. package/dist/templates/todo/AGENTS.md +2 -1
  62. package/dist/templates/todo/Dockerfile.dev +34 -3
  63. package/dist/templates/todo/apps/api/Dockerfile +68 -7
  64. package/dist/templates/todo/apps/web/Dockerfile +60 -8
  65. package/dist/templates/todo/apps/web/server.js +31 -0
  66. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.test.ts +21 -0
  67. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +15 -1
  68. package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.js +267 -0
  69. package/dist/templates/todo/apps/web/src/lib/server/ws-upgrade.test.js +134 -0
  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,148 +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
- function parseCommand(value, field) {
87
- if (!Array.isArray(value) || value.length === 0) {
88
- throw new Error(`Deployment profile field "${field}" must be a non-empty string array.`);
89
- }
90
- return value.map((entry, index) => {
91
- if (typeof entry !== "string" || entry.trim() === "") {
92
- throw new Error(`Deployment profile field "${field}[${index}]" must be a non-empty string.`);
93
- }
94
- if (/[\u0000-\u001f\u007f]/.test(entry)) {
95
- throw new Error(`Deployment profile field "${field}[${index}]" must not contain control characters.`);
96
- }
97
- return entry;
98
- });
99
- }
100
19
  function parseNamedSecret(value, field) {
101
- if (!isRecord(value)) {
20
+ if (!(0, deploy_schema_1.isRecord)(value)) {
102
21
  throw new Error(`Deployment profile field "${field}" must be an object.`);
103
22
  }
104
- assertOnlyKeys(value, ["name", "requiredKeys"], field);
23
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["name", "requiredKeys"], field);
105
24
  const parsed = {
106
- name: requiredString(value, "name"),
107
- 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`),
108
27
  };
109
- validateDnsLabel(parsed.name, `${field}.name`);
28
+ (0, deploy_schema_1.validateDnsLabel)(parsed.name, `${field}.name`);
110
29
  return parsed;
111
30
  }
112
- function validateDnsLabel(value, field) {
113
- if (!DNS_LABEL.test(value)) {
114
- throw new Error(`Deployment profile field "${field}" must be a Kubernetes DNS label.`);
115
- }
116
- }
117
- function validateDnsSubdomain(value, field) {
118
- if (value.length > 253 ||
119
- !value.split(".").every((label) => DNS_LABEL.test(label))) {
120
- throw new Error(`Deployment profile field "${field}" must be a Kubernetes DNS subdomain.`);
121
- }
122
- }
123
- function parseImageReference(value) {
124
- if (value.length > 512 || value.includes("://") || /[\s\u0000-\u001f\u007f]/.test(value)) {
125
- return null;
126
- }
127
- const at = value.indexOf("@");
128
- if (at !== -1 && at !== value.lastIndexOf("@"))
129
- return null;
130
- const digest = at === -1 ? null : value.slice(at + 1);
131
- const tagged = at === -1 ? value : value.slice(0, at);
132
- const lastSlash = tagged.lastIndexOf("/");
133
- const lastColon = tagged.lastIndexOf(":");
134
- const hasTag = lastColon > lastSlash;
135
- const name = hasTag ? tagged.slice(0, lastColon) : tagged;
136
- const tag = hasTag ? tagged.slice(lastColon + 1) : null;
137
- if (!IMAGE_NAME.test(name) ||
138
- (tag !== null && !IMAGE_TAG.test(tag)) ||
139
- (digest !== null && !/^sha256:[a-f0-9]{64}$/.test(digest))) {
140
- return null;
141
- }
142
- return { name, tag, digest };
143
- }
144
- function validateImage(value, field) {
145
- const parsed = parseImageReference(value);
146
- if (!parsed || parsed.tag === "latest" || (!parsed.tag && !parsed.digest)) {
147
- throw new Error(`Deployment profile field "${field}" must use an explicit non-latest image tag or digest.`);
148
- }
149
- }
150
- function validateImageRepository(value, field) {
151
- const parsed = parseImageReference(value);
152
- if (!parsed || parsed.tag || parsed.digest) {
153
- throw new Error(`Deployment profile field "${field}" must be an image repository without a tag or digest.`);
154
- }
155
- }
156
31
  function validateResource(value, field) {
157
32
  if (!RESOURCE_VALUE.test(value)) {
158
33
  throw new Error(`Deployment profile field "${field}" is not a supported resource quantity.`);
@@ -164,22 +39,22 @@ function validateStorage(value, field) {
164
39
  }
165
40
  }
166
41
  function parseResources(value, field) {
167
- assertOnlyKeys(value, ["cpuRequest", "cpuLimit", "memoryRequest", "memoryLimit"], field);
42
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["cpuRequest", "cpuLimit", "memoryRequest", "memoryLimit"], field);
168
43
  const parsed = {
169
- cpuRequest: requiredString(value, "cpuRequest"),
170
- cpuLimit: requiredString(value, "cpuLimit"),
171
- memoryRequest: requiredString(value, "memoryRequest"),
172
- 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"),
173
48
  };
174
49
  for (const [key, quantity] of Object.entries(parsed))
175
50
  validateResource(quantity, `${field}.${key}`);
176
51
  return parsed;
177
52
  }
178
53
  function parseWorkload(value, field) {
179
- assertOnlyKeys(value, ["replicas", "resources"], field);
54
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["replicas", "resources"], field);
180
55
  return {
181
- replicas: requiredNumber(value, "replicas"),
182
- 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`),
183
58
  };
184
59
  }
185
60
  function parseMode(value, field) {
@@ -189,16 +64,16 @@ function parseMode(value, field) {
189
64
  return value;
190
65
  }
191
66
  function parseStatefulDependency(value, field, extraKeys = []) {
192
- assertOnlyKeys(value, ["mode", "image", "secretName", "storageClassName", "storageSize", ...extraKeys], field);
67
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["mode", "image", "secretName", "storageClassName", "storageSize", ...extraKeys], field);
193
68
  const parsed = {
194
69
  mode: parseMode(value.mode, `${field}.mode`),
195
- image: requiredString(value, "image"),
196
- secretName: requiredString(value, "secretName"),
70
+ image: (0, deploy_schema_1.requiredString)(value, "image"),
71
+ secretName: (0, deploy_schema_1.requiredString)(value, "secretName"),
197
72
  storageClassName: typeof value.storageClassName === "string" ? value.storageClassName : "",
198
- storageSize: requiredString(value, "storageSize"),
73
+ storageSize: (0, deploy_schema_1.requiredString)(value, "storageSize"),
199
74
  };
200
- validateImage(parsed.image, `${field}.image`);
201
- 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`);
202
77
  validateStorage(parsed.storageSize, `${field}.storageSize`);
203
78
  if (parsed.storageClassName && parsed.storageClassName.length > 253) {
204
79
  throw new Error(`Deployment profile field "${field}.storageClassName" is too long.`);
@@ -208,25 +83,10 @@ function parseStatefulDependency(value, field, extraKeys = []) {
208
83
  function parseRedisDependency(value, field) {
209
84
  return parseStatefulDependency(value, field);
210
85
  }
211
- function validateHttpsOrigin(value) {
212
- let parsed;
213
- try {
214
- parsed = new URL(value);
215
- }
216
- catch {
217
- throw new Error(`Deployment verification baseUrl "${value}" is invalid.`);
218
- }
219
- if (parsed.username || parsed.password) {
220
- throw new Error("Deployment verification baseUrl must not contain URL credentials; use a credential-free HTTPS origin.");
221
- }
222
- if (parsed.protocol !== "https:" || parsed.pathname !== "/" || parsed.search || parsed.hash) {
223
- throw new Error("Deployment verification baseUrl must be an HTTPS origin without a path.");
224
- }
225
- }
226
86
  function parseProfile(value) {
227
- if (!isRecord(value))
87
+ if (!(0, deploy_schema_1.isRecord)(value))
228
88
  throw new Error("Deployment profile must be a JSON object.");
229
- assertOnlyKeys(value, [
89
+ (0, deploy_schema_1.assertOnlyKeys)(value, [
230
90
  "schemaVersion",
231
91
  "driver",
232
92
  "target",
@@ -245,67 +105,67 @@ function parseProfile(value) {
245
105
  if (value.driver !== "kubernetes-helm") {
246
106
  throw new Error(`Unsupported deployment driver: ${String(value.driver)}`);
247
107
  }
248
- const target = requiredRecord(value, "target");
249
- const release = requiredRecord(value, "release");
250
- const exposure = requiredRecord(value, "exposure");
251
- const workloads = requiredRecord(value, "workloads");
252
- const dependencies = requiredRecord(value, "dependencies");
253
- const secrets = requiredRecord(value, "secrets");
254
- const migration = value.migration === undefined ? undefined : requiredRecord(value, "migration");
255
- const verification = requiredRecord(value, "verification");
256
- const runtimeConfig = requiredRecord(value, "runtimeConfig");
257
- assertOnlyKeys(target, ["context", "clusterFingerprint", "namespace", "release", "createNamespace"], "target");
258
- assertOnlyKeys(release, ["strategy", "tagPattern", "apiRepository", "webRepository"], "release");
259
- assertOnlyKeys(exposure, ["mode", "host", "ingressClassName", "tlsSecretName"], "exposure");
260
- assertOnlyKeys(workloads, ["api", "web", "worker"], "workloads");
261
- assertOnlyKeys(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
262
- assertOnlyKeys(secrets, ["api", "web", "imagePull"], "secrets");
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");
263
123
  if (migration)
264
- assertOnlyKeys(migration, ["command"], "migration");
265
- assertOnlyKeys(verification, ["baseUrl", "checks"], "verification");
266
- const context = requiredString(target, "context");
267
- const clusterFingerprint = requiredString(target, "clusterFingerprint");
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");
268
128
  if (!/^sha256:[a-f0-9]{64}$/.test(clusterFingerprint)) {
269
129
  throw new Error("Deployment target.clusterFingerprint must be a sha256 fingerprint.");
270
130
  }
271
- const namespace = requiredString(target, "namespace");
272
- const releaseName = requiredString(target, "release");
273
- validateDnsLabel(namespace, "target.namespace");
274
- 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");
275
135
  if (releaseName.length > 40) {
276
136
  throw new Error("Deployment target.release must be at most 40 characters.");
277
137
  }
278
138
  if (target.createNamespace !== false) {
279
139
  throw new Error("Deployment target.createNamespace must be false in schema version 1.");
280
140
  }
281
- const tagPattern = requiredString(release, "tagPattern");
282
- 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) {
283
143
  throw new Error("Deployment release.tagPattern must require stable vMAJOR.MINOR.PATCH tags.");
284
144
  }
285
145
  if (release.strategy !== "shared-tag") {
286
146
  throw new Error("Deployment release.strategy must be shared-tag.");
287
147
  }
288
- const apiRepository = requiredString(release, "apiRepository");
289
- const webRepository = requiredString(release, "webRepository");
290
- validateImageRepository(apiRepository, "release.apiRepository");
291
- 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");
292
152
  const exposureMode = exposure.mode;
293
153
  if (exposureMode !== "ingress" && exposureMode !== "nodePort") {
294
154
  throw new Error("Deployment exposure.mode must be ingress or nodePort.");
295
155
  }
296
- const host = requiredString(exposure, "host");
156
+ const host = (0, deploy_schema_1.requiredString)(exposure, "host");
297
157
  if (exposureMode === "ingress") {
298
- validateDnsSubdomain(host, "exposure.host");
158
+ (0, deploy_schema_1.validateDnsSubdomain)(host, "exposure.host");
299
159
  }
300
- const ingressClassName = requiredString(exposure, "ingressClassName");
301
- validateDnsSubdomain(ingressClassName, "exposure.ingressClassName");
302
- 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");
303
163
  if (tlsSecretName)
304
- validateDnsSubdomain(tlsSecretName, "exposure.tlsSecretName");
305
- const postgres = parseStatefulDependency(requiredRecord(dependencies, "postgres"), "dependencies.postgres");
306
- const redis = parseRedisDependency(requiredRecord(dependencies, "redis"), "dependencies.redis");
307
- const objectStorageRecord = requiredRecord(dependencies, "objectStorage");
308
- 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, [
309
169
  "mode",
310
170
  "image",
311
171
  "secretName",
@@ -316,73 +176,13 @@ function parseProfile(value) {
316
176
  ], "dependencies.objectStorage");
317
177
  const objectStorage = {
318
178
  ...parseStatefulDependency(objectStorageRecord, "dependencies.objectStorage", ["bucket", "clientImage"]),
319
- bucket: requiredString(objectStorageRecord, "bucket"),
320
- clientImage: requiredString(objectStorageRecord, "clientImage"),
179
+ bucket: (0, deploy_schema_1.requiredString)(objectStorageRecord, "bucket"),
180
+ clientImage: (0, deploy_schema_1.requiredString)(objectStorageRecord, "clientImage"),
321
181
  };
322
- validateDnsLabel(objectStorage.bucket, "dependencies.objectStorage.bucket");
323
- validateImage(objectStorage.clientImage, "dependencies.objectStorage.clientImage");
324
- const parsedConfig = {};
325
- for (const [key, entry] of Object.entries(runtimeConfig)) {
326
- if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
327
- throw new Error(`Deployment runtimeConfig key "${key}" must be an uppercase environment name.`);
328
- }
329
- if (SENSITIVE_KEY.test(key)) {
330
- throw new Error(`Deployment runtimeConfig key "${key}" may contain a secret; use a Kubernetes Secret.`);
331
- }
332
- if (["PORT", "BACKEND_INTERNAL_URL"].includes(key)) {
333
- throw new Error(`Deployment runtimeConfig key "${key}" is managed by PodoKit.`);
334
- }
335
- if (typeof entry !== "string") {
336
- throw new Error(`Deployment runtimeConfig value "${key}" must be a string.`);
337
- }
338
- if (containsUrlCredentials(entry)) {
339
- throw new Error(`Deployment runtimeConfig value "${key}" contains URL credentials; use a Kubernetes Secret.`);
340
- }
341
- parsedConfig[key] = entry;
342
- }
343
- const baseUrl = requiredString(verification, "baseUrl");
344
- validateHttpsOrigin(baseUrl);
345
- const baseOrigin = new URL(baseUrl).origin;
346
- const checksValue = verification.checks;
347
- if (!Array.isArray(checksValue) || checksValue.length === 0) {
348
- throw new Error("Deployment verification.checks must be a non-empty array.");
349
- }
350
- const checks = checksValue.map((entry, index) => {
351
- if (!isRecord(entry)) {
352
- throw new Error(`Deployment verification.checks[${index}] must be an object.`);
353
- }
354
- assertOnlyKeys(entry, ["path", "expectedStatus", "expectedJson"], `verification.checks[${index}]`);
355
- const path = requiredString(entry, "path");
356
- if (!path.startsWith("/") ||
357
- path.startsWith("//") ||
358
- path.includes("\\") ||
359
- /[\u0000-\u001f\u007f]/.test(path) ||
360
- new URL(path, baseUrl).origin !== baseOrigin) {
361
- throw new Error("Every deployment verification path must be an absolute URL path.");
362
- }
363
- const expectedStatus = requiredNumber(entry, "expectedStatus");
364
- if (expectedStatus < 100 || expectedStatus > 599) {
365
- throw new Error("Deployment verification expectedStatus must be an HTTP status.");
366
- }
367
- const expectedJsonValue = entry.expectedJson;
368
- let expectedJson;
369
- if (expectedJsonValue !== undefined) {
370
- if (!isRecord(expectedJsonValue)) {
371
- throw new Error("Deployment verification expectedJson must be an object.");
372
- }
373
- expectedJson = {};
374
- for (const [key, value] of Object.entries(expectedJsonValue)) {
375
- if (value !== null &&
376
- typeof value !== "string" &&
377
- typeof value !== "number" &&
378
- typeof value !== "boolean") {
379
- throw new Error("Deployment verification expectedJson values must be scalar.");
380
- }
381
- expectedJson[key] = value;
382
- }
383
- }
384
- return { path, expectedStatus, ...(expectedJson ? { expectedJson } : {}) };
385
- });
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);
386
186
  const profile = {
387
187
  schemaVersion: 1,
388
188
  driver: "kubernetes-helm",
@@ -406,11 +206,11 @@ function parseProfile(value) {
406
206
  tlsSecretName,
407
207
  },
408
208
  workloads: {
409
- api: parseWorkload(requiredRecord(workloads, "api"), "workloads.api"),
410
- 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"),
411
211
  worker: workloads.worker === null
412
212
  ? null
413
- : parseWorkload(requiredRecord(workloads, "worker"), "workloads.worker"),
213
+ : parseWorkload((0, deploy_schema_1.requiredRecord)(workloads, "worker"), "workloads.worker"),
414
214
  },
415
215
  dependencies: { postgres, redis, objectStorage },
416
216
  secrets: {
@@ -418,28 +218,19 @@ function parseProfile(value) {
418
218
  web: secrets.web === null
419
219
  ? null
420
220
  : parseNamedSecret(secrets.web, "secrets.web"),
421
- imagePull: requiredString(secrets, "imagePull"),
221
+ imagePull: (0, deploy_schema_1.requiredString)(secrets, "imagePull"),
422
222
  },
423
223
  ...(migration
424
- ? { migration: { command: parseCommand(migration.command, "migration.command") } }
224
+ ? { migration: { command: (0, deploy_schema_1.parseCommand)(migration.command, "migration.command") } }
425
225
  : {}),
426
226
  runtimeConfig: parsedConfig,
427
227
  verification: { baseUrl, checks },
428
228
  };
429
- validateDnsLabel(profile.secrets.imagePull, "secrets.imagePull");
229
+ (0, deploy_schema_1.validateDnsLabel)(profile.secrets.imagePull, "secrets.imagePull");
430
230
  return profile;
431
231
  }
432
- function profileDirectory(projectRoot) {
433
- return (0, node_path_1.join)((0, node_path_1.resolve)(projectRoot), ".podokit", "deploy");
434
- }
435
- function profilePath(projectRoot, name) {
436
- if (!PROFILE_NAME.test(name)) {
437
- throw new Error(`Invalid deployment profile name "${name}".`);
438
- }
439
- return (0, node_path_1.join)(profileDirectory(projectRoot), `${name}.json`);
440
- }
441
232
  function loadDeploymentProfile(projectRoot, name) {
442
- const path = profilePath(projectRoot, name);
233
+ const path = (0, deploy_schema_1.profilePath)(projectRoot, name);
443
234
  if (!(0, node_fs_1.existsSync)(path))
444
235
  throw new Error(`Deployment profile "${name}" does not exist at ${path}.`);
445
236
  let parsed;
@@ -452,7 +243,7 @@ function loadDeploymentProfile(projectRoot, name) {
452
243
  return parseProfile(parsed);
453
244
  }
454
245
  function listDeploymentProfiles(projectRoot) {
455
- const directory = profileDirectory(projectRoot);
246
+ const directory = (0, deploy_schema_1.profileDirectory)(projectRoot);
456
247
  if (!(0, node_fs_1.existsSync)(directory))
457
248
  return [];
458
249
  return (0, node_fs_1.readdirSync)(directory)
@@ -470,7 +261,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
470
261
  const projectName = manifest.answers.projectName;
471
262
  if (!projectName)
472
263
  throw new Error("PodoKit manifest is missing answers.projectName.");
473
- const path = profilePath(projectRoot, name);
264
+ const path = (0, deploy_schema_1.profilePath)(projectRoot, name);
474
265
  if ((0, node_fs_1.existsSync)(path))
475
266
  throw new Error(`Deployment profile "${name}" already exists.`);
476
267
  const releaseName = projectName
@@ -501,7 +292,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
501
292
  },
502
293
  release: {
503
294
  strategy: "shared-tag",
504
- tagPattern: STABLE_SEMVER_TAG_PATTERN,
295
+ tagPattern: deploy_schema_1.STABLE_SEMVER_TAG_PATTERN,
505
296
  apiRepository: `ghcr.io/example/${releaseName}-api`,
506
297
  webRepository: `ghcr.io/example/${releaseName}-web`,
507
298
  },
@@ -610,7 +401,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
610
401
  },
611
402
  };
612
403
  const validated = parseProfile(defaultProfile);
613
- (0, node_fs_1.mkdirSync)(profileDirectory(projectRoot), { recursive: true });
404
+ (0, node_fs_1.mkdirSync)((0, deploy_schema_1.profileDirectory)(projectRoot), { recursive: true });
614
405
  (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(validated, null, 2)}\n`, { flag: "wx" });
615
406
  return { name, path, profile: validated };
616
407
  }
@@ -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;