@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
@@ -0,0 +1,361 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseComposeProfile = parseComposeProfile;
4
+ exports.buildDefaultComposeProfile = buildDefaultComposeProfile;
5
+ exports.initializeComposeProfile = initializeComposeProfile;
6
+ exports.composeProfileDigest = composeProfileDigest;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const node_fs_1 = require("node:fs");
9
+ const lockfile_1 = require("./lockfile");
10
+ const deploy_schema_1 = require("./deploy-schema");
11
+ /** Decimal core count, e.g. "0.25" or "2". */
12
+ const CPU_VALUE = /^(?:0\.[0-9]{1,3}|[1-9][0-9]{0,2}(?:\.[0-9]{1,3})?)$/;
13
+ /** Compose byte suffix, e.g. "512m" or "2g". */
14
+ const MEMORY_VALUE = /^[1-9][0-9]*[kmg]$/;
15
+ /** A Compose project name: the same alphabet Compose itself accepts. */
16
+ const PROJECT_NAME = /^[a-z0-9][a-z0-9_-]*$/;
17
+ /** An absolute POSIX path on the target host, with no traversal or control bytes. */
18
+ const ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/?)+$/;
19
+ const VOLUME_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
20
+ function validateCpu(value, field) {
21
+ if (!CPU_VALUE.test(value)) {
22
+ throw new Error(`Deployment profile field "${field}" must be a decimal CPU count.`);
23
+ }
24
+ }
25
+ function validateMemory(value, field) {
26
+ if (!MEMORY_VALUE.test(value)) {
27
+ throw new Error(`Deployment profile field "${field}" must be a Compose memory quantity.`);
28
+ }
29
+ }
30
+ function validateHostPath(value, field) {
31
+ if (!ABSOLUTE_PATH.test(value) || value.includes("..") || value.endsWith("/")) {
32
+ throw new Error(`Deployment profile field "${field}" must be an absolute path on the target host.`);
33
+ }
34
+ }
35
+ function parseComposeResources(value, field) {
36
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["cpuLimit", "memoryLimit"], field);
37
+ const parsed = {
38
+ cpuLimit: (0, deploy_schema_1.requiredString)(value, "cpuLimit"),
39
+ memoryLimit: (0, deploy_schema_1.requiredString)(value, "memoryLimit"),
40
+ };
41
+ validateCpu(parsed.cpuLimit, `${field}.cpuLimit`);
42
+ validateMemory(parsed.memoryLimit, `${field}.memoryLimit`);
43
+ return parsed;
44
+ }
45
+ function parseComposeWorkload(value, field) {
46
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["replicas", "resources"], field);
47
+ return {
48
+ replicas: (0, deploy_schema_1.requiredNumber)(value, "replicas"),
49
+ resources: parseComposeResources((0, deploy_schema_1.requiredRecord)(value, "resources"), `${field}.resources`),
50
+ };
51
+ }
52
+ function parseComposeMode(value, field) {
53
+ if (value !== "managed" && value !== "external" && value !== "disabled") {
54
+ throw new Error(`Deployment profile field "${field}" must be managed, external, or disabled.`);
55
+ }
56
+ return value;
57
+ }
58
+ function parseComposeDependency(value, field, extraKeys = []) {
59
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["mode", "image", "envFile", "requiredKeys", "volume", ...extraKeys], field);
60
+ const parsed = {
61
+ mode: parseComposeMode(value.mode, `${field}.mode`),
62
+ image: (0, deploy_schema_1.requiredString)(value, "image"),
63
+ envFile: (0, deploy_schema_1.requiredString)(value, "envFile"),
64
+ requiredKeys: (0, deploy_schema_1.parseRequiredKeys)(value.requiredKeys, `${field}.requiredKeys`),
65
+ volume: (0, deploy_schema_1.requiredString)(value, "volume"),
66
+ };
67
+ (0, deploy_schema_1.validateImage)(parsed.image, `${field}.image`);
68
+ validateHostPath(parsed.envFile, `${field}.envFile`);
69
+ if (!VOLUME_NAME.test(parsed.volume)) {
70
+ throw new Error(`Deployment profile field "${field}.volume" must be a Docker volume name.`);
71
+ }
72
+ return parsed;
73
+ }
74
+ /** A project-relative POSIX path: no absolute paths, no traversal, no drift upward. */
75
+ const PROJECT_RELATIVE_PATH = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
76
+ function parseSyncProfile(value, field) {
77
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["exclude"], field);
78
+ const exclude = value.exclude;
79
+ if (!Array.isArray(exclude)) {
80
+ throw new Error(`Deployment profile field "${field}.exclude" must be a string array.`);
81
+ }
82
+ const parsed = exclude.map((entry, index) => {
83
+ if (typeof entry !== "string" ||
84
+ !PROJECT_RELATIVE_PATH.test(entry) ||
85
+ entry.split("/").includes("..")) {
86
+ throw new Error(`Deployment profile field "${field}.exclude[${index}]" must be a project-relative path.`);
87
+ }
88
+ return entry;
89
+ });
90
+ if (new Set(parsed).size !== parsed.length) {
91
+ throw new Error(`Deployment profile field "${field}.exclude" contains duplicate paths.`);
92
+ }
93
+ return { exclude: parsed.sort() };
94
+ }
95
+ function parseEnvFileProfile(value, field) {
96
+ if (!(0, deploy_schema_1.isRecord)(value))
97
+ throw new Error(`Deployment profile field "${field}" must be an object.`);
98
+ (0, deploy_schema_1.assertOnlyKeys)(value, ["path", "requiredKeys"], field);
99
+ const parsed = {
100
+ path: (0, deploy_schema_1.requiredString)(value, "path"),
101
+ requiredKeys: (0, deploy_schema_1.parseRequiredKeys)(value.requiredKeys, `${field}.requiredKeys`),
102
+ };
103
+ validateHostPath(parsed.path, `${field}.path`);
104
+ return parsed;
105
+ }
106
+ function parseComposeProfile(value) {
107
+ if (!(0, deploy_schema_1.isRecord)(value))
108
+ throw new Error("Deployment profile must be a JSON object.");
109
+ (0, deploy_schema_1.assertOnlyKeys)(value, [
110
+ "schemaVersion",
111
+ "driver",
112
+ "target",
113
+ "release",
114
+ "exposure",
115
+ "workloads",
116
+ "dependencies",
117
+ "secrets",
118
+ "migration",
119
+ "sync",
120
+ "runtimeConfig",
121
+ "verification",
122
+ ], "root");
123
+ if (value.schemaVersion !== 1) {
124
+ throw new Error(`Unsupported deployment profile schemaVersion: ${String(value.schemaVersion)}`);
125
+ }
126
+ if (value.driver !== "docker-compose") {
127
+ throw new Error(`Unsupported deployment driver: ${String(value.driver)}`);
128
+ }
129
+ const target = (0, deploy_schema_1.requiredRecord)(value, "target");
130
+ const exposure = (0, deploy_schema_1.requiredRecord)(value, "exposure");
131
+ const workloads = (0, deploy_schema_1.requiredRecord)(value, "workloads");
132
+ const dependencies = (0, deploy_schema_1.requiredRecord)(value, "dependencies");
133
+ const secrets = (0, deploy_schema_1.requiredRecord)(value, "secrets");
134
+ const migration = value.migration === undefined ? undefined : (0, deploy_schema_1.requiredRecord)(value, "migration");
135
+ const sync = value.sync === undefined ? undefined : (0, deploy_schema_1.requiredRecord)(value, "sync");
136
+ (0, deploy_schema_1.assertOnlyKeys)(target, ["context", "endpointFingerprint", "project"], "target");
137
+ (0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "bindAddress", "port"], "exposure");
138
+ (0, deploy_schema_1.assertOnlyKeys)(workloads, ["api", "web", "worker"], "workloads");
139
+ (0, deploy_schema_1.assertOnlyKeys)(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
140
+ (0, deploy_schema_1.assertOnlyKeys)(secrets, ["api", "web", "registryLogin"], "secrets");
141
+ if (migration)
142
+ (0, deploy_schema_1.assertOnlyKeys)(migration, ["command"], "migration");
143
+ const context = (0, deploy_schema_1.requiredString)(target, "context");
144
+ const endpointFingerprint = (0, deploy_schema_1.requiredString)(target, "endpointFingerprint");
145
+ if (!/^sha256:[a-f0-9]{64}$/.test(endpointFingerprint)) {
146
+ throw new Error("Deployment target.endpointFingerprint must be a sha256 fingerprint.");
147
+ }
148
+ const project = (0, deploy_schema_1.requiredString)(target, "project");
149
+ if (!PROJECT_NAME.test(project) || project.length > 40) {
150
+ throw new Error("Deployment target.project must be a Compose project name of at most 40 characters.");
151
+ }
152
+ if (exposure.mode !== "publishedPort") {
153
+ throw new Error("Deployment exposure.mode must be publishedPort for the docker-compose driver.");
154
+ }
155
+ const host = (0, deploy_schema_1.requiredString)(exposure, "host");
156
+ (0, deploy_schema_1.validateDnsSubdomain)(host, "exposure.host");
157
+ const bindAddress = (0, deploy_schema_1.requiredString)(exposure, "bindAddress");
158
+ if (!/^(?:\d{1,3}\.){3}\d{1,3}$/.test(bindAddress) || bindAddress.split(".").some((o) => Number(o) > 255)) {
159
+ throw new Error("Deployment exposure.bindAddress must be an IPv4 address.");
160
+ }
161
+ const port = (0, deploy_schema_1.requiredNumber)(exposure, "port");
162
+ if (port > 65535)
163
+ throw new Error("Deployment exposure.port must be a TCP port.");
164
+ const web = parseComposeWorkload((0, deploy_schema_1.requiredRecord)(workloads, "web"), "workloads.web");
165
+ // Compose has no load balancer. Two replicas of the service that publishes the
166
+ // host port means the second one cannot bind it, and the deployment fails half
167
+ // way through the rollout with "port is already allocated". Scaling the public
168
+ // tier is what the kubernetes-helm driver is for.
169
+ if (web.replicas > 1) {
170
+ throw new Error("Deployment workloads.web.replicas must be 1: the web service publishes a host port, and Compose cannot share one across replicas.");
171
+ }
172
+ const objectStorageRecord = (0, deploy_schema_1.requiredRecord)(dependencies, "objectStorage");
173
+ const objectStorage = {
174
+ ...parseComposeDependency(objectStorageRecord, "dependencies.objectStorage", [
175
+ "bucket",
176
+ "clientImage",
177
+ ]),
178
+ bucket: (0, deploy_schema_1.requiredString)(objectStorageRecord, "bucket"),
179
+ clientImage: (0, deploy_schema_1.requiredString)(objectStorageRecord, "clientImage"),
180
+ };
181
+ (0, deploy_schema_1.validateDnsLabel)(objectStorage.bucket, "dependencies.objectStorage.bucket");
182
+ (0, deploy_schema_1.validateImage)(objectStorage.clientImage, "dependencies.objectStorage.clientImage");
183
+ if (typeof secrets.registryLogin !== "boolean") {
184
+ throw new Error("Deployment secrets.registryLogin must be a boolean.");
185
+ }
186
+ const profile = {
187
+ schemaVersion: 1,
188
+ driver: "docker-compose",
189
+ target: { context, endpointFingerprint, project },
190
+ release: (0, deploy_schema_1.parseReleaseProfile)((0, deploy_schema_1.requiredRecord)(value, "release")),
191
+ exposure: { mode: "publishedPort", host, bindAddress, port },
192
+ workloads: {
193
+ api: parseComposeWorkload((0, deploy_schema_1.requiredRecord)(workloads, "api"), "workloads.api"),
194
+ web,
195
+ worker: workloads.worker === null
196
+ ? null
197
+ : parseComposeWorkload((0, deploy_schema_1.requiredRecord)(workloads, "worker"), "workloads.worker"),
198
+ },
199
+ dependencies: {
200
+ postgres: parseComposeDependency((0, deploy_schema_1.requiredRecord)(dependencies, "postgres"), "dependencies.postgres"),
201
+ redis: parseComposeDependency((0, deploy_schema_1.requiredRecord)(dependencies, "redis"), "dependencies.redis"),
202
+ objectStorage,
203
+ },
204
+ secrets: {
205
+ api: parseEnvFileProfile(secrets.api, "secrets.api"),
206
+ web: secrets.web === null ? null : parseEnvFileProfile(secrets.web, "secrets.web"),
207
+ registryLogin: secrets.registryLogin,
208
+ },
209
+ ...(migration
210
+ ? { migration: { command: (0, deploy_schema_1.parseCommand)(migration.command, "migration.command") } }
211
+ : {}),
212
+ ...(sync ? { sync: parseSyncProfile(sync, "sync") } : {}),
213
+ runtimeConfig: (0, deploy_schema_1.parseRuntimeConfig)((0, deploy_schema_1.requiredRecord)(value, "runtimeConfig")),
214
+ verification: (0, deploy_schema_1.parseVerification)((0, deploy_schema_1.requiredRecord)(value, "verification")),
215
+ };
216
+ return profile;
217
+ }
218
+ function buildDefaultComposeProfile(projectName, options) {
219
+ const project = projectName
220
+ .toLowerCase()
221
+ .replace(/[^a-z0-9-]+/g, "-")
222
+ .replace(/^-+|-+$/g, "")
223
+ .slice(0, 40);
224
+ if (!project || !deploy_schema_1.DNS_LABEL.test(project)) {
225
+ throw new Error("Project name cannot be converted to a Compose project name.");
226
+ }
227
+ const host = options.host ?? "app.example.com";
228
+ const secretsDirectory = options.secretsDirectory ?? `/etc/podokit/${project}`;
229
+ validateHostPath(secretsDirectory, "secretsDirectory");
230
+ return {
231
+ schemaVersion: 1,
232
+ driver: "docker-compose",
233
+ target: {
234
+ context: options.context,
235
+ endpointFingerprint: options.endpointFingerprint,
236
+ project,
237
+ },
238
+ release: {
239
+ strategy: "shared-tag",
240
+ tagPattern: deploy_schema_1.STABLE_SEMVER_TAG_PATTERN,
241
+ apiRepository: `ghcr.io/example/${project}-api`,
242
+ webRepository: `ghcr.io/example/${project}-web`,
243
+ },
244
+ exposure: {
245
+ mode: "publishedPort",
246
+ host,
247
+ // Loopback by default: a reverse proxy on the same host is the expected front
248
+ // door, and binding every interface would publish the app straight to the
249
+ // network before TLS exists.
250
+ bindAddress: "127.0.0.1",
251
+ port: 8080,
252
+ },
253
+ workloads: {
254
+ api: { replicas: 2, resources: { cpuLimit: "1", memoryLimit: "1g" } },
255
+ // One, not two: this is the service that publishes the host port.
256
+ web: { replicas: 1, resources: { cpuLimit: "0.5", memoryLimit: "1g" } },
257
+ worker: null,
258
+ },
259
+ dependencies: {
260
+ postgres: {
261
+ mode: "managed",
262
+ image: "postgres:16.10-alpine@sha256:029660641a0cfc575b14f336ba448fb8a75fd595d42e1fa316b9fb4378742297",
263
+ envFile: `${secretsDirectory}/postgres.env`,
264
+ requiredKeys: ["POSTGRES_DB", "POSTGRES_PASSWORD", "POSTGRES_USER"],
265
+ volume: `${project}-postgres`,
266
+ },
267
+ redis: {
268
+ mode: "disabled",
269
+ image: "redis:7.4.5-alpine",
270
+ envFile: `${secretsDirectory}/redis.env`,
271
+ requiredKeys: ["REDIS_PASSWORD"],
272
+ volume: `${project}-redis`,
273
+ },
274
+ objectStorage: {
275
+ mode: "disabled",
276
+ image: "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z",
277
+ clientImage: "quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z",
278
+ envFile: `${secretsDirectory}/object-storage.env`,
279
+ requiredKeys: [
280
+ "MINIO_ROOT_PASSWORD",
281
+ "MINIO_ROOT_USER",
282
+ "S3_ACCESS_KEY_ID",
283
+ "S3_SECRET_ACCESS_KEY",
284
+ ],
285
+ volume: `${project}-object-storage`,
286
+ bucket: "app",
287
+ },
288
+ },
289
+ secrets: {
290
+ api: { path: `${secretsDirectory}/api.env`, requiredKeys: [] },
291
+ web: null,
292
+ registryLogin: true,
293
+ },
294
+ runtimeConfig: {
295
+ NODE_ENV: "production",
296
+ CORS_ORIGIN: `https://${host}`,
297
+ ADDRESS_HEADER: "x-forwarded-for",
298
+ XFF_DEPTH: "1",
299
+ },
300
+ verification: {
301
+ baseUrl: `https://${host}`,
302
+ checks: [
303
+ { path: "/", expectedStatus: 200 },
304
+ { path: "/api/health", expectedStatus: 200, expectedJson: { status: "ok" } },
305
+ { path: "/api/health/ready", expectedStatus: 200, expectedJson: { status: "ready" } },
306
+ ],
307
+ },
308
+ };
309
+ }
310
+ function initializeComposeProfile(projectRoot, name, options) {
311
+ const manifest = (0, lockfile_1.readManifest)(projectRoot);
312
+ if (!manifest)
313
+ throw new Error("Not a PodoKit project: .podokit/manifest.json is missing.");
314
+ const projectName = manifest.answers.projectName;
315
+ if (!projectName)
316
+ throw new Error("PodoKit manifest is missing answers.projectName.");
317
+ const path = (0, deploy_schema_1.profilePath)(projectRoot, name);
318
+ if ((0, node_fs_1.existsSync)(path))
319
+ throw new Error(`Deployment profile "${name}" already exists.`);
320
+ const draft = buildDefaultComposeProfile(projectName, options);
321
+ const modules = new Set(manifest.modules.map((module) => module.name));
322
+ const usesRedis = [...modules].some((module) => ["redis", "bullmq", "job-progress", "rate-limit", "sse"].includes(module));
323
+ draft.dependencies.redis.mode = usesRedis ? "managed" : "disabled";
324
+ draft.dependencies.objectStorage.mode = modules.has("object-storage-s3")
325
+ ? "managed"
326
+ : "disabled";
327
+ if (modules.has("bullmq")) {
328
+ draft.workloads.worker = { replicas: 1, resources: { cpuLimit: "0.5", memoryLimit: "512m" } };
329
+ }
330
+ const usesAuth = modules.has("auth");
331
+ draft.secrets.api.requiredKeys = [
332
+ ...(usesAuth ? ["BETTER_AUTH_SECRET"] : []),
333
+ ...(modules.has("api-key-auth") ? ["API_KEYS"] : []),
334
+ ].sort();
335
+ const host = draft.exposure.host;
336
+ draft.runtimeConfig = {
337
+ ...draft.runtimeConfig,
338
+ ...(usesAuth ? { BETTER_AUTH_URL: `https://${host}` } : {}),
339
+ ...(modules.has("sse") && usesRedis ? { SSE_TRANSPORT: "redis" } : {}),
340
+ ...(modules.has("rate-limit")
341
+ ? {
342
+ RATE_LIMIT_TTL: "60",
343
+ RATE_LIMIT_MAX: "300",
344
+ RATE_LIMIT_AUTH_TTL: "60",
345
+ RATE_LIMIT_AUTH_MAX: "20",
346
+ RATE_LIMIT_RUNTIME_MAX: "1000",
347
+ RATE_LIMIT_TRUSTED_PROXY_HOPS: "1",
348
+ RATE_LIMIT_PROXY_HEADER: "x-forwarded-for",
349
+ RATE_LIMIT_STORAGE_TIMEOUT_MS: "1000",
350
+ RATE_LIMIT_UNAVAILABLE_RETRY_AFTER: "1",
351
+ }
352
+ : {}),
353
+ };
354
+ const validated = parseComposeProfile(draft);
355
+ (0, node_fs_1.mkdirSync)((0, deploy_schema_1.profileDirectory)(projectRoot), { recursive: true });
356
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(validated, null, 2)}\n`, { flag: "wx" });
357
+ return { name, path, profile: validated };
358
+ }
359
+ function composeProfileDigest(profile) {
360
+ return `sha256:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify(profile)).digest("hex")}`;
361
+ }
@@ -0,0 +1,48 @@
1
+ import type { DockerComposeProfileV1 } from "./deploy-compose-profile";
2
+ /**
3
+ * Compose project rendering.
4
+ *
5
+ * The Kubernetes driver renders Helm charts; this one renders a single Compose file
6
+ * plus a one-shot migration file. Both are written into `.podokit/runtime/deploy/`
7
+ * and both are pure functions of the profile, the release, and the resolved image
8
+ * digests — so the same inputs always produce the same bytes, which is what lets the
9
+ * plan hash mean anything.
10
+ */
11
+ export interface ComposeRuntime {
12
+ root: string;
13
+ composeFile: string;
14
+ migrationFile: string;
15
+ composeDocument: string;
16
+ migrationDocument: string;
17
+ }
18
+ export interface ComposeImages {
19
+ api: string;
20
+ web: string;
21
+ postgres: string | null;
22
+ redis: string | null;
23
+ objectStorage: string | null;
24
+ objectStorageClient: string | null;
25
+ }
26
+ export declare function serviceName(profile: DockerComposeProfileV1, role: string): string;
27
+ /**
28
+ * Configuration PodoKit derives rather than accepts, so a profile cannot point the
29
+ * app at the wrong service or port.
30
+ */
31
+ export declare function derivedRuntimeConfig(profile: DockerComposeProfileV1): Record<string, string>;
32
+ /**
33
+ * Identity of everything the containers read but Compose cannot see a change in.
34
+ *
35
+ * Compose recreates a container when its definition changes; it has no idea an env
36
+ * file on the host was edited. Stamping this digest as a label makes an edited
37
+ * secret or a changed runtime config a definition change, so `up` actually rolls
38
+ * the containers instead of leaving stale values running.
39
+ */
40
+ export declare function rolloutStateDigest(profile: DockerComposeProfileV1, secretIdentities: Record<string, string>): string;
41
+ export declare function renderComposeDocument(profile: DockerComposeProfileV1, release: string, images: ComposeImages, stateDigest: string): string;
42
+ /**
43
+ * The migration runs as its own Compose project so it can be started, waited on,
44
+ * and removed without touching the running application definition.
45
+ */
46
+ export declare function renderMigrationDocument(profile: DockerComposeProfileV1, release: string, images: ComposeImages): string;
47
+ export declare function defaultComposeImages(profile: DockerComposeProfileV1, release: string): ComposeImages;
48
+ export declare function renderComposeDeployment(projectRoot: string, profileName: string, profile: DockerComposeProfileV1, release: string, resolvedImages?: ComposeImages, stateDigest?: string): ComposeRuntime;