@keystrokehq/hosting 0.1.15 → 0.1.17
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/docker.cjs +154 -0
- package/dist/docker.cjs.map +1 -0
- package/dist/docker.d.cts +23 -0
- package/dist/docker.d.cts.map +1 -0
- package/dist/docker.d.mts +23 -0
- package/dist/docker.d.mts.map +1 -0
- package/dist/docker.mjs +152 -0
- package/dist/docker.mjs.map +1 -0
- package/dist/index.cjs +135 -8422
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -122
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +68 -122
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +103 -8372
- package/dist/index.mjs.map +1 -1
- package/dist/org-artifacts.cjs +4 -0
- package/dist/org-artifacts.d.cts +2 -0
- package/dist/org-artifacts.d.mts +2 -0
- package/dist/org-artifacts.mjs +2 -0
- package/dist/plugin-4cuXkf42.d.cts +44 -0
- package/dist/plugin-4cuXkf42.d.cts.map +1 -0
- package/dist/plugin-eiAXqE7P.d.mts +44 -0
- package/dist/plugin-eiAXqE7P.d.mts.map +1 -0
- package/dist/runtime-BmfsHLrk.mjs +19 -0
- package/dist/runtime-BmfsHLrk.mjs.map +1 -0
- package/dist/runtime-CV7pSfMl.cjs +30 -0
- package/dist/runtime-CV7pSfMl.cjs.map +1 -0
- package/dist/runtime-Cp906goK.d.cts +88 -0
- package/dist/runtime-Cp906goK.d.cts.map +1 -0
- package/dist/runtime-Cp906goK.d.mts +88 -0
- package/dist/runtime-Cp906goK.d.mts.map +1 -0
- package/dist/wait-for-health-DM55_NQh.cjs +340 -0
- package/dist/wait-for-health-DM55_NQh.cjs.map +1 -0
- package/dist/wait-for-health-XEzPPQIO.mjs +209 -0
- package/dist/wait-for-health-XEzPPQIO.mjs.map +1 -0
- package/package.json +13 -3
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
const require_runtime = require("./runtime-CV7pSfMl.cjs");
|
|
2
|
+
let _keystrokehq_shared_platform_origin = require("@keystrokehq/shared/platform-origin");
|
|
3
|
+
let node_crypto = require("node:crypto");
|
|
4
|
+
//#region src/constants.ts
|
|
5
|
+
const PROJECT_SERVER_IMAGE = "keystroke/project-server:local";
|
|
6
|
+
const PROJECT_SERVER_PORT = 3e3;
|
|
7
|
+
const PROJECT_SERVER_ROOT = "/app";
|
|
8
|
+
const PROJECT_SERVER_FRAMEWORK_ROOT = "/opt/keystroke";
|
|
9
|
+
const PROJECT_SERVER_FRAMEWORK_NODE_MODULES = `${PROJECT_SERVER_FRAMEWORK_ROOT}/node_modules`;
|
|
10
|
+
const PROJECT_SERVER_HEALTH = {
|
|
11
|
+
path: "/health",
|
|
12
|
+
port: PROJECT_SERVER_PORT
|
|
13
|
+
};
|
|
14
|
+
const PROJECT_SERVER_DEPLOY_STATUS = {
|
|
15
|
+
path: "/deploy-status",
|
|
16
|
+
port: PROJECT_SERVER_PORT
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/worker-runtime-config.ts
|
|
20
|
+
var WorkerRuntimeConfigError = class extends Error {
|
|
21
|
+
missing;
|
|
22
|
+
constructor(missing) {
|
|
23
|
+
super(`Worker runtime config incomplete (missing: ${missing.join(", ")}). In production set PLATFORM_WORKER_TOKEN and PUBLIC_PLATFORM_URL (or PLATFORM_URL).`);
|
|
24
|
+
this.name = "WorkerRuntimeConfigError";
|
|
25
|
+
this.missing = missing;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Fail fast when the platform cannot start worker-mode project servers. */
|
|
29
|
+
function resolveWorkerRuntimeConfig(source = process.env) {
|
|
30
|
+
const platformUrl = resolveWorkerPlatformUrl(source);
|
|
31
|
+
const workerToken = resolvePlatformWorkerToken(source);
|
|
32
|
+
const missing = [];
|
|
33
|
+
if (!platformUrl) missing.push("PUBLIC_PLATFORM_URL or PLATFORM_URL");
|
|
34
|
+
if (!workerToken) missing.push("PLATFORM_WORKER_TOKEN");
|
|
35
|
+
if (missing.length > 0) throw new WorkerRuntimeConfigError(missing);
|
|
36
|
+
return {
|
|
37
|
+
platformUrl,
|
|
38
|
+
workerToken
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/org-worker-token.ts
|
|
43
|
+
function sign(secret, organizationId) {
|
|
44
|
+
return (0, node_crypto.createHmac)("sha256", secret).update(`org-worker:${organizationId}`).digest("base64url");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-org worker token: `{organizationId}.{hmac}`. Self-describing so the
|
|
48
|
+
* platform can tell which org a machine is calling as, and stateless — verified
|
|
49
|
+
* by re-signing with the shared `PLATFORM_WORKER_TOKEN` secret.
|
|
50
|
+
*/
|
|
51
|
+
function mintOrgWorkerToken(secret, organizationId) {
|
|
52
|
+
return `${organizationId}.${sign(secret, organizationId)}`;
|
|
53
|
+
}
|
|
54
|
+
function verifyOrgWorkerToken(secret, token) {
|
|
55
|
+
if (!secret || !token) return;
|
|
56
|
+
const separator = token.lastIndexOf(".");
|
|
57
|
+
if (separator <= 0) return;
|
|
58
|
+
const organizationId = token.slice(0, separator);
|
|
59
|
+
const signature = Buffer.from(token.slice(separator + 1));
|
|
60
|
+
const expected = Buffer.from(sign(secret, organizationId));
|
|
61
|
+
if (signature.length !== expected.length || !(0, node_crypto.timingSafeEqual)(signature, expected)) return;
|
|
62
|
+
return { organizationId };
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/runtime-env.ts
|
|
66
|
+
/** Non-secret runtime config forwarded into org workers. Never include platform secrets. */
|
|
67
|
+
const RUNTIME_ENV_KEYS = [
|
|
68
|
+
"PUBLIC_PLATFORM_URL",
|
|
69
|
+
"PUBLIC_PLATFORM_PROXY_URL",
|
|
70
|
+
"PUBLIC_WEB_URL",
|
|
71
|
+
"UPSTASH_REDIS_URL",
|
|
72
|
+
"REDIS_URL",
|
|
73
|
+
"KEYSTROKE_QUEUE_PREFIX"
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Observability ingest config for org workers (PostHog error tracking + crash reports).
|
|
77
|
+
* Ingest tokens only — never forward `POSTHOG_API_KEY` (management) into workers.
|
|
78
|
+
*/
|
|
79
|
+
const OBSERVABILITY_ENV_KEYS = [
|
|
80
|
+
"POSTHOG_PROJECT_TOKEN",
|
|
81
|
+
"POSTHOG_HOST",
|
|
82
|
+
"POSTHOG_FLUSH_MODE",
|
|
83
|
+
"KEYSTROKE_ENVIRONMENT",
|
|
84
|
+
"KEYSTROKE_RELEASE"
|
|
85
|
+
];
|
|
86
|
+
/** Env keys that must never be forwarded into untrusted org workers. */
|
|
87
|
+
const FORBIDDEN_WORKER_ENV_KEYS = [
|
|
88
|
+
"BETTER_AUTH_SECRET",
|
|
89
|
+
"DATABASE_URL",
|
|
90
|
+
"POSTHOG_API_KEY",
|
|
91
|
+
"POSTGRES_PASSWORD",
|
|
92
|
+
"PLATFORM_WORKER_TOKEN",
|
|
93
|
+
"STRIPE_SECRET_KEY",
|
|
94
|
+
"CREDENTIAL_ENCRYPTION_KEY"
|
|
95
|
+
];
|
|
96
|
+
/** Provider and gateway env forwarded from the platform into project servers. */
|
|
97
|
+
const PROVIDER_ENV_KEYS = [
|
|
98
|
+
"ANTHROPIC_API_KEY",
|
|
99
|
+
"OPENAI_API_KEY",
|
|
100
|
+
"AI_GATEWAY_API_KEY",
|
|
101
|
+
"AI_GATEWAY_BASE_URL"
|
|
102
|
+
];
|
|
103
|
+
/** Shared dev token when `PLATFORM_WORKER_TOKEN` is unset (non-production only). */
|
|
104
|
+
const DEV_PLATFORM_WORKER_TOKEN = "dev-platform-worker-token";
|
|
105
|
+
/**
|
|
106
|
+
* Builds the launch spec for an org worker. Each deploy artifact is encoded in
|
|
107
|
+
* `KEYSTROKE_ORG_ARTIFACTS` — there is no base image, so at least one artifact is required.
|
|
108
|
+
*/
|
|
109
|
+
function resolveRuntimeLaunch(options, artifacts, database, extraEnv) {
|
|
110
|
+
if (artifacts.length === 0) throw new Error("Cannot start an org worker without at least one artifact");
|
|
111
|
+
const env = options.env ?? process.env;
|
|
112
|
+
return {
|
|
113
|
+
image: resolveProjectServerImage(env, options.image),
|
|
114
|
+
env: buildRuntimeEnv(env, artifacts, database, extraEnv),
|
|
115
|
+
port: PROJECT_SERVER_PORT,
|
|
116
|
+
health: PROJECT_SERVER_HEALTH
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Env vars passed into the org worker container/machine. */
|
|
120
|
+
function buildRuntimeEnv(source, artifacts, database, extraEnv) {
|
|
121
|
+
const entries = new Map([
|
|
122
|
+
["PORT", String(PROJECT_SERVER_PORT)],
|
|
123
|
+
["KEYSTROKE_ROOT", PROJECT_SERVER_ROOT],
|
|
124
|
+
["KEYSTROKE_RUNTIME_NODE_MODULES", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],
|
|
125
|
+
["KEYSTROKE_ORG_ARTIFACTS", require_runtime.encodeOrgArtifacts(artifacts)],
|
|
126
|
+
["ORGANIZATION_ID", database.organizationId],
|
|
127
|
+
["TENANT_ID", database.organizationId]
|
|
128
|
+
]);
|
|
129
|
+
for (const key of RUNTIME_ENV_KEYS) {
|
|
130
|
+
const value = source[key]?.trim();
|
|
131
|
+
if (!value) continue;
|
|
132
|
+
entries.set(key, value);
|
|
133
|
+
}
|
|
134
|
+
for (const key of OBSERVABILITY_ENV_KEYS) {
|
|
135
|
+
const value = source[key]?.trim();
|
|
136
|
+
if (!value) continue;
|
|
137
|
+
entries.set(key, value);
|
|
138
|
+
}
|
|
139
|
+
if (!extraEnv) for (const key of PROVIDER_ENV_KEYS) {
|
|
140
|
+
const value = source[key]?.trim();
|
|
141
|
+
if (value) entries.set(key, value);
|
|
142
|
+
}
|
|
143
|
+
const worker = resolveWorkerRuntimeConfig(source);
|
|
144
|
+
entries.set("PLATFORM_URL", worker.platformUrl);
|
|
145
|
+
entries.set("WORKER_INTERNAL_TOKEN", mintOrgWorkerToken(worker.workerToken, database.organizationId));
|
|
146
|
+
if (extraEnv) for (const [key, value] of Object.entries(extraEnv)) entries.set(key, value);
|
|
147
|
+
for (const key of FORBIDDEN_WORKER_ENV_KEYS) entries.delete(key);
|
|
148
|
+
return Object.fromEntries(entries);
|
|
149
|
+
}
|
|
150
|
+
function resolvePlatformWorkerToken(source = process.env) {
|
|
151
|
+
const explicit = source.PLATFORM_WORKER_TOKEN?.trim() ?? source.WORKER_INTERNAL_TOKEN?.trim();
|
|
152
|
+
if (explicit) return explicit;
|
|
153
|
+
if (source.NODE_ENV === "production") return;
|
|
154
|
+
return DEV_PLATFORM_WORKER_TOKEN;
|
|
155
|
+
}
|
|
156
|
+
/** Platform API URL reachable from a project-server container. */
|
|
157
|
+
function resolveWorkerPlatformUrl(source = process.env) {
|
|
158
|
+
const raw = source.PLATFORM_URL?.trim() ?? (0, _keystrokehq_shared_platform_origin.resolveExplicitPublicPlatformOrigin)(source) ?? (source.NODE_ENV === "production" ? void 0 : _keystrokehq_shared_platform_origin.LOCAL_PLATFORM_ORIGIN);
|
|
159
|
+
if (!raw) return;
|
|
160
|
+
return rewriteLoopbackUrl(raw);
|
|
161
|
+
}
|
|
162
|
+
/** Rewrite loopback hosts so containers can reach Postgres and other host services. */
|
|
163
|
+
function rewriteLoopbackHost(host) {
|
|
164
|
+
if (host === "localhost" || host === "127.0.0.1") return "host.docker.internal";
|
|
165
|
+
return host;
|
|
166
|
+
}
|
|
167
|
+
function rewriteLoopbackUrl(url) {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = new URL(url);
|
|
170
|
+
parsed.hostname = rewriteLoopbackHost(parsed.hostname);
|
|
171
|
+
return parsed.toString().replace(/\/$/, "");
|
|
172
|
+
} catch {
|
|
173
|
+
return url;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** dockerode expects `KEY=value` strings. */
|
|
177
|
+
function formatDockerEnv(env) {
|
|
178
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
179
|
+
}
|
|
180
|
+
function resolveProjectServerImage(env = process.env, override) {
|
|
181
|
+
return override ?? env.PROJECT_DOCKER_IMAGE ?? env.TENANT_DOCKER_IMAGE ?? "keystroke/project-server:local";
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/wait-for-health.ts
|
|
185
|
+
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
186
|
+
const DEFAULT_INTERVAL_MS = 500;
|
|
187
|
+
async function waitForHealth(baseUrl, options = {}) {
|
|
188
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
189
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
190
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
191
|
+
const deadline = Date.now() + timeoutMs;
|
|
192
|
+
const url = new URL(PROJECT_SERVER_HEALTH.path, baseUrl);
|
|
193
|
+
while (Date.now() < deadline) {
|
|
194
|
+
try {
|
|
195
|
+
if ((await fetchImpl(url, { signal: AbortSignal.timeout(2e3) })).ok) return;
|
|
196
|
+
} catch {}
|
|
197
|
+
await sleep(intervalMs);
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Project server health check timed out after ${timeoutMs}ms (${url})`);
|
|
200
|
+
}
|
|
201
|
+
function sleep(ms) {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
setTimeout(resolve, ms);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
Object.defineProperty(exports, "DEV_PLATFORM_WORKER_TOKEN", {
|
|
208
|
+
enumerable: true,
|
|
209
|
+
get: function() {
|
|
210
|
+
return DEV_PLATFORM_WORKER_TOKEN;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
Object.defineProperty(exports, "FORBIDDEN_WORKER_ENV_KEYS", {
|
|
214
|
+
enumerable: true,
|
|
215
|
+
get: function() {
|
|
216
|
+
return FORBIDDEN_WORKER_ENV_KEYS;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
Object.defineProperty(exports, "PROJECT_SERVER_DEPLOY_STATUS", {
|
|
220
|
+
enumerable: true,
|
|
221
|
+
get: function() {
|
|
222
|
+
return PROJECT_SERVER_DEPLOY_STATUS;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
Object.defineProperty(exports, "PROJECT_SERVER_FRAMEWORK_NODE_MODULES", {
|
|
226
|
+
enumerable: true,
|
|
227
|
+
get: function() {
|
|
228
|
+
return PROJECT_SERVER_FRAMEWORK_NODE_MODULES;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
Object.defineProperty(exports, "PROJECT_SERVER_FRAMEWORK_ROOT", {
|
|
232
|
+
enumerable: true,
|
|
233
|
+
get: function() {
|
|
234
|
+
return PROJECT_SERVER_FRAMEWORK_ROOT;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
Object.defineProperty(exports, "PROJECT_SERVER_HEALTH", {
|
|
238
|
+
enumerable: true,
|
|
239
|
+
get: function() {
|
|
240
|
+
return PROJECT_SERVER_HEALTH;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
Object.defineProperty(exports, "PROJECT_SERVER_IMAGE", {
|
|
244
|
+
enumerable: true,
|
|
245
|
+
get: function() {
|
|
246
|
+
return PROJECT_SERVER_IMAGE;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
Object.defineProperty(exports, "PROJECT_SERVER_PORT", {
|
|
250
|
+
enumerable: true,
|
|
251
|
+
get: function() {
|
|
252
|
+
return PROJECT_SERVER_PORT;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
Object.defineProperty(exports, "PROJECT_SERVER_ROOT", {
|
|
256
|
+
enumerable: true,
|
|
257
|
+
get: function() {
|
|
258
|
+
return PROJECT_SERVER_ROOT;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
Object.defineProperty(exports, "WorkerRuntimeConfigError", {
|
|
262
|
+
enumerable: true,
|
|
263
|
+
get: function() {
|
|
264
|
+
return WorkerRuntimeConfigError;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
Object.defineProperty(exports, "buildRuntimeEnv", {
|
|
268
|
+
enumerable: true,
|
|
269
|
+
get: function() {
|
|
270
|
+
return buildRuntimeEnv;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
Object.defineProperty(exports, "formatDockerEnv", {
|
|
274
|
+
enumerable: true,
|
|
275
|
+
get: function() {
|
|
276
|
+
return formatDockerEnv;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
Object.defineProperty(exports, "mintOrgWorkerToken", {
|
|
280
|
+
enumerable: true,
|
|
281
|
+
get: function() {
|
|
282
|
+
return mintOrgWorkerToken;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
Object.defineProperty(exports, "resolvePlatformWorkerToken", {
|
|
286
|
+
enumerable: true,
|
|
287
|
+
get: function() {
|
|
288
|
+
return resolvePlatformWorkerToken;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
Object.defineProperty(exports, "resolveProjectServerImage", {
|
|
292
|
+
enumerable: true,
|
|
293
|
+
get: function() {
|
|
294
|
+
return resolveProjectServerImage;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
Object.defineProperty(exports, "resolveRuntimeLaunch", {
|
|
298
|
+
enumerable: true,
|
|
299
|
+
get: function() {
|
|
300
|
+
return resolveRuntimeLaunch;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
Object.defineProperty(exports, "resolveWorkerPlatformUrl", {
|
|
304
|
+
enumerable: true,
|
|
305
|
+
get: function() {
|
|
306
|
+
return resolveWorkerPlatformUrl;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
Object.defineProperty(exports, "resolveWorkerRuntimeConfig", {
|
|
310
|
+
enumerable: true,
|
|
311
|
+
get: function() {
|
|
312
|
+
return resolveWorkerRuntimeConfig;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
Object.defineProperty(exports, "rewriteLoopbackHost", {
|
|
316
|
+
enumerable: true,
|
|
317
|
+
get: function() {
|
|
318
|
+
return rewriteLoopbackHost;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
Object.defineProperty(exports, "rewriteLoopbackUrl", {
|
|
322
|
+
enumerable: true,
|
|
323
|
+
get: function() {
|
|
324
|
+
return rewriteLoopbackUrl;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
Object.defineProperty(exports, "verifyOrgWorkerToken", {
|
|
328
|
+
enumerable: true,
|
|
329
|
+
get: function() {
|
|
330
|
+
return verifyOrgWorkerToken;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
Object.defineProperty(exports, "waitForHealth", {
|
|
334
|
+
enumerable: true,
|
|
335
|
+
get: function() {
|
|
336
|
+
return waitForHealth;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
//# sourceMappingURL=wait-for-health-DM55_NQh.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait-for-health-DM55_NQh.cjs","names":["encodeOrgArtifacts","LOCAL_PLATFORM_ORIGIN"],"sources":["../src/constants.ts","../src/worker-runtime-config.ts","../src/org-worker-token.ts","../src/runtime-env.ts","../src/wait-for-health.ts"],"sourcesContent":["export const PROJECT_SERVER_IMAGE = \"keystroke/project-server:local\";\n\nexport const PROJECT_SERVER_PORT = 3000;\n\nexport const PROJECT_SERVER_ROOT = \"/app\";\n\nexport const PROJECT_SERVER_FRAMEWORK_ROOT = \"/opt/keystroke\";\n\nexport const PROJECT_SERVER_FRAMEWORK_NODE_MODULES = `${PROJECT_SERVER_FRAMEWORK_ROOT}/node_modules`;\n\nexport const PROJECT_SERVER_HEALTH = {\n path: \"/health\",\n port: PROJECT_SERVER_PORT,\n} as const;\n\nexport const PROJECT_SERVER_DEPLOY_STATUS = {\n path: \"/deploy-status\",\n port: PROJECT_SERVER_PORT,\n} as const;\n","import { resolvePlatformWorkerToken, resolveWorkerPlatformUrl } from \"./runtime-env\";\n\n/** Resolved platform ↔ project-server worker auth contract. */\nexport type WorkerRuntimeConfig = {\n /** Platform API base URL reachable from the project-server runtime. */\n platformUrl: string;\n /** Bearer token for platform `/internal` routes. */\n workerToken: string;\n};\n\nexport class WorkerRuntimeConfigError extends Error {\n readonly missing: readonly string[];\n\n constructor(missing: string[]) {\n super(\n `Worker runtime config incomplete (missing: ${missing.join(\", \")}). ` +\n \"In production set PLATFORM_WORKER_TOKEN and PUBLIC_PLATFORM_URL (or PLATFORM_URL).\",\n );\n this.name = \"WorkerRuntimeConfigError\";\n this.missing = missing;\n }\n}\n\n/** Fail fast when the platform cannot start worker-mode project servers. */\nexport function resolveWorkerRuntimeConfig(\n source: NodeJS.ProcessEnv = process.env,\n): WorkerRuntimeConfig {\n const platformUrl = resolveWorkerPlatformUrl(source);\n const workerToken = resolvePlatformWorkerToken(source);\n const missing: string[] = [];\n\n if (!platformUrl) {\n missing.push(\"PUBLIC_PLATFORM_URL or PLATFORM_URL\");\n }\n\n if (!workerToken) {\n missing.push(\"PLATFORM_WORKER_TOKEN\");\n }\n\n if (missing.length > 0) {\n throw new WorkerRuntimeConfigError(missing);\n }\n\n return { platformUrl: platformUrl!, workerToken: workerToken! };\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\nfunction sign(secret: string, organizationId: string): string {\n return createHmac(\"sha256\", secret).update(`org-worker:${organizationId}`).digest(\"base64url\");\n}\n\n/**\n * Per-org worker token: `{organizationId}.{hmac}`. Self-describing so the\n * platform can tell which org a machine is calling as, and stateless — verified\n * by re-signing with the shared `PLATFORM_WORKER_TOKEN` secret.\n */\nexport function mintOrgWorkerToken(secret: string, organizationId: string): string {\n return `${organizationId}.${sign(secret, organizationId)}`;\n}\n\nexport type VerifiedOrgWorkerToken = {\n organizationId: string;\n};\n\nexport function verifyOrgWorkerToken(\n secret: string | undefined,\n token: string | undefined,\n): VerifiedOrgWorkerToken | undefined {\n if (!secret || !token) {\n return undefined;\n }\n\n // The signature is base64url (no dots), so the last dot ends the org id.\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) {\n return undefined;\n }\n\n const organizationId = token.slice(0, separator);\n const signature = Buffer.from(token.slice(separator + 1));\n const expected = Buffer.from(sign(secret, organizationId));\n if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {\n return undefined;\n }\n\n return { organizationId };\n}\n","import {\n LOCAL_PLATFORM_ORIGIN,\n resolveExplicitPublicPlatformOrigin,\n} from \"@keystrokehq/shared/platform-origin\";\n\nimport type { ProjectRuntimeDatabase } from \"./runtime\";\nimport { encodeOrgArtifacts, type OrgArtifactSpec } from \"./runtime\";\nimport {\n PROJECT_SERVER_FRAMEWORK_NODE_MODULES,\n PROJECT_SERVER_HEALTH,\n PROJECT_SERVER_IMAGE,\n PROJECT_SERVER_PORT,\n PROJECT_SERVER_ROOT,\n} from \"./constants\";\nimport type { HostingOptions } from \"./plugin\";\nimport { resolveWorkerRuntimeConfig } from \"./worker-runtime-config\";\nimport { mintOrgWorkerToken } from \"./org-worker-token\";\n\n/** Non-secret runtime config forwarded into org workers. Never include platform secrets. */\nconst RUNTIME_ENV_KEYS = [\n \"PUBLIC_PLATFORM_URL\",\n \"PUBLIC_PLATFORM_PROXY_URL\",\n \"PUBLIC_WEB_URL\",\n \"UPSTASH_REDIS_URL\",\n \"REDIS_URL\",\n \"KEYSTROKE_QUEUE_PREFIX\",\n] as const;\n\n/**\n * Observability ingest config for org workers (PostHog error tracking + crash reports).\n * Ingest tokens only — never forward `POSTHOG_API_KEY` (management) into workers.\n */\nconst OBSERVABILITY_ENV_KEYS = [\n \"POSTHOG_PROJECT_TOKEN\",\n \"POSTHOG_HOST\",\n \"POSTHOG_FLUSH_MODE\",\n \"KEYSTROKE_ENVIRONMENT\",\n \"KEYSTROKE_RELEASE\",\n] as const;\n\n/** Env keys that must never be forwarded into untrusted org workers. */\nexport const FORBIDDEN_WORKER_ENV_KEYS = [\n \"BETTER_AUTH_SECRET\",\n \"DATABASE_URL\",\n \"POSTHOG_API_KEY\",\n \"POSTGRES_PASSWORD\",\n \"PLATFORM_WORKER_TOKEN\",\n \"STRIPE_SECRET_KEY\",\n \"CREDENTIAL_ENCRYPTION_KEY\",\n] as const;\n\n/** Provider and gateway env forwarded from the platform into project servers. */\nconst PROVIDER_ENV_KEYS = [\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AI_GATEWAY_API_KEY\",\n \"AI_GATEWAY_BASE_URL\",\n] as const;\n\n/** Shared dev token when `PLATFORM_WORKER_TOKEN` is unset (non-production only). */\nexport const DEV_PLATFORM_WORKER_TOKEN = \"dev-platform-worker-token\";\n\nexport type RuntimeLaunchSpec = {\n image: string;\n env: Record<string, string>;\n port: number;\n health: typeof PROJECT_SERVER_HEALTH;\n};\n\n/**\n * Builds the launch spec for an org worker. Each deploy artifact is encoded in\n * `KEYSTROKE_ORG_ARTIFACTS` — there is no base image, so at least one artifact is required.\n */\nexport function resolveRuntimeLaunch(\n options: HostingOptions,\n artifacts: OrgArtifactSpec[],\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n if (artifacts.length === 0) {\n throw new Error(\"Cannot start an org worker without at least one artifact\");\n }\n\n const env = options.env ?? process.env;\n\n return {\n image: resolveProjectServerImage(env, options.image),\n env: buildRuntimeEnv(env, artifacts, database, extraEnv),\n port: PROJECT_SERVER_PORT,\n health: PROJECT_SERVER_HEALTH,\n };\n}\n\n/** @deprecated Single-project launch — prefer org {@link resolveRuntimeLaunch}. */\nexport function resolveSingleProjectRuntimeLaunch(\n options: HostingOptions,\n artifact: { storageKey: string; artifactId: string; artifactVersion: number },\n database: ProjectRuntimeDatabase & { projectId: string },\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n return resolveRuntimeLaunch(\n options,\n [\n {\n projectId: database.projectId,\n artifactStorageKey: artifact.storageKey,\n artifactId: artifact.artifactId,\n artifactVersion: artifact.artifactVersion,\n },\n ],\n database,\n extraEnv,\n );\n}\n\n/** Env vars passed into the org worker container/machine. */\nexport function buildRuntimeEnv(\n source: NodeJS.ProcessEnv,\n artifacts: OrgArtifactSpec[],\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): Record<string, string> {\n const entries = new Map<string, string>([\n [\"PORT\", String(PROJECT_SERVER_PORT)],\n [\"KEYSTROKE_ROOT\", PROJECT_SERVER_ROOT],\n [\"KEYSTROKE_RUNTIME_NODE_MODULES\", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],\n [\"KEYSTROKE_ORG_ARTIFACTS\", encodeOrgArtifacts(artifacts)],\n [\"ORGANIZATION_ID\", database.organizationId],\n [\"TENANT_ID\", database.organizationId],\n ]);\n\n for (const key of RUNTIME_ENV_KEYS) {\n const value = source[key]?.trim();\n if (!value) {\n continue;\n }\n entries.set(key, value);\n }\n\n for (const key of OBSERVABILITY_ENV_KEYS) {\n const value = source[key]?.trim();\n if (!value) {\n continue;\n }\n entries.set(key, value);\n }\n\n // OSS self-host: forward BYO provider/gateway env. Cloud llmProxy supplies extraEnv instead.\n if (!extraEnv) {\n for (const key of PROVIDER_ENV_KEYS) {\n const value = source[key]?.trim();\n if (value) {\n entries.set(key, value);\n }\n }\n }\n\n // Project containers always run as workers: they resolve credentials/secrets through\n // the platform internal API and are deliberately never given the decryption key.\n // The token is minted per org so the platform knows which org a machine calls as.\n const worker = resolveWorkerRuntimeConfig(source);\n entries.set(\"PLATFORM_URL\", worker.platformUrl);\n entries.set(\n \"WORKER_INTERNAL_TOKEN\",\n mintOrgWorkerToken(worker.workerToken, database.organizationId),\n );\n\n if (extraEnv) {\n for (const [key, value] of Object.entries(extraEnv)) {\n entries.set(key, value);\n }\n }\n\n for (const key of FORBIDDEN_WORKER_ENV_KEYS) {\n entries.delete(key);\n }\n\n return Object.fromEntries(entries);\n}\n\nexport function resolvePlatformWorkerToken(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const explicit = source.PLATFORM_WORKER_TOKEN?.trim() ?? source.WORKER_INTERNAL_TOKEN?.trim();\n if (explicit) {\n return explicit;\n }\n\n if (source.NODE_ENV === \"production\") {\n return undefined;\n }\n\n return DEV_PLATFORM_WORKER_TOKEN;\n}\n\n/** Platform API URL reachable from a project-server container. */\nexport function resolveWorkerPlatformUrl(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const raw =\n source.PLATFORM_URL?.trim() ??\n resolveExplicitPublicPlatformOrigin(source) ??\n (source.NODE_ENV === \"production\" ? undefined : LOCAL_PLATFORM_ORIGIN);\n if (!raw) {\n return undefined;\n }\n\n return rewriteLoopbackUrl(raw);\n}\n\n/** Rewrite loopback hosts so containers can reach Postgres and other host services. */\nexport function rewriteLoopbackHost(host: string): string {\n if (host === \"localhost\" || host === \"127.0.0.1\") {\n return \"host.docker.internal\";\n }\n\n return host;\n}\n\nexport function rewriteLoopbackUrl(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.hostname = rewriteLoopbackHost(parsed.hostname);\n return parsed.toString().replace(/\\/$/, \"\");\n } catch {\n return url;\n }\n}\n\n/** dockerode expects `KEY=value` strings. */\nexport function formatDockerEnv(env: Record<string, string>): string[] {\n return Object.entries(env).map(([key, value]) => `${key}=${value}`);\n}\n\nexport function resolveProjectServerImage(\n env: NodeJS.ProcessEnv = process.env,\n override?: string,\n): string {\n return override ?? env.PROJECT_DOCKER_IMAGE ?? env.TENANT_DOCKER_IMAGE ?? PROJECT_SERVER_IMAGE;\n}\n","import { PROJECT_SERVER_HEALTH } from \"./constants\";\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\nconst DEFAULT_INTERVAL_MS = 500;\n\nexport type WaitForHealthOptions = {\n timeoutMs?: number;\n intervalMs?: number;\n fetchImpl?: typeof fetch;\n};\n\nexport async function waitForHealth(\n baseUrl: string,\n options: WaitForHealthOptions = {},\n): Promise<void> {\n const fetchImpl = options.fetchImpl ?? fetch;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const deadline = Date.now() + timeoutMs;\n const url = new URL(PROJECT_SERVER_HEALTH.path, baseUrl);\n\n while (Date.now() < deadline) {\n try {\n const response = await fetchImpl(url, { signal: AbortSignal.timeout(2_000) });\n if (response.ok) {\n return;\n }\n } catch {\n // retry until timeout\n }\n\n await sleep(intervalMs);\n }\n\n throw new Error(`Project server health check timed out after ${timeoutMs}ms (${url})`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { t as encodeOrgArtifacts } from "./runtime-BmfsHLrk.mjs";
|
|
2
|
+
import { LOCAL_PLATFORM_ORIGIN, resolveExplicitPublicPlatformOrigin } from "@keystrokehq/shared/platform-origin";
|
|
3
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4
|
+
//#region src/constants.ts
|
|
5
|
+
const PROJECT_SERVER_IMAGE = "keystroke/project-server:local";
|
|
6
|
+
const PROJECT_SERVER_PORT = 3e3;
|
|
7
|
+
const PROJECT_SERVER_ROOT = "/app";
|
|
8
|
+
const PROJECT_SERVER_FRAMEWORK_ROOT = "/opt/keystroke";
|
|
9
|
+
const PROJECT_SERVER_FRAMEWORK_NODE_MODULES = `${PROJECT_SERVER_FRAMEWORK_ROOT}/node_modules`;
|
|
10
|
+
const PROJECT_SERVER_HEALTH = {
|
|
11
|
+
path: "/health",
|
|
12
|
+
port: PROJECT_SERVER_PORT
|
|
13
|
+
};
|
|
14
|
+
const PROJECT_SERVER_DEPLOY_STATUS = {
|
|
15
|
+
path: "/deploy-status",
|
|
16
|
+
port: PROJECT_SERVER_PORT
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/worker-runtime-config.ts
|
|
20
|
+
var WorkerRuntimeConfigError = class extends Error {
|
|
21
|
+
missing;
|
|
22
|
+
constructor(missing) {
|
|
23
|
+
super(`Worker runtime config incomplete (missing: ${missing.join(", ")}). In production set PLATFORM_WORKER_TOKEN and PUBLIC_PLATFORM_URL (or PLATFORM_URL).`);
|
|
24
|
+
this.name = "WorkerRuntimeConfigError";
|
|
25
|
+
this.missing = missing;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Fail fast when the platform cannot start worker-mode project servers. */
|
|
29
|
+
function resolveWorkerRuntimeConfig(source = process.env) {
|
|
30
|
+
const platformUrl = resolveWorkerPlatformUrl(source);
|
|
31
|
+
const workerToken = resolvePlatformWorkerToken(source);
|
|
32
|
+
const missing = [];
|
|
33
|
+
if (!platformUrl) missing.push("PUBLIC_PLATFORM_URL or PLATFORM_URL");
|
|
34
|
+
if (!workerToken) missing.push("PLATFORM_WORKER_TOKEN");
|
|
35
|
+
if (missing.length > 0) throw new WorkerRuntimeConfigError(missing);
|
|
36
|
+
return {
|
|
37
|
+
platformUrl,
|
|
38
|
+
workerToken
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/org-worker-token.ts
|
|
43
|
+
function sign(secret, organizationId) {
|
|
44
|
+
return createHmac("sha256", secret).update(`org-worker:${organizationId}`).digest("base64url");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-org worker token: `{organizationId}.{hmac}`. Self-describing so the
|
|
48
|
+
* platform can tell which org a machine is calling as, and stateless — verified
|
|
49
|
+
* by re-signing with the shared `PLATFORM_WORKER_TOKEN` secret.
|
|
50
|
+
*/
|
|
51
|
+
function mintOrgWorkerToken(secret, organizationId) {
|
|
52
|
+
return `${organizationId}.${sign(secret, organizationId)}`;
|
|
53
|
+
}
|
|
54
|
+
function verifyOrgWorkerToken(secret, token) {
|
|
55
|
+
if (!secret || !token) return;
|
|
56
|
+
const separator = token.lastIndexOf(".");
|
|
57
|
+
if (separator <= 0) return;
|
|
58
|
+
const organizationId = token.slice(0, separator);
|
|
59
|
+
const signature = Buffer.from(token.slice(separator + 1));
|
|
60
|
+
const expected = Buffer.from(sign(secret, organizationId));
|
|
61
|
+
if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) return;
|
|
62
|
+
return { organizationId };
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/runtime-env.ts
|
|
66
|
+
/** Non-secret runtime config forwarded into org workers. Never include platform secrets. */
|
|
67
|
+
const RUNTIME_ENV_KEYS = [
|
|
68
|
+
"PUBLIC_PLATFORM_URL",
|
|
69
|
+
"PUBLIC_PLATFORM_PROXY_URL",
|
|
70
|
+
"PUBLIC_WEB_URL",
|
|
71
|
+
"UPSTASH_REDIS_URL",
|
|
72
|
+
"REDIS_URL",
|
|
73
|
+
"KEYSTROKE_QUEUE_PREFIX"
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Observability ingest config for org workers (PostHog error tracking + crash reports).
|
|
77
|
+
* Ingest tokens only — never forward `POSTHOG_API_KEY` (management) into workers.
|
|
78
|
+
*/
|
|
79
|
+
const OBSERVABILITY_ENV_KEYS = [
|
|
80
|
+
"POSTHOG_PROJECT_TOKEN",
|
|
81
|
+
"POSTHOG_HOST",
|
|
82
|
+
"POSTHOG_FLUSH_MODE",
|
|
83
|
+
"KEYSTROKE_ENVIRONMENT",
|
|
84
|
+
"KEYSTROKE_RELEASE"
|
|
85
|
+
];
|
|
86
|
+
/** Env keys that must never be forwarded into untrusted org workers. */
|
|
87
|
+
const FORBIDDEN_WORKER_ENV_KEYS = [
|
|
88
|
+
"BETTER_AUTH_SECRET",
|
|
89
|
+
"DATABASE_URL",
|
|
90
|
+
"POSTHOG_API_KEY",
|
|
91
|
+
"POSTGRES_PASSWORD",
|
|
92
|
+
"PLATFORM_WORKER_TOKEN",
|
|
93
|
+
"STRIPE_SECRET_KEY",
|
|
94
|
+
"CREDENTIAL_ENCRYPTION_KEY"
|
|
95
|
+
];
|
|
96
|
+
/** Provider and gateway env forwarded from the platform into project servers. */
|
|
97
|
+
const PROVIDER_ENV_KEYS = [
|
|
98
|
+
"ANTHROPIC_API_KEY",
|
|
99
|
+
"OPENAI_API_KEY",
|
|
100
|
+
"AI_GATEWAY_API_KEY",
|
|
101
|
+
"AI_GATEWAY_BASE_URL"
|
|
102
|
+
];
|
|
103
|
+
/** Shared dev token when `PLATFORM_WORKER_TOKEN` is unset (non-production only). */
|
|
104
|
+
const DEV_PLATFORM_WORKER_TOKEN = "dev-platform-worker-token";
|
|
105
|
+
/**
|
|
106
|
+
* Builds the launch spec for an org worker. Each deploy artifact is encoded in
|
|
107
|
+
* `KEYSTROKE_ORG_ARTIFACTS` — there is no base image, so at least one artifact is required.
|
|
108
|
+
*/
|
|
109
|
+
function resolveRuntimeLaunch(options, artifacts, database, extraEnv) {
|
|
110
|
+
if (artifacts.length === 0) throw new Error("Cannot start an org worker without at least one artifact");
|
|
111
|
+
const env = options.env ?? process.env;
|
|
112
|
+
return {
|
|
113
|
+
image: resolveProjectServerImage(env, options.image),
|
|
114
|
+
env: buildRuntimeEnv(env, artifacts, database, extraEnv),
|
|
115
|
+
port: PROJECT_SERVER_PORT,
|
|
116
|
+
health: PROJECT_SERVER_HEALTH
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Env vars passed into the org worker container/machine. */
|
|
120
|
+
function buildRuntimeEnv(source, artifacts, database, extraEnv) {
|
|
121
|
+
const entries = new Map([
|
|
122
|
+
["PORT", String(PROJECT_SERVER_PORT)],
|
|
123
|
+
["KEYSTROKE_ROOT", PROJECT_SERVER_ROOT],
|
|
124
|
+
["KEYSTROKE_RUNTIME_NODE_MODULES", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],
|
|
125
|
+
["KEYSTROKE_ORG_ARTIFACTS", encodeOrgArtifacts(artifacts)],
|
|
126
|
+
["ORGANIZATION_ID", database.organizationId],
|
|
127
|
+
["TENANT_ID", database.organizationId]
|
|
128
|
+
]);
|
|
129
|
+
for (const key of RUNTIME_ENV_KEYS) {
|
|
130
|
+
const value = source[key]?.trim();
|
|
131
|
+
if (!value) continue;
|
|
132
|
+
entries.set(key, value);
|
|
133
|
+
}
|
|
134
|
+
for (const key of OBSERVABILITY_ENV_KEYS) {
|
|
135
|
+
const value = source[key]?.trim();
|
|
136
|
+
if (!value) continue;
|
|
137
|
+
entries.set(key, value);
|
|
138
|
+
}
|
|
139
|
+
if (!extraEnv) for (const key of PROVIDER_ENV_KEYS) {
|
|
140
|
+
const value = source[key]?.trim();
|
|
141
|
+
if (value) entries.set(key, value);
|
|
142
|
+
}
|
|
143
|
+
const worker = resolveWorkerRuntimeConfig(source);
|
|
144
|
+
entries.set("PLATFORM_URL", worker.platformUrl);
|
|
145
|
+
entries.set("WORKER_INTERNAL_TOKEN", mintOrgWorkerToken(worker.workerToken, database.organizationId));
|
|
146
|
+
if (extraEnv) for (const [key, value] of Object.entries(extraEnv)) entries.set(key, value);
|
|
147
|
+
for (const key of FORBIDDEN_WORKER_ENV_KEYS) entries.delete(key);
|
|
148
|
+
return Object.fromEntries(entries);
|
|
149
|
+
}
|
|
150
|
+
function resolvePlatformWorkerToken(source = process.env) {
|
|
151
|
+
const explicit = source.PLATFORM_WORKER_TOKEN?.trim() ?? source.WORKER_INTERNAL_TOKEN?.trim();
|
|
152
|
+
if (explicit) return explicit;
|
|
153
|
+
if (source.NODE_ENV === "production") return;
|
|
154
|
+
return DEV_PLATFORM_WORKER_TOKEN;
|
|
155
|
+
}
|
|
156
|
+
/** Platform API URL reachable from a project-server container. */
|
|
157
|
+
function resolveWorkerPlatformUrl(source = process.env) {
|
|
158
|
+
const raw = source.PLATFORM_URL?.trim() ?? resolveExplicitPublicPlatformOrigin(source) ?? (source.NODE_ENV === "production" ? void 0 : LOCAL_PLATFORM_ORIGIN);
|
|
159
|
+
if (!raw) return;
|
|
160
|
+
return rewriteLoopbackUrl(raw);
|
|
161
|
+
}
|
|
162
|
+
/** Rewrite loopback hosts so containers can reach Postgres and other host services. */
|
|
163
|
+
function rewriteLoopbackHost(host) {
|
|
164
|
+
if (host === "localhost" || host === "127.0.0.1") return "host.docker.internal";
|
|
165
|
+
return host;
|
|
166
|
+
}
|
|
167
|
+
function rewriteLoopbackUrl(url) {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = new URL(url);
|
|
170
|
+
parsed.hostname = rewriteLoopbackHost(parsed.hostname);
|
|
171
|
+
return parsed.toString().replace(/\/$/, "");
|
|
172
|
+
} catch {
|
|
173
|
+
return url;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** dockerode expects `KEY=value` strings. */
|
|
177
|
+
function formatDockerEnv(env) {
|
|
178
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
179
|
+
}
|
|
180
|
+
function resolveProjectServerImage(env = process.env, override) {
|
|
181
|
+
return override ?? env.PROJECT_DOCKER_IMAGE ?? env.TENANT_DOCKER_IMAGE ?? "keystroke/project-server:local";
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/wait-for-health.ts
|
|
185
|
+
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
186
|
+
const DEFAULT_INTERVAL_MS = 500;
|
|
187
|
+
async function waitForHealth(baseUrl, options = {}) {
|
|
188
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
189
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
190
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
191
|
+
const deadline = Date.now() + timeoutMs;
|
|
192
|
+
const url = new URL(PROJECT_SERVER_HEALTH.path, baseUrl);
|
|
193
|
+
while (Date.now() < deadline) {
|
|
194
|
+
try {
|
|
195
|
+
if ((await fetchImpl(url, { signal: AbortSignal.timeout(2e3) })).ok) return;
|
|
196
|
+
} catch {}
|
|
197
|
+
await sleep(intervalMs);
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Project server health check timed out after ${timeoutMs}ms (${url})`);
|
|
200
|
+
}
|
|
201
|
+
function sleep(ms) {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
setTimeout(resolve, ms);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
export { PROJECT_SERVER_ROOT as S, PROJECT_SERVER_FRAMEWORK_NODE_MODULES as _, formatDockerEnv as a, PROJECT_SERVER_IMAGE as b, resolveRuntimeLaunch as c, rewriteLoopbackUrl as d, mintOrgWorkerToken as f, PROJECT_SERVER_DEPLOY_STATUS as g, resolveWorkerRuntimeConfig as h, buildRuntimeEnv as i, resolveWorkerPlatformUrl as l, WorkerRuntimeConfigError as m, DEV_PLATFORM_WORKER_TOKEN as n, resolvePlatformWorkerToken as o, verifyOrgWorkerToken as p, FORBIDDEN_WORKER_ENV_KEYS as r, resolveProjectServerImage as s, waitForHealth as t, rewriteLoopbackHost as u, PROJECT_SERVER_FRAMEWORK_ROOT as v, PROJECT_SERVER_PORT as x, PROJECT_SERVER_HEALTH as y };
|
|
208
|
+
|
|
209
|
+
//# sourceMappingURL=wait-for-health-XEzPPQIO.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait-for-health-XEzPPQIO.mjs","names":[],"sources":["../src/constants.ts","../src/worker-runtime-config.ts","../src/org-worker-token.ts","../src/runtime-env.ts","../src/wait-for-health.ts"],"sourcesContent":["export const PROJECT_SERVER_IMAGE = \"keystroke/project-server:local\";\n\nexport const PROJECT_SERVER_PORT = 3000;\n\nexport const PROJECT_SERVER_ROOT = \"/app\";\n\nexport const PROJECT_SERVER_FRAMEWORK_ROOT = \"/opt/keystroke\";\n\nexport const PROJECT_SERVER_FRAMEWORK_NODE_MODULES = `${PROJECT_SERVER_FRAMEWORK_ROOT}/node_modules`;\n\nexport const PROJECT_SERVER_HEALTH = {\n path: \"/health\",\n port: PROJECT_SERVER_PORT,\n} as const;\n\nexport const PROJECT_SERVER_DEPLOY_STATUS = {\n path: \"/deploy-status\",\n port: PROJECT_SERVER_PORT,\n} as const;\n","import { resolvePlatformWorkerToken, resolveWorkerPlatformUrl } from \"./runtime-env\";\n\n/** Resolved platform ↔ project-server worker auth contract. */\nexport type WorkerRuntimeConfig = {\n /** Platform API base URL reachable from the project-server runtime. */\n platformUrl: string;\n /** Bearer token for platform `/internal` routes. */\n workerToken: string;\n};\n\nexport class WorkerRuntimeConfigError extends Error {\n readonly missing: readonly string[];\n\n constructor(missing: string[]) {\n super(\n `Worker runtime config incomplete (missing: ${missing.join(\", \")}). ` +\n \"In production set PLATFORM_WORKER_TOKEN and PUBLIC_PLATFORM_URL (or PLATFORM_URL).\",\n );\n this.name = \"WorkerRuntimeConfigError\";\n this.missing = missing;\n }\n}\n\n/** Fail fast when the platform cannot start worker-mode project servers. */\nexport function resolveWorkerRuntimeConfig(\n source: NodeJS.ProcessEnv = process.env,\n): WorkerRuntimeConfig {\n const platformUrl = resolveWorkerPlatformUrl(source);\n const workerToken = resolvePlatformWorkerToken(source);\n const missing: string[] = [];\n\n if (!platformUrl) {\n missing.push(\"PUBLIC_PLATFORM_URL or PLATFORM_URL\");\n }\n\n if (!workerToken) {\n missing.push(\"PLATFORM_WORKER_TOKEN\");\n }\n\n if (missing.length > 0) {\n throw new WorkerRuntimeConfigError(missing);\n }\n\n return { platformUrl: platformUrl!, workerToken: workerToken! };\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\nfunction sign(secret: string, organizationId: string): string {\n return createHmac(\"sha256\", secret).update(`org-worker:${organizationId}`).digest(\"base64url\");\n}\n\n/**\n * Per-org worker token: `{organizationId}.{hmac}`. Self-describing so the\n * platform can tell which org a machine is calling as, and stateless — verified\n * by re-signing with the shared `PLATFORM_WORKER_TOKEN` secret.\n */\nexport function mintOrgWorkerToken(secret: string, organizationId: string): string {\n return `${organizationId}.${sign(secret, organizationId)}`;\n}\n\nexport type VerifiedOrgWorkerToken = {\n organizationId: string;\n};\n\nexport function verifyOrgWorkerToken(\n secret: string | undefined,\n token: string | undefined,\n): VerifiedOrgWorkerToken | undefined {\n if (!secret || !token) {\n return undefined;\n }\n\n // The signature is base64url (no dots), so the last dot ends the org id.\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) {\n return undefined;\n }\n\n const organizationId = token.slice(0, separator);\n const signature = Buffer.from(token.slice(separator + 1));\n const expected = Buffer.from(sign(secret, organizationId));\n if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {\n return undefined;\n }\n\n return { organizationId };\n}\n","import {\n LOCAL_PLATFORM_ORIGIN,\n resolveExplicitPublicPlatformOrigin,\n} from \"@keystrokehq/shared/platform-origin\";\n\nimport type { ProjectRuntimeDatabase } from \"./runtime\";\nimport { encodeOrgArtifacts, type OrgArtifactSpec } from \"./runtime\";\nimport {\n PROJECT_SERVER_FRAMEWORK_NODE_MODULES,\n PROJECT_SERVER_HEALTH,\n PROJECT_SERVER_IMAGE,\n PROJECT_SERVER_PORT,\n PROJECT_SERVER_ROOT,\n} from \"./constants\";\nimport type { HostingOptions } from \"./plugin\";\nimport { resolveWorkerRuntimeConfig } from \"./worker-runtime-config\";\nimport { mintOrgWorkerToken } from \"./org-worker-token\";\n\n/** Non-secret runtime config forwarded into org workers. Never include platform secrets. */\nconst RUNTIME_ENV_KEYS = [\n \"PUBLIC_PLATFORM_URL\",\n \"PUBLIC_PLATFORM_PROXY_URL\",\n \"PUBLIC_WEB_URL\",\n \"UPSTASH_REDIS_URL\",\n \"REDIS_URL\",\n \"KEYSTROKE_QUEUE_PREFIX\",\n] as const;\n\n/**\n * Observability ingest config for org workers (PostHog error tracking + crash reports).\n * Ingest tokens only — never forward `POSTHOG_API_KEY` (management) into workers.\n */\nconst OBSERVABILITY_ENV_KEYS = [\n \"POSTHOG_PROJECT_TOKEN\",\n \"POSTHOG_HOST\",\n \"POSTHOG_FLUSH_MODE\",\n \"KEYSTROKE_ENVIRONMENT\",\n \"KEYSTROKE_RELEASE\",\n] as const;\n\n/** Env keys that must never be forwarded into untrusted org workers. */\nexport const FORBIDDEN_WORKER_ENV_KEYS = [\n \"BETTER_AUTH_SECRET\",\n \"DATABASE_URL\",\n \"POSTHOG_API_KEY\",\n \"POSTGRES_PASSWORD\",\n \"PLATFORM_WORKER_TOKEN\",\n \"STRIPE_SECRET_KEY\",\n \"CREDENTIAL_ENCRYPTION_KEY\",\n] as const;\n\n/** Provider and gateway env forwarded from the platform into project servers. */\nconst PROVIDER_ENV_KEYS = [\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AI_GATEWAY_API_KEY\",\n \"AI_GATEWAY_BASE_URL\",\n] as const;\n\n/** Shared dev token when `PLATFORM_WORKER_TOKEN` is unset (non-production only). */\nexport const DEV_PLATFORM_WORKER_TOKEN = \"dev-platform-worker-token\";\n\nexport type RuntimeLaunchSpec = {\n image: string;\n env: Record<string, string>;\n port: number;\n health: typeof PROJECT_SERVER_HEALTH;\n};\n\n/**\n * Builds the launch spec for an org worker. Each deploy artifact is encoded in\n * `KEYSTROKE_ORG_ARTIFACTS` — there is no base image, so at least one artifact is required.\n */\nexport function resolveRuntimeLaunch(\n options: HostingOptions,\n artifacts: OrgArtifactSpec[],\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n if (artifacts.length === 0) {\n throw new Error(\"Cannot start an org worker without at least one artifact\");\n }\n\n const env = options.env ?? process.env;\n\n return {\n image: resolveProjectServerImage(env, options.image),\n env: buildRuntimeEnv(env, artifacts, database, extraEnv),\n port: PROJECT_SERVER_PORT,\n health: PROJECT_SERVER_HEALTH,\n };\n}\n\n/** @deprecated Single-project launch — prefer org {@link resolveRuntimeLaunch}. */\nexport function resolveSingleProjectRuntimeLaunch(\n options: HostingOptions,\n artifact: { storageKey: string; artifactId: string; artifactVersion: number },\n database: ProjectRuntimeDatabase & { projectId: string },\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n return resolveRuntimeLaunch(\n options,\n [\n {\n projectId: database.projectId,\n artifactStorageKey: artifact.storageKey,\n artifactId: artifact.artifactId,\n artifactVersion: artifact.artifactVersion,\n },\n ],\n database,\n extraEnv,\n );\n}\n\n/** Env vars passed into the org worker container/machine. */\nexport function buildRuntimeEnv(\n source: NodeJS.ProcessEnv,\n artifacts: OrgArtifactSpec[],\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): Record<string, string> {\n const entries = new Map<string, string>([\n [\"PORT\", String(PROJECT_SERVER_PORT)],\n [\"KEYSTROKE_ROOT\", PROJECT_SERVER_ROOT],\n [\"KEYSTROKE_RUNTIME_NODE_MODULES\", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],\n [\"KEYSTROKE_ORG_ARTIFACTS\", encodeOrgArtifacts(artifacts)],\n [\"ORGANIZATION_ID\", database.organizationId],\n [\"TENANT_ID\", database.organizationId],\n ]);\n\n for (const key of RUNTIME_ENV_KEYS) {\n const value = source[key]?.trim();\n if (!value) {\n continue;\n }\n entries.set(key, value);\n }\n\n for (const key of OBSERVABILITY_ENV_KEYS) {\n const value = source[key]?.trim();\n if (!value) {\n continue;\n }\n entries.set(key, value);\n }\n\n // OSS self-host: forward BYO provider/gateway env. Cloud llmProxy supplies extraEnv instead.\n if (!extraEnv) {\n for (const key of PROVIDER_ENV_KEYS) {\n const value = source[key]?.trim();\n if (value) {\n entries.set(key, value);\n }\n }\n }\n\n // Project containers always run as workers: they resolve credentials/secrets through\n // the platform internal API and are deliberately never given the decryption key.\n // The token is minted per org so the platform knows which org a machine calls as.\n const worker = resolveWorkerRuntimeConfig(source);\n entries.set(\"PLATFORM_URL\", worker.platformUrl);\n entries.set(\n \"WORKER_INTERNAL_TOKEN\",\n mintOrgWorkerToken(worker.workerToken, database.organizationId),\n );\n\n if (extraEnv) {\n for (const [key, value] of Object.entries(extraEnv)) {\n entries.set(key, value);\n }\n }\n\n for (const key of FORBIDDEN_WORKER_ENV_KEYS) {\n entries.delete(key);\n }\n\n return Object.fromEntries(entries);\n}\n\nexport function resolvePlatformWorkerToken(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const explicit = source.PLATFORM_WORKER_TOKEN?.trim() ?? source.WORKER_INTERNAL_TOKEN?.trim();\n if (explicit) {\n return explicit;\n }\n\n if (source.NODE_ENV === \"production\") {\n return undefined;\n }\n\n return DEV_PLATFORM_WORKER_TOKEN;\n}\n\n/** Platform API URL reachable from a project-server container. */\nexport function resolveWorkerPlatformUrl(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const raw =\n source.PLATFORM_URL?.trim() ??\n resolveExplicitPublicPlatformOrigin(source) ??\n (source.NODE_ENV === \"production\" ? undefined : LOCAL_PLATFORM_ORIGIN);\n if (!raw) {\n return undefined;\n }\n\n return rewriteLoopbackUrl(raw);\n}\n\n/** Rewrite loopback hosts so containers can reach Postgres and other host services. */\nexport function rewriteLoopbackHost(host: string): string {\n if (host === \"localhost\" || host === \"127.0.0.1\") {\n return \"host.docker.internal\";\n }\n\n return host;\n}\n\nexport function rewriteLoopbackUrl(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.hostname = rewriteLoopbackHost(parsed.hostname);\n return parsed.toString().replace(/\\/$/, \"\");\n } catch {\n return url;\n }\n}\n\n/** dockerode expects `KEY=value` strings. */\nexport function formatDockerEnv(env: Record<string, string>): string[] {\n return Object.entries(env).map(([key, value]) => `${key}=${value}`);\n}\n\nexport function resolveProjectServerImage(\n env: NodeJS.ProcessEnv = process.env,\n override?: string,\n): string {\n return override ?? env.PROJECT_DOCKER_IMAGE ?? env.TENANT_DOCKER_IMAGE ?? PROJECT_SERVER_IMAGE;\n}\n","import { PROJECT_SERVER_HEALTH } from \"./constants\";\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\nconst DEFAULT_INTERVAL_MS = 500;\n\nexport type WaitForHealthOptions = {\n timeoutMs?: number;\n intervalMs?: number;\n fetchImpl?: typeof fetch;\n};\n\nexport async function waitForHealth(\n baseUrl: string,\n options: WaitForHealthOptions = {},\n): Promise<void> {\n const fetchImpl = options.fetchImpl ?? fetch;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const deadline = Date.now() + timeoutMs;\n const url = new URL(PROJECT_SERVER_HEALTH.path, baseUrl);\n\n while (Date.now() < deadline) {\n try {\n const response = await fetchImpl(url, { signal: AbortSignal.timeout(2_000) });\n if (response.ok) {\n return;\n }\n } catch {\n // retry until timeout\n }\n\n await sleep(intervalMs);\n }\n\n throw new Error(`Project server health check timed out after ${timeoutMs}ms (${url})`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keystrokehq/hosting",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/keystrokehq/keystroke.git",
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
"types": "./dist/index.d.mts",
|
|
19
19
|
"import": "./dist/index.mjs",
|
|
20
20
|
"require": "./dist/index.cjs"
|
|
21
|
+
},
|
|
22
|
+
"./org-artifacts": {
|
|
23
|
+
"types": "./dist/org-artifacts.d.mts",
|
|
24
|
+
"import": "./dist/org-artifacts.mjs",
|
|
25
|
+
"require": "./dist/org-artifacts.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./docker": {
|
|
28
|
+
"types": "./dist/docker.d.mts",
|
|
29
|
+
"import": "./dist/docker.mjs",
|
|
30
|
+
"require": "./dist/docker.cjs"
|
|
21
31
|
}
|
|
22
32
|
},
|
|
23
33
|
"publishConfig": {
|
|
@@ -26,7 +36,8 @@
|
|
|
26
36
|
"registry": "https://registry.npmjs.org"
|
|
27
37
|
},
|
|
28
38
|
"dependencies": {
|
|
29
|
-
"dockerode": "^4.0.9"
|
|
39
|
+
"dockerode": "^4.0.9",
|
|
40
|
+
"@keystrokehq/shared": "0.1.53"
|
|
30
41
|
},
|
|
31
42
|
"devDependencies": {
|
|
32
43
|
"@types/dockerode": "^3.3.47",
|
|
@@ -36,7 +47,6 @@
|
|
|
36
47
|
"tsdown": "^0.22.0",
|
|
37
48
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
38
49
|
"vitest": "^4.1.7",
|
|
39
|
-
"@keystrokehq/shared": "0.1.49",
|
|
40
50
|
"@keystrokehq/oxlint-config": "0.0.4",
|
|
41
51
|
"@keystrokehq/tsconfig": "0.0.3",
|
|
42
52
|
"@keystrokehq/tsdown-config": "0.0.5",
|