@keystrokehq/hosting 0.1.3 → 0.1.4

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/index.cjs CHANGED
@@ -34,6 +34,22 @@ const PROJECT_SERVER_HEALTH = {
34
34
  port: PROJECT_SERVER_PORT
35
35
  };
36
36
  //#endregion
37
+ //#region src/runtime.ts
38
+ /** Builds base64 `KEYSTROKE_ORG_ARTIFACTS` payload for org worker containers. */
39
+ function encodeOrgArtifacts(artifacts) {
40
+ return Buffer.from(JSON.stringify(artifacts)).toString("base64");
41
+ }
42
+ function parseOrgArtifactsFromEnv(env = process.env) {
43
+ const raw = env.KEYSTROKE_ORG_ARTIFACTS?.trim();
44
+ if (!raw) return;
45
+ const parsed = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
46
+ if (!Array.isArray(parsed)) throw new Error("KEYSTROKE_ORG_ARTIFACTS must decode to a JSON array");
47
+ return parsed.map((entry) => {
48
+ if (!entry || typeof entry !== "object" || typeof entry.projectId !== "string" || typeof entry.artifactStorageKey !== "string") throw new Error("KEYSTROKE_ORG_ARTIFACTS entries require projectId and artifactStorageKey");
49
+ return entry;
50
+ });
51
+ }
52
+ //#endregion
37
53
  //#region src/runtime-env.ts
38
54
  const RUNTIME_ENV_KEYS = [
39
55
  "BETTER_AUTH_SECRET",
@@ -73,28 +89,26 @@ function projectDatabaseRuntimeEnv(database) {
73
89
  };
74
90
  }
75
91
  /**
76
- * Builds the launch spec for a single project server. `artifactStorageKey` is the
77
- * object key for that deploy's built `dist` tarball in the org bucket there is
78
- * no base image, so it is required.
92
+ * Builds the launch spec for an org worker. Each deploy artifact is encoded in
93
+ * `KEYSTROKE_ORG_ARTIFACTS` there is no base image, so at least one artifact is required.
79
94
  */
