@podosoft/podokit 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add.d.ts +13 -2
- package/dist/add.js +135 -20
- package/dist/api-paths.d.ts +34 -0
- package/dist/api-paths.js +103 -0
- package/dist/deploy-compose-profile.d.ts +4 -0
- package/dist/deploy-compose-profile.js +13 -2
- package/dist/deploy-compose-render.d.ts +1 -1
- package/dist/deploy-compose-render.js +23 -7
- package/dist/deploy-compose.js +2 -1
- package/dist/deploy-profile.d.ts +3 -0
- package/dist/deploy-profile.js +12 -4
- package/dist/deploy-render.js +54 -8
- package/dist/deploy.js +21 -1
- package/dist/dev.d.ts +14 -0
- package/dist/dev.js +152 -10
- package/dist/index.js +23 -2
- package/dist/lib.d.ts +4 -2
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +6 -1
- package/dist/lockfile.js +26 -6
- package/dist/module-api-paths.d.ts +52 -0
- package/dist/module-api-paths.js +189 -0
- package/dist/remove.d.ts +3 -0
- package/dist/remove.js +12 -3
- package/dist/templates/fullstack/AGENTS.md +17 -2
- package/dist/templates/fullstack/README.md +19 -5
- package/dist/templates/fullstack/apps/api/package.json +1 -1
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
- package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -0
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +20 -1
- package/dist/templates/fullstack/apps/api/src/app.ts +11 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.spec.ts +48 -2
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.ts +45 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +46 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +48 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +1 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +196 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +181 -2
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +8 -1
- package/dist/templates/fullstack/apps/api/src/database/database.ts +29 -10
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.spec.ts +188 -0
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.ts +151 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +28 -4
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +10 -2
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -1
- package/dist/templates/fullstack/apps/web/components.json +3 -1
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/index.ts +7 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/textarea.svelte +22 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +276 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +194 -9
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +15 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-headers.ts +12 -0
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-elysia-endpoint/SKILL.md +5 -0
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +14 -1
- package/dist/templates/fullstack/dot-devcontainer/devcontainer.json +1 -0
- package/dist/templates/fullstack/{dot-env.docker → dot-env.docker.example} +7 -4
- package/dist/templates/fullstack/dot-env.example +10 -0
- package/dist/templates/fullstack/dot-gitignore +1 -0
- package/dist/templates/fullstack/dot-podokit/dev.json +2 -1
- package/dist/templates/fullstack/infra/k3s/README.md +3 -1
- package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +27 -9
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +59 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +81 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/asset-paths.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.test.ts +125 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.ts +107 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +13 -19
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +15 -12
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/object-response.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +14 -15
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +13 -8
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +14 -3
- package/dist/templates/modules/admin-dashboard/module.manifest.json +4 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +53 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.spec.ts +26 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +17 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +8 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.spec.ts +114 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.ts +50 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +4 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +11 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance-command.ts +5 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance.ts +14 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.spec.ts +136 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.ts +40 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +16 -37
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -2
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +121 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +108 -10
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +3 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +31 -3
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +2 -4
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.spec.ts +224 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +124 -35
- package/dist/templates/modules/events-redis/module.manifest.json +4 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.routes.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.spec.ts +636 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.ts +673 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +10 -4
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +37 -8
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-config.ts +99 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/migrations/1792100000000-InitFileUploadSessions.ts +49 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.test.ts +456 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.ts +356 -0
- package/dist/templates/modules/file-upload/files/tests/api/chunked-uploads.api.spec.ts +147 -0
- package/dist/templates/modules/file-upload/module.manifest.json +34 -6
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +1 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +23 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +30 -20
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +20 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +79 -12
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +16 -1
- package/dist/templates/modules/object-storage-local/module.manifest.json +11 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.spec.ts +115 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.ts +171 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +222 -9
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +55 -19
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.spec.ts +71 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.ts +98 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +6 -3
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +7 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +187 -44
- package/dist/templates/modules/object-storage-s3/module.manifest.json +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +11 -22
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +113 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +122 -0
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +24 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +7 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +14 -1
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +9 -12
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +16 -4
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +233 -5
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +134 -32
- package/dist/templates/modules/redis/module.manifest.json +4 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +11 -4
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +48 -1
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +15 -2
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +68 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +25 -1
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
- package/dist/templates/modules/sse/module.manifest.json +3 -1
- package/dist/templates/todo/AGENTS.md +9 -1
- package/dist/templates/todo/README.md +18 -5
- package/dist/update.d.ts +22 -0
- package/dist/update.js +68 -8
- package/package.json +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-validation.ts +0 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/skip-schema-validation.ts +0 -5
package/dist/add.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type TemplateVars } from "@podosoft/podokit-template-engine";
|
|
2
|
-
import { type ProviderCapability } from "@podosoft/podokit-runtime";
|
|
2
|
+
import { type ProviderCapability, type ProviderSelections } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { type ApiPathResult } from "./module-api-paths";
|
|
3
4
|
interface Injection {
|
|
4
5
|
file: string;
|
|
5
6
|
marker: string;
|
|
@@ -9,7 +10,7 @@ interface Injection {
|
|
|
9
10
|
}
|
|
10
11
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
11
12
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
12
|
-
export declare const SUPPORTED_MANIFEST_VERSION =
|
|
13
|
+
export declare const SUPPORTED_MANIFEST_VERSION = 4;
|
|
13
14
|
export interface ModuleRuntimeConstraints {
|
|
14
15
|
/** Maximum safe API replicas for process-local providers. */
|
|
15
16
|
maxApiReplicas?: number;
|
|
@@ -79,6 +80,12 @@ export interface ModuleManifest {
|
|
|
79
80
|
requiredProjectFiles?: string[];
|
|
80
81
|
/** Stable source contracts that must be present in generated baseline files. */
|
|
81
82
|
requiredProjectContents?: Record<string, string[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Path prefixes under /api/ that the gateway must send straight to the API,
|
|
85
|
+
* such as routes taking bodies above the web server's BODY_SIZE_LIMIT. Merged
|
|
86
|
+
* into `.podokit/dev.json` and every deployment profile.
|
|
87
|
+
*/
|
|
88
|
+
apiPaths?: string[];
|
|
82
89
|
}
|
|
83
90
|
export interface PackageOverlay {
|
|
84
91
|
dependencies?: Record<string, string>;
|
|
@@ -111,6 +118,8 @@ export interface AddResult {
|
|
|
111
118
|
adopted: string[];
|
|
112
119
|
/** Files shipped, merged, or injected while applying the module. */
|
|
113
120
|
touched: string[];
|
|
121
|
+
/** Direct API paths merged into `.podokit/dev.json` and deployment profiles. */
|
|
122
|
+
apiPaths: ApiPathResult;
|
|
114
123
|
}
|
|
115
124
|
export interface ResolvedModule {
|
|
116
125
|
dir: string;
|
|
@@ -127,6 +136,8 @@ export declare function resolveModuleDir(name: string, modulesDir: string, proje
|
|
|
127
136
|
/** Resolve a module and capture the package identity needed to replay external
|
|
128
137
|
* modules during `podo update`. */
|
|
129
138
|
export declare function resolveModule(name: string, modulesDir: string, projectRoot: string): ResolvedModule | null;
|
|
139
|
+
/** Name of the module that implements the selected provider for a capability. */
|
|
140
|
+
export declare function selectedProviderModule(modulesDir: string, projectRoot: string, providers: ProviderSelections, capability: ProviderCapability): string;
|
|
130
141
|
/** List modules available to a project: the bundled ones under `modulesDir` plus,
|
|
131
142
|
* when `projectRoot` is given, any `@podosoft/podokit-module-*` packages installed
|
|
132
143
|
* in it. Bundled names take precedence on a clash. */
|
package/dist/add.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.SUPPORTED_MANIFEST_VERSION = void 0;
|
|
|
4
4
|
exports.modulePackageOverlays = modulePackageOverlays;
|
|
5
5
|
exports.resolveModuleDir = resolveModuleDir;
|
|
6
6
|
exports.resolveModule = resolveModule;
|
|
7
|
+
exports.selectedProviderModule = selectedProviderModule;
|
|
7
8
|
exports.listModules = listModules;
|
|
8
9
|
exports.readModuleManifest = readModuleManifest;
|
|
9
10
|
exports.addModule = addModule;
|
|
@@ -13,9 +14,11 @@ const node_module_1 = require("node:module");
|
|
|
13
14
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
14
15
|
const podokit_runtime_1 = require("@podosoft/podokit-runtime");
|
|
15
16
|
const lockfile_1 = require("./lockfile");
|
|
17
|
+
const api_paths_1 = require("./api-paths");
|
|
18
|
+
const module_api_paths_1 = require("./module-api-paths");
|
|
16
19
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
17
20
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
18
|
-
exports.SUPPORTED_MANIFEST_VERSION =
|
|
21
|
+
exports.SUPPORTED_MANIFEST_VERSION = 4;
|
|
19
22
|
/** Return the legacy targetApp package fields plus any additional app overlays. */
|
|
20
23
|
function modulePackageOverlays(manifest) {
|
|
21
24
|
const overlays = new Map();
|
|
@@ -100,17 +103,21 @@ function assertManifestVersion(name, manifest) {
|
|
|
100
103
|
throw new Error(`Module "${name}" declares invalid provider ${capability}=${String(provider)}.`);
|
|
101
104
|
}
|
|
102
105
|
}
|
|
106
|
+
try {
|
|
107
|
+
(0, api_paths_1.parseApiPaths)(manifest.apiPaths ?? [], "apiPaths", []);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new Error(`Module "${name}" declares invalid apiPaths. ${error.message}`);
|
|
111
|
+
}
|
|
103
112
|
const maxApiReplicas = manifest.runtimeConstraints?.maxApiReplicas;
|
|
104
113
|
if (maxApiReplicas !== undefined
|
|
105
114
|
&& (!Number.isSafeInteger(maxApiReplicas) || maxApiReplicas < 1)) {
|
|
106
115
|
throw new Error(`Module "${name}" declares an invalid maxApiReplicas constraint.`);
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new Error("Provider capabilities require a PodoKit project manifest.");
|
|
113
|
-
const selected = project.providers[capability];
|
|
118
|
+
/** Name of the module that implements the selected provider for a capability. */
|
|
119
|
+
function selectedProviderModule(modulesDir, projectRoot, providers, capability) {
|
|
120
|
+
const selected = providers[capability];
|
|
114
121
|
for (const available of listModules(modulesDir, projectRoot)) {
|
|
115
122
|
const resolved = resolveModule(available.name, modulesDir, projectRoot);
|
|
116
123
|
if (!resolved)
|
|
@@ -122,6 +129,12 @@ function providerModuleName(projectRoot, modulesDir, capability) {
|
|
|
122
129
|
throw new Error(`No module provides the selected ${capability} provider "${selected}". ` +
|
|
123
130
|
`Choose another provider with podo provider set ${capability} <provider> --apply.`);
|
|
124
131
|
}
|
|
132
|
+
function providerModuleName(projectRoot, modulesDir, capability) {
|
|
133
|
+
const project = (0, lockfile_1.readManifest)(projectRoot);
|
|
134
|
+
if (!project)
|
|
135
|
+
throw new Error("Provider capabilities require a PodoKit project manifest.");
|
|
136
|
+
return selectedProviderModule(modulesDir, projectRoot, project.providers, capability);
|
|
137
|
+
}
|
|
125
138
|
function requiredModules(projectRoot, modulesDir, manifest) {
|
|
126
139
|
const capabilities = (manifest.requiresCapabilities ?? []).map((capability) => providerModuleName(projectRoot, modulesDir, capability));
|
|
127
140
|
return [...new Set([...(manifest.requires ?? []), ...capabilities])];
|
|
@@ -217,14 +230,24 @@ function projectName(projectRoot) {
|
|
|
217
230
|
const pkg = readJson((0, node_path_1.join)(projectRoot, "package.json"));
|
|
218
231
|
return typeof pkg.name === "string" ? pkg.name : "app";
|
|
219
232
|
}
|
|
220
|
-
function appendEnv(projectRoot, lines) {
|
|
233
|
+
function appendEnv(projectRoot, lines, anchors = []) {
|
|
221
234
|
const file = (0, node_path_1.join)(projectRoot, ".env.example");
|
|
222
235
|
if (!(0, node_fs_1.existsSync)(file))
|
|
223
236
|
return;
|
|
224
237
|
const current = (0, node_fs_1.readFileSync)(file, "utf8");
|
|
225
|
-
const
|
|
238
|
+
const existing = current.split("\n");
|
|
239
|
+
const missing = lines.filter((line) => !existing.includes(line));
|
|
226
240
|
if (missing.length === 0)
|
|
227
241
|
return;
|
|
242
|
+
// Each module appends its own blank-line separated block. Keep that shape
|
|
243
|
+
// when the block belongs in front of a displaced module's block.
|
|
244
|
+
const wanted = new Set(anchors);
|
|
245
|
+
const index = existing.findIndex((line, position) => position > 0 && existing[position - 1] === "" && wanted.has(line));
|
|
246
|
+
if (index !== -1) {
|
|
247
|
+
existing.splice(index, 0, ...missing, "");
|
|
248
|
+
(0, node_fs_1.writeFileSync)(file, existing.join("\n"));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
228
251
|
const separator = current.endsWith("\n") ? "" : "\n";
|
|
229
252
|
(0, node_fs_1.writeFileSync)(file, `${current}${separator}\n${missing.join("\n")}\n`);
|
|
230
253
|
}
|
|
@@ -246,6 +269,91 @@ function isApplied(projectRoot, modulesDir, module) {
|
|
|
246
269
|
}
|
|
247
270
|
return false;
|
|
248
271
|
}
|
|
272
|
+
function injectionKey(file, marker) {
|
|
273
|
+
return `${file}\n${marker}`;
|
|
274
|
+
}
|
|
275
|
+
/** Modules `applyModule` would newly apply, in application order. */
|
|
276
|
+
function plannedModules(projectRoot, module, modulesDir, planned = new Set(), order = []) {
|
|
277
|
+
planned.add(module);
|
|
278
|
+
const resolved = resolveModule(module, modulesDir, projectRoot);
|
|
279
|
+
if (!resolved)
|
|
280
|
+
return order;
|
|
281
|
+
for (const required of requiredModules(projectRoot, modulesDir, readModuleManifest(resolved.dir))) {
|
|
282
|
+
if (planned.has(required) || isApplied(projectRoot, modulesDir, required))
|
|
283
|
+
continue;
|
|
284
|
+
plannedModules(projectRoot, required, modulesDir, planned, order);
|
|
285
|
+
}
|
|
286
|
+
order.push(module);
|
|
287
|
+
return order;
|
|
288
|
+
}
|
|
289
|
+
function modulePlacement(projectRoot, module, modulesDir) {
|
|
290
|
+
const project = (0, lockfile_1.readManifest)(projectRoot);
|
|
291
|
+
if (!project)
|
|
292
|
+
return undefined;
|
|
293
|
+
const installed = [...project.modules].sort((a, b) => a.order - b.order);
|
|
294
|
+
const installedNames = new Set(installed.map((entry) => entry.name));
|
|
295
|
+
const fresh = new Set(plannedModules(projectRoot, module, modulesDir).filter((name) => !installedNames.has(name)));
|
|
296
|
+
if (!fresh.size)
|
|
297
|
+
return undefined;
|
|
298
|
+
const provided = new Set();
|
|
299
|
+
for (const name of fresh) {
|
|
300
|
+
const resolved = resolveModule(name, modulesDir, projectRoot);
|
|
301
|
+
if (!resolved)
|
|
302
|
+
continue;
|
|
303
|
+
for (const capability of Object.keys(readModuleManifest(resolved.dir).provides ?? {})) {
|
|
304
|
+
provided.add(capability);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const vars = {
|
|
308
|
+
projectName: projectName(projectRoot),
|
|
309
|
+
...(0, lockfile_1.manifestTemplateVars)(project),
|
|
310
|
+
};
|
|
311
|
+
const manifests = installed.map((entry) => {
|
|
312
|
+
const resolved = resolveModule(entry.packageName ?? entry.name, modulesDir, projectRoot);
|
|
313
|
+
return resolved ? readModuleManifest(resolved.dir, vars) : undefined;
|
|
314
|
+
});
|
|
315
|
+
const first = manifests.findIndex((manifest) => manifest !== undefined && ((manifest.requires ?? []).some((name) => fresh.has(name))
|
|
316
|
+
|| (manifest.requiresCapabilities ?? []).some((capability) => provided.has(capability))));
|
|
317
|
+
if (first === -1)
|
|
318
|
+
return undefined;
|
|
319
|
+
const injections = new Map();
|
|
320
|
+
const env = [];
|
|
321
|
+
for (const manifest of manifests.slice(first)) {
|
|
322
|
+
if (!manifest)
|
|
323
|
+
continue;
|
|
324
|
+
for (const injection of manifest.inject ?? []) {
|
|
325
|
+
const key = injectionKey(injection.file, injection.marker);
|
|
326
|
+
injections.set(key, [...(injections.get(key) ?? []), injection.text]);
|
|
327
|
+
}
|
|
328
|
+
env.push(...(manifest.env ?? []));
|
|
329
|
+
}
|
|
330
|
+
return { before: installed[first].name, injections, env };
|
|
331
|
+
}
|
|
332
|
+
/** Insert wiring at its marker, or in front of the first displaced module's wiring. */
|
|
333
|
+
function injectAt(content, injection, anchors = []) {
|
|
334
|
+
if (!anchors.length || content.includes(injection.text)) {
|
|
335
|
+
return (0, podokit_template_engine_1.insertAtMarker)(content, injection.marker, injection.text);
|
|
336
|
+
}
|
|
337
|
+
const lines = content.split("\n");
|
|
338
|
+
const end = lines.findIndex((line) => line.includes(injection.marker));
|
|
339
|
+
if (end === -1)
|
|
340
|
+
throw new Error(`Marker not found: ${injection.marker}`);
|
|
341
|
+
const beginMarker = injection.marker.replace("podokit:end:", "podokit:begin:");
|
|
342
|
+
const begin = beginMarker === injection.marker
|
|
343
|
+
? -1
|
|
344
|
+
: lines.slice(0, end).findIndex((line) => line.includes(beginMarker));
|
|
345
|
+
const wanted = new Set(anchors.map((text) => text.split("\n")[0].trim()));
|
|
346
|
+
let index = end;
|
|
347
|
+
for (let line = begin + 1; line < end; line += 1) {
|
|
348
|
+
if (wanted.has(lines[line].trim())) {
|
|
349
|
+
index = line;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const indent = lines[end].match(/^\s*/)?.[0] ?? "";
|
|
354
|
+
lines.splice(index, 0, `${indent}${injection.text}`);
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
249
357
|
/**
|
|
250
358
|
* Apply a module to an existing generated project: overlay files, merge the
|
|
251
359
|
* target app's package.json dependencies and scripts, append env example lines,
|
|
@@ -262,16 +370,21 @@ function addModule(options) {
|
|
|
262
370
|
})
|
|
263
371
|
.map(([path]) => path)
|
|
264
372
|
: [];
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
373
|
+
const placement = modulePlacement(options.projectRoot, options.module, options.modulesDir);
|
|
374
|
+
const applied = applyModule(options.projectRoot, options.module, options.modulesDir, new Set(), options.adopt ?? false, placement);
|
|
375
|
+
if (options.adopt && applied.adopted.length) {
|
|
376
|
+
releaseOwnedPaths(options.projectRoot, applied.adopted);
|
|
268
377
|
}
|
|
269
378
|
// Record the module (and any auto-added requirements), fold in the modules'
|
|
270
379
|
// declared ownedGlobs, and refresh the lock.
|
|
271
|
-
const records = [...
|
|
380
|
+
const records = [...applied.added, applied.module]
|
|
272
381
|
.map((name) => resolveModule(name, options.modulesDir, options.projectRoot))
|
|
273
382
|
.filter((module) => module !== null)
|
|
274
383
|
.map(moduleRecord);
|
|
384
|
+
const result = {
|
|
385
|
+
...applied,
|
|
386
|
+
apiPaths: (0, module_api_paths_1.addApiPaths)(options.projectRoot, records.flatMap((record) => (record.appliedApiPaths ?? []).map((path) => ({ module: record.name, path })))),
|
|
387
|
+
};
|
|
275
388
|
(0, lockfile_1.recordModules)(options.projectRoot, records, options.podokitVersion, result.ownedGlobs, previousLock
|
|
276
389
|
? {
|
|
277
390
|
previous: previousLock,
|
|
@@ -279,10 +392,10 @@ function addModule(options) {
|
|
|
279
392
|
modulePaths: [...new Set(result.touched)],
|
|
280
393
|
adoptedPaths: result.adopted,
|
|
281
394
|
}
|
|
282
|
-
: undefined, result.managedOverrides);
|
|
395
|
+
: undefined, result.managedOverrides, placement?.before);
|
|
283
396
|
return result;
|
|
284
397
|
}
|
|
285
|
-
function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
398
|
+
function applyModule(projectRoot, module, modulesDir, applied, adopt, placement) {
|
|
286
399
|
const resolved = resolveModule(module, modulesDir, projectRoot);
|
|
287
400
|
if (!resolved) {
|
|
288
401
|
const available = listModules(modulesDir, projectRoot).map((m) => m.name);
|
|
@@ -318,8 +431,8 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
318
431
|
for (const required of requiredModules(projectRoot, modulesDir, manifest)) {
|
|
319
432
|
if (applied.has(required) || isApplied(projectRoot, modulesDir, required))
|
|
320
433
|
continue;
|
|
321
|
-
const result = applyModule(projectRoot, required, modulesDir, applied, adopt);
|
|
322
|
-
added.push(
|
|
434
|
+
const result = applyModule(projectRoot, required, modulesDir, applied, adopt, placement);
|
|
435
|
+
added.push(...result.added, required);
|
|
323
436
|
ownedGlobs.push(...result.ownedGlobs);
|
|
324
437
|
managedOverrides.push(...result.managedOverrides);
|
|
325
438
|
preserved.push(...result.preserved);
|
|
@@ -356,7 +469,7 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
356
469
|
}
|
|
357
470
|
// 3) append env example lines
|
|
358
471
|
if (manifest.env?.length) {
|
|
359
|
-
appendEnv(projectRoot, manifest.env);
|
|
472
|
+
appendEnv(projectRoot, manifest.env, placement?.env);
|
|
360
473
|
touched.push(".env.example");
|
|
361
474
|
}
|
|
362
475
|
// 4) inject wiring at markers
|
|
@@ -370,7 +483,8 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
370
483
|
const content = (0, node_fs_1.readFileSync)(target, "utf8");
|
|
371
484
|
if (injection.optional && !content.includes(injection.marker))
|
|
372
485
|
continue;
|
|
373
|
-
|
|
486
|
+
const anchors = placement?.injections.get(injectionKey(injection.file, injection.marker));
|
|
487
|
+
(0, node_fs_1.writeFileSync)(target, injectAt(content, injection, anchors));
|
|
374
488
|
touched.push(injection.file);
|
|
375
489
|
}
|
|
376
490
|
const instructions = (manifest.instructions ?? []).map((line) => line.replace(/<app>/g, appName));
|
|
@@ -386,12 +500,13 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
386
500
|
};
|
|
387
501
|
}
|
|
388
502
|
function moduleRecord(module) {
|
|
389
|
-
const
|
|
503
|
+
const manifest = readModuleManifest(module.dir);
|
|
390
504
|
return {
|
|
391
505
|
name: module.name,
|
|
392
506
|
packageName: module.packageName,
|
|
393
507
|
moduleVersion: module.moduleVersion,
|
|
394
|
-
appliedMigrations: migrations.map((migration) => migration.id),
|
|
508
|
+
appliedMigrations: (manifest.migrations ?? []).map((migration) => migration.id),
|
|
509
|
+
...(manifest.apiPaths?.length ? { appliedApiPaths: manifest.apiPaths } : {}),
|
|
395
510
|
};
|
|
396
511
|
}
|
|
397
512
|
/** Copy a module overlay without clobbering app-owned presentation files. */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The prefix the gateway removes before a direct request reaches the API. */
|
|
2
|
+
export declare const API_PATH_PREFIX = "/api";
|
|
3
|
+
/** The path an entry names, without its optional trailing slash. */
|
|
4
|
+
export declare function apiPathBase(path: string): string;
|
|
5
|
+
/** The entry in `paths` that already routes `path` directly, if any. */
|
|
6
|
+
export declare function coveringApiPath(paths: readonly string[], path: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Validate the direct API path allowlist. Each entry covers its own path and
|
|
9
|
+
* every path below it, and must stay under /api/ outside the authentication
|
|
10
|
+
* routes, because the gateway removes /api before the request reaches the API.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseApiPaths(value: unknown, field: string, webSocketPaths: readonly string[]): string[];
|
|
13
|
+
export interface ApiPathMerge {
|
|
14
|
+
/** The resulting list: the current entries followed by the added ones. */
|
|
15
|
+
paths: string[];
|
|
16
|
+
added: string[];
|
|
17
|
+
/** Paths left out because the list would no longer validate. */
|
|
18
|
+
rejected: {
|
|
19
|
+
path: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append `additions` to `current`, skipping paths an entry already covers and
|
|
25
|
+
* paths that would make `validate` throw, such as an entry covering an existing
|
|
26
|
+
* one or a WebSocket path.
|
|
27
|
+
*/
|
|
28
|
+
export declare function mergeApiPaths(current: readonly string[], additions: readonly string[], validate: (paths: string[]) => void): ApiPathMerge;
|
|
29
|
+
/** The direct API paths installed modules contributed, as a valid list for a new configuration. */
|
|
30
|
+
export declare function installedApiPaths(modules: readonly {
|
|
31
|
+
appliedApiPaths?: string[];
|
|
32
|
+
}[]): string[];
|
|
33
|
+
/** Plan text naming the paths that reach the API without the web proxy. */
|
|
34
|
+
export declare function directApiRoutes(apiPaths: readonly string[]): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.API_PATH_PREFIX = void 0;
|
|
4
|
+
exports.apiPathBase = apiPathBase;
|
|
5
|
+
exports.coveringApiPath = coveringApiPath;
|
|
6
|
+
exports.parseApiPaths = parseApiPaths;
|
|
7
|
+
exports.mergeApiPaths = mergeApiPaths;
|
|
8
|
+
exports.installedApiPaths = installedApiPaths;
|
|
9
|
+
exports.directApiRoutes = directApiRoutes;
|
|
10
|
+
const MAX_PATHS = 32;
|
|
11
|
+
const MAX_PATH_LENGTH = 2_048;
|
|
12
|
+
const API_PATH = /^\/api(?:\/[A-Za-z0-9._~-]+)+\/?$/;
|
|
13
|
+
/** The prefix the gateway removes before a direct request reaches the API. */
|
|
14
|
+
exports.API_PATH_PREFIX = "/api";
|
|
15
|
+
/** The path an entry names, without its optional trailing slash. */
|
|
16
|
+
function apiPathBase(path) {
|
|
17
|
+
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
18
|
+
}
|
|
19
|
+
/** Whether `base` covers `path`: the same path or any path below it. */
|
|
20
|
+
function covers(base, path) {
|
|
21
|
+
return path === base || path.startsWith(`${base}/`);
|
|
22
|
+
}
|
|
23
|
+
/** The entry in `paths` that already routes `path` directly, if any. */
|
|
24
|
+
function coveringApiPath(paths, path) {
|
|
25
|
+
const base = apiPathBase(path);
|
|
26
|
+
return paths.find((entry) => covers(apiPathBase(entry), base));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate the direct API path allowlist. Each entry covers its own path and
|
|
30
|
+
* every path below it, and must stay under /api/ outside the authentication
|
|
31
|
+
* routes, because the gateway removes /api before the request reaches the API.
|
|
32
|
+
*/
|
|
33
|
+
function parseApiPaths(value, field, webSocketPaths) {
|
|
34
|
+
if (!Array.isArray(value)) {
|
|
35
|
+
throw new Error(`Configuration field "${field}" must be an array of path prefixes under /api/.`);
|
|
36
|
+
}
|
|
37
|
+
if (value.length > MAX_PATHS) {
|
|
38
|
+
throw new Error(`Configuration field "${field}" must contain at most ${MAX_PATHS} paths.`);
|
|
39
|
+
}
|
|
40
|
+
const paths = [];
|
|
41
|
+
for (const path of value) {
|
|
42
|
+
if (typeof path !== "string" ||
|
|
43
|
+
path.length > MAX_PATH_LENGTH ||
|
|
44
|
+
!API_PATH.test(path) ||
|
|
45
|
+
path.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
46
|
+
throw new Error(`Configuration field "${field}" must contain only static path prefixes under /api/, such as "/api/files/uploads/". /api/ itself, patterns, queries, fragments, encoded characters, empty segments, and traversal segments are not allowed.`);
|
|
47
|
+
}
|
|
48
|
+
const base = apiPathBase(path);
|
|
49
|
+
if (covers("/api/auth", base)) {
|
|
50
|
+
throw new Error(`Configuration field "${field}" cannot list "${path}": the API serves /api/auth/ with its prefix, so authentication routes always go through the web proxy.`);
|
|
51
|
+
}
|
|
52
|
+
const overlap = paths.find((other) => {
|
|
53
|
+
const otherBase = apiPathBase(other);
|
|
54
|
+
return covers(otherBase, base) || covers(base, otherBase);
|
|
55
|
+
});
|
|
56
|
+
if (overlap !== undefined) {
|
|
57
|
+
throw new Error(overlap === path
|
|
58
|
+
? `Configuration field "${field}" contains duplicate path "${path}".`
|
|
59
|
+
: `Configuration field "${field}" contains overlapping paths "${overlap}" and "${path}".`);
|
|
60
|
+
}
|
|
61
|
+
const socket = webSocketPaths.find((socketPath) => covers(base, socketPath));
|
|
62
|
+
if (socket !== undefined) {
|
|
63
|
+
throw new Error(`Configuration field "${field}" entry "${path}" covers WebSocket path "${socket}", which goes to the API without removing /api.`);
|
|
64
|
+
}
|
|
65
|
+
paths.push(path);
|
|
66
|
+
}
|
|
67
|
+
return paths;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Append `additions` to `current`, skipping paths an entry already covers and
|
|
71
|
+
* paths that would make `validate` throw, such as an entry covering an existing
|
|
72
|
+
* one or a WebSocket path.
|
|
73
|
+
*/
|
|
74
|
+
function mergeApiPaths(current, additions, validate) {
|
|
75
|
+
const paths = [...current];
|
|
76
|
+
const added = [];
|
|
77
|
+
const rejected = [];
|
|
78
|
+
for (const path of additions) {
|
|
79
|
+
if (coveringApiPath(paths, path) !== undefined)
|
|
80
|
+
continue;
|
|
81
|
+
const candidate = [...paths, path];
|
|
82
|
+
try {
|
|
83
|
+
validate(candidate);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
rejected.push({ path, reason: error.message });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
paths.push(path);
|
|
90
|
+
added.push(path);
|
|
91
|
+
}
|
|
92
|
+
return { paths, added, rejected };
|
|
93
|
+
}
|
|
94
|
+
/** The direct API paths installed modules contributed, as a valid list for a new configuration. */
|
|
95
|
+
function installedApiPaths(modules) {
|
|
96
|
+
return mergeApiPaths([], modules.flatMap((module) => module.appliedApiPaths ?? []), (paths) => parseApiPaths(paths, "apiPaths", [])).paths;
|
|
97
|
+
}
|
|
98
|
+
/** Plan text naming the paths that reach the API without the web proxy. */
|
|
99
|
+
function directApiRoutes(apiPaths) {
|
|
100
|
+
return apiPaths.length
|
|
101
|
+
? ` Route ${apiPaths.join(", ")} directly to the API without /api.`
|
|
102
|
+
: "";
|
|
103
|
+
}
|
|
@@ -69,6 +69,8 @@ export interface DockerComposeProfileV1 {
|
|
|
69
69
|
bindAddress: string;
|
|
70
70
|
port: number;
|
|
71
71
|
webSocketPaths: string[];
|
|
72
|
+
/** Path prefixes under /api/ that the gateway sends to the API with /api removed. */
|
|
73
|
+
apiPaths: string[];
|
|
72
74
|
trustedProxyCidrs: string[];
|
|
73
75
|
gatewayImage: string;
|
|
74
76
|
};
|
|
@@ -107,4 +109,6 @@ export interface ComposeProfileSummary {
|
|
|
107
109
|
profile: DockerComposeProfileV1;
|
|
108
110
|
}
|
|
109
111
|
export declare function initializeComposeProfile(projectRoot: string, name: string, options: InitializeComposeProfileOptions): ComposeProfileSummary;
|
|
112
|
+
/** The gateway runs only when some path must bypass the web service. */
|
|
113
|
+
export declare function composeHasGateway(profile: DockerComposeProfileV1): boolean;
|
|
110
114
|
export declare function composeProfileDigest(profile: DockerComposeProfileV1): string;
|
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseComposeProfile = parseComposeProfile;
|
|
4
4
|
exports.buildDefaultComposeProfile = buildDefaultComposeProfile;
|
|
5
5
|
exports.initializeComposeProfile = initializeComposeProfile;
|
|
6
|
+
exports.composeHasGateway = composeHasGateway;
|
|
6
7
|
exports.composeProfileDigest = composeProfileDigest;
|
|
7
8
|
const node_crypto_1 = require("node:crypto");
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
9
10
|
const node_net_1 = require("node:net");
|
|
10
11
|
const lockfile_1 = require("./lockfile");
|
|
11
12
|
const deploy_schema_1 = require("./deploy-schema");
|
|
13
|
+
const api_paths_1 = require("./api-paths");
|
|
12
14
|
const websocket_paths_1 = require("./websocket-paths");
|
|
13
15
|
/** Decimal core count, e.g. "0.25" or "2". */
|
|
14
16
|
const CPU_VALUE = /^(?:0\.[0-9]{1,3}|[1-9][0-9]{0,2}(?:\.[0-9]{1,3})?)$/;
|
|
@@ -167,6 +169,7 @@ function parseComposeProfile(value) {
|
|
|
167
169
|
"bindAddress",
|
|
168
170
|
"port",
|
|
169
171
|
"webSocketPaths",
|
|
172
|
+
"apiPaths",
|
|
170
173
|
"trustedProxyCidrs",
|
|
171
174
|
"gatewayImage",
|
|
172
175
|
], "exposure");
|
|
@@ -197,9 +200,10 @@ function parseComposeProfile(value) {
|
|
|
197
200
|
if (port > 65535)
|
|
198
201
|
throw new Error("Deployment exposure.port must be a TCP port.");
|
|
199
202
|
const webSocketPaths = (0, websocket_paths_1.parseExactWebSocketPaths)(exposure.webSocketPaths ?? [], "exposure.webSocketPaths");
|
|
203
|
+
const apiPaths = (0, api_paths_1.parseApiPaths)(exposure.apiPaths ?? [], "exposure.apiPaths", webSocketPaths);
|
|
200
204
|
const trustedProxyCidrs = parseTrustedProxyCidrs(exposure.trustedProxyCidrs ?? [], "exposure.trustedProxyCidrs");
|
|
201
|
-
if (trustedProxyCidrs.length > 0 && webSocketPaths.length === 0) {
|
|
202
|
-
throw new Error("Deployment exposure.trustedProxyCidrs requires at least one exposure.webSocketPaths entry.");
|
|
205
|
+
if (trustedProxyCidrs.length > 0 && webSocketPaths.length === 0 && apiPaths.length === 0) {
|
|
206
|
+
throw new Error("Deployment exposure.trustedProxyCidrs requires at least one exposure.webSocketPaths or exposure.apiPaths entry.");
|
|
203
207
|
}
|
|
204
208
|
const gatewayImage = exposure.gatewayImage === undefined
|
|
205
209
|
? "caddy:2.10-alpine"
|
|
@@ -238,6 +242,7 @@ function parseComposeProfile(value) {
|
|
|
238
242
|
bindAddress,
|
|
239
243
|
port,
|
|
240
244
|
webSocketPaths,
|
|
245
|
+
apiPaths,
|
|
241
246
|
trustedProxyCidrs,
|
|
242
247
|
gatewayImage,
|
|
243
248
|
},
|
|
@@ -302,6 +307,7 @@ function buildDefaultComposeProfile(projectName, options) {
|
|
|
302
307
|
bindAddress: "127.0.0.1",
|
|
303
308
|
port: 8080,
|
|
304
309
|
webSocketPaths: [],
|
|
310
|
+
apiPaths: [],
|
|
305
311
|
trustedProxyCidrs: [],
|
|
306
312
|
gatewayImage: "caddy:2.10-alpine",
|
|
307
313
|
},
|
|
@@ -385,6 +391,7 @@ function initializeComposeProfile(projectRoot, name, options) {
|
|
|
385
391
|
|| providers.events === "memory"
|
|
386
392
|
|| providers.jobs === "local";
|
|
387
393
|
draft.workloads.api.replicas = usesEmbeddedProviders ? 1 : 2;
|
|
394
|
+
draft.exposure.apiPaths = (0, api_paths_1.installedApiPaths)(manifest.modules);
|
|
388
395
|
draft.dependencies.postgres.mode = providers.database === "postgres" ? "managed" : "disabled";
|
|
389
396
|
draft.dependencies.redis.mode = usesRedis ? "managed" : "disabled";
|
|
390
397
|
draft.dependencies.objectStorage.mode = providers["object-storage"] === "s3"
|
|
@@ -423,6 +430,10 @@ function initializeComposeProfile(projectRoot, name, options) {
|
|
|
423
430
|
(0, node_fs_1.writeFileSync)(path, `${JSON.stringify(validated, null, 2)}\n`, { flag: "wx" });
|
|
424
431
|
return { name, path, profile: validated };
|
|
425
432
|
}
|
|
433
|
+
/** The gateway runs only when some path must bypass the web service. */
|
|
434
|
+
function composeHasGateway(profile) {
|
|
435
|
+
return profile.exposure.webSocketPaths.length > 0 || profile.exposure.apiPaths.length > 0;
|
|
436
|
+
}
|
|
426
437
|
function composeProfileDigest(profile) {
|
|
427
438
|
return `sha256:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify(profile)).digest("hex")}`;
|
|
428
439
|
}
|
|
@@ -10,6 +10,8 @@ exports.renderComposeDeployment = renderComposeDeployment;
|
|
|
10
10
|
const node_crypto_1 = require("node:crypto");
|
|
11
11
|
const node_fs_1 = require("node:fs");
|
|
12
12
|
const node_path_1 = require("node:path");
|
|
13
|
+
const api_paths_1 = require("./api-paths");
|
|
14
|
+
const deploy_compose_profile_1 = require("./deploy-compose-profile");
|
|
13
15
|
const deploy_schema_1 = require("./deploy-schema");
|
|
14
16
|
const lockfile_1 = require("./lockfile");
|
|
15
17
|
const toolchain_1 = require("./toolchain");
|
|
@@ -249,12 +251,26 @@ function gatewayService(profile, image, release, stateDigest) {
|
|
|
249
251
|
}
|
|
250
252
|
`
|
|
251
253
|
: "";
|
|
254
|
+
const api = `${serviceName(profile, "api")}:${CONTAINER_PORT}`;
|
|
255
|
+
const webSocketRoutes = profile.exposure.webSocketPaths.length
|
|
256
|
+
? ` @api_websockets path ${profile.exposure.webSocketPaths.join(" ")}\n` +
|
|
257
|
+
` handle @api_websockets {\n` +
|
|
258
|
+
` reverse_proxy ${api}\n` +
|
|
259
|
+
` }\n`
|
|
260
|
+
: "";
|
|
261
|
+
const apiPathRoutes = profile.exposure.apiPaths.length
|
|
262
|
+
? ` @api_paths path ${profile.exposure.apiPaths
|
|
263
|
+
.map((path) => `${(0, api_paths_1.apiPathBase)(path)} ${(0, api_paths_1.apiPathBase)(path)}/*`)
|
|
264
|
+
.join(" ")}\n` +
|
|
265
|
+
` handle @api_paths {\n` +
|
|
266
|
+
` uri strip_prefix ${api_paths_1.API_PATH_PREFIX}\n` +
|
|
267
|
+
` reverse_proxy ${api}\n` +
|
|
268
|
+
` }\n`
|
|
269
|
+
: "";
|
|
252
270
|
const caddyfile = globalOptions +
|
|
253
271
|
`:3000 {\n` +
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
` reverse_proxy ${serviceName(profile, "api")}:${CONTAINER_PORT}\n` +
|
|
257
|
-
` }\n` +
|
|
272
|
+
webSocketRoutes +
|
|
273
|
+
apiPathRoutes +
|
|
258
274
|
` handle {\n` +
|
|
259
275
|
` reverse_proxy ${serviceName(profile, "web")}:${CONTAINER_PORT}\n` +
|
|
260
276
|
` }\n` +
|
|
@@ -278,7 +294,7 @@ function gatewayService(profile, image, release, stateDigest) {
|
|
|
278
294
|
}
|
|
279
295
|
function renderComposeDocument(profile, release, images, stateDigest, workerCommand = ["bun", "dist/main-worker.js"]) {
|
|
280
296
|
const services = [];
|
|
281
|
-
const hasGateway = profile
|
|
297
|
+
const hasGateway = (0, deploy_compose_profile_1.composeHasGateway)(profile);
|
|
282
298
|
if (profile.dependencies.postgres.mode === "managed" && images.postgres) {
|
|
283
299
|
services.push(postgresService(profile, images.postgres));
|
|
284
300
|
}
|
|
@@ -300,7 +316,7 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
|
|
|
300
316
|
}));
|
|
301
317
|
if (hasGateway) {
|
|
302
318
|
if (!images.gateway) {
|
|
303
|
-
throw new Error("A resolved gateway image is required when WebSocket paths are configured.");
|
|
319
|
+
throw new Error("A resolved gateway image is required when WebSocket or API paths are configured.");
|
|
304
320
|
}
|
|
305
321
|
services.push(gatewayService(profile, images.gateway, release, stateDigest));
|
|
306
322
|
}
|
|
@@ -350,7 +366,7 @@ function defaultComposeImages(profile, release) {
|
|
|
350
366
|
return {
|
|
351
367
|
api: `${profile.release.apiRepository}:${release}`,
|
|
352
368
|
web: `${profile.release.webRepository}:${release}`,
|
|
353
|
-
gateway: profile
|
|
369
|
+
gateway: (0, deploy_compose_profile_1.composeHasGateway)(profile) ? profile.exposure.gatewayImage : null,
|
|
354
370
|
postgres: profile.dependencies.postgres.mode === "managed"
|
|
355
371
|
? profile.dependencies.postgres.image
|
|
356
372
|
: null,
|
package/dist/deploy-compose.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.rollbackComposeDeployment = rollbackComposeDeployment;
|
|
|
11
11
|
exports.composeRuntimeRoot = composeRuntimeRoot;
|
|
12
12
|
const node_crypto_1 = require("node:crypto");
|
|
13
13
|
const node_path_1 = require("node:path");
|
|
14
|
+
const api_paths_1 = require("./api-paths");
|
|
14
15
|
const deploy_driver_1 = require("./deploy-driver");
|
|
15
16
|
const deploy_compose_exec_1 = require("./deploy-compose-exec");
|
|
16
17
|
const deploy_compose_sync_1 = require("./deploy-compose-sync");
|
|
@@ -373,7 +374,7 @@ function planComposeDeployment(projectRoot, profileName, release, runner = deplo
|
|
|
373
374
|
actions.push({
|
|
374
375
|
order: order++,
|
|
375
376
|
kind: "application",
|
|
376
|
-
description: `Roll out api, web${profile.workloads.worker ? ", worker" : ""}${profile
|
|
377
|
+
description: `Roll out api, web${profile.workloads.worker ? ", worker" : ""}${(0, deploy_compose_profile_1.composeHasGateway)(profile) ? ", gateway" : ""} and wait for health.${(0, api_paths_1.directApiRoutes)(profile.exposure.apiPaths)}`,
|
|
377
378
|
});
|
|
378
379
|
actions.push({
|
|
379
380
|
order: order++,
|
package/dist/deploy-profile.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface DeployProfileV1 {
|
|
|
50
50
|
ingressClassName: string;
|
|
51
51
|
tlsSecretName: string | null;
|
|
52
52
|
webSocketPaths: string[];
|
|
53
|
+
/** Path prefixes under /api/ that the Ingress sends to the API with /api removed. */
|
|
54
|
+
apiPaths: string[];
|
|
53
55
|
};
|
|
54
56
|
workloads: {
|
|
55
57
|
api: WorkloadProfile;
|
|
@@ -78,6 +80,7 @@ export interface DeploymentProfileSummary {
|
|
|
78
80
|
path: string;
|
|
79
81
|
profile: DeployProfileV1;
|
|
80
82
|
}
|
|
83
|
+
export declare function parseDeploymentProfile(value: unknown): DeployProfileV1;
|
|
81
84
|
export declare function loadDeploymentProfile(projectRoot: string, name: string): DeployProfileV1;
|
|
82
85
|
export declare function listDeploymentProfiles(projectRoot: string): DeploymentProfileSummary[];
|
|
83
86
|
export interface InitializeDeploymentProfileOptions {
|