80
- function resolveRuntimeLaunch(options, artifactStorageKey, database, extraEnv) {
81
- if (!artifactStorageKey) throw new Error("Cannot start a project server without an artifact storage key");
95
+ function resolveRuntimeLaunch(options, artifacts, database, extraEnv) {
96
+ if (artifacts.length === 0) throw new Error("Cannot start an org worker without at least one artifact");
82
97
  const env = options.env ?? process.env;
83
98
  return {
84
99
  image: resolveProjectServerImage(env, options.image),
85
- env: buildRuntimeEnv(env, artifactStorageKey, database, extraEnv),
100
+ env: buildRuntimeEnv(env, artifacts, database, extraEnv),
86
101
  port: PROJECT_SERVER_PORT,
87
102
  health: PROJECT_SERVER_HEALTH
88
103
  };
89
104
  }
90
- /** Env vars passed into the project server container/machine. */
91
- function buildRuntimeEnv(source, artifactStorageKey, database, extraEnv) {
105
+ /** Env vars passed into the org worker container/machine. */
106
+ function buildRuntimeEnv(source, artifacts, database, extraEnv) {
92
107
  const entries = new Map([
93
108
  ["PORT", String(PROJECT_SERVER_PORT)],
94
109
  ["KEYSTROKE_ROOT", PROJECT_SERVER_ROOT],
95
110
  ["KEYSTROKE_RUNTIME_NODE_MODULES", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],
96
- ["ARTIFACT_STORAGE_KEY", artifactStorageKey],
97
- ["PROJECT_ID", database.projectId],
111
+ ["KEYSTROKE_ORG_ARTIFACTS", encodeOrgArtifacts(artifacts)],
98
112
  ["ORGANIZATION_ID", database.organizationId],
99
113
  ["TENANT_ID", database.organizationId]
100
114
  ]);
@@ -203,15 +217,19 @@ const MINIO_DOCKER_NETWORK = process.env.MINIO_DOCKER_NETWORK ?? "keystroke";
203
217
  function createDockerRuntime(options = {}) {
204
218
  const docker = options.docker ?? new dockerode.default();
205
219
  return { async start(input) {
206
- const launch = resolveRuntimeLaunch(options, input.artifactStorageKey, input.database, input.env);
220
+ if (input.projects.length === 0) throw new Error("Docker org runtime requires at least one project deploy");
221
+ const launch = resolveRuntimeLaunch(options, input.projects.map((project) => ({
222
+ projectId: project.projectId,
223
+ artifactStorageKey: project.artifactStorageKey
224
+ })), input.database, input.env);
207
225
  await ensureImage(docker, launch.image);
208
- await removeExistingContainer(docker, input.projectId);
226
+ await removeExistingContainer(docker, input.organizationId);
209
227
  const container = await docker.createContainer({
210
228
  Image: launch.image,
211
- name: containerName(input.projectId),
229
+ name: containerName(input.organizationId),
212
230
  Labels: {
213
- "keystroke.project.id": input.projectId,
214
- "keystroke.project.name": input.name
231
+ "keystroke.organization.id": input.organizationId,
232
+ "keystroke.project.ids": input.projects.map((project) => project.projectId).join(",")
215
233
  },
216
234
  ExposedPorts: { [`${launch.port}/tcp`]: {} },
217
235
  HostConfig: {
@@ -224,7 +242,7 @@ function createDockerRuntime(options = {}) {
224
242
  await container.start();
225
243
  const inspect = await container.inspect();
226
244
  const binding = inspect.NetworkSettings.Ports?.[`${launch.port}/tcp`]?.[0];
227
- if (!binding?.HostPort) throw new Error("Failed to resolve project container host port");
245
+ if (!binding?.HostPort) throw new Error("Failed to resolve org container host port");
228
246
  const baseUrl = `http://127.0.0.1:${binding.HostPort}`;
229
247
  await waitForHealth(baseUrl, {
230
248
  fetchImpl: options.fetchImpl,
@@ -236,12 +254,12 @@ function createDockerRuntime(options = {}) {
236
254
  };
237
255
  } };
238
256
  }
239
- function containerName(projectId) {
240
- return `keystroke-project-${projectId}`;
257
+ function containerName(organizationId) {
258
+ return `keystroke-org-${organizationId}`;
241
259
  }
242
- async function removeExistingContainer(docker, projectId) {
260
+ async function removeExistingContainer(docker, organizationId) {
243
261
  try {
244
- const existing = docker.getContainer(containerName(projectId));
262
+ const existing = docker.getContainer(containerName(organizationId));
245
263
  await existing.stop({ t: 5 }).catch(() => void 0);
246
264
  await existing.remove({ force: true });
247
265
  } catch {}
@@ -336,7 +354,9 @@ exports.buildRuntimeEnv = buildRuntimeEnv;
336
354
  exports.canPingProjectTarget = canPingProjectTarget;
337
355
  exports.defaultHostingPlugin = defaultHostingPlugin;
338
356
  exports.dockerPlugin = dockerPlugin;
357
+ exports.encodeOrgArtifacts = encodeOrgArtifacts;
339
358
  exports.formatDockerEnv = formatDockerEnv;
359
+ exports.parseOrgArtifactsFromEnv = parseOrgArtifactsFromEnv;
340
360
  exports.pingProject = pingProject;
341
361
  exports.pingProjectTarget = pingProjectTarget;
342
362
  exports.projectDatabaseRuntimeEnv = projectDatabaseRuntimeEnv;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Docker"],"sources":["../src/constants.ts","../src/runtime-env.ts","../src/worker-runtime-config.ts","../src/wait-for-health.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.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","import type { ProjectRuntimeDatabase } 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\";\n\nconst RUNTIME_ENV_KEYS = [\n \"BETTER_AUTH_SECRET\",\n \"PUBLIC_SERVER_URL\",\n \"PUBLIC_WEB_URL\",\n \"POSTGRES_SSL\",\n \"UPSTASH_REDIS_URL\",\n \"REDIS_URL\",\n \"KEYSTROKE_QUEUE_PREFIX\",\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 const PROJECT_DATABASE_ENV_KEYS = [\n \"POSTGRES_HOST\",\n \"POSTGRES_PORT\",\n \"POSTGRES_DB\",\n \"POSTGRES_USER\",\n \"POSTGRES_PASSWORD\",\n \"DATABASE_SEARCH_PATH\",\n] as const;\n\nexport type RuntimeLaunchSpec = {\n image: string;\n env: Record<string, string>;\n port: number;\n health: typeof PROJECT_SERVER_HEALTH;\n};\n\n/** Per-project postgres env injected into every project server container/machine. */\nexport function projectDatabaseRuntimeEnv(\n database: ProjectRuntimeDatabase,\n): Record<(typeof PROJECT_DATABASE_ENV_KEYS)[number], string> {\n return {\n POSTGRES_HOST: rewriteLoopbackHost(database.host),\n POSTGRES_PORT: String(database.port),\n POSTGRES_DB: database.database,\n POSTGRES_USER: database.user,\n POSTGRES_PASSWORD: database.password,\n DATABASE_SEARCH_PATH: database.searchPath,\n };\n}\n\n/**\n * Builds the launch spec for a single project server. `artifactStorageKey` is the\n * object key for that deploy's built `dist` tarball in the org bucket — there is\n * no base image, so it is required.\n */\nexport function resolveRuntimeLaunch(\n options: HostingOptions,\n artifactStorageKey: string,\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n if (!artifactStorageKey) {\n throw new Error(\"Cannot start a project server without an artifact storage key\");\n }\n\n const env = options.env ?? process.env;\n\n return {\n image: resolveProjectServerImage(env, options.image),\n env: buildRuntimeEnv(env, artifactStorageKey, database, extraEnv),\n port: PROJECT_SERVER_PORT,\n health: PROJECT_SERVER_HEALTH,\n };\n}\n\n/** Env vars passed into the project server container/machine. */\nexport function buildRuntimeEnv(\n source: NodeJS.ProcessEnv,\n artifactStorageKey: string,\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): Record<string, string> {\n const entries = new Map<string, string>([\n // Container bind port — not derived from PUBLIC_SERVER_URL (external URL metadata).\n [\"PORT\", String(PROJECT_SERVER_PORT)],\n [\"KEYSTROKE_ROOT\", PROJECT_SERVER_ROOT],\n [\"KEYSTROKE_RUNTIME_NODE_MODULES\", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],\n [\"ARTIFACT_STORAGE_KEY\", artifactStorageKey],\n [\"PROJECT_ID\", database.projectId],\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 if (key === \"POSTGRES_SSL\") {\n throw new Error(\"POSTGRES_SSL is required\");\n }\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 const worker = resolveWorkerRuntimeConfig(source);\n entries.set(\"PLATFORM_URL\", worker.platformUrl);\n entries.set(\"WORKER_INTERNAL_TOKEN\", worker.workerToken);\n\n for (const [key, value] of Object.entries(projectDatabaseRuntimeEnv(database))) {\n entries.set(key, value);\n }\n\n if (extraEnv) {\n for (const [key, value] of Object.entries(extraEnv)) {\n entries.set(key, value);\n }\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. */\nconst DEV_PLATFORM_URL = \"http://localhost:3002\";\n\nexport function resolveWorkerPlatformUrl(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const raw =\n source.PLATFORM_URL?.trim() ??\n source.PUBLIC_PLATFORM_URL?.trim() ??\n (source.NODE_ENV === \"production\" ? undefined : DEV_PLATFORM_URL);\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 { 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 { 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","import Docker from \"dockerode\";\n\nimport type { ProjectRuntime } from \"../runtime\";\n\nimport type { HostingOptions } from \"../plugin\";\nimport { formatDockerEnv, resolveRuntimeLaunch } from \"../runtime-env\";\nimport { waitForHealth } from \"../wait-for-health\";\n\nexport type DockerPluginOptions = HostingOptions & {\n docker?: Docker;\n};\n\nconst MINIO_DOCKER_NETWORK = process.env.MINIO_DOCKER_NETWORK ?? \"keystroke\";\n\nexport function createDockerRuntime(options: DockerPluginOptions = {}): ProjectRuntime {\n const docker = options.docker ?? new Docker();\n\n return {\n async start(input) {\n const launch = resolveRuntimeLaunch(\n options,\n input.artifactStorageKey,\n input.database,\n input.env,\n );\n await ensureImage(docker, launch.image);\n await removeExistingContainer(docker, input.projectId);\n\n const container = await docker.createContainer({\n Image: launch.image,\n name: containerName(input.projectId),\n Labels: {\n \"keystroke.project.id\": input.projectId,\n \"keystroke.project.name\": input.name,\n },\n ExposedPorts: {\n [`${launch.port}/tcp`]: {},\n },\n HostConfig: {\n ExtraHosts: [\"host.docker.internal:host-gateway\"],\n PortBindings: {\n [`${launch.port}/tcp`]: [{ HostPort: \"0\" }],\n },\n },\n NetworkingConfig: {\n EndpointsConfig: {\n [MINIO_DOCKER_NETWORK]: {},\n },\n },\n Env: formatDockerEnv(launch.env),\n });\n\n await container.start();\n\n const inspect = await container.inspect();\n const binding = inspect.NetworkSettings.Ports?.[`${launch.port}/tcp`]?.[0];\n if (!binding?.HostPort) {\n throw new Error(\"Failed to resolve project container host port\");\n }\n\n const baseUrl = `http://127.0.0.1:${binding.HostPort}`;\n await waitForHealth(baseUrl, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.healthTimeoutMs,\n });\n\n return {\n runtimeId: inspect.Id,\n baseUrl,\n };\n },\n };\n}\n\nfunction containerName(projectId: string): string {\n return `keystroke-project-${projectId}`;\n}\n\nasync function removeExistingContainer(docker: Docker, projectId: string): Promise<void> {\n try {\n const existing = docker.getContainer(containerName(projectId));\n await existing.stop({ t: 5 }).catch(() => undefined);\n await existing.remove({ force: true });\n } catch {\n // no existing container\n }\n}\n\nasync function ensureImage(docker: Docker, image: string): Promise<void> {\n try {\n await docker.getImage(image).inspect();\n return;\n } catch {\n // pull below\n }\n\n await new Promise<void>((resolvePromise, reject) => {\n docker.pull(image, (error: Error | null, stream: NodeJS.ReadableStream | undefined) => {\n if (error) {\n reject(error);\n return;\n }\n\n if (!stream) {\n reject(new Error(`Failed to pull image ${image}`));\n return;\n }\n\n docker.modem.followProgress(stream, (progressError: Error | null) => {\n if (progressError) {\n reject(progressError);\n return;\n }\n\n resolvePromise();\n });\n });\n });\n}\n","import type { HostingPlugin } from \"../plugin\";\nimport { resolveWorkerRuntimeConfig } from \"../worker-runtime-config\";\n\nimport { createDockerRuntime } from \"./create-docker-runtime\";\nimport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport function dockerPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n const env = options.env ?? process.env;\n\n return {\n name: \"docker\",\n workerRuntime: resolveWorkerRuntimeConfig(env),\n createRuntime: () => createDockerRuntime({ ...options, env }),\n };\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport { dockerPlugin, type DockerPluginOptions } from \"./docker/plugin\";\n\n/** Local Docker hosting — default for platform and dev. */\nexport function defaultHostingPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n return dockerPlugin(options);\n}\n","/** Timeout for a single project runtime `/health` probe. */\nexport const RUNTIME_PING_TIMEOUT_MS = 15_000;\n","import { PROJECT_SERVER_HEALTH } from \"./constants\";\nimport type { HostingPlugin } from \"./plugin\";\nimport { RUNTIME_PING_TIMEOUT_MS } from \"./runtime-constants\";\n\nexport type PingProjectOptions = {\n runtimeId?: string | null;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n plugin?: Pick<HostingPlugin, \"pingRequestHeaders\">;\n};\n\nexport async function pingProject(\n baseUrl: string,\n options: PingProjectOptions = {},\n): Promise<boolean> {\n const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n const headers = options.plugin?.pingRequestHeaders?.(options.runtimeId ?? null) ?? {};\n\n try {\n const response = await (options.fetchImpl ?? fetch)(\n new URL(PROJECT_SERVER_HEALTH.path, baseUrl),\n {\n signal: AbortSignal.timeout(timeoutMs),\n headers,\n },\n );\n\n return response.ok;\n } catch {\n return false;\n }\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport type { ProjectPingTarget } from \"./runtime\";\nimport { pingProject, type PingProjectOptions } from \"./ping-project\";\n\nexport function canPingProjectTarget(\n target: ProjectPingTarget,\n plugin?: Pick<HostingPlugin, \"canPingTarget\">,\n): target is { baseUrl: string; runtimeId: string | null } {\n if (plugin?.canPingTarget) {\n return plugin.canPingTarget(target);\n }\n\n return !!target.baseUrl;\n}\n\nexport async function pingProjectTarget(\n target: ProjectPingTarget,\n options: PingProjectOptions & {\n plugin?: Pick<HostingPlugin, \"canPingTarget\" | \"pingRequestHeaders\">;\n } = {},\n): Promise<boolean> {\n if (!canPingProjectTarget(target, options.plugin)) {\n return false;\n }\n\n return pingProject(target.baseUrl, {\n ...options,\n runtimeId: target.runtimeId,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["Docker"],"sources":["../src/constants.ts","../src/runtime.ts","../src/runtime-env.ts","../src/worker-runtime-config.ts","../src/wait-for-health.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.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","export type ProjectRuntimeDatabase = {\n host: string;\n port: number;\n database: string;\n user: string;\n password: string;\n searchPath: string;\n organizationId: string;\n /** @deprecated Per-project workers only — org workers omit this. */\n projectId?: string;\n};\n\nexport type OrgProjectDeploy = {\n projectId: string;\n name: string;\n artifactId: string;\n artifactStorageKey: string;\n};\n\nexport type OrgArtifactSpec = {\n projectId: string;\n artifactStorageKey: string;\n};\n\nexport type ProjectRuntimeHosting = {\n appName: string;\n};\n\nexport type ProjectRuntimeInput = {\n organizationId: string;\n projects: OrgProjectDeploy[];\n database: ProjectRuntimeDatabase;\n hosting?: ProjectRuntimeHosting;\n /** Extra env merged into the project server launch spec (e.g. from an llmProxy plugin). */\n env?: Record<string, string>;\n /** Previous runtime to retire after a successful cutover. */\n previousRuntimeId?: string | null;\n};\n\n/** Builds base64 `KEYSTROKE_ORG_ARTIFACTS` payload for org worker containers. */\nexport function encodeOrgArtifacts(artifacts: OrgArtifactSpec[]): string {\n return Buffer.from(JSON.stringify(artifacts)).toString(\"base64\");\n}\n\nexport function parseOrgArtifactsFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): OrgArtifactSpec[] | undefined {\n const raw = env.KEYSTROKE_ORG_ARTIFACTS?.trim();\n if (!raw) {\n return undefined;\n }\n\n const parsed = JSON.parse(Buffer.from(raw, \"base64\").toString(\"utf8\")) as unknown;\n if (!Array.isArray(parsed)) {\n throw new Error(\"KEYSTROKE_ORG_ARTIFACTS must decode to a JSON array\");\n }\n\n return parsed.map((entry) => {\n if (\n !entry ||\n typeof entry !== \"object\" ||\n typeof (entry as OrgArtifactSpec).projectId !== \"string\" ||\n typeof (entry as OrgArtifactSpec).artifactStorageKey !== \"string\"\n ) {\n throw new Error(\"KEYSTROKE_ORG_ARTIFACTS entries require projectId and artifactStorageKey\");\n }\n\n return entry as OrgArtifactSpec;\n });\n}\n\nexport type ProjectRuntimeDestroyInput = {\n runtimeId: string;\n hosting?: ProjectRuntimeHosting;\n};\n\nexport type ProjectRuntimeResult = {\n runtimeId: string;\n baseUrl: string;\n};\n\nexport type ProjectRuntime = {\n start(input: ProjectRuntimeInput): Promise<ProjectRuntimeResult>;\n destroy?(input: ProjectRuntimeDestroyInput): Promise<void>;\n};\n\nexport type ProjectPingTarget = {\n baseUrl: string | null;\n runtimeId: string | null;\n};\n","import 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\";\n\nconst RUNTIME_ENV_KEYS = [\n \"BETTER_AUTH_SECRET\",\n \"PUBLIC_SERVER_URL\",\n \"PUBLIC_WEB_URL\",\n \"POSTGRES_SSL\",\n \"UPSTASH_REDIS_URL\",\n \"REDIS_URL\",\n \"KEYSTROKE_QUEUE_PREFIX\",\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 const PROJECT_DATABASE_ENV_KEYS = [\n \"POSTGRES_HOST\",\n \"POSTGRES_PORT\",\n \"POSTGRES_DB\",\n \"POSTGRES_USER\",\n \"POSTGRES_PASSWORD\",\n \"DATABASE_SEARCH_PATH\",\n] as const;\n\nexport type RuntimeLaunchSpec = {\n image: string;\n env: Record<string, string>;\n port: number;\n health: typeof PROJECT_SERVER_HEALTH;\n};\n\n/** Per-project postgres env injected into every project server container/machine. */\nexport function projectDatabaseRuntimeEnv(\n database: ProjectRuntimeDatabase,\n): Record<(typeof PROJECT_DATABASE_ENV_KEYS)[number], string> {\n return {\n POSTGRES_HOST: rewriteLoopbackHost(database.host),\n POSTGRES_PORT: String(database.port),\n POSTGRES_DB: database.database,\n POSTGRES_USER: database.user,\n POSTGRES_PASSWORD: database.password,\n DATABASE_SEARCH_PATH: database.searchPath,\n };\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 artifactStorageKey: string,\n database: ProjectRuntimeDatabase & { projectId: string },\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n return resolveRuntimeLaunch(\n options,\n [{ projectId: database.projectId, artifactStorageKey }],\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 if (key === \"POSTGRES_SSL\") {\n throw new Error(\"POSTGRES_SSL is required\");\n }\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 const worker = resolveWorkerRuntimeConfig(source);\n entries.set(\"PLATFORM_URL\", worker.platformUrl);\n entries.set(\"WORKER_INTERNAL_TOKEN\", worker.workerToken);\n\n for (const [key, value] of Object.entries(projectDatabaseRuntimeEnv(database))) {\n entries.set(key, value);\n }\n\n if (extraEnv) {\n for (const [key, value] of Object.entries(extraEnv)) {\n entries.set(key, value);\n }\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. */\nconst DEV_PLATFORM_URL = \"http://localhost:3002\";\n\nexport function resolveWorkerPlatformUrl(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const raw =\n source.PLATFORM_URL?.trim() ??\n source.PUBLIC_PLATFORM_URL?.trim() ??\n (source.NODE_ENV === \"production\" ? undefined : DEV_PLATFORM_URL);\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 { 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 { 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","import Docker from \"dockerode\";\n\nimport type { ProjectRuntime } from \"../runtime\";\n\nimport type { HostingOptions } from \"../plugin\";\nimport { formatDockerEnv, resolveRuntimeLaunch } from \"../runtime-env\";\nimport { waitForHealth } from \"../wait-for-health\";\n\nexport type DockerPluginOptions = HostingOptions & {\n docker?: Docker;\n};\n\nconst MINIO_DOCKER_NETWORK = process.env.MINIO_DOCKER_NETWORK ?? \"keystroke\";\n\nexport function createDockerRuntime(options: DockerPluginOptions = {}): ProjectRuntime {\n const docker = options.docker ?? new Docker();\n\n return {\n async start(input) {\n if (input.projects.length === 0) {\n throw new Error(\"Docker org runtime requires at least one project deploy\");\n }\n\n const launch = resolveRuntimeLaunch(\n options,\n input.projects.map((project) => ({\n projectId: project.projectId,\n artifactStorageKey: project.artifactStorageKey,\n })),\n input.database,\n input.env,\n );\n await ensureImage(docker, launch.image);\n await removeExistingContainer(docker, input.organizationId);\n\n const container = await docker.createContainer({\n Image: launch.image,\n name: containerName(input.organizationId),\n Labels: {\n \"keystroke.organization.id\": input.organizationId,\n \"keystroke.project.ids\": input.projects.map((project) => project.projectId).join(\",\"),\n },\n ExposedPorts: {\n [`${launch.port}/tcp`]: {},\n },\n HostConfig: {\n ExtraHosts: [\"host.docker.internal:host-gateway\"],\n PortBindings: {\n [`${launch.port}/tcp`]: [{ HostPort: \"0\" }],\n },\n },\n NetworkingConfig: {\n EndpointsConfig: {\n [MINIO_DOCKER_NETWORK]: {},\n },\n },\n Env: formatDockerEnv(launch.env),\n });\n\n await container.start();\n\n const inspect = await container.inspect();\n const binding = inspect.NetworkSettings.Ports?.[`${launch.port}/tcp`]?.[0];\n if (!binding?.HostPort) {\n throw new Error(\"Failed to resolve org container host port\");\n }\n\n const baseUrl = `http://127.0.0.1:${binding.HostPort}`;\n await waitForHealth(baseUrl, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.healthTimeoutMs,\n });\n\n return {\n runtimeId: inspect.Id,\n baseUrl,\n };\n },\n };\n}\n\nfunction containerName(organizationId: string): string {\n return `keystroke-org-${organizationId}`;\n}\n\nasync function removeExistingContainer(docker: Docker, organizationId: string): Promise<void> {\n try {\n const existing = docker.getContainer(containerName(organizationId));\n await existing.stop({ t: 5 }).catch(() => undefined);\n await existing.remove({ force: true });\n } catch {\n // no existing container\n }\n}\n\nasync function ensureImage(docker: Docker, image: string): Promise<void> {\n try {\n await docker.getImage(image).inspect();\n return;\n } catch {\n // pull below\n }\n\n await new Promise<void>((resolvePromise, reject) => {\n docker.pull(image, (error: Error | null, stream: NodeJS.ReadableStream | undefined) => {\n if (error) {\n reject(error);\n return;\n }\n\n if (!stream) {\n reject(new Error(`Failed to pull image ${image}`));\n return;\n }\n\n docker.modem.followProgress(stream, (progressError: Error | null) => {\n if (progressError) {\n reject(progressError);\n return;\n }\n\n resolvePromise();\n });\n });\n });\n}\n","import type { HostingPlugin } from \"../plugin\";\nimport { resolveWorkerRuntimeConfig } from \"../worker-runtime-config\";\n\nimport { createDockerRuntime } from \"./create-docker-runtime\";\nimport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport function dockerPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n const env = options.env ?? process.env;\n\n return {\n name: \"docker\",\n workerRuntime: resolveWorkerRuntimeConfig(env),\n createRuntime: () => createDockerRuntime({ ...options, env }),\n };\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport { dockerPlugin, type DockerPluginOptions } from \"./docker/plugin\";\n\n/** Local Docker hosting — default for platform and dev. */\nexport function defaultHostingPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n return dockerPlugin(options);\n}\n","/** Timeout for a single project runtime `/health` probe. */\nexport const RUNTIME_PING_TIMEOUT_MS = 15_000;\n","import { PROJECT_SERVER_HEALTH } from \"./constants\";\nimport type { HostingPlugin } from \"./plugin\";\nimport { RUNTIME_PING_TIMEOUT_MS } from \"./runtime-constants\";\n\nexport type PingProjectOptions = {\n runtimeId?: string | null;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n plugin?: Pick<HostingPlugin, \"pingRequestHeaders\">;\n};\n\nexport async function pingProject(\n baseUrl: string,\n options: PingProjectOptions = {},\n): Promise<boolean> {\n const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n const headers = options.plugin?.pingRequestHeaders?.(options.runtimeId ?? null) ?? {};\n\n try {\n const response = await (options.fetchImpl ?? fetch)(\n new URL(PROJECT_SERVER_HEALTH.path, baseUrl),\n {\n signal: AbortSignal.timeout(timeoutMs),\n headers,\n },\n );\n\n return response.ok;\n } catch {\n return false;\n }\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport type { ProjectPingTarget } from \"./runtime\";\nimport { pingProject, type PingProjectOptions } from \"./ping-project\";\n\nexport function canPingProjectTarget(\n target: ProjectPingTarget,\n plugin?: Pick<HostingPlugin, \"canPingTarget\">,\n): target is { baseUrl: string; runtimeId: string | null } {\n if (plugin?.canPingTarget) {\n return plugin.canPingTarget(target);\n }\n\n return !!target.baseUrl;\n}\n\nexport async function pingProjectTarget(\n target: ProjectPingTarget,\n options: PingProjectOptions & {\n plugin?: Pick<HostingPlugin, \"canPingTarget\" | \"pingRequestHeaders\">;\n } = {},\n): Promise<boolean> {\n if (!canPingProjectTarget(target, options.plugin)) {\n return false;\n }\n\n return pingProject(target.baseUrl, {\n ...options,\n runtimeId: target.runtimeId,\n });\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -19,22 +19,33 @@ type ProjectRuntimeDatabase = {
19
19
  user: string;
20
20
  password: string;
21
21
  searchPath: string;
22
+ organizationId: string; /** @deprecated Per-project workers only — org workers omit this. */
23
+ projectId?: string;
24
+ };
25
+ type OrgProjectDeploy = {
22
26
  projectId: string;
23
- organizationId: string;
27
+ name: string;
28
+ artifactId: string;
29
+ artifactStorageKey: string;
30
+ };
31
+ type OrgArtifactSpec = {
32
+ projectId: string;
33
+ artifactStorageKey: string;
24
34
  };
25
35
  type ProjectRuntimeHosting = {
26
36
  appName: string;
27
37
  };
28
38
  type ProjectRuntimeInput = {
29
- projectId: string;
30
- name: string;
31
- artifactId: string;
32
- artifactStorageKey: string;
39
+ organizationId: string;
40
+ projects: OrgProjectDeploy[];
33
41
  database: ProjectRuntimeDatabase;
34
42
  hosting?: ProjectRuntimeHosting; /** Extra env merged into the project server launch spec (e.g. from an llmProxy plugin). */
35
43
  env?: Record<string, string>; /** Previous runtime to retire after a successful cutover. */
36
44
  previousRuntimeId?: string | null;
37
45
  };
46
+ /** Builds base64 `KEYSTROKE_ORG_ARTIFACTS` payload for org worker containers. */
47
+ declare function encodeOrgArtifacts(artifacts: OrgArtifactSpec[]): string;
48
+ declare function parseOrgArtifactsFromEnv(env?: NodeJS.ProcessEnv): OrgArtifactSpec[] | undefined;
38
49
  type ProjectRuntimeDestroyInput = {
39
50
  runtimeId: string;
40
51
  hosting?: ProjectRuntimeHosting;
@@ -117,13 +128,12 @@ type RuntimeLaunchSpec = {
117
128
  /** Per-project postgres env injected into every project server container/machine. */
118
129
  declare function projectDatabaseRuntimeEnv(database: ProjectRuntimeDatabase): Record<(typeof PROJECT_DATABASE_ENV_KEYS)[number], string>;
119
130
  /**
120
- * Builds the launch spec for a single project server. `artifactStorageKey` is the
121
- * object key for that deploy's built `dist` tarball in the org bucket there is
122
- * no base image, so it is required.
131
+ * Builds the launch spec for an org worker. Each deploy artifact is encoded in
132
+ * `KEYSTROKE_ORG_ARTIFACTS` there is no base image, so at least one artifact is required.
123
133
  */
124
- declare function resolveRuntimeLaunch(options: HostingOptions, artifactStorageKey: string, database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): RuntimeLaunchSpec;
125
- /** Env vars passed into the project server container/machine. */
126
- declare function buildRuntimeEnv(source: NodeJS.ProcessEnv, artifactStorageKey: string, database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): Record<string, string>;
134
+ declare function resolveRuntimeLaunch(options: HostingOptions, artifacts: OrgArtifactSpec[], database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): RuntimeLaunchSpec;
135
+ /** Env vars passed into the org worker container/machine. */
136
+ declare function buildRuntimeEnv(source: NodeJS.ProcessEnv, artifacts: OrgArtifactSpec[], database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): Record<string, string>;
127
137
  declare function resolvePlatformWorkerToken(source?: NodeJS.ProcessEnv): string | undefined;
128
138
  declare function resolveWorkerPlatformUrl(source?: NodeJS.ProcessEnv): string | undefined;
129
139
  /** Rewrite loopback hosts so containers can reach Postgres and other host services. */
@@ -163,5 +173,5 @@ declare function pingProjectTarget(target: ProjectPingTarget, options?: PingProj
163
173
  plugin?: Pick<HostingPlugin, "canPingTarget" | "pingRequestHeaders">;
164
174
  }): Promise<boolean>;
165
175
  //#endregion
166
- export { DEV_PLATFORM_WORKER_TOKEN, type DockerPluginOptions, type HostingOptions, type HostingPlugin, type OrganizationHostingInput, type OrganizationHostingResult, PROJECT_DATABASE_ENV_KEYS, PROJECT_SERVER_FRAMEWORK_NODE_MODULES, PROJECT_SERVER_FRAMEWORK_ROOT, PROJECT_SERVER_HEALTH, PROJECT_SERVER_IMAGE, PROJECT_SERVER_PORT, PROJECT_SERVER_ROOT, type PingProjectOptions, type ProjectPingTarget, type ProjectRuntime, type ProjectRuntimeDatabase, type ProjectRuntimeDestroyInput, type ProjectRuntimeHosting, type ProjectRuntimeInput, type ProjectRuntimeResult, RUNTIME_PING_TIMEOUT_MS, type RuntimeLaunchSpec, type WaitForHealthOptions, type WorkerRuntimeConfig, WorkerRuntimeConfigError, buildRuntimeEnv, canPingProjectTarget, defaultHostingPlugin, dockerPlugin, formatDockerEnv, pingProject, pingProjectTarget, projectDatabaseRuntimeEnv, resolvePlatformWorkerToken, resolveProjectServerImage, resolveRuntimeLaunch, resolveWorkerPlatformUrl, resolveWorkerRuntimeConfig, rewriteLoopbackHost, rewriteLoopbackUrl, waitForHealth };
176
+ export { DEV_PLATFORM_WORKER_TOKEN, type DockerPluginOptions, type HostingOptions, type HostingPlugin, type OrgArtifactSpec, type OrgProjectDeploy, type OrganizationHostingInput, type OrganizationHostingResult, PROJECT_DATABASE_ENV_KEYS, PROJECT_SERVER_FRAMEWORK_NODE_MODULES, PROJECT_SERVER_FRAMEWORK_ROOT, PROJECT_SERVER_HEALTH, PROJECT_SERVER_IMAGE, PROJECT_SERVER_PORT, PROJECT_SERVER_ROOT, type PingProjectOptions, type ProjectPingTarget, type ProjectRuntime, type ProjectRuntimeDatabase, type ProjectRuntimeDestroyInput, type ProjectRuntimeHosting, type ProjectRuntimeInput, type ProjectRuntimeResult, RUNTIME_PING_TIMEOUT_MS, type RuntimeLaunchSpec, type WaitForHealthOptions, type WorkerRuntimeConfig, WorkerRuntimeConfigError, buildRuntimeEnv, canPingProjectTarget, defaultHostingPlugin, dockerPlugin, encodeOrgArtifacts, formatDockerEnv, parseOrgArtifactsFromEnv, pingProject, pingProjectTarget, projectDatabaseRuntimeEnv, resolvePlatformWorkerToken, resolveProjectServerImage, resolveRuntimeLaunch, resolveWorkerPlatformUrl, resolveWorkerRuntimeConfig, rewriteLoopbackHost, rewriteLoopbackUrl, waitForHealth };
167
177
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/constants.ts","../src/runtime.ts","../src/worker-runtime-config.ts","../src/plugin.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-env.ts","../src/wait-for-health.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/constants.ts","../src/runtime.ts","../src/worker-runtime-config.ts","../src/plugin.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-env.ts","../src/wait-for-health.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.ts"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -19,22 +19,33 @@ type ProjectRuntimeDatabase = {
19
19
  user: string;
20
20
  password: string;
21
21
  searchPath: string;
22
+ organizationId: string; /** @deprecated Per-project workers only — org workers omit this. */
23
+ projectId?: string;
24
+ };
25
+ type OrgProjectDeploy = {
22
26
  projectId: string;
23
- organizationId: string;
27
+ name: string;
28
+ artifactId: string;
29
+ artifactStorageKey: string;
30
+ };
31
+ type OrgArtifactSpec = {
32
+ projectId: string;
33
+ artifactStorageKey: string;
24
34
  };
25
35
  type ProjectRuntimeHosting = {
26
36
  appName: string;
27
37
  };
28
38
  type ProjectRuntimeInput = {
29
- projectId: string;
30
- name: string;
31
- artifactId: string;
32
- artifactStorageKey: string;
39
+ organizationId: string;
40
+ projects: OrgProjectDeploy[];
33
41
  database: ProjectRuntimeDatabase;
34
42
  hosting?: ProjectRuntimeHosting; /** Extra env merged into the project server launch spec (e.g. from an llmProxy plugin). */
35
43
  env?: Record<string, string>; /** Previous runtime to retire after a successful cutover. */
36
44
  previousRuntimeId?: string | null;
37
45
  };
46
+ /** Builds base64 `KEYSTROKE_ORG_ARTIFACTS` payload for org worker containers. */
47
+ declare function encodeOrgArtifacts(artifacts: OrgArtifactSpec[]): string;
48
+ declare function parseOrgArtifactsFromEnv(env?: NodeJS.ProcessEnv): OrgArtifactSpec[] | undefined;
38
49
  type ProjectRuntimeDestroyInput = {
39
50
  runtimeId: string;
40
51
  hosting?: ProjectRuntimeHosting;
@@ -117,13 +128,12 @@ type RuntimeLaunchSpec = {
117
128
  /** Per-project postgres env injected into every project server container/machine. */
118
129
  declare function projectDatabaseRuntimeEnv(database: ProjectRuntimeDatabase): Record<(typeof PROJECT_DATABASE_ENV_KEYS)[number], string>;
119
130
  /**
120
- * Builds the launch spec for a single project server. `artifactStorageKey` is the
121
- * object key for that deploy's built `dist` tarball in the org bucket there is
122
- * no base image, so it is required.
131
+ * Builds the launch spec for an org worker. Each deploy artifact is encoded in
132
+ * `KEYSTROKE_ORG_ARTIFACTS` there is no base image, so at least one artifact is required.
123
133
  */
124
- declare function resolveRuntimeLaunch(options: HostingOptions, artifactStorageKey: string, database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): RuntimeLaunchSpec;
125
- /** Env vars passed into the project server container/machine. */
126
- declare function buildRuntimeEnv(source: NodeJS.ProcessEnv, artifactStorageKey: string, database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): Record<string, string>;
134
+ declare function resolveRuntimeLaunch(options: HostingOptions, artifacts: OrgArtifactSpec[], database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): RuntimeLaunchSpec;
135
+ /** Env vars passed into the org worker container/machine. */
136
+ declare function buildRuntimeEnv(source: NodeJS.ProcessEnv, artifacts: OrgArtifactSpec[], database: ProjectRuntimeDatabase, extraEnv?: Record<string, string>): Record<string, string>;
127
137
  declare function resolvePlatformWorkerToken(source?: NodeJS.ProcessEnv): string | undefined;
128
138
  declare function resolveWorkerPlatformUrl(source?: NodeJS.ProcessEnv): string | undefined;
129
139
  /** Rewrite loopback hosts so containers can reach Postgres and other host services. */
@@ -163,5 +173,5 @@ declare function pingProjectTarget(target: ProjectPingTarget, options?: PingProj
163
173
  plugin?: Pick<HostingPlugin, "canPingTarget" | "pingRequestHeaders">;
164
174
  }): Promise<boolean>;
165
175
  //#endregion
166
- export { DEV_PLATFORM_WORKER_TOKEN, type DockerPluginOptions, type HostingOptions, type HostingPlugin, type OrganizationHostingInput, type OrganizationHostingResult, PROJECT_DATABASE_ENV_KEYS, PROJECT_SERVER_FRAMEWORK_NODE_MODULES, PROJECT_SERVER_FRAMEWORK_ROOT, PROJECT_SERVER_HEALTH, PROJECT_SERVER_IMAGE, PROJECT_SERVER_PORT, PROJECT_SERVER_ROOT, type PingProjectOptions, type ProjectPingTarget, type ProjectRuntime, type ProjectRuntimeDatabase, type ProjectRuntimeDestroyInput, type ProjectRuntimeHosting, type ProjectRuntimeInput, type ProjectRuntimeResult, RUNTIME_PING_TIMEOUT_MS, type RuntimeLaunchSpec, type WaitForHealthOptions, type WorkerRuntimeConfig, WorkerRuntimeConfigError, buildRuntimeEnv, canPingProjectTarget, defaultHostingPlugin, dockerPlugin, formatDockerEnv, pingProject, pingProjectTarget, projectDatabaseRuntimeEnv, resolvePlatformWorkerToken, resolveProjectServerImage, resolveRuntimeLaunch, resolveWorkerPlatformUrl, resolveWorkerRuntimeConfig, rewriteLoopbackHost, rewriteLoopbackUrl, waitForHealth };
176
+ export { DEV_PLATFORM_WORKER_TOKEN, type DockerPluginOptions, type HostingOptions, type HostingPlugin, type OrgArtifactSpec, type OrgProjectDeploy, type OrganizationHostingInput, type OrganizationHostingResult, PROJECT_DATABASE_ENV_KEYS, PROJECT_SERVER_FRAMEWORK_NODE_MODULES, PROJECT_SERVER_FRAMEWORK_ROOT, PROJECT_SERVER_HEALTH, PROJECT_SERVER_IMAGE, PROJECT_SERVER_PORT, PROJECT_SERVER_ROOT, type PingProjectOptions, type ProjectPingTarget, type ProjectRuntime, type ProjectRuntimeDatabase, type ProjectRuntimeDestroyInput, type ProjectRuntimeHosting, type ProjectRuntimeInput, type ProjectRuntimeResult, RUNTIME_PING_TIMEOUT_MS, type RuntimeLaunchSpec, type WaitForHealthOptions, type WorkerRuntimeConfig, WorkerRuntimeConfigError, buildRuntimeEnv, canPingProjectTarget, defaultHostingPlugin, dockerPlugin, encodeOrgArtifacts, formatDockerEnv, parseOrgArtifactsFromEnv, pingProject, pingProjectTarget, projectDatabaseRuntimeEnv, resolvePlatformWorkerToken, resolveProjectServerImage, resolveRuntimeLaunch, resolveWorkerPlatformUrl, resolveWorkerRuntimeConfig, rewriteLoopbackHost, rewriteLoopbackUrl, waitForHealth };
167
177
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/constants.ts","../src/runtime.ts","../src/worker-runtime-config.ts","../src/plugin.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-env.ts","../src/wait-for-health.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.ts"],"mappings":";;;cAAa,oBAAA;AAAA,cAEA,mBAAA;AAAA,cAEA,mBAAA;AAAA,cAEA,6BAAA;AAAA,cAEA,qCAAA;AAAA,cAEA,qBAAA;EAAA,SAGH,IAAA;EAAA,SAAA,IAAA;AAAA;;;KCbE,sBAAA;EACV,IAAA;EACA,IAAA;EACA,QAAA;EACA,IAAA;EACA,QAAA;EACA,UAAA;EACA,SAAA;EACA,cAAA;AAAA;AAAA,KAGU,qBAAA;EACV,OAAO;AAAA;AAAA,KAGG,mBAAA;EACV,SAAA;EACA,IAAA;EACA,UAAA;EACA,kBAAA;EACA,QAAA,EAAU,sBAAA;EACV,OAAA,GAAU,qBAAA;EAEV,GAAA,GAAM,MAAA,kBDjBkC;ECmBxC,iBAAA;AAAA;AAAA,KAGU,0BAAA;EACV,SAAA;EACA,OAAA,GAAU,qBAAqB;AAAA;AAAA,KAGrB,oBAAA;EACV,SAAA;EACA,OAAO;AAAA;AAAA,KAGG,cAAA;EACV,KAAA,CAAM,KAAA,EAAO,mBAAA,GAAsB,OAAA,CAAQ,oBAAA;EAC3C,OAAA,EAAS,KAAA,EAAO,0BAAA,GAA6B,OAAA;AAAA;AAAA,KAGnC,iBAAA;EACV,OAAA;EACA,SAAS;AAAA;;;;KC1CC,mBAAA;yEAEV,WAAA,UFL+B;EEO/B,WAAW;AAAA;AAAA,cAGA,wBAAA,SAAiC,KAAK;EAAA,SACxC,OAAA;cAEG,OAAA;AAAA;;iBAWE,0BAAA,CACd,MAAA,GAAQ,MAAA,CAAO,UAAA,GACd,mBAAmB;;;KCvBV,cAAA;EACV,KAAA;EACA,GAAA,GAAM,MAAA,CAAO,UAAA;EACb,SAAA,UAAmB,KAAK;EACxB,eAAA;AAAA;AAAA,KAGU,wBAAA;EACV,cAAA;EACA,IAAI;AAAA;AAAA,KAGM,yBAAA;EACV,OAAO;AAAA;AAAA,KAGG,aAAA;EACV,IAAA,UHhB8B;EGkB9B,aAAA,EAAe,mBAAA;EACf,aAAA,IAAiB,cAAA;EACjB,qBAAA,EAAuB,KAAA,EAAO,wBAAA,GAA2B,OAAA,CAAQ,yBAAA;EACjE,uBAAA,EAAyB,MAAA,EAAQ,yBAAA,GAA4B,OAAA;EAC7D,aAAA,EACE,MAAA,EAAQ,iBAAA,GACP,MAAA;IAAY,OAAA;IAAiB,SAAA;EAAA;EAChC,kBAAA,EAAoB,SAAA,kBAA2B,MAAA;AAAA;;;KCrBrC,mBAAA,GAAsB,cAAA;EAChC,MAAA,GAAS,MAAM;AAAA;;;iBCDD,YAAA,CAAa,OAAA,GAAS,mBAAA,GAA2B,aAAa;;;;iBCJ9D,oBAAA,CAAqB,OAAA,GAAS,mBAAA,GAA2B,aAAa;;;ANJtF;AAAA,cO8Ba,yBAAA;AAAA,cAEA,yBAAA;AAAA,KASD,iBAAA;EACV,KAAA;EACA,GAAA,EAAK,MAAA;EACL,IAAA;EACA,MAAA,SAAe,qBAAqB;AAAA;AP3CN;AAAA,iBO+ChB,yBAAA,CACd,QAAA,EAAU,sBAAA,GACT,MAAA,SAAe,yBAAA;;;;AP/Cc;AAEhC;iBO6DgB,oBAAA,CACd,OAAA,EAAS,cAAA,EACT,kBAAA,UACA,QAAA,EAAU,sBAAA,EACV,QAAA,GAAW,MAAA,mBACV,iBAAA;;iBAgBa,eAAA,CACd,MAAA,EAAQ,MAAA,CAAO,UAAA,EACf,kBAAA,UACA,QAAA,EAAU,sBAAA,EACV,QAAA,GAAW,MAAA,mBACV,MAAA;AAAA,iBAoDa,0BAAA,CACd,MAAA,GAAQ,MAAA,CAAO,UAAwB;AAAA,iBAiBzB,wBAAA,CACd,MAAA,GAAQ,MAAA,CAAO,UAAwB;;iBAczB,mBAAA,CAAoB,IAAY;AAAA,iBAQhC,kBAAA,CAAmB,GAAW;APlLI;AAAA,iBO6LlC,eAAA,CAAgB,GAA2B,EAAtB,MAAM;AAAA,iBAI3B,yBAAA,CACd,GAAA,GAAK,MAAA,CAAO,UAAwB,EACpC,QAAA;;;KCtMU,oBAAA;EACV,SAAA;EACA,UAAA;EACA,SAAA,UAAmB,KAAK;AAAA;AAAA,iBAGJ,aAAA,CACpB,OAAA,UACA,OAAA,GAAS,oBAAA,GACR,OAAO;;;;cCbG,uBAAA;;;KCGD,kBAAA;EACV,SAAA;EACA,SAAA,UAAmB,KAAA;EACnB,SAAA;EACA,MAAA,GAAS,IAAA,CAAK,aAAA;AAAA;AAAA,iBAGM,WAAA,CACpB,OAAA,UACA,OAAA,GAAS,kBAAA,GACR,OAAO;;;iBCVM,oBAAA,CACd,MAAA,EAAQ,iBAAA,EACR,MAAA,GAAS,IAAA,CAAK,aAAA,qBACb,MAAA;EAAY,OAAA;EAAiB,SAAA;AAAA;AAAA,iBAQV,iBAAA,CACpB,MAAA,EAAQ,iBAAA,EACR,OAAA,GAAS,kBAAA;EACP,MAAA,GAAS,IAAA,CAAK,aAAA;AAAA,IAEf,OAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/constants.ts","../src/runtime.ts","../src/worker-runtime-config.ts","../src/plugin.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-env.ts","../src/wait-for-health.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -10,6 +10,22 @@ const PROJECT_SERVER_HEALTH = {
10
10
  port: PROJECT_SERVER_PORT
11
11
  };
12
12
  //#endregion
13
+ //#region src/runtime.ts
14
+ /** Builds base64 `KEYSTROKE_ORG_ARTIFACTS` payload for org worker containers. */
15
+ function encodeOrgArtifacts(artifacts) {
16
+ return Buffer.from(JSON.stringify(artifacts)).toString("base64");
17
+ }
18
+ function parseOrgArtifactsFromEnv(env = process.env) {
19
+ const raw = env.KEYSTROKE_ORG_ARTIFACTS?.trim();
20
+ if (!raw) return;
21
+ const parsed = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
22
+ if (!Array.isArray(parsed)) throw new Error("KEYSTROKE_ORG_ARTIFACTS must decode to a JSON array");
23
+ return parsed.map((entry) => {
24
+ if (!entry || typeof entry !== "object" || typeof entry.projectId !== "string" || typeof entry.artifactStorageKey !== "string") throw new Error("KEYSTROKE_ORG_ARTIFACTS entries require projectId and artifactStorageKey");
25
+ return entry;
26
+ });
27
+ }
28
+ //#endregion
13
29
  //#region src/runtime-env.ts
14
30
  const RUNTIME_ENV_KEYS = [
15
31
  "BETTER_AUTH_SECRET",
@@ -49,28 +65,26 @@ function projectDatabaseRuntimeEnv(database) {
49
65
  };
50
66
  }
51
67
  /**
52
- * Builds the launch spec for a single project server. `artifactStorageKey` is the
53
- * object key for that deploy's built `dist` tarball in the org bucket there is
54
- * no base image, so it is required.
68
+ * Builds the launch spec for an org worker. Each deploy artifact is encoded in
69
+ * `KEYSTROKE_ORG_ARTIFACTS` there is no base image, so at least one artifact is required.
55
70
  */
56
- function resolveRuntimeLaunch(options, artifactStorageKey, database, extraEnv) {
57
- if (!artifactStorageKey) throw new Error("Cannot start a project server without an artifact storage key");
71
+ function resolveRuntimeLaunch(options, artifacts, database, extraEnv) {
72
+ if (artifacts.length === 0) throw new Error("Cannot start an org worker without at least one artifact");
58
73
  const env = options.env ?? process.env;
59
74
  return {
60
75
  image: resolveProjectServerImage(env, options.image),
61
- env: buildRuntimeEnv(env, artifactStorageKey, database, extraEnv),
76
+ env: buildRuntimeEnv(env, artifacts, database, extraEnv),
62
77
  port: PROJECT_SERVER_PORT,
63
78
  health: PROJECT_SERVER_HEALTH
64
79
  };
65
80
  }
66
- /** Env vars passed into the project server container/machine. */
67
- function buildRuntimeEnv(source, artifactStorageKey, database, extraEnv) {
81
+ /** Env vars passed into the org worker container/machine. */
82
+ function buildRuntimeEnv(source, artifacts, database, extraEnv) {
68
83
  const entries = new Map([
69
84
  ["PORT", String(PROJECT_SERVER_PORT)],
70
85
  ["KEYSTROKE_ROOT", PROJECT_SERVER_ROOT],
71
86
  ["KEYSTROKE_RUNTIME_NODE_MODULES", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],
72
- ["ARTIFACT_STORAGE_KEY", artifactStorageKey],
73
- ["PROJECT_ID", database.projectId],
87
+ ["KEYSTROKE_ORG_ARTIFACTS", encodeOrgArtifacts(artifacts)],
74
88
  ["ORGANIZATION_ID", database.organizationId],
75
89
  ["TENANT_ID", database.organizationId]
76
90
  ]);
@@ -179,15 +193,19 @@ const MINIO_DOCKER_NETWORK = process.env.MINIO_DOCKER_NETWORK ?? "keystroke";
179
193
  function createDockerRuntime(options = {}) {
180
194
  const docker = options.docker ?? new Docker();
181
195
  return { async start(input) {
182
- const launch = resolveRuntimeLaunch(options, input.artifactStorageKey, input.database, input.env);
196
+ if (input.projects.length === 0) throw new Error("Docker org runtime requires at least one project deploy");
197
+ const launch = resolveRuntimeLaunch(options, input.projects.map((project) => ({
198
+ projectId: project.projectId,
199
+ artifactStorageKey: project.artifactStorageKey
200
+ })), input.database, input.env);
183
201
  await ensureImage(docker, launch.image);
184
- await removeExistingContainer(docker, input.projectId);
202
+ await removeExistingContainer(docker, input.organizationId);
185
203
  const container = await docker.createContainer({
186
204
  Image: launch.image,
187
- name: containerName(input.projectId),
205
+ name: containerName(input.organizationId),
188
206
  Labels: {
189
- "keystroke.project.id": input.projectId,
190
- "keystroke.project.name": input.name
207
+ "keystroke.organization.id": input.organizationId,
208
+ "keystroke.project.ids": input.projects.map((project) => project.projectId).join(",")
191
209
  },
192
210
  ExposedPorts: { [`${launch.port}/tcp`]: {} },
193
211
  HostConfig: {
@@ -200,7 +218,7 @@ function createDockerRuntime(options = {}) {
200
218
  await container.start();
201
219
  const inspect = await container.inspect();
202
220
  const binding = inspect.NetworkSettings.Ports?.[`${launch.port}/tcp`]?.[0];
203
- if (!binding?.HostPort) throw new Error("Failed to resolve project container host port");
221
+ if (!binding?.HostPort) throw new Error("Failed to resolve org container host port");
204
222
  const baseUrl = `http://127.0.0.1:${binding.HostPort}`;
205
223
  await waitForHealth(baseUrl, {
206
224
  fetchImpl: options.fetchImpl,
@@ -212,12 +230,12 @@ function createDockerRuntime(options = {}) {
212
230
  };
213
231
  } };
214
232
  }
215
- function containerName(projectId) {
216
- return `keystroke-project-${projectId}`;
233
+ function containerName(organizationId) {
234
+ return `keystroke-org-${organizationId}`;
217
235
  }
218
- async function removeExistingContainer(docker, projectId) {
236
+ async function removeExistingContainer(docker, organizationId) {
219
237
  try {
220
- const existing = docker.getContainer(containerName(projectId));
238
+ const existing = docker.getContainer(containerName(organizationId));
221
239
  await existing.stop({ t: 5 }).catch(() => void 0);
222
240
  await existing.remove({ force: true });
223
241
  } catch {}
@@ -298,6 +316,6 @@ async function pingProjectTarget(target, options = {}) {
298
316
  });
299
317
  }
300
318
  //#endregion
301
- export { DEV_PLATFORM_WORKER_TOKEN, PROJECT_DATABASE_ENV_KEYS, PROJECT_SERVER_FRAMEWORK_NODE_MODULES, PROJECT_SERVER_FRAMEWORK_ROOT, PROJECT_SERVER_HEALTH, PROJECT_SERVER_IMAGE, PROJECT_SERVER_PORT, PROJECT_SERVER_ROOT, RUNTIME_PING_TIMEOUT_MS, WorkerRuntimeConfigError, buildRuntimeEnv, canPingProjectTarget, defaultHostingPlugin, dockerPlugin, formatDockerEnv, pingProject, pingProjectTarget, projectDatabaseRuntimeEnv, resolvePlatformWorkerToken, resolveProjectServerImage, resolveRuntimeLaunch, resolveWorkerPlatformUrl, resolveWorkerRuntimeConfig, rewriteLoopbackHost, rewriteLoopbackUrl, waitForHealth };
319
+ export { DEV_PLATFORM_WORKER_TOKEN, PROJECT_DATABASE_ENV_KEYS, PROJECT_SERVER_FRAMEWORK_NODE_MODULES, PROJECT_SERVER_FRAMEWORK_ROOT, PROJECT_SERVER_HEALTH, PROJECT_SERVER_IMAGE, PROJECT_SERVER_PORT, PROJECT_SERVER_ROOT, RUNTIME_PING_TIMEOUT_MS, WorkerRuntimeConfigError, buildRuntimeEnv, canPingProjectTarget, defaultHostingPlugin, dockerPlugin, encodeOrgArtifacts, formatDockerEnv, parseOrgArtifactsFromEnv, pingProject, pingProjectTarget, projectDatabaseRuntimeEnv, resolvePlatformWorkerToken, resolveProjectServerImage, resolveRuntimeLaunch, resolveWorkerPlatformUrl, resolveWorkerRuntimeConfig, rewriteLoopbackHost, rewriteLoopbackUrl, waitForHealth };
302
320
 
303
321
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/runtime-env.ts","../src/worker-runtime-config.ts","../src/wait-for-health.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.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","import type { ProjectRuntimeDatabase } 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\";\n\nconst RUNTIME_ENV_KEYS = [\n \"BETTER_AUTH_SECRET\",\n \"PUBLIC_SERVER_URL\",\n \"PUBLIC_WEB_URL\",\n \"POSTGRES_SSL\",\n \"UPSTASH_REDIS_URL\",\n \"REDIS_URL\",\n \"KEYSTROKE_QUEUE_PREFIX\",\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 const PROJECT_DATABASE_ENV_KEYS = [\n \"POSTGRES_HOST\",\n \"POSTGRES_PORT\",\n \"POSTGRES_DB\",\n \"POSTGRES_USER\",\n \"POSTGRES_PASSWORD\",\n \"DATABASE_SEARCH_PATH\",\n] as const;\n\nexport type RuntimeLaunchSpec = {\n image: string;\n env: Record<string, string>;\n port: number;\n health: typeof PROJECT_SERVER_HEALTH;\n};\n\n/** Per-project postgres env injected into every project server container/machine. */\nexport function projectDatabaseRuntimeEnv(\n database: ProjectRuntimeDatabase,\n): Record<(typeof PROJECT_DATABASE_ENV_KEYS)[number], string> {\n return {\n POSTGRES_HOST: rewriteLoopbackHost(database.host),\n POSTGRES_PORT: String(database.port),\n POSTGRES_DB: database.database,\n POSTGRES_USER: database.user,\n POSTGRES_PASSWORD: database.password,\n DATABASE_SEARCH_PATH: database.searchPath,\n };\n}\n\n/**\n * Builds the launch spec for a single project server. `artifactStorageKey` is the\n * object key for that deploy's built `dist` tarball in the org bucket — there is\n * no base image, so it is required.\n */\nexport function resolveRuntimeLaunch(\n options: HostingOptions,\n artifactStorageKey: string,\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n if (!artifactStorageKey) {\n throw new Error(\"Cannot start a project server without an artifact storage key\");\n }\n\n const env = options.env ?? process.env;\n\n return {\n image: resolveProjectServerImage(env, options.image),\n env: buildRuntimeEnv(env, artifactStorageKey, database, extraEnv),\n port: PROJECT_SERVER_PORT,\n health: PROJECT_SERVER_HEALTH,\n };\n}\n\n/** Env vars passed into the project server container/machine. */\nexport function buildRuntimeEnv(\n source: NodeJS.ProcessEnv,\n artifactStorageKey: string,\n database: ProjectRuntimeDatabase,\n extraEnv?: Record<string, string>,\n): Record<string, string> {\n const entries = new Map<string, string>([\n // Container bind port — not derived from PUBLIC_SERVER_URL (external URL metadata).\n [\"PORT\", String(PROJECT_SERVER_PORT)],\n [\"KEYSTROKE_ROOT\", PROJECT_SERVER_ROOT],\n [\"KEYSTROKE_RUNTIME_NODE_MODULES\", PROJECT_SERVER_FRAMEWORK_NODE_MODULES],\n [\"ARTIFACT_STORAGE_KEY\", artifactStorageKey],\n [\"PROJECT_ID\", database.projectId],\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 if (key === \"POSTGRES_SSL\") {\n throw new Error(\"POSTGRES_SSL is required\");\n }\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 const worker = resolveWorkerRuntimeConfig(source);\n entries.set(\"PLATFORM_URL\", worker.platformUrl);\n entries.set(\"WORKER_INTERNAL_TOKEN\", worker.workerToken);\n\n for (const [key, value] of Object.entries(projectDatabaseRuntimeEnv(database))) {\n entries.set(key, value);\n }\n\n if (extraEnv) {\n for (const [key, value] of Object.entries(extraEnv)) {\n entries.set(key, value);\n }\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. */\nconst DEV_PLATFORM_URL = \"http://localhost:3002\";\n\nexport function resolveWorkerPlatformUrl(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const raw =\n source.PLATFORM_URL?.trim() ??\n source.PUBLIC_PLATFORM_URL?.trim() ??\n (source.NODE_ENV === \"production\" ? undefined : DEV_PLATFORM_URL);\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 { 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 { 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","import Docker from \"dockerode\";\n\nimport type { ProjectRuntime } from \"../runtime\";\n\nimport type { HostingOptions } from \"../plugin\";\nimport { formatDockerEnv, resolveRuntimeLaunch } from \"../runtime-env\";\nimport { waitForHealth } from \"../wait-for-health\";\n\nexport type DockerPluginOptions = HostingOptions & {\n docker?: Docker;\n};\n\nconst MINIO_DOCKER_NETWORK = process.env.MINIO_DOCKER_NETWORK ?? \"keystroke\";\n\nexport function createDockerRuntime(options: DockerPluginOptions = {}): ProjectRuntime {\n const docker = options.docker ?? new Docker();\n\n return {\n async start(input) {\n const launch = resolveRuntimeLaunch(\n options,\n input.artifactStorageKey,\n input.database,\n input.env,\n );\n await ensureImage(docker, launch.image);\n await removeExistingContainer(docker, input.projectId);\n\n const container = await docker.createContainer({\n Image: launch.image,\n name: containerName(input.projectId),\n Labels: {\n \"keystroke.project.id\": input.projectId,\n \"keystroke.project.name\": input.name,\n },\n ExposedPorts: {\n [`${launch.port}/tcp`]: {},\n },\n HostConfig: {\n ExtraHosts: [\"host.docker.internal:host-gateway\"],\n PortBindings: {\n [`${launch.port}/tcp`]: [{ HostPort: \"0\" }],\n },\n },\n NetworkingConfig: {\n EndpointsConfig: {\n [MINIO_DOCKER_NETWORK]: {},\n },\n },\n Env: formatDockerEnv(launch.env),\n });\n\n await container.start();\n\n const inspect = await container.inspect();\n const binding = inspect.NetworkSettings.Ports?.[`${launch.port}/tcp`]?.[0];\n if (!binding?.HostPort) {\n throw new Error(\"Failed to resolve project container host port\");\n }\n\n const baseUrl = `http://127.0.0.1:${binding.HostPort}`;\n await waitForHealth(baseUrl, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.healthTimeoutMs,\n });\n\n return {\n runtimeId: inspect.Id,\n baseUrl,\n };\n },\n };\n}\n\nfunction containerName(projectId: string): string {\n return `keystroke-project-${projectId}`;\n}\n\nasync function removeExistingContainer(docker: Docker, projectId: string): Promise<void> {\n try {\n const existing = docker.getContainer(containerName(projectId));\n await existing.stop({ t: 5 }).catch(() => undefined);\n await existing.remove({ force: true });\n } catch {\n // no existing container\n }\n}\n\nasync function ensureImage(docker: Docker, image: string): Promise<void> {\n try {\n await docker.getImage(image).inspect();\n return;\n } catch {\n // pull below\n }\n\n await new Promise<void>((resolvePromise, reject) => {\n docker.pull(image, (error: Error | null, stream: NodeJS.ReadableStream | undefined) => {\n if (error) {\n reject(error);\n return;\n }\n\n if (!stream) {\n reject(new Error(`Failed to pull image ${image}`));\n return;\n }\n\n docker.modem.followProgress(stream, (progressError: Error | null) => {\n if (progressError) {\n reject(progressError);\n return;\n }\n\n resolvePromise();\n });\n });\n });\n}\n","import type { HostingPlugin } from \"../plugin\";\nimport { resolveWorkerRuntimeConfig } from \"../worker-runtime-config\";\n\nimport { createDockerRuntime } from \"./create-docker-runtime\";\nimport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport function dockerPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n const env = options.env ?? process.env;\n\n return {\n name: \"docker\",\n workerRuntime: resolveWorkerRuntimeConfig(env),\n createRuntime: () => createDockerRuntime({ ...options, env }),\n };\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport { dockerPlugin, type DockerPluginOptions } from \"./docker/plugin\";\n\n/** Local Docker hosting — default for platform and dev. */\nexport function defaultHostingPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n return dockerPlugin(options);\n}\n","/** Timeout for a single project runtime `/health` probe. */\nexport const RUNTIME_PING_TIMEOUT_MS = 15_000;\n","import { PROJECT_SERVER_HEALTH } from \"./constants\";\nimport type { HostingPlugin } from \"./plugin\";\nimport { RUNTIME_PING_TIMEOUT_MS } from \"./runtime-constants\";\n\nexport type PingProjectOptions = {\n runtimeId?: string | null;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n plugin?: Pick<HostingPlugin, \"pingRequestHeaders\">;\n};\n\nexport async function pingProject(\n baseUrl: string,\n options: PingProjectOptions = {},\n): Promise<boolean> {\n const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n const headers = options.plugin?.pingRequestHeaders?.(options.runtimeId ?? null) ?? {};\n\n try {\n const response = await (options.fetchImpl ?? fetch)(\n new URL(PROJECT_SERVER_HEALTH.path, baseUrl),\n {\n signal: AbortSignal.timeout(timeoutMs),\n headers,\n },\n );\n\n return response.ok;\n } catch {\n return false;\n }\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport type { ProjectPingTarget } from \"./runtime\";\nimport { pingProject, type PingProjectOptions } from \"./ping-project\";\n\nexport function canPingProjectTarget(\n target: ProjectPingTarget,\n plugin?: Pick<HostingPlugin, \"canPingTarget\">,\n): target is { baseUrl: string; runtimeId: string | null } {\n if (plugin?.canPingTarget) {\n return plugin.canPingTarget(target);\n }\n\n return !!target.baseUrl;\n}\n\nexport async function pingProjectTarget(\n target: ProjectPingTarget,\n options: PingProjectOptions & {\n plugin?: Pick<HostingPlugin, \"canPingTarget\" | \"pingRequestHeaders\">;\n } = {},\n): Promise<boolean> {\n if (!canPingProjectTarget(target, options.plugin)) {\n return false;\n }\n\n return pingProject(target.baseUrl, {\n ...options,\n runtimeId: target.runtimeId,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/runtime.ts","../src/runtime-env.ts","../src/worker-runtime-config.ts","../src/wait-for-health.ts","../src/docker/create-docker-runtime.ts","../src/docker/plugin.ts","../src/default-plugin.ts","../src/runtime-constants.ts","../src/ping-project.ts","../src/ping-project-target.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","export type ProjectRuntimeDatabase = {\n host: string;\n port: number;\n database: string;\n user: string;\n password: string;\n searchPath: string;\n organizationId: string;\n /** @deprecated Per-project workers only — org workers omit this. */\n projectId?: string;\n};\n\nexport type OrgProjectDeploy = {\n projectId: string;\n name: string;\n artifactId: string;\n artifactStorageKey: string;\n};\n\nexport type OrgArtifactSpec = {\n projectId: string;\n artifactStorageKey: string;\n};\n\nexport type ProjectRuntimeHosting = {\n appName: string;\n};\n\nexport type ProjectRuntimeInput = {\n organizationId: string;\n projects: OrgProjectDeploy[];\n database: ProjectRuntimeDatabase;\n hosting?: ProjectRuntimeHosting;\n /** Extra env merged into the project server launch spec (e.g. from an llmProxy plugin). */\n env?: Record<string, string>;\n /** Previous runtime to retire after a successful cutover. */\n previousRuntimeId?: string | null;\n};\n\n/** Builds base64 `KEYSTROKE_ORG_ARTIFACTS` payload for org worker containers. */\nexport function encodeOrgArtifacts(artifacts: OrgArtifactSpec[]): string {\n return Buffer.from(JSON.stringify(artifacts)).toString(\"base64\");\n}\n\nexport function parseOrgArtifactsFromEnv(\n env: NodeJS.ProcessEnv = process.env,\n): OrgArtifactSpec[] | undefined {\n const raw = env.KEYSTROKE_ORG_ARTIFACTS?.trim();\n if (!raw) {\n return undefined;\n }\n\n const parsed = JSON.parse(Buffer.from(raw, \"base64\").toString(\"utf8\")) as unknown;\n if (!Array.isArray(parsed)) {\n throw new Error(\"KEYSTROKE_ORG_ARTIFACTS must decode to a JSON array\");\n }\n\n return parsed.map((entry) => {\n if (\n !entry ||\n typeof entry !== \"object\" ||\n typeof (entry as OrgArtifactSpec).projectId !== \"string\" ||\n typeof (entry as OrgArtifactSpec).artifactStorageKey !== \"string\"\n ) {\n throw new Error(\"KEYSTROKE_ORG_ARTIFACTS entries require projectId and artifactStorageKey\");\n }\n\n return entry as OrgArtifactSpec;\n });\n}\n\nexport type ProjectRuntimeDestroyInput = {\n runtimeId: string;\n hosting?: ProjectRuntimeHosting;\n};\n\nexport type ProjectRuntimeResult = {\n runtimeId: string;\n baseUrl: string;\n};\n\nexport type ProjectRuntime = {\n start(input: ProjectRuntimeInput): Promise<ProjectRuntimeResult>;\n destroy?(input: ProjectRuntimeDestroyInput): Promise<void>;\n};\n\nexport type ProjectPingTarget = {\n baseUrl: string | null;\n runtimeId: string | null;\n};\n","import 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\";\n\nconst RUNTIME_ENV_KEYS = [\n \"BETTER_AUTH_SECRET\",\n \"PUBLIC_SERVER_URL\",\n \"PUBLIC_WEB_URL\",\n \"POSTGRES_SSL\",\n \"UPSTASH_REDIS_URL\",\n \"REDIS_URL\",\n \"KEYSTROKE_QUEUE_PREFIX\",\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 const PROJECT_DATABASE_ENV_KEYS = [\n \"POSTGRES_HOST\",\n \"POSTGRES_PORT\",\n \"POSTGRES_DB\",\n \"POSTGRES_USER\",\n \"POSTGRES_PASSWORD\",\n \"DATABASE_SEARCH_PATH\",\n] as const;\n\nexport type RuntimeLaunchSpec = {\n image: string;\n env: Record<string, string>;\n port: number;\n health: typeof PROJECT_SERVER_HEALTH;\n};\n\n/** Per-project postgres env injected into every project server container/machine. */\nexport function projectDatabaseRuntimeEnv(\n database: ProjectRuntimeDatabase,\n): Record<(typeof PROJECT_DATABASE_ENV_KEYS)[number], string> {\n return {\n POSTGRES_HOST: rewriteLoopbackHost(database.host),\n POSTGRES_PORT: String(database.port),\n POSTGRES_DB: database.database,\n POSTGRES_USER: database.user,\n POSTGRES_PASSWORD: database.password,\n DATABASE_SEARCH_PATH: database.searchPath,\n };\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 artifactStorageKey: string,\n database: ProjectRuntimeDatabase & { projectId: string },\n extraEnv?: Record<string, string>,\n): RuntimeLaunchSpec {\n return resolveRuntimeLaunch(\n options,\n [{ projectId: database.projectId, artifactStorageKey }],\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 if (key === \"POSTGRES_SSL\") {\n throw new Error(\"POSTGRES_SSL is required\");\n }\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 const worker = resolveWorkerRuntimeConfig(source);\n entries.set(\"PLATFORM_URL\", worker.platformUrl);\n entries.set(\"WORKER_INTERNAL_TOKEN\", worker.workerToken);\n\n for (const [key, value] of Object.entries(projectDatabaseRuntimeEnv(database))) {\n entries.set(key, value);\n }\n\n if (extraEnv) {\n for (const [key, value] of Object.entries(extraEnv)) {\n entries.set(key, value);\n }\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. */\nconst DEV_PLATFORM_URL = \"http://localhost:3002\";\n\nexport function resolveWorkerPlatformUrl(\n source: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n const raw =\n source.PLATFORM_URL?.trim() ??\n source.PUBLIC_PLATFORM_URL?.trim() ??\n (source.NODE_ENV === \"production\" ? undefined : DEV_PLATFORM_URL);\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 { 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 { 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","import Docker from \"dockerode\";\n\nimport type { ProjectRuntime } from \"../runtime\";\n\nimport type { HostingOptions } from \"../plugin\";\nimport { formatDockerEnv, resolveRuntimeLaunch } from \"../runtime-env\";\nimport { waitForHealth } from \"../wait-for-health\";\n\nexport type DockerPluginOptions = HostingOptions & {\n docker?: Docker;\n};\n\nconst MINIO_DOCKER_NETWORK = process.env.MINIO_DOCKER_NETWORK ?? \"keystroke\";\n\nexport function createDockerRuntime(options: DockerPluginOptions = {}): ProjectRuntime {\n const docker = options.docker ?? new Docker();\n\n return {\n async start(input) {\n if (input.projects.length === 0) {\n throw new Error(\"Docker org runtime requires at least one project deploy\");\n }\n\n const launch = resolveRuntimeLaunch(\n options,\n input.projects.map((project) => ({\n projectId: project.projectId,\n artifactStorageKey: project.artifactStorageKey,\n })),\n input.database,\n input.env,\n );\n await ensureImage(docker, launch.image);\n await removeExistingContainer(docker, input.organizationId);\n\n const container = await docker.createContainer({\n Image: launch.image,\n name: containerName(input.organizationId),\n Labels: {\n \"keystroke.organization.id\": input.organizationId,\n \"keystroke.project.ids\": input.projects.map((project) => project.projectId).join(\",\"),\n },\n ExposedPorts: {\n [`${launch.port}/tcp`]: {},\n },\n HostConfig: {\n ExtraHosts: [\"host.docker.internal:host-gateway\"],\n PortBindings: {\n [`${launch.port}/tcp`]: [{ HostPort: \"0\" }],\n },\n },\n NetworkingConfig: {\n EndpointsConfig: {\n [MINIO_DOCKER_NETWORK]: {},\n },\n },\n Env: formatDockerEnv(launch.env),\n });\n\n await container.start();\n\n const inspect = await container.inspect();\n const binding = inspect.NetworkSettings.Ports?.[`${launch.port}/tcp`]?.[0];\n if (!binding?.HostPort) {\n throw new Error(\"Failed to resolve org container host port\");\n }\n\n const baseUrl = `http://127.0.0.1:${binding.HostPort}`;\n await waitForHealth(baseUrl, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.healthTimeoutMs,\n });\n\n return {\n runtimeId: inspect.Id,\n baseUrl,\n };\n },\n };\n}\n\nfunction containerName(organizationId: string): string {\n return `keystroke-org-${organizationId}`;\n}\n\nasync function removeExistingContainer(docker: Docker, organizationId: string): Promise<void> {\n try {\n const existing = docker.getContainer(containerName(organizationId));\n await existing.stop({ t: 5 }).catch(() => undefined);\n await existing.remove({ force: true });\n } catch {\n // no existing container\n }\n}\n\nasync function ensureImage(docker: Docker, image: string): Promise<void> {\n try {\n await docker.getImage(image).inspect();\n return;\n } catch {\n // pull below\n }\n\n await new Promise<void>((resolvePromise, reject) => {\n docker.pull(image, (error: Error | null, stream: NodeJS.ReadableStream | undefined) => {\n if (error) {\n reject(error);\n return;\n }\n\n if (!stream) {\n reject(new Error(`Failed to pull image ${image}`));\n return;\n }\n\n docker.modem.followProgress(stream, (progressError: Error | null) => {\n if (progressError) {\n reject(progressError);\n return;\n }\n\n resolvePromise();\n });\n });\n });\n}\n","import type { HostingPlugin } from \"../plugin\";\nimport { resolveWorkerRuntimeConfig } from \"../worker-runtime-config\";\n\nimport { createDockerRuntime } from \"./create-docker-runtime\";\nimport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport type { DockerPluginOptions } from \"./create-docker-runtime\";\n\nexport function dockerPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n const env = options.env ?? process.env;\n\n return {\n name: \"docker\",\n workerRuntime: resolveWorkerRuntimeConfig(env),\n createRuntime: () => createDockerRuntime({ ...options, env }),\n };\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport { dockerPlugin, type DockerPluginOptions } from \"./docker/plugin\";\n\n/** Local Docker hosting — default for platform and dev. */\nexport function defaultHostingPlugin(options: DockerPluginOptions = {}): HostingPlugin {\n return dockerPlugin(options);\n}\n","/** Timeout for a single project runtime `/health` probe. */\nexport const RUNTIME_PING_TIMEOUT_MS = 15_000;\n","import { PROJECT_SERVER_HEALTH } from \"./constants\";\nimport type { HostingPlugin } from \"./plugin\";\nimport { RUNTIME_PING_TIMEOUT_MS } from \"./runtime-constants\";\n\nexport type PingProjectOptions = {\n runtimeId?: string | null;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n plugin?: Pick<HostingPlugin, \"pingRequestHeaders\">;\n};\n\nexport async function pingProject(\n baseUrl: string,\n options: PingProjectOptions = {},\n): Promise<boolean> {\n const timeoutMs = options.timeoutMs ?? RUNTIME_PING_TIMEOUT_MS;\n const headers = options.plugin?.pingRequestHeaders?.(options.runtimeId ?? null) ?? {};\n\n try {\n const response = await (options.fetchImpl ?? fetch)(\n new URL(PROJECT_SERVER_HEALTH.path, baseUrl),\n {\n signal: AbortSignal.timeout(timeoutMs),\n headers,\n },\n );\n\n return response.ok;\n } catch {\n return false;\n }\n}\n","import type { HostingPlugin } from \"./plugin\";\nimport type { ProjectPingTarget } from \"./runtime\";\nimport { pingProject, type PingProjectOptions } from \"./ping-project\";\n\nexport function canPingProjectTarget(\n target: ProjectPingTarget,\n plugin?: Pick<HostingPlugin, \"canPingTarget\">,\n): target is { baseUrl: string; runtimeId: string | null } {\n if (plugin?.canPingTarget) {\n return plugin.canPingTarget(target);\n }\n\n return !!target.baseUrl;\n}\n\nexport async function pingProjectTarget(\n target: ProjectPingTarget,\n options: PingProjectOptions & {\n plugin?: Pick<HostingPlugin, \"canPingTarget\" | \"pingRequestHeaders\">;\n } = {},\n): Promise<boolean> {\n if (!canPingProjectTarget(target, options.plugin)) {\n return false;\n }\n\n return pingProject(target.baseUrl, {\n ...options,\n runtimeId: target.runtimeId,\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",
3
+ "version": "0.1.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/keystrokehq/keystroke.git",
@@ -36,7 +36,7 @@
36
36
  "typescript": "^6.0.3",
37
37
  "vitest": "^4.1.7",
38
38
  "@keystrokehq/oxlint-config": "0.0.4",
39
- "@keystrokehq/shared": "0.0.95",
39
+ "@keystrokehq/shared": "0.0.96",
40
40
  "@keystrokehq/tsconfig": "0.0.3",
41
41
  "@keystrokehq/tsdown-config": "0.0.3",
42
42
  "@keystrokehq/vitest-config": "0.0.5"