@absolutejs/deploy 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloudTarget.d.ts +4 -4
- package/dist/cloudflare.d.ts +2 -2
- package/dist/cloudflare.js.map +2 -2
- package/dist/deployer.d.ts +9 -9
- package/dist/digitalocean.d.ts +5 -5
- package/dist/digitalocean.js +8 -2
- package/dist/digitalocean.js.map +5 -5
- package/dist/digitaloceanDns.d.ts +2 -2
- package/dist/digitaloceanDns.js +8 -2
- package/dist/digitaloceanDns.js.map +6 -6
- package/dist/digitaloceanInfrastructure.js +8 -2
- package/dist/digitaloceanInfrastructure.js.map +6 -6
- package/dist/digitaloceanIngress.js +8 -2
- package/dist/digitaloceanIngress.js.map +5 -5
- package/dist/dns.d.ts +1 -1
- package/dist/dns.js.map +2 -2
- package/dist/env.d.ts +1 -1
- package/dist/env.js.map +2 -2
- package/dist/gcp.js +6 -2
- package/dist/gcp.js.map +3 -3
- package/dist/hetzner.d.ts +3 -3
- package/dist/hetzner.js +8 -2
- package/dist/hetzner.js.map +5 -5
- package/dist/hetznerDns.d.ts +2 -2
- package/dist/hetznerDns.js.map +2 -2
- package/dist/hetznerInfrastructure.js +8 -2
- package/dist/hetznerInfrastructure.js.map +7 -7
- package/dist/index.d.ts +10 -10
- package/dist/index.js +64 -16
- package/dist/index.js.map +5 -5
- package/dist/linode.d.ts +3 -3
- package/dist/linode.js +8 -2
- package/dist/linode.js.map +5 -5
- package/dist/linodeInfrastructure.js +13 -3
- package/dist/linodeInfrastructure.js.map +7 -7
- package/dist/managedPreview.d.ts +1 -0
- package/dist/managedPreview.js +3 -2
- package/dist/managedPreview.js.map +3 -3
- package/dist/preview.d.ts +3 -3
- package/dist/preview.js +5 -2
- package/dist/preview.js.map +3 -3
- package/dist/processManagers.d.ts +4 -4
- package/dist/route53.d.ts +2 -2
- package/dist/route53.js.map +2 -2
- package/dist/targets.d.ts +1 -1
- package/dist/tls.d.ts +5 -5
- package/dist/tls.js +5 -2
- package/dist/tls.js.map +3 -3
- package/dist/vultr.d.ts +4 -4
- package/dist/vultr.js +8 -2
- package/dist/vultr.js.map +5 -5
- package/dist/vultrInfrastructure.js +8 -2
- package/dist/vultrInfrastructure.js.map +7 -7
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/managedPreview.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Durable, transport-neutral preview lifecycle.\n *\n * Unlike `createPreviewFleet`, this surface does not assume that the caller can\n * construct a Deployer for the destination. It is intended for control planes\n * that stream an immutable artifact to a remote fleet agent and own routing in\n * a separate edge service.\n */\n\nexport type ManagedPreviewStatus =\n | \"provisioning\"\n | \"running\"\n | \"failed\"\n | \"deleting\";\n\nexport type ManagedPreviewRecord<\n Context = Record<string, unknown>,\n Output = unknown,\n> = {\n previewId: string;\n runtimeId: string;\n status: ManagedPreviewStatus;\n context: Context;\n createdAt: number;\n updatedAt: number;\n expiresAt?: number;\n releaseId?: string;\n url?: string;\n output?: Output;\n error?: string;\n};\n\nexport type ManagedPreviewStore<\n Context = Record<string, unknown>,\n Output = unknown,\n> = {\n list: () => Promise<ManagedPreviewRecord<Context, Output>[]>;\n get: (\n previewId: string,\n ) => Promise<ManagedPreviewRecord<Context, Output> | null>;\n put: (record: ManagedPreviewRecord<Context, Output>) => Promise<void>;\n remove: (previewId: string) => Promise<void>;\n};\n\nexport type ManagedPreviewPublication<Output = unknown> = {\n releaseId: string;\n url: string;\n output?: Output;\n};\n\nexport type CreateManagedPreviewInput<Context> = {\n previewId: string;\n context: Context;\n runtimeId?: string;\n expiresAt?: number;\n};\n\nexport type ManagedPreviewFleetOptions<Context, Output> = {\n store: ManagedPreviewStore<Context, Output>;\n publish: (\n record: ManagedPreviewRecord<Context, Output>,\n ) => Promise<ManagedPreviewPublication<Output>>;\n destroy: (record: ManagedPreviewRecord<Context, Output>) => Promise<void>;\n createRuntimeId?: () => string;\n clock?: () => number;\n};\n\nexport type ManagedPreviewFleet<Context, Output> = {\n create: (\n input: CreateManagedPreviewInput<Context>,\n ) => Promise<ManagedPreviewRecord<Context, Output>>;\n resume: (previewId: string) => Promise<ManagedPreviewRecord<Context, Output>>;\n teardown: (previewId: string) => Promise<void>;\n list: () => Promise<ManagedPreviewRecord<Context, Output>[]>;\n get: (\n previewId: string,\n ) => Promise<ManagedPreviewRecord<Context, Output> | null>;\n gc: () => Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }>;\n};\n\nconst errorMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\nexport const createManagedPreviewFleet = <Context, Output = unknown>(\n options: ManagedPreviewFleetOptions<Context, Output>,\n): ManagedPreviewFleet<Context, Output> => {\n const clock = options.clock ?? Date.now;\n const createRuntimeId =\n options.createRuntimeId ?? (() => crypto.randomUUID());\n const operations = new Map<string, Promise<void>>();\n\n const exclusive = async <Result>(\n previewId: string,\n operation: () => Promise<Result>,\n ): Promise<Result> => {\n const prior = operations.get(previewId) ?? Promise.resolve();\n let release: () => void = () => undefined;\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const queued = prior.then(() => current);\n operations.set(previewId, queued);\n await prior;\n try {\n return await operation();\n } finally {\n release();\n if (operations.get(previewId) === queued) operations.delete(previewId);\n }\n };\n\n const publish = async (\n record: ManagedPreviewRecord<Context, Output>,\n ): Promise<ManagedPreviewRecord<Context, Output>> => {\n const provisioning: ManagedPreviewRecord<Context, Output> = {\n ...record,\n status: \"provisioning\",\n updatedAt: clock(),\n };\n delete provisioning.error;\n await options.store.put(provisioning);\n\n try {\n const publication = await options.publish(provisioning);\n const running: ManagedPreviewRecord<Context, Output> = {\n ...provisioning,\n releaseId: publication.releaseId,\n url: publication.url,\n status: \"running\",\n updatedAt: clock(),\n ...(publication.output === undefined\n ? {}\n : { output: publication.output }),\n };\n await options.store.put(running);\n return running;\n } catch (error) {\n const failed: ManagedPreviewRecord<Context, Output> = {\n ...provisioning,\n error: errorMessage(error),\n status: \"failed\",\n updatedAt: clock(),\n };\n await options.store.put(failed);\n throw error;\n }\n };\n\n const create = async (\n input: CreateManagedPreviewInput<Context>,\n ): Promise<ManagedPreviewRecord<Context, Output>> =>\n exclusive(input.previewId, async () => {\n const existing = await options.store.get(input.previewId);\n const now = clock();\n const record: ManagedPreviewRecord<Context, Output> = {\n ...(existing ?? {\n createdAt: now,\n runtimeId: input.runtimeId ?? createRuntimeId(),\n }),\n context: input.context,\n previewId: input.previewId,\n status: \"provisioning\",\n updatedAt: now,\n ...(input.expiresAt === undefined\n ? existing?.expiresAt === undefined\n ? {}\n : { expiresAt: existing.expiresAt }\n : { expiresAt: input.expiresAt }),\n };\n return publish(record);\n });\n\n const resume = async (\n previewId: string,\n ): Promise<ManagedPreviewRecord<Context, Output>> =>\n exclusive(previewId, async () => {\n const record = await options.store.get(previewId);\n if (record === null) {\n throw new Error(`managed-preview: unknown preview ${previewId}`);\n }\n if (record.status === \"deleting\") {\n throw new Error(`managed-preview: preview ${previewId} is deleting`);\n }\n return publish(record);\n });\n\n const teardown = async (previewId: string): Promise<void> =>\n exclusive(previewId, async () => {\n const record = await options.store.get(previewId);\n if (record === null) return;\n const deleting: ManagedPreviewRecord<Context, Output> = {\n ...record,\n status: \"deleting\",\n updatedAt: clock(),\n };\n await options.store.put(deleting);\n try {\n await options.destroy(deleting);\n await options.store.remove(previewId);\n } catch (error) {\n await options.store.put({\n ...deleting,\n error: errorMessage(error),\n status: \"failed\",\n updatedAt: clock(),\n });\n throw error;\n }\n });\n\n const gc = async (): Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }> => {\n const now = clock();\n const expired = (await options.store.list()).filter(\n (record) => record.expiresAt !== undefined && record.expiresAt <= now,\n );\n const removed: string[] = [];\n const errors: { previewId: string; error: Error }[] = [];\n for (const record of expired) {\n try {\n await teardown(record.previewId);\n removed.push(record.previewId);\n } catch (error) {\n errors.push({\n error: error instanceof Error ? error : new Error(String(error)),\n previewId: record.previewId,\n });\n }\n }\n return { errors, removed };\n };\n\n return {\n create,\n gc,\n get: (previewId) => options.store.get(previewId),\n list: () => options.store.list(),\n resume,\n teardown,\n };\n};\n"
|
|
5
|
+
"/**\n * Durable, transport-neutral preview lifecycle.\n *\n * Unlike `createPreviewFleet`, this surface does not assume that the caller can\n * construct a Deployer for the destination. It is intended for control planes\n * that stream an immutable artifact to a remote fleet agent and own routing in\n * a separate edge service.\n */\n\nexport type ManagedPreviewStatus =\n | \"provisioning\"\n | \"running\"\n | \"failed\"\n | \"deleting\";\n\nexport type ManagedPreviewRecord<\n Context = Record<string, unknown>,\n Output = unknown,\n> = {\n previewId: string;\n runtimeId: string;\n status: ManagedPreviewStatus;\n context: Context;\n createdAt: number;\n updatedAt: number;\n expiresAt?: number;\n releaseId?: string;\n url?: string;\n output?: Output;\n error?: string;\n};\n\nexport type ManagedPreviewStore<\n Context = Record<string, unknown>,\n Output = unknown,\n> = {\n list: () => Promise<ManagedPreviewRecord<Context, Output>[]>;\n get: (\n previewId: string,\n ) => Promise<ManagedPreviewRecord<Context, Output> | null>;\n put: (record: ManagedPreviewRecord<Context, Output>) => Promise<void>;\n remove: (previewId: string) => Promise<void>;\n};\n\nexport type ManagedPreviewPublication<Output = unknown> = {\n releaseId: string;\n url: string;\n output?: Output;\n};\n\nexport type CreateManagedPreviewInput<Context> = {\n previewId: string;\n context: Context;\n runtimeId?: string;\n expiresAt?: number;\n releaseId?: string;\n};\n\nexport type ManagedPreviewFleetOptions<Context, Output> = {\n store: ManagedPreviewStore<Context, Output>;\n publish: (\n record: ManagedPreviewRecord<Context, Output>,\n ) => Promise<ManagedPreviewPublication<Output>>;\n destroy: (record: ManagedPreviewRecord<Context, Output>) => Promise<void>;\n createRuntimeId?: () => string;\n clock?: () => number;\n};\n\nexport type ManagedPreviewFleet<Context, Output> = {\n create: (\n input: CreateManagedPreviewInput<Context>,\n ) => Promise<ManagedPreviewRecord<Context, Output>>;\n resume: (previewId: string) => Promise<ManagedPreviewRecord<Context, Output>>;\n teardown: (previewId: string) => Promise<void>;\n list: () => Promise<ManagedPreviewRecord<Context, Output>[]>;\n get: (\n previewId: string,\n ) => Promise<ManagedPreviewRecord<Context, Output> | null>;\n gc: () => Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }>;\n};\n\nconst errorMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\nexport const createManagedPreviewFleet = <Context, Output = unknown>(\n options: ManagedPreviewFleetOptions<Context, Output>,\n): ManagedPreviewFleet<Context, Output> => {\n const clock = options.clock ?? Date.now;\n const createRuntimeId =\n options.createRuntimeId ?? (() => crypto.randomUUID());\n const operations = new Map<string, Promise<void>>();\n\n const exclusive = async <Result>(\n previewId: string,\n operation: () => Promise<Result>,\n ): Promise<Result> => {\n const prior = operations.get(previewId) ?? Promise.resolve();\n let release: () => void = () => undefined;\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const queued = prior.then(() => current);\n operations.set(previewId, queued);\n await prior;\n try {\n return await operation();\n } finally {\n release();\n if (operations.get(previewId) === queued) operations.delete(previewId);\n }\n };\n\n const publish = async (\n record: ManagedPreviewRecord<Context, Output>,\n ): Promise<ManagedPreviewRecord<Context, Output>> => {\n const provisioning: ManagedPreviewRecord<Context, Output> = {\n ...record,\n status: \"provisioning\",\n updatedAt: clock(),\n };\n delete provisioning.error;\n await options.store.put(provisioning);\n\n try {\n const publication = await options.publish(provisioning);\n const running: ManagedPreviewRecord<Context, Output> = {\n ...provisioning,\n releaseId: publication.releaseId,\n url: publication.url,\n status: \"running\",\n updatedAt: clock(),\n ...(publication.output === undefined\n ? {}\n : { output: publication.output }),\n };\n await options.store.put(running);\n return running;\n } catch (error) {\n const failed: ManagedPreviewRecord<Context, Output> = {\n ...provisioning,\n error: errorMessage(error),\n status: \"failed\",\n updatedAt: clock(),\n };\n await options.store.put(failed);\n throw error;\n }\n };\n\n const create = async (\n input: CreateManagedPreviewInput<Context>,\n ): Promise<ManagedPreviewRecord<Context, Output>> =>\n exclusive(input.previewId, async () => {\n const existing = await options.store.get(input.previewId);\n const now = clock();\n const record: ManagedPreviewRecord<Context, Output> = {\n ...(existing ?? {\n createdAt: now,\n runtimeId: input.runtimeId ?? createRuntimeId(),\n }),\n context: input.context,\n previewId: input.previewId,\n status: \"provisioning\",\n updatedAt: now,\n ...(input.expiresAt === undefined\n ? existing?.expiresAt === undefined\n ? {}\n : { expiresAt: existing.expiresAt }\n : { expiresAt: input.expiresAt }),\n ...(input.releaseId === undefined\n ? {}\n : { releaseId: input.releaseId }),\n };\n return publish(record);\n });\n\n const resume = async (\n previewId: string,\n ): Promise<ManagedPreviewRecord<Context, Output>> =>\n exclusive(previewId, async () => {\n const record = await options.store.get(previewId);\n if (record === null) {\n throw new Error(`managed-preview: unknown preview ${previewId}`);\n }\n if (record.status === \"deleting\") {\n throw new Error(`managed-preview: preview ${previewId} is deleting`);\n }\n return publish(record);\n });\n\n const teardown = async (previewId: string): Promise<void> =>\n exclusive(previewId, async () => {\n const record = await options.store.get(previewId);\n if (record === null) return;\n const deleting: ManagedPreviewRecord<Context, Output> = {\n ...record,\n status: \"deleting\",\n updatedAt: clock(),\n };\n await options.store.put(deleting);\n try {\n await options.destroy(deleting);\n await options.store.remove(previewId);\n } catch (error) {\n await options.store.put({\n ...deleting,\n error: errorMessage(error),\n status: \"failed\",\n updatedAt: clock(),\n });\n throw error;\n }\n });\n\n const gc = async (): Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }> => {\n const now = clock();\n const expired = (await options.store.list()).filter(\n (record) => record.expiresAt !== undefined && record.expiresAt <= now,\n );\n const removed: string[] = [];\n const errors: { previewId: string; error: Error }[] = [];\n for (const record of expired) {\n try {\n await teardown(record.previewId);\n removed.push(record.previewId);\n } catch (error) {\n errors.push({\n error: error instanceof Error ? error : new Error(String(error)),\n previewId: record.previewId,\n });\n }\n }\n return { errors, removed };\n };\n\n return {\n create,\n gc,\n get: (previewId) => options.store.get(previewId),\n list: () => options.store.list(),\n resume,\n teardown,\n };\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,IAAM,eAAe,CAAC,UACpB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAEhD,IAAM,4BAA4B,CACvC,YACyC;AAAA,EACzC,MAAM,QAAQ,QAAQ,SAAS,KAAK;AAAA,EACpC,MAAM,kBACJ,QAAQ,oBAAoB,MAAM,OAAO,WAAW;AAAA,EACtD,MAAM,aAAa,IAAI;AAAA,EAEvB,MAAM,YAAY,OAChB,WACA,cACoB;AAAA,IACpB,MAAM,QAAQ,WAAW,IAAI,SAAS,KAAK,QAAQ,QAAQ;AAAA,IAC3D,IAAI,UAAsB,MAAG;AAAA,MAAG;AAAA;AAAA,IAChC,MAAM,UAAU,IAAI,QAAc,CAAC,YAAY;AAAA,MAC7C,UAAU;AAAA,KACX;AAAA,IACD,MAAM,SAAS,MAAM,KAAK,MAAM,OAAO;AAAA,IACvC,WAAW,IAAI,WAAW,MAAM;AAAA,IAChC,MAAM;AAAA,IACN,IAAI;AAAA,MACF,OAAO,MAAM,UAAU;AAAA,cACvB;AAAA,MACA,QAAQ;AAAA,MACR,IAAI,WAAW,IAAI,SAAS,MAAM;AAAA,QAAQ,WAAW,OAAO,SAAS;AAAA;AAAA;AAAA,EAIzE,MAAM,UAAU,OACd,WACmD;AAAA,IACnD,MAAM,eAAsD;AAAA,SACvD;AAAA,MACH,QAAQ;AAAA,MACR,WAAW,MAAM;AAAA,IACnB;AAAA,IACA,OAAO,aAAa;AAAA,IACpB,MAAM,QAAQ,MAAM,IAAI,YAAY;AAAA,IAEpC,IAAI;AAAA,MACF,MAAM,cAAc,MAAM,QAAQ,QAAQ,YAAY;AAAA,MACtD,MAAM,UAAiD;AAAA,WAClD;AAAA,QACH,WAAW,YAAY;AAAA,QACvB,KAAK,YAAY;AAAA,QACjB,QAAQ;AAAA,QACR,WAAW,MAAM;AAAA,WACb,YAAY,WAAW,YACvB,CAAC,IACD,EAAE,QAAQ,YAAY,OAAO;AAAA,MACnC;AAAA,MACA,MAAM,QAAQ,MAAM,IAAI,OAAO;AAAA,MAC/B,OAAO;AAAA,MACP,OAAO,OAAO;AAAA,MACd,MAAM,SAAgD;AAAA,WACjD;AAAA,QACH,OAAO,aAAa,KAAK;AAAA,QACzB,QAAQ;AAAA,QACR,WAAW,MAAM;AAAA,MACnB;AAAA,MACA,MAAM,QAAQ,MAAM,IAAI,MAAM;AAAA,MAC9B,MAAM;AAAA;AAAA;AAAA,EAIV,MAAM,SAAS,OACb,UAEA,UAAU,MAAM,WAAW,YAAY;AAAA,IACrC,MAAM,WAAW,MAAM,QAAQ,MAAM,IAAI,MAAM,SAAS;AAAA,IACxD,MAAM,MAAM,MAAM;AAAA,IAClB,MAAM,SAAgD;AAAA,SAChD,YAAY;AAAA,QACd,WAAW;AAAA,QACX,WAAW,MAAM,aAAa,gBAAgB;AAAA,MAChD;AAAA,MACA,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,MACjB,QAAQ;AAAA,MACR,WAAW;AAAA,SACP,MAAM,cAAc,YACpB,UAAU,cAAc,YACtB,CAAC,IACD,EAAE,WAAW,SAAS,UAAU,IAClC,EAAE,WAAW,MAAM,UAAU;AAAA,SAC7B,MAAM,cAAc,YACpB,CAAC,IACD,EAAE,WAAW,MAAM,UAAU;AAAA,IACnC;AAAA,IACA,OAAO,QAAQ,MAAM;AAAA,GACtB;AAAA,EAEH,MAAM,SAAS,OACb,cAEA,UAAU,WAAW,YAAY;AAAA,IAC/B,MAAM,SAAS,MAAM,QAAQ,MAAM,IAAI,SAAS;AAAA,IAChD,IAAI,WAAW,MAAM;AAAA,MACnB,MAAM,IAAI,MAAM,oCAAoC,WAAW;AAAA,IACjE;AAAA,IACA,IAAI,OAAO,WAAW,YAAY;AAAA,MAChC,MAAM,IAAI,MAAM,4BAA4B,uBAAuB;AAAA,IACrE;AAAA,IACA,OAAO,QAAQ,MAAM;AAAA,GACtB;AAAA,EAEH,MAAM,WAAW,OAAO,cACtB,UAAU,WAAW,YAAY;AAAA,IAC/B,MAAM,SAAS,MAAM,QAAQ,MAAM,IAAI,SAAS;AAAA,IAChD,IAAI,WAAW;AAAA,MAAM;AAAA,IACrB,MAAM,WAAkD;AAAA,SACnD;AAAA,MACH,QAAQ;AAAA,MACR,WAAW,MAAM;AAAA,IACnB;AAAA,IACA,MAAM,QAAQ,MAAM,IAAI,QAAQ;AAAA,IAChC,IAAI;AAAA,MACF,MAAM,QAAQ,QAAQ,QAAQ;AAAA,MAC9B,MAAM,QAAQ,MAAM,OAAO,SAAS;AAAA,MACpC,OAAO,OAAO;AAAA,MACd,MAAM,QAAQ,MAAM,IAAI;AAAA,WACnB;AAAA,QACH,OAAO,aAAa,KAAK;AAAA,QACzB,QAAQ;AAAA,QACR,WAAW,MAAM;AAAA,MACnB,CAAC;AAAA,MACD,MAAM;AAAA;AAAA,GAET;AAAA,EAEH,MAAM,KAAK,YAGL;AAAA,IACJ,MAAM,MAAM,MAAM;AAAA,IAClB,MAAM,WAAW,MAAM,QAAQ,MAAM,KAAK,GAAG,OAC3C,CAAC,WAAW,OAAO,cAAc,aAAa,OAAO,aAAa,GACpE;AAAA,IACA,MAAM,UAAoB,CAAC;AAAA,IAC3B,MAAM,SAAgD,CAAC;AAAA,IACvD,WAAW,UAAU,SAAS;AAAA,MAC5B,IAAI;AAAA,QACF,MAAM,SAAS,OAAO,SAAS;AAAA,QAC/B,QAAQ,KAAK,OAAO,SAAS;AAAA,QAC7B,OAAO,OAAO;AAAA,QACd,OAAO,KAAK;AAAA,UACV,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,UAC/D,WAAW,OAAO;AAAA,QACpB,CAAC;AAAA;AAAA,IAEL;AAAA,IACA,OAAO,EAAE,QAAQ,QAAQ;AAAA;AAAA,EAG3B,OAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,KAAK,CAAC,cAAc,QAAQ,MAAM,IAAI,SAAS;AAAA,IAC/C,MAAM,MAAM,QAAQ,MAAM,KAAK;AAAA,IAC/B;AAAA,IACA;AAAA,EACF;AAAA;",
|
|
8
|
+
"debugId": "6747D9124F0D08D764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/preview.d.ts
CHANGED
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
* stopping the process) is a caller-supplied hook. The fleet owns
|
|
30
30
|
* fleet bookkeeping, not application logic.
|
|
31
31
|
*/
|
|
32
|
-
import type { Deployer, DeployResult, ReleaseAnnotations } from
|
|
33
|
-
import type { DnsProvider } from
|
|
32
|
+
import type { Deployer, DeployResult, ReleaseAnnotations } from "./deployer";
|
|
33
|
+
import type { DnsProvider } from "./dns";
|
|
34
34
|
export type PreviewRecord = {
|
|
35
35
|
previewId: string;
|
|
36
36
|
hostname: string;
|
|
@@ -64,7 +64,7 @@ export type PreviewFleetOptions = {
|
|
|
64
64
|
* Optional URL scheme. Defaults to `'https'`. Set `'http'` for
|
|
65
65
|
* local-loop previews behind a localhost proxy.
|
|
66
66
|
*/
|
|
67
|
-
scheme?:
|
|
67
|
+
scheme?: "http" | "https";
|
|
68
68
|
/**
|
|
69
69
|
* DNS provider — `cloudflareProvider`, `route53Provider`, etc.
|
|
70
70
|
* Optional: when absent, `fleet.create` skips DNS work and the
|
package/dist/preview.js
CHANGED
|
@@ -144,7 +144,10 @@ var createPreviewFleet = (options) => {
|
|
|
144
144
|
if (existing !== null) {
|
|
145
145
|
port = existing.port;
|
|
146
146
|
} else if (options.allocatePort !== undefined) {
|
|
147
|
-
port = await options.allocatePort({
|
|
147
|
+
port = await options.allocatePort({
|
|
148
|
+
hostname,
|
|
149
|
+
previewId: input.previewId
|
|
150
|
+
});
|
|
148
151
|
} else {
|
|
149
152
|
const used = new Set((await store.list()).map((record2) => record2.port));
|
|
150
153
|
port = defaultAllocatePort(used, portRange);
|
|
@@ -224,5 +227,5 @@ export {
|
|
|
224
227
|
createFilePreviewStore
|
|
225
228
|
};
|
|
226
229
|
|
|
227
|
-
//# debugId=
|
|
230
|
+
//# debugId=6DE77966EC79779D64756E2164756E21
|
|
228
231
|
//# sourceMappingURL=preview.js.map
|
package/dist/preview.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/preview.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Preview-deploys fleet for `@absolutejs/deploy`.\n *\n * A `PreviewFleet` is a tenant-aware orchestrator that creates,\n * lists, and tears down ephemeral environments — one per PR /\n * branch / commit. It composes the existing `Deployer` (release\n * dirs + atomic symlink swap + rollback) with a `DnsProvider` and a\n * persistent registry so a deploy bot can:\n *\n * const url = await fleet.create({ previewId: 'pr-42', ... });\n *\n * await fleet.teardown('pr-42');\n *\n * await fleet.gc({ olderThanMs: 7 * 24 * 60 * 60 * 1000 });\n *\n * Substrate responsibilities:\n *\n * - allocate a free port from a configurable pool\n * - build the hostname `<previewId>.<baseDomain>`\n * - upsert an A record for that hostname (DNS provider injected)\n * - call a caller-supplied `makeDeployer({ previewId, port,\n * hostname })` factory, then run `deployer.deploy()` so the\n * preview lands as a normal release on disk\n * - persist the preview registry so we can list / GC later\n * - on teardown, run a caller-supplied stop callback, remove the\n * DNS record, drop the registry entry\n *\n * Everything tenant-specific (secrets snapshotting, db seeding,\n * stopping the process) is a caller-supplied hook. The fleet owns\n * fleet bookkeeping, not application logic.\n */\n\nimport {\n\texistsSync,\n\tmkdirSync,\n\treadFileSync,\n\trenameSync,\n\twriteFileSync\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport type {\n\tDeployer,\n\tDeployResult,\n\tReleaseAnnotations\n} from './deployer';\nimport type { DnsProvider, DnsRecord, DnsRecordSpec } from './dns';\n\n// =============================================================================\n// Registry shape\n// =============================================================================\n\nexport type PreviewRecord = {\n\tpreviewId: string;\n\thostname: string;\n\turl: string;\n\tport: number;\n\tdnsRecordId?: string;\n\treleaseId: string;\n\tcommitSha?: string;\n\tcreatedAt: number;\n\tannotations?: ReleaseAnnotations;\n};\n\nexport type PreviewStore = {\n\tlist: () => Promise<PreviewRecord[]>;\n\tget: (previewId: string) => Promise<PreviewRecord | null>;\n\tput: (record: PreviewRecord) => Promise<void>;\n\tremove: (previewId: string) => Promise<void>;\n};\n\n// =============================================================================\n// Hooks + options\n// =============================================================================\n\nexport type PreviewDeployerContext = {\n\tpreviewId: string;\n\tport: number;\n\thostname: string;\n\turl: string;\n};\n\nexport type PreviewFleetOptions = {\n\t/**\n\t * Apex used to build preview hostnames: `<previewId>.<baseDomain>`.\n\t * `previewId` is slugified before use (alphanumeric + `-`).\n\t */\n\tbaseDomain: string;\n\t/**\n\t * Optional URL scheme. Defaults to `'https'`. Set `'http'` for\n\t * local-loop previews behind a localhost proxy.\n\t */\n\tscheme?: 'http' | 'https';\n\t/**\n\t * DNS provider — `cloudflareProvider`, `route53Provider`, etc.\n\t * Optional: when absent, `fleet.create` skips DNS work and the\n\t * caller is responsible for routing requests at the hostname.\n\t */\n\tdns?: DnsProvider;\n\t/**\n\t * Public IPv4 the A record should point at. Required when `dns`\n\t * is set. The fleet calls `dns.upsert({ name, type: 'A',\n\t * content: ipv4 })`.\n\t */\n\tipv4?: string;\n\t/**\n\t * TTL applied to created A records. Default 60s — previews come\n\t * and go and we want low cache lifetime.\n\t */\n\tdnsTtl?: number;\n\t/**\n\t * Proxied flag (Cloudflare orange-cloud). Default `false` — most\n\t * previews want a real IP, not a Cloudflare proxy.\n\t */\n\tdnsProxied?: boolean;\n\t/**\n\t * Port range that the fleet allocates from. Default `[3100, 3899]`\n\t * (3000 squat is documented in memory).\n\t */\n\tportRange?: { start: number; end: number };\n\t/** Override port allocation entirely (e.g. talk to a port-leaser). */\n\tallocatePort?: (record: {\n\t\tpreviewId: string;\n\t\thostname: string;\n\t}) => Promise<number>;\n\t/**\n\t * REQUIRED. Build a `Deployer` for an individual preview. The\n\t * factory receives the resolved id, port, hostname, and URL —\n\t * use them to set the right env, processManager, and target.\n\t */\n\tmakeDeployer: (\n\t\tctx: PreviewDeployerContext\n\t) => Deployer | Promise<Deployer>;\n\t/**\n\t * Stop callback. Called by `teardown` BEFORE DNS removal so the\n\t * preview process stops accepting connections before the record\n\t * disappears. Use it to call `processManager.stop()`, kill the\n\t * port, etc.\n\t */\n\tstop?: (record: PreviewRecord) => Promise<void> | void;\n\t/**\n\t * After-teardown callback. Called after DNS + registry removal —\n\t * good place to delete the release directory, drop a tenant\n\t * schema, clear secrets, etc.\n\t */\n\tafterTeardown?: (record: PreviewRecord) => Promise<void> | void;\n\t/** Registry. Default = filesystem store at `<root>`. */\n\tstore?: PreviewStore;\n\t/** Directory the file-based default store writes into. */\n\tregistryRoot?: string;\n\t/** Override `Date.now()` for tests. */\n\tclock?: () => number;\n};\n\n// =============================================================================\n// File-based PreviewStore (default)\n// =============================================================================\n\n/**\n * Single-file JSON registry. Reads + writes atomically (temp-file\n * + rename). Adequate for a deploy bot on one host — swap in a\n * Postgres-backed store for distributed deploy bots.\n */\nexport const createFilePreviewStore = (root: string): PreviewStore => {\n\tconst path = join(root, 'previews.json');\n\n\tconst readAll = (): PreviewRecord[] => {\n\t\tif (!existsSync(path)) return [];\n\t\ttry {\n\t\t\tconst raw = readFileSync(path, 'utf8');\n\t\t\tif (raw.trim() === '') return [];\n\t\t\tconst parsed = JSON.parse(raw) as { previews?: PreviewRecord[] };\n\t\t\treturn Array.isArray(parsed.previews) ? parsed.previews : [];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t};\n\n\tconst writeAll = (records: PreviewRecord[]): void => {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\tconst tmp = `${path}.tmp-${process.pid}-${Date.now()}`;\n\t\twriteFileSync(tmp, JSON.stringify({ previews: records }, null, 2));\n\t\trenameSync(tmp, path);\n\t};\n\n\treturn {\n\t\tget: async (previewId) =>\n\t\t\treadAll().find((r) => r.previewId === previewId) ?? null,\n\t\tlist: async () => readAll(),\n\t\tput: async (record) => {\n\t\t\tconst records = readAll().filter(\n\t\t\t\t(r) => r.previewId !== record.previewId\n\t\t\t);\n\t\t\trecords.push(record);\n\t\t\twriteAll(records);\n\t\t},\n\t\tremove: async (previewId) => {\n\t\t\tconst records = readAll().filter((r) => r.previewId !== previewId);\n\t\t\twriteAll(records);\n\t\t}\n\t};\n};\n\n// =============================================================================\n// In-memory store (for tests + ephemeral fleets)\n// =============================================================================\n\nexport const createMemoryPreviewStore = (): PreviewStore => {\n\tconst map = new Map<string, PreviewRecord>();\n\treturn {\n\t\tget: async (previewId) => map.get(previewId) ?? null,\n\t\tlist: async () => Array.from(map.values()),\n\t\tput: async (record) => {\n\t\t\tmap.set(record.previewId, record);\n\t\t},\n\t\tremove: async (previewId) => {\n\t\t\tmap.delete(previewId);\n\t\t}\n\t};\n};\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nconst slugify = (id: string): string =>\n\tid\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]+/g, '-')\n\t\t.replace(/^-+|-+$/g, '')\n\t\t.slice(0, 50);\n\nconst defaultAllocatePort = (\n\tused: Set<number>,\n\trange: { start: number; end: number }\n): number => {\n\tfor (let port = range.start; port <= range.end; port += 1) {\n\t\tif (!used.has(port)) return port;\n\t}\n\tthrow new Error(\n\t\t`preview-fleet: no free ports in [${range.start}, ${range.end}] (${used.size} in use)`\n\t);\n};\n\n// =============================================================================\n// Fleet\n// =============================================================================\n\nexport type CreatePreviewInput = {\n\tpreviewId: string;\n\tcommitSha?: string;\n\tannotations?: ReleaseAnnotations;\n\t/**\n\t * Override hostname. Default = `<slug(previewId)>.<baseDomain>`.\n\t * Use this for previews with a vanity name that shouldn't echo\n\t * the PR id directly.\n\t */\n\thostname?: string;\n};\n\nexport type CreatePreviewResult = {\n\trecord: PreviewRecord;\n\tdeploy: DeployResult;\n};\n\nexport type PreviewFleet = {\n\t/**\n\t * Spin up a preview. Idempotent on `previewId` — if a record\n\t * already exists, the fleet re-uses its port + DNS and runs a\n\t * fresh `deployer.deploy()` (so PRs that push new commits roll\n\t * forward).\n\t */\n\tcreate: (input: CreatePreviewInput) => Promise<CreatePreviewResult>;\n\t/** Tear down a single preview (stop → DNS remove → registry remove → afterTeardown). */\n\tteardown: (previewId: string) => Promise<void>;\n\t/** List active previews. */\n\tlist: () => Promise<PreviewRecord[]>;\n\t/** Get one preview by id. */\n\tget: (previewId: string) => Promise<PreviewRecord | null>;\n\t/**\n\t * Tear down all previews older than `olderThanMs`. Returns the\n\t * list of preview ids torn down. Failures on individual previews\n\t * are swallowed and reported on `errors`.\n\t */\n\tgc: (options: { olderThanMs: number }) => Promise<{\n\t\tremoved: string[];\n\t\terrors: { previewId: string; error: Error }[];\n\t}>;\n};\n\nexport const createPreviewFleet = (\n\toptions: PreviewFleetOptions\n): PreviewFleet => {\n\tconst scheme = options.scheme ?? 'https';\n\tconst portRange = options.portRange ?? { end: 3899, start: 3100 };\n\tconst dnsTtl = options.dnsTtl ?? 60;\n\tconst dnsProxied = options.dnsProxied ?? false;\n\tconst clock = options.clock ?? Date.now;\n\tconst store =\n\t\toptions.store ??\n\t\tcreateFilePreviewStore(\n\t\t\toptions.registryRoot ?? join(process.cwd(), '.preview-fleet')\n\t\t);\n\n\tif (options.dns !== undefined && options.ipv4 === undefined) {\n\t\tthrow new Error(\n\t\t\t'preview-fleet: `ipv4` is required when `dns` is configured'\n\t\t);\n\t}\n\n\tconst ensureDns = async (\n\t\thostname: string\n\t): Promise<DnsRecord | undefined> => {\n\t\tif (options.dns === undefined) return undefined;\n\t\tconst spec: DnsRecordSpec = {\n\t\t\tcontent: options.ipv4!,\n\t\t\tname: hostname,\n\t\t\tproxied: dnsProxied,\n\t\t\tttl: dnsTtl,\n\t\t\ttype: 'A'\n\t\t};\n\t\treturn await options.dns.upsert(spec);\n\t};\n\n\tconst removeDns = async (record: PreviewRecord): Promise<void> => {\n\t\tif (options.dns === undefined || record.dnsRecordId === undefined) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait options.dns.delete(record.dnsRecordId);\n\t\t} catch {\n\t\t\t// Idempotent teardown — the record may already be gone. Don't\n\t\t\t// block the rest of teardown on a stale id.\n\t\t}\n\t};\n\n\tconst buildHostname = (previewId: string, override?: string): string => {\n\t\tif (override !== undefined) return override;\n\t\tconst slug = slugify(previewId);\n\t\tif (slug === '') {\n\t\t\tthrow new Error(\n\t\t\t\t`preview-fleet: previewId ${JSON.stringify(previewId)} slugifies to empty`\n\t\t\t);\n\t\t}\n\t\treturn `${slug}.${options.baseDomain}`;\n\t};\n\n\tconst create = async (\n\t\tinput: CreatePreviewInput\n\t): Promise<CreatePreviewResult> => {\n\t\tconst existing = await store.get(input.previewId);\n\t\tconst hostname = buildHostname(input.previewId, input.hostname);\n\t\tconst url = `${scheme}://${hostname}`;\n\n\t\t// Allocate a port — reuse existing if we're re-deploying.\n\t\tlet port: number;\n\t\tif (existing !== null) {\n\t\t\tport = existing.port;\n\t\t} else if (options.allocatePort !== undefined) {\n\t\t\tport = await options.allocatePort({ hostname, previewId: input.previewId });\n\t\t} else {\n\t\t\tconst used = new Set(\n\t\t\t\t(await store.list()).map((record) => record.port)\n\t\t\t);\n\t\t\tport = defaultAllocatePort(used, portRange);\n\t\t}\n\n\t\tconst deployer = await options.makeDeployer({\n\t\t\thostname,\n\t\t\tport,\n\t\t\tpreviewId: input.previewId,\n\t\t\turl\n\t\t});\n\n\t\tconst deployOptions =\n\t\t\tinput.annotations !== undefined\n\t\t\t\t? { annotations: input.annotations }\n\t\t\t\t: input.commitSha !== undefined\n\t\t\t\t\t? { annotations: { commitSha: input.commitSha } }\n\t\t\t\t\t: undefined;\n\t\tconst deployResult = await deployer.deploy(deployOptions);\n\n\t\t// DNS only after the deploy succeeded — no point flipping the\n\t\t// record at a half-baked release.\n\t\tconst dnsRecord = await ensureDns(hostname);\n\n\t\tconst record: PreviewRecord = {\n\t\t\tcreatedAt: existing?.createdAt ?? clock(),\n\t\t\thostname,\n\t\t\tport,\n\t\t\tpreviewId: input.previewId,\n\t\t\treleaseId: deployResult.releaseId,\n\t\t\turl,\n\t\t\t...(dnsRecord !== undefined ? { dnsRecordId: dnsRecord.id } : {}),\n\t\t\t...(input.commitSha !== undefined ? { commitSha: input.commitSha } : {}),\n\t\t\t...(input.annotations !== undefined\n\t\t\t\t? { annotations: input.annotations }\n\t\t\t\t: {})\n\t\t};\n\n\t\tawait store.put(record);\n\t\treturn { deploy: deployResult, record };\n\t};\n\n\tconst teardown = async (previewId: string): Promise<void> => {\n\t\tconst record = await store.get(previewId);\n\t\tif (record === null) return;\n\n\t\tif (options.stop !== undefined) {\n\t\t\ttry {\n\t\t\t\tawait options.stop(record);\n\t\t\t} catch {\n\t\t\t\t// Don't block DNS removal on a stop failure. The caller\n\t\t\t\t// can see the error via afterTeardown by re-querying.\n\t\t\t}\n\t\t}\n\n\t\tawait removeDns(record);\n\t\tawait store.remove(previewId);\n\n\t\tif (options.afterTeardown !== undefined) {\n\t\t\ttry {\n\t\t\t\tawait options.afterTeardown(record);\n\t\t\t} catch {\n\t\t\t\t// Caller's own teardown errors are their problem; the\n\t\t\t\t// substrate has already removed the record + DNS.\n\t\t\t}\n\t\t}\n\t};\n\n\tconst gc = async ({\n\t\tolderThanMs\n\t}: {\n\t\tolderThanMs: number;\n\t}): Promise<{\n\t\tremoved: string[];\n\t\terrors: { previewId: string; error: Error }[];\n\t}> => {\n\t\tconst cutoff = clock() - olderThanMs;\n\t\tconst all = await store.list();\n\t\tconst expired = all.filter((record) => record.createdAt < cutoff);\n\t\tconst removed: string[] = [];\n\t\tconst errors: { previewId: string; error: Error }[] = [];\n\t\tfor (const record of expired) {\n\t\t\ttry {\n\t\t\t\tawait teardown(record.previewId);\n\t\t\t\tremoved.push(record.previewId);\n\t\t\t} catch (e) {\n\t\t\t\terrors.push({\n\t\t\t\t\terror: e instanceof Error ? e : new Error(String(e)),\n\t\t\t\t\tpreviewId: record.previewId\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn { errors, removed };\n\t};\n\n\treturn {\n\t\tcreate,\n\t\tgc,\n\t\tget: (previewId) => store.get(previewId),\n\t\tlist: () => store.list(),\n\t\tteardown\n\t};\n};\n"
|
|
5
|
+
"/**\n * Preview-deploys fleet for `@absolutejs/deploy`.\n *\n * A `PreviewFleet` is a tenant-aware orchestrator that creates,\n * lists, and tears down ephemeral environments — one per PR /\n * branch / commit. It composes the existing `Deployer` (release\n * dirs + atomic symlink swap + rollback) with a `DnsProvider` and a\n * persistent registry so a deploy bot can:\n *\n * const url = await fleet.create({ previewId: 'pr-42', ... });\n *\n * await fleet.teardown('pr-42');\n *\n * await fleet.gc({ olderThanMs: 7 * 24 * 60 * 60 * 1000 });\n *\n * Substrate responsibilities:\n *\n * - allocate a free port from a configurable pool\n * - build the hostname `<previewId>.<baseDomain>`\n * - upsert an A record for that hostname (DNS provider injected)\n * - call a caller-supplied `makeDeployer({ previewId, port,\n * hostname })` factory, then run `deployer.deploy()` so the\n * preview lands as a normal release on disk\n * - persist the preview registry so we can list / GC later\n * - on teardown, run a caller-supplied stop callback, remove the\n * DNS record, drop the registry entry\n *\n * Everything tenant-specific (secrets snapshotting, db seeding,\n * stopping the process) is a caller-supplied hook. The fleet owns\n * fleet bookkeeping, not application logic.\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { Deployer, DeployResult, ReleaseAnnotations } from \"./deployer\";\nimport type { DnsProvider, DnsRecord, DnsRecordSpec } from \"./dns\";\n\n// =============================================================================\n// Registry shape\n// =============================================================================\n\nexport type PreviewRecord = {\n previewId: string;\n hostname: string;\n url: string;\n port: number;\n dnsRecordId?: string;\n releaseId: string;\n commitSha?: string;\n createdAt: number;\n annotations?: ReleaseAnnotations;\n};\n\nexport type PreviewStore = {\n list: () => Promise<PreviewRecord[]>;\n get: (previewId: string) => Promise<PreviewRecord | null>;\n put: (record: PreviewRecord) => Promise<void>;\n remove: (previewId: string) => Promise<void>;\n};\n\n// =============================================================================\n// Hooks + options\n// =============================================================================\n\nexport type PreviewDeployerContext = {\n previewId: string;\n port: number;\n hostname: string;\n url: string;\n};\n\nexport type PreviewFleetOptions = {\n /**\n * Apex used to build preview hostnames: `<previewId>.<baseDomain>`.\n * `previewId` is slugified before use (alphanumeric + `-`).\n */\n baseDomain: string;\n /**\n * Optional URL scheme. Defaults to `'https'`. Set `'http'` for\n * local-loop previews behind a localhost proxy.\n */\n scheme?: \"http\" | \"https\";\n /**\n * DNS provider — `cloudflareProvider`, `route53Provider`, etc.\n * Optional: when absent, `fleet.create` skips DNS work and the\n * caller is responsible for routing requests at the hostname.\n */\n dns?: DnsProvider;\n /**\n * Public IPv4 the A record should point at. Required when `dns`\n * is set. The fleet calls `dns.upsert({ name, type: 'A',\n * content: ipv4 })`.\n */\n ipv4?: string;\n /**\n * TTL applied to created A records. Default 60s — previews come\n * and go and we want low cache lifetime.\n */\n dnsTtl?: number;\n /**\n * Proxied flag (Cloudflare orange-cloud). Default `false` — most\n * previews want a real IP, not a Cloudflare proxy.\n */\n dnsProxied?: boolean;\n /**\n * Port range that the fleet allocates from. Default `[3100, 3899]`\n * (3000 squat is documented in memory).\n */\n portRange?: { start: number; end: number };\n /** Override port allocation entirely (e.g. talk to a port-leaser). */\n allocatePort?: (record: {\n previewId: string;\n hostname: string;\n }) => Promise<number>;\n /**\n * REQUIRED. Build a `Deployer` for an individual preview. The\n * factory receives the resolved id, port, hostname, and URL —\n * use them to set the right env, processManager, and target.\n */\n makeDeployer: (ctx: PreviewDeployerContext) => Deployer | Promise<Deployer>;\n /**\n * Stop callback. Called by `teardown` BEFORE DNS removal so the\n * preview process stops accepting connections before the record\n * disappears. Use it to call `processManager.stop()`, kill the\n * port, etc.\n */\n stop?: (record: PreviewRecord) => Promise<void> | void;\n /**\n * After-teardown callback. Called after DNS + registry removal —\n * good place to delete the release directory, drop a tenant\n * schema, clear secrets, etc.\n */\n afterTeardown?: (record: PreviewRecord) => Promise<void> | void;\n /** Registry. Default = filesystem store at `<root>`. */\n store?: PreviewStore;\n /** Directory the file-based default store writes into. */\n registryRoot?: string;\n /** Override `Date.now()` for tests. */\n clock?: () => number;\n};\n\n// =============================================================================\n// File-based PreviewStore (default)\n// =============================================================================\n\n/**\n * Single-file JSON registry. Reads + writes atomically (temp-file\n * + rename). Adequate for a deploy bot on one host — swap in a\n * Postgres-backed store for distributed deploy bots.\n */\nexport const createFilePreviewStore = (root: string): PreviewStore => {\n const path = join(root, \"previews.json\");\n\n const readAll = (): PreviewRecord[] => {\n if (!existsSync(path)) return [];\n try {\n const raw = readFileSync(path, \"utf8\");\n if (raw.trim() === \"\") return [];\n const parsed = JSON.parse(raw) as { previews?: PreviewRecord[] };\n return Array.isArray(parsed.previews) ? parsed.previews : [];\n } catch {\n return [];\n }\n };\n\n const writeAll = (records: PreviewRecord[]): void => {\n mkdirSync(dirname(path), { recursive: true });\n const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;\n writeFileSync(tmp, JSON.stringify({ previews: records }, null, 2));\n renameSync(tmp, path);\n };\n\n return {\n get: async (previewId) =>\n readAll().find((r) => r.previewId === previewId) ?? null,\n list: async () => readAll(),\n put: async (record) => {\n const records = readAll().filter((r) => r.previewId !== record.previewId);\n records.push(record);\n writeAll(records);\n },\n remove: async (previewId) => {\n const records = readAll().filter((r) => r.previewId !== previewId);\n writeAll(records);\n },\n };\n};\n\n// =============================================================================\n// In-memory store (for tests + ephemeral fleets)\n// =============================================================================\n\nexport const createMemoryPreviewStore = (): PreviewStore => {\n const map = new Map<string, PreviewRecord>();\n return {\n get: async (previewId) => map.get(previewId) ?? null,\n list: async () => Array.from(map.values()),\n put: async (record) => {\n map.set(record.previewId, record);\n },\n remove: async (previewId) => {\n map.delete(previewId);\n },\n };\n};\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nconst slugify = (id: string): string =>\n id\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 50);\n\nconst defaultAllocatePort = (\n used: Set<number>,\n range: { start: number; end: number },\n): number => {\n for (let port = range.start; port <= range.end; port += 1) {\n if (!used.has(port)) return port;\n }\n throw new Error(\n `preview-fleet: no free ports in [${range.start}, ${range.end}] (${used.size} in use)`,\n );\n};\n\n// =============================================================================\n// Fleet\n// =============================================================================\n\nexport type CreatePreviewInput = {\n previewId: string;\n commitSha?: string;\n annotations?: ReleaseAnnotations;\n /**\n * Override hostname. Default = `<slug(previewId)>.<baseDomain>`.\n * Use this for previews with a vanity name that shouldn't echo\n * the PR id directly.\n */\n hostname?: string;\n};\n\nexport type CreatePreviewResult = {\n record: PreviewRecord;\n deploy: DeployResult;\n};\n\nexport type PreviewFleet = {\n /**\n * Spin up a preview. Idempotent on `previewId` — if a record\n * already exists, the fleet re-uses its port + DNS and runs a\n * fresh `deployer.deploy()` (so PRs that push new commits roll\n * forward).\n */\n create: (input: CreatePreviewInput) => Promise<CreatePreviewResult>;\n /** Tear down a single preview (stop → DNS remove → registry remove → afterTeardown). */\n teardown: (previewId: string) => Promise<void>;\n /** List active previews. */\n list: () => Promise<PreviewRecord[]>;\n /** Get one preview by id. */\n get: (previewId: string) => Promise<PreviewRecord | null>;\n /**\n * Tear down all previews older than `olderThanMs`. Returns the\n * list of preview ids torn down. Failures on individual previews\n * are swallowed and reported on `errors`.\n */\n gc: (options: { olderThanMs: number }) => Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }>;\n};\n\nexport const createPreviewFleet = (\n options: PreviewFleetOptions,\n): PreviewFleet => {\n const scheme = options.scheme ?? \"https\";\n const portRange = options.portRange ?? { end: 3899, start: 3100 };\n const dnsTtl = options.dnsTtl ?? 60;\n const dnsProxied = options.dnsProxied ?? false;\n const clock = options.clock ?? Date.now;\n const store =\n options.store ??\n createFilePreviewStore(\n options.registryRoot ?? join(process.cwd(), \".preview-fleet\"),\n );\n\n if (options.dns !== undefined && options.ipv4 === undefined) {\n throw new Error(\n \"preview-fleet: `ipv4` is required when `dns` is configured\",\n );\n }\n\n const ensureDns = async (\n hostname: string,\n ): Promise<DnsRecord | undefined> => {\n if (options.dns === undefined) return undefined;\n const spec: DnsRecordSpec = {\n content: options.ipv4!,\n name: hostname,\n proxied: dnsProxied,\n ttl: dnsTtl,\n type: \"A\",\n };\n return await options.dns.upsert(spec);\n };\n\n const removeDns = async (record: PreviewRecord): Promise<void> => {\n if (options.dns === undefined || record.dnsRecordId === undefined) {\n return;\n }\n try {\n await options.dns.delete(record.dnsRecordId);\n } catch {\n // Idempotent teardown — the record may already be gone. Don't\n // block the rest of teardown on a stale id.\n }\n };\n\n const buildHostname = (previewId: string, override?: string): string => {\n if (override !== undefined) return override;\n const slug = slugify(previewId);\n if (slug === \"\") {\n throw new Error(\n `preview-fleet: previewId ${JSON.stringify(previewId)} slugifies to empty`,\n );\n }\n return `${slug}.${options.baseDomain}`;\n };\n\n const create = async (\n input: CreatePreviewInput,\n ): Promise<CreatePreviewResult> => {\n const existing = await store.get(input.previewId);\n const hostname = buildHostname(input.previewId, input.hostname);\n const url = `${scheme}://${hostname}`;\n\n // Allocate a port — reuse existing if we're re-deploying.\n let port: number;\n if (existing !== null) {\n port = existing.port;\n } else if (options.allocatePort !== undefined) {\n port = await options.allocatePort({\n hostname,\n previewId: input.previewId,\n });\n } else {\n const used = new Set((await store.list()).map((record) => record.port));\n port = defaultAllocatePort(used, portRange);\n }\n\n const deployer = await options.makeDeployer({\n hostname,\n port,\n previewId: input.previewId,\n url,\n });\n\n const deployOptions =\n input.annotations !== undefined\n ? { annotations: input.annotations }\n : input.commitSha !== undefined\n ? { annotations: { commitSha: input.commitSha } }\n : undefined;\n const deployResult = await deployer.deploy(deployOptions);\n\n // DNS only after the deploy succeeded — no point flipping the\n // record at a half-baked release.\n const dnsRecord = await ensureDns(hostname);\n\n const record: PreviewRecord = {\n createdAt: existing?.createdAt ?? clock(),\n hostname,\n port,\n previewId: input.previewId,\n releaseId: deployResult.releaseId,\n url,\n ...(dnsRecord !== undefined ? { dnsRecordId: dnsRecord.id } : {}),\n ...(input.commitSha !== undefined ? { commitSha: input.commitSha } : {}),\n ...(input.annotations !== undefined\n ? { annotations: input.annotations }\n : {}),\n };\n\n await store.put(record);\n return { deploy: deployResult, record };\n };\n\n const teardown = async (previewId: string): Promise<void> => {\n const record = await store.get(previewId);\n if (record === null) return;\n\n if (options.stop !== undefined) {\n try {\n await options.stop(record);\n } catch {\n // Don't block DNS removal on a stop failure. The caller\n // can see the error via afterTeardown by re-querying.\n }\n }\n\n await removeDns(record);\n await store.remove(previewId);\n\n if (options.afterTeardown !== undefined) {\n try {\n await options.afterTeardown(record);\n } catch {\n // Caller's own teardown errors are their problem; the\n // substrate has already removed the record + DNS.\n }\n }\n };\n\n const gc = async ({\n olderThanMs,\n }: {\n olderThanMs: number;\n }): Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }> => {\n const cutoff = clock() - olderThanMs;\n const all = await store.list();\n const expired = all.filter((record) => record.createdAt < cutoff);\n const removed: string[] = [];\n const errors: { previewId: string; error: Error }[] = [];\n for (const record of expired) {\n try {\n await teardown(record.previewId);\n removed.push(record.previewId);\n } catch (e) {\n errors.push({\n error: e instanceof Error ? e : new Error(String(e)),\n previewId: record.previewId,\n });\n }\n }\n return { errors, removed };\n };\n\n return {\n create,\n gc,\n get: (previewId) => store.get(previewId),\n list: () => store.list(),\n teardown,\n };\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "6DE77966EC79779D64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `reload` / `status` against a `Target` works. PM2, supervisord, runit,
|
|
9
9
|
* @absolutejs/runtime all fit if someone writes the adapter.
|
|
10
10
|
*/
|
|
11
|
-
import type { Target } from
|
|
11
|
+
import type { Target } from "./targets";
|
|
12
12
|
export type ProcessManagerContext = {
|
|
13
13
|
/** Absolute path on the target to the active release dir (the symlink target). */
|
|
14
14
|
currentPath: string;
|
|
@@ -21,7 +21,7 @@ export type ProcessManagerContext = {
|
|
|
21
21
|
/** Optional env to set on the process. */
|
|
22
22
|
env: Record<string, string>;
|
|
23
23
|
/** Log sink for any commands the manager runs. */
|
|
24
|
-
onLog?: (line: string, stream:
|
|
24
|
+
onLog?: (line: string, stream: "stdout" | "stderr") => void;
|
|
25
25
|
};
|
|
26
26
|
export type ProcessManager = {
|
|
27
27
|
/** Bring the new release up. Called after the `current` symlink has been swapped. */
|
|
@@ -29,7 +29,7 @@ export type ProcessManager = {
|
|
|
29
29
|
/** Stop the running process. */
|
|
30
30
|
stop?: (target: Target, ctx: ProcessManagerContext) => Promise<void>;
|
|
31
31
|
/** Return current status (best-effort; used by callers for diagnostics). */
|
|
32
|
-
status?: (target: Target, ctx: ProcessManagerContext) => Promise<
|
|
32
|
+
status?: (target: Target, ctx: ProcessManagerContext) => Promise<"running" | "stopped" | "unknown">;
|
|
33
33
|
};
|
|
34
34
|
export type BareManagerOptions = {
|
|
35
35
|
/** Command to run. Default `bun run start`. */
|
|
@@ -48,7 +48,7 @@ export type SystemdManagerOptions = {
|
|
|
48
48
|
/** Group. Default the deploy user. */
|
|
49
49
|
group?: string;
|
|
50
50
|
/** Restart policy. Default `always`. */
|
|
51
|
-
restart?:
|
|
51
|
+
restart?: "always" | "on-failure" | "no";
|
|
52
52
|
/** systemctl path. Default `systemctl`. */
|
|
53
53
|
systemctl?: string;
|
|
54
54
|
/** Unit file directory. Default `/etc/systemd/system`. */
|
package/dist/route53.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* of an `@aws-sdk/client-route-53` dependency. Wire your existing
|
|
14
14
|
* AWS SDK client with a 4-line shim — see README.
|
|
15
15
|
*/
|
|
16
|
-
import type { DnsProvider } from
|
|
16
|
+
import type { DnsProvider } from "./dns";
|
|
17
17
|
export type Route53ResourceRecord = {
|
|
18
18
|
Value: string;
|
|
19
19
|
};
|
|
@@ -30,7 +30,7 @@ export type Route53ResourceRecordSet = {
|
|
|
30
30
|
SetIdentifier?: string;
|
|
31
31
|
};
|
|
32
32
|
export type Route53Change = {
|
|
33
|
-
Action:
|
|
33
|
+
Action: "CREATE" | "DELETE" | "UPSERT";
|
|
34
34
|
ResourceRecordSet: Route53ResourceRecordSet;
|
|
35
35
|
};
|
|
36
36
|
export type Route53ListInput = {
|
package/dist/route53.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/route53.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @absolutejs/deploy/route53 — AWS Route 53 DNS implementing the\n * {@link DnsProvider} contract from `./dns`.\n *\n * Different shape from Cloudflare/DO/Hetzner DNS: Route 53 has no\n * per-record id. Records are identified by their (Name, Type)\n * composite, and mutations are done via `ChangeResourceRecordSets`\n * with a batch of CREATE / UPSERT / DELETE actions. We exploit\n * Route 53's native `UPSERT` action — one API call replaces the\n * find-then-create-or-update dance the other providers need.\n *\n * Narrow `Route53ClientLike` interface so the package stays free\n * of an `@aws-sdk/client-route-53` dependency. Wire your existing\n * AWS SDK client with a 4-line shim — see README.\n */\n\nimport type {\n\tDnsProvider,\n\tDnsRecord,\n\tDnsRecordFilter,\n\tDnsRecordSpec,\n\tDnsRecordType\n} from './dns';\n\n// =============================================================================\n// AWS API shapes — narrowed to what we use\n// =============================================================================\n\nexport type Route53ResourceRecord = { Value: string };\n\nexport type Route53ResourceRecordSet = {\n\tName: string;\n\tType: string;\n\tTTL?: number;\n\tResourceRecords?: Route53ResourceRecord[];\n\tAliasTarget?: {\n\t\tHostedZoneId: string;\n\t\tDNSName: string;\n\t\tEvaluateTargetHealth: boolean;\n\t};\n\tSetIdentifier?: string;\n};\n\nexport type Route53Change = {\n\tAction: 'CREATE' | 'DELETE' | 'UPSERT';\n\tResourceRecordSet: Route53ResourceRecordSet;\n};\n\nexport type Route53ListInput = {\n\tHostedZoneId: string;\n\tStartRecordName?: string;\n\tStartRecordType?: string;\n\tMaxItems?: string;\n};\n\nexport type Route53ListOutput = {\n\tResourceRecordSets: Route53ResourceRecordSet[];\n\tIsTruncated?: boolean;\n\tNextRecordName?: string;\n\tNextRecordType?: string;\n};\n\nexport type Route53ChangeInput = {\n\tHostedZoneId: string;\n\tChangeBatch: { Changes: Route53Change[]; Comment?: string };\n};\n\nexport type Route53ChangeOutput = {\n\tChangeInfo: { Id: string; Status: string };\n};\n\n/**\n * Minimal subset of the Route 53 client we need. Wire your\n * `@aws-sdk/client-route-53` `Route53Client` with a shim:\n *\n * ```ts\n * import {\n * Route53Client,\n * ListResourceRecordSetsCommand,\n * ChangeResourceRecordSetsCommand,\n * } from '@aws-sdk/client-route-53';\n *\n * const aws = new Route53Client({ region: 'us-east-1' });\n * const client: Route53ClientLike = {\n * listResourceRecordSets: (input) =>\n * aws.send(new ListResourceRecordSetsCommand(input)) as any,\n * changeResourceRecordSets: (input) =>\n * aws.send(new ChangeResourceRecordSetsCommand(input)) as any,\n * };\n * ```\n *\n * Or hand-roll a SigV4-signed fetch client if you want zero deps.\n */\nexport type Route53ClientLike = {\n\tlistResourceRecordSets: (\n\t\tinput: Route53ListInput\n\t) => Promise<Route53ListOutput>;\n\tchangeResourceRecordSets: (\n\t\tinput: Route53ChangeInput\n\t) => Promise<Route53ChangeOutput>;\n};\n\nexport type Route53ProviderOptions = {\n\tclient: Route53ClientLike;\n\t/** Hosted zone id, e.g. `'Z2FDTNDATAQYW2'`. */\n\thostedZoneId: string;\n\t/** Zone name for log + name conversion, e.g. `'example.com'`. */\n\tzoneName: string;\n\t/** Default TTL when `spec.ttl` is omitted. Default 300. */\n\tdefaultTtl?: number;\n};\n\n// =============================================================================\n// Name + TXT-value normalization\n// =============================================================================\n\nconst ensureFqdn = (name: string, zoneName: string): string => {\n\tif (name === '@') return ensureTrailingDot(zoneName);\n\tif (name === zoneName) return ensureTrailingDot(name);\n\tif (name.endsWith('.')) return name;\n\treturn `${name}.`;\n};\n\nconst ensureTrailingDot = (s: string): string => (s.endsWith('.') ? s : `${s}.`);\n\nconst stripTrailingDot = (s: string): string =>\n\ts.endsWith('.') ? s.slice(0, -1) : s;\n\n/**\n * Route 53 TXT values MUST be enclosed in double quotes in the wire\n * format. Strings > 255 chars get split into multiple quoted chunks.\n * Bare unquoted input → wrap. Already-quoted input → pass through.\n */\nconst encodeTxtValue = (raw: string): string => {\n\tif (raw.startsWith('\"') && raw.endsWith('\"')) return raw;\n\tconst CHUNK = 255;\n\tconst chunks: string[] = [];\n\tfor (let i = 0; i < raw.length; i += CHUNK) {\n\t\tchunks.push(`\"${raw.slice(i, i + CHUNK).replaceAll('\"', '\\\\\"')}\"`);\n\t}\n\treturn chunks.join(' ');\n};\n\n/** Inverse of encodeTxtValue: unwrap \" ... \" \" ... \" back to raw. */\nconst decodeTxtValue = (wire: string): string => {\n\tconst parts = wire.match(/\"((?:\\\\.|[^\"])*)\"/g);\n\tif (parts === null) return wire;\n\treturn parts\n\t\t.map((part) => part.slice(1, -1).replaceAll('\\\\\"', '\"'))\n\t\t.join('');\n};\n\n// =============================================================================\n// Synthetic-id encoding — Route 53 has no per-record id\n// =============================================================================\n\n/**\n * Route 53 deletes require the CURRENT record state (Name + Type +\n * TTL + Values), not an opaque id. We encode that state as a\n * base64-JSON synthetic id so the DnsProvider contract (which\n * expects `delete(id)`) works without a second API round-trip.\n */\ntype Route53IdPayload = {\n\tn: string; // Name (FQDN with trailing dot)\n\tt: string; // Type\n\tv: string[]; // Values (as written to ResourceRecords[])\n\tl?: number; // TTL\n};\n\nconst encodeId = (payload: Route53IdPayload): string =>\n\tbtoa(JSON.stringify(payload))\n\t\t.replaceAll('+', '-')\n\t\t.replaceAll('/', '_')\n\t\t.replaceAll('=', '');\n\nconst decodeId = (id: string): Route53IdPayload => {\n\tconst padded = id\n\t\t.replaceAll('-', '+')\n\t\t.replaceAll('_', '/')\n\t\t.padEnd(Math.ceil(id.length / 4) * 4, '=');\n\treturn JSON.parse(atob(padded)) as Route53IdPayload;\n};\n\n// =============================================================================\n// Conversion: AWS RecordSet ↔ DnsRecord\n// =============================================================================\n\nconst toDnsRecord = (\n\tset: Route53ResourceRecordSet,\n\tzoneName: string\n): DnsRecord | undefined => {\n\t// Alias records (no ResourceRecords) don't fit our string-content model.\n\tconst values = set.ResourceRecords?.map((r) => r.Value) ?? [];\n\tif (values.length === 0) return undefined;\n\t// For TXT, decode the wire format back to raw plaintext.\n\tconst content =\n\t\tset.Type === 'TXT' ? decodeTxtValue(values.join(' ')) : (values[0] as string);\n\tconst idPayload: Route53IdPayload = {\n\t\tn: set.Name,\n\t\tt: set.Type,\n\t\tv: values,\n\t\t...(set.TTL !== undefined ? { l: set.TTL } : {})\n\t};\n\tconst fqdn = stripTrailingDot(set.Name);\n\tconst name = fqdn === zoneName ? zoneName : fqdn;\n\treturn {\n\t\tcontent,\n\t\tid: encodeId(idPayload),\n\t\tname,\n\t\ttype: set.Type as DnsRecordType,\n\t\t...(set.TTL !== undefined ? { ttl: set.TTL } : {})\n\t};\n};\n\nconst buildRecordSet = (\n\tspec: DnsRecordSpec,\n\tzoneName: string,\n\tdefaultTtl: number\n): Route53ResourceRecordSet => {\n\tconst Name = ensureFqdn(spec.name, zoneName);\n\tconst ttl = spec.ttl ?? defaultTtl;\n\tconst value = spec.type === 'TXT' ? encodeTxtValue(spec.content) : spec.content;\n\treturn {\n\t\tName,\n\t\tResourceRecords: [{ Value: value }],\n\t\tTTL: ttl,\n\t\tType: spec.type\n\t};\n};\n\n// =============================================================================\n// Provider factory\n// =============================================================================\n\nexport const route53DnsProvider = (\n\toptions: Route53ProviderOptions\n): DnsProvider => {\n\tconst { client, hostedZoneId, zoneName } = options;\n\tconst defaultTtl = options.defaultTtl ?? 300;\n\tconst zoneLabel = zoneName;\n\n\tconst fetchAll = async (\n\t\tfilter?: DnsRecordFilter\n\t): Promise<DnsRecord[]> => {\n\t\tconst out: DnsRecord[] = [];\n\t\tlet startName: string | undefined;\n\t\tlet startType: string | undefined;\n\t\tfor (;;) {\n\t\t\tconst params: Route53ListInput = {\n\t\t\t\tHostedZoneId: hostedZoneId,\n\t\t\t\tMaxItems: '500',\n\t\t\t\t...(startName !== undefined ? { StartRecordName: startName } : {}),\n\t\t\t\t...(startType !== undefined ? { StartRecordType: startType } : {})\n\t\t\t};\n\t\t\tconst page = await client.listResourceRecordSets(params);\n\t\t\tfor (const set of page.ResourceRecordSets) {\n\t\t\t\tconst dnsRecord = toDnsRecord(set, zoneName);\n\t\t\t\tif (dnsRecord === undefined) continue;\n\t\t\t\tif (\n\t\t\t\t\tfilter?.name !== undefined &&\n\t\t\t\t\tdnsRecord.name !== filter.name\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\tfilter?.type !== undefined &&\n\t\t\t\t\tdnsRecord.type !== filter.type\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tout.push(dnsRecord);\n\t\t\t}\n\t\t\tif (page.IsTruncated !== true) break;\n\t\t\tstartName = page.NextRecordName;\n\t\t\tstartType = page.NextRecordType;\n\t\t}\n\t\treturn out;\n\t};\n\n\tconst list = (filter?: DnsRecordFilter): Promise<DnsRecord[]> =>\n\t\tfetchAll(filter);\n\n\tconst find = async (key: {\n\t\tname: string;\n\t\ttype: DnsRecordType;\n\t}): Promise<DnsRecord | undefined> => {\n\t\tconst matches = await fetchAll(key);\n\t\tif (matches.length === 0) return undefined;\n\t\tif (matches.length > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t`[deploy/route53] multiple ${key.type} records for \"${key.name}\" in zone ${zoneLabel} — drifted state; resolve manually before upsert.`\n\t\t\t);\n\t\t}\n\t\treturn matches[0];\n\t};\n\n\tconst submitChange = async (change: Route53Change): Promise<void> => {\n\t\tawait client.changeResourceRecordSets({\n\t\t\tChangeBatch: { Changes: [change] },\n\t\t\tHostedZoneId: hostedZoneId\n\t\t});\n\t};\n\n\tconst create = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n\t\tconst set = buildRecordSet(spec, zoneName, defaultTtl);\n\t\tawait submitChange({ Action: 'CREATE', ResourceRecordSet: set });\n\t\tconst synthesized = toDnsRecord(set, zoneName);\n\t\tif (synthesized === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t'[deploy/route53] internal: built record had no ResourceRecords'\n\t\t\t);\n\t\t}\n\t\treturn synthesized;\n\t};\n\n\tconst upsert = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n\t\tconst set = buildRecordSet(spec, zoneName, defaultTtl);\n\t\tawait submitChange({ Action: 'UPSERT', ResourceRecordSet: set });\n\t\tconst synthesized = toDnsRecord(set, zoneName);\n\t\tif (synthesized === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t'[deploy/route53] internal: built record had no ResourceRecords'\n\t\t\t);\n\t\t}\n\t\treturn synthesized;\n\t};\n\n\tconst update = async (\n\t\t_id: string,\n\t\tspec: DnsRecordSpec\n\t): Promise<DnsRecord> => upsert(spec);\n\n\tconst deleteRecord = async (id: string): Promise<void> => {\n\t\t// Route 53's DELETE requires the exact current state. We reconstruct\n\t\t// it from the synthetic id we issued at read/write time.\n\t\tconst payload = decodeId(id);\n\t\tconst set: Route53ResourceRecordSet = {\n\t\t\tName: payload.n,\n\t\t\tResourceRecords: payload.v.map((Value) => ({ Value })),\n\t\t\tType: payload.t,\n\t\t\t...(payload.l !== undefined ? { TTL: payload.l } : {})\n\t\t};\n\t\ttry {\n\t\t\tawait submitChange({ Action: 'DELETE', ResourceRecordSet: set });\n\t\t} catch (error) {\n\t\t\t// Route 53 throws InvalidChangeBatch when the record doesn't exist\n\t\t\t// (or its state has drifted from what we encoded). Treat the\n\t\t\t// \"doesn't exist\" subset as idempotent success.\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : String(error);\n\t\t\tif (\n\t\t\t\tmessage.includes('not found') ||\n\t\t\t\tmessage.includes('NoSuchHostedZone') ||\n\t\t\t\tmessage.includes(\n\t\t\t\t\t'but it was not found'\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\treturn {\n\t\tcreate,\n\t\tdelete: deleteRecord,\n\t\tdescription: `route53 zone \"${zoneLabel}\"`,\n\t\tfind,\n\t\tlist,\n\t\tupdate,\n\t\tupsert\n\t};\n};\n"
|
|
5
|
+
"/**\n * @absolutejs/deploy/route53 — AWS Route 53 DNS implementing the\n * {@link DnsProvider} contract from `./dns`.\n *\n * Different shape from Cloudflare/DO/Hetzner DNS: Route 53 has no\n * per-record id. Records are identified by their (Name, Type)\n * composite, and mutations are done via `ChangeResourceRecordSets`\n * with a batch of CREATE / UPSERT / DELETE actions. We exploit\n * Route 53's native `UPSERT` action — one API call replaces the\n * find-then-create-or-update dance the other providers need.\n *\n * Narrow `Route53ClientLike` interface so the package stays free\n * of an `@aws-sdk/client-route-53` dependency. Wire your existing\n * AWS SDK client with a 4-line shim — see README.\n */\n\nimport type {\n DnsProvider,\n DnsRecord,\n DnsRecordFilter,\n DnsRecordSpec,\n DnsRecordType,\n} from \"./dns\";\n\n// =============================================================================\n// AWS API shapes — narrowed to what we use\n// =============================================================================\n\nexport type Route53ResourceRecord = { Value: string };\n\nexport type Route53ResourceRecordSet = {\n Name: string;\n Type: string;\n TTL?: number;\n ResourceRecords?: Route53ResourceRecord[];\n AliasTarget?: {\n HostedZoneId: string;\n DNSName: string;\n EvaluateTargetHealth: boolean;\n };\n SetIdentifier?: string;\n};\n\nexport type Route53Change = {\n Action: \"CREATE\" | \"DELETE\" | \"UPSERT\";\n ResourceRecordSet: Route53ResourceRecordSet;\n};\n\nexport type Route53ListInput = {\n HostedZoneId: string;\n StartRecordName?: string;\n StartRecordType?: string;\n MaxItems?: string;\n};\n\nexport type Route53ListOutput = {\n ResourceRecordSets: Route53ResourceRecordSet[];\n IsTruncated?: boolean;\n NextRecordName?: string;\n NextRecordType?: string;\n};\n\nexport type Route53ChangeInput = {\n HostedZoneId: string;\n ChangeBatch: { Changes: Route53Change[]; Comment?: string };\n};\n\nexport type Route53ChangeOutput = {\n ChangeInfo: { Id: string; Status: string };\n};\n\n/**\n * Minimal subset of the Route 53 client we need. Wire your\n * `@aws-sdk/client-route-53` `Route53Client` with a shim:\n *\n * ```ts\n * import {\n * Route53Client,\n * ListResourceRecordSetsCommand,\n * ChangeResourceRecordSetsCommand,\n * } from '@aws-sdk/client-route-53';\n *\n * const aws = new Route53Client({ region: 'us-east-1' });\n * const client: Route53ClientLike = {\n * listResourceRecordSets: (input) =>\n * aws.send(new ListResourceRecordSetsCommand(input)) as any,\n * changeResourceRecordSets: (input) =>\n * aws.send(new ChangeResourceRecordSetsCommand(input)) as any,\n * };\n * ```\n *\n * Or hand-roll a SigV4-signed fetch client if you want zero deps.\n */\nexport type Route53ClientLike = {\n listResourceRecordSets: (\n input: Route53ListInput,\n ) => Promise<Route53ListOutput>;\n changeResourceRecordSets: (\n input: Route53ChangeInput,\n ) => Promise<Route53ChangeOutput>;\n};\n\nexport type Route53ProviderOptions = {\n client: Route53ClientLike;\n /** Hosted zone id, e.g. `'Z2FDTNDATAQYW2'`. */\n hostedZoneId: string;\n /** Zone name for log + name conversion, e.g. `'example.com'`. */\n zoneName: string;\n /** Default TTL when `spec.ttl` is omitted. Default 300. */\n defaultTtl?: number;\n};\n\n// =============================================================================\n// Name + TXT-value normalization\n// =============================================================================\n\nconst ensureFqdn = (name: string, zoneName: string): string => {\n if (name === \"@\") return ensureTrailingDot(zoneName);\n if (name === zoneName) return ensureTrailingDot(name);\n if (name.endsWith(\".\")) return name;\n return `${name}.`;\n};\n\nconst ensureTrailingDot = (s: string): string =>\n s.endsWith(\".\") ? s : `${s}.`;\n\nconst stripTrailingDot = (s: string): string =>\n s.endsWith(\".\") ? s.slice(0, -1) : s;\n\n/**\n * Route 53 TXT values MUST be enclosed in double quotes in the wire\n * format. Strings > 255 chars get split into multiple quoted chunks.\n * Bare unquoted input → wrap. Already-quoted input → pass through.\n */\nconst encodeTxtValue = (raw: string): string => {\n if (raw.startsWith('\"') && raw.endsWith('\"')) return raw;\n const CHUNK = 255;\n const chunks: string[] = [];\n for (let i = 0; i < raw.length; i += CHUNK) {\n chunks.push(`\"${raw.slice(i, i + CHUNK).replaceAll('\"', '\\\\\"')}\"`);\n }\n return chunks.join(\" \");\n};\n\n/** Inverse of encodeTxtValue: unwrap \" ... \" \" ... \" back to raw. */\nconst decodeTxtValue = (wire: string): string => {\n const parts = wire.match(/\"((?:\\\\.|[^\"])*)\"/g);\n if (parts === null) return wire;\n return parts.map((part) => part.slice(1, -1).replaceAll('\\\\\"', '\"')).join(\"\");\n};\n\n// =============================================================================\n// Synthetic-id encoding — Route 53 has no per-record id\n// =============================================================================\n\n/**\n * Route 53 deletes require the CURRENT record state (Name + Type +\n * TTL + Values), not an opaque id. We encode that state as a\n * base64-JSON synthetic id so the DnsProvider contract (which\n * expects `delete(id)`) works without a second API round-trip.\n */\ntype Route53IdPayload = {\n n: string; // Name (FQDN with trailing dot)\n t: string; // Type\n v: string[]; // Values (as written to ResourceRecords[])\n l?: number; // TTL\n};\n\nconst encodeId = (payload: Route53IdPayload): string =>\n btoa(JSON.stringify(payload))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replaceAll(\"=\", \"\");\n\nconst decodeId = (id: string): Route53IdPayload => {\n const padded = id\n .replaceAll(\"-\", \"+\")\n .replaceAll(\"_\", \"/\")\n .padEnd(Math.ceil(id.length / 4) * 4, \"=\");\n return JSON.parse(atob(padded)) as Route53IdPayload;\n};\n\n// =============================================================================\n// Conversion: AWS RecordSet ↔ DnsRecord\n// =============================================================================\n\nconst toDnsRecord = (\n set: Route53ResourceRecordSet,\n zoneName: string,\n): DnsRecord | undefined => {\n // Alias records (no ResourceRecords) don't fit our string-content model.\n const values = set.ResourceRecords?.map((r) => r.Value) ?? [];\n if (values.length === 0) return undefined;\n // For TXT, decode the wire format back to raw plaintext.\n const content =\n set.Type === \"TXT\"\n ? decodeTxtValue(values.join(\" \"))\n : (values[0] as string);\n const idPayload: Route53IdPayload = {\n n: set.Name,\n t: set.Type,\n v: values,\n ...(set.TTL !== undefined ? { l: set.TTL } : {}),\n };\n const fqdn = stripTrailingDot(set.Name);\n const name = fqdn === zoneName ? zoneName : fqdn;\n return {\n content,\n id: encodeId(idPayload),\n name,\n type: set.Type as DnsRecordType,\n ...(set.TTL !== undefined ? { ttl: set.TTL } : {}),\n };\n};\n\nconst buildRecordSet = (\n spec: DnsRecordSpec,\n zoneName: string,\n defaultTtl: number,\n): Route53ResourceRecordSet => {\n const Name = ensureFqdn(spec.name, zoneName);\n const ttl = spec.ttl ?? defaultTtl;\n const value =\n spec.type === \"TXT\" ? encodeTxtValue(spec.content) : spec.content;\n return {\n Name,\n ResourceRecords: [{ Value: value }],\n TTL: ttl,\n Type: spec.type,\n };\n};\n\n// =============================================================================\n// Provider factory\n// =============================================================================\n\nexport const route53DnsProvider = (\n options: Route53ProviderOptions,\n): DnsProvider => {\n const { client, hostedZoneId, zoneName } = options;\n const defaultTtl = options.defaultTtl ?? 300;\n const zoneLabel = zoneName;\n\n const fetchAll = async (filter?: DnsRecordFilter): Promise<DnsRecord[]> => {\n const out: DnsRecord[] = [];\n let startName: string | undefined;\n let startType: string | undefined;\n for (;;) {\n const params: Route53ListInput = {\n HostedZoneId: hostedZoneId,\n MaxItems: \"500\",\n ...(startName !== undefined ? { StartRecordName: startName } : {}),\n ...(startType !== undefined ? { StartRecordType: startType } : {}),\n };\n const page = await client.listResourceRecordSets(params);\n for (const set of page.ResourceRecordSets) {\n const dnsRecord = toDnsRecord(set, zoneName);\n if (dnsRecord === undefined) continue;\n if (filter?.name !== undefined && dnsRecord.name !== filter.name) {\n continue;\n }\n if (filter?.type !== undefined && dnsRecord.type !== filter.type) {\n continue;\n }\n out.push(dnsRecord);\n }\n if (page.IsTruncated !== true) break;\n startName = page.NextRecordName;\n startType = page.NextRecordType;\n }\n return out;\n };\n\n const list = (filter?: DnsRecordFilter): Promise<DnsRecord[]> =>\n fetchAll(filter);\n\n const find = async (key: {\n name: string;\n type: DnsRecordType;\n }): Promise<DnsRecord | undefined> => {\n const matches = await fetchAll(key);\n if (matches.length === 0) return undefined;\n if (matches.length > 1) {\n throw new Error(\n `[deploy/route53] multiple ${key.type} records for \"${key.name}\" in zone ${zoneLabel} — drifted state; resolve manually before upsert.`,\n );\n }\n return matches[0];\n };\n\n const submitChange = async (change: Route53Change): Promise<void> => {\n await client.changeResourceRecordSets({\n ChangeBatch: { Changes: [change] },\n HostedZoneId: hostedZoneId,\n });\n };\n\n const create = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n const set = buildRecordSet(spec, zoneName, defaultTtl);\n await submitChange({ Action: \"CREATE\", ResourceRecordSet: set });\n const synthesized = toDnsRecord(set, zoneName);\n if (synthesized === undefined) {\n throw new Error(\n \"[deploy/route53] internal: built record had no ResourceRecords\",\n );\n }\n return synthesized;\n };\n\n const upsert = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n const set = buildRecordSet(spec, zoneName, defaultTtl);\n await submitChange({ Action: \"UPSERT\", ResourceRecordSet: set });\n const synthesized = toDnsRecord(set, zoneName);\n if (synthesized === undefined) {\n throw new Error(\n \"[deploy/route53] internal: built record had no ResourceRecords\",\n );\n }\n return synthesized;\n };\n\n const update = async (_id: string, spec: DnsRecordSpec): Promise<DnsRecord> =>\n upsert(spec);\n\n const deleteRecord = async (id: string): Promise<void> => {\n // Route 53's DELETE requires the exact current state. We reconstruct\n // it from the synthetic id we issued at read/write time.\n const payload = decodeId(id);\n const set: Route53ResourceRecordSet = {\n Name: payload.n,\n ResourceRecords: payload.v.map((Value) => ({ Value })),\n Type: payload.t,\n ...(payload.l !== undefined ? { TTL: payload.l } : {}),\n };\n try {\n await submitChange({ Action: \"DELETE\", ResourceRecordSet: set });\n } catch (error) {\n // Route 53 throws InvalidChangeBatch when the record doesn't exist\n // (or its state has drifted from what we encoded). Treat the\n // \"doesn't exist\" subset as idempotent success.\n const message = error instanceof Error ? error.message : String(error);\n if (\n message.includes(\"not found\") ||\n message.includes(\"NoSuchHostedZone\") ||\n message.includes(\"but it was not found\")\n ) {\n return;\n }\n throw error;\n }\n };\n\n return {\n create,\n delete: deleteRecord,\n description: `route53 zone \"${zoneLabel}\"`,\n find,\n list,\n update,\n upsert,\n };\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoHA,IAAM,aAAa,CAAC,MAAc,aAA6B;AAAA,
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "9560FABB96092BEC64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/targets.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type ExecOptions = {
|
|
|
28
28
|
/** Hard kill after this many ms. Default 600_000 (10 min). 0 disables. */
|
|
29
29
|
timeoutMs?: number;
|
|
30
30
|
/** Pipe stdout/stderr through here as it streams (lines, newline-stripped). */
|
|
31
|
-
onLog?: (line: string, stream:
|
|
31
|
+
onLog?: (line: string, stream: "stdout" | "stderr") => void;
|
|
32
32
|
/** Stdin payload — a string is written verbatim. */
|
|
33
33
|
stdin?: string;
|
|
34
34
|
};
|
package/dist/tls.d.ts
CHANGED
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
* (reuse the same account across cert renewals — cheaper, doesn't
|
|
21
21
|
* hit Let's Encrypt's account-creation rate limit)
|
|
22
22
|
*/
|
|
23
|
-
import type { Target } from
|
|
24
|
-
import type { DnsProvider } from
|
|
23
|
+
import type { Target } from "./targets";
|
|
24
|
+
import type { DnsProvider } from "./dns";
|
|
25
25
|
export declare const LETSENCRYPT_PRODUCTION = "https://acme-v02.api.letsencrypt.org/directory";
|
|
26
26
|
export declare const LETSENCRYPT_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory";
|
|
27
27
|
type JwsHeader = {
|
|
28
|
-
alg:
|
|
28
|
+
alg: "ES256";
|
|
29
29
|
nonce: string;
|
|
30
30
|
url: string;
|
|
31
31
|
jwk?: JsonWebKey;
|
|
@@ -191,10 +191,10 @@ export type RenewCertificateOptions = IssueCertificateOptions & {
|
|
|
191
191
|
export type RenewalResult = {
|
|
192
192
|
renewed: true;
|
|
193
193
|
certificate: IssuedCertificate;
|
|
194
|
-
reason:
|
|
194
|
+
reason: "forced" | "no-current-cert" | "expiring-soon";
|
|
195
195
|
} | {
|
|
196
196
|
renewed: false;
|
|
197
|
-
reason:
|
|
197
|
+
reason: "still-fresh";
|
|
198
198
|
inspection: CertificateInspection;
|
|
199
199
|
};
|
|
200
200
|
/**
|
package/dist/tls.js
CHANGED
|
@@ -219,7 +219,10 @@ var buildExtensionRequestAttribute = (domains) => {
|
|
|
219
219
|
derSet([extensions])
|
|
220
220
|
]);
|
|
221
221
|
};
|
|
222
|
-
var generateCertKeyPair = async () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
222
|
+
var generateCertKeyPair = async () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
223
|
+
"sign",
|
|
224
|
+
"verify"
|
|
225
|
+
]);
|
|
223
226
|
var buildCsr = async (domains, keypair) => {
|
|
224
227
|
if (domains.length === 0) {
|
|
225
228
|
throw new Error("[deploy/tls] CSR requires at least one domain");
|
|
@@ -565,5 +568,5 @@ export {
|
|
|
565
568
|
AcmeError
|
|
566
569
|
};
|
|
567
570
|
|
|
568
|
-
//# debugId=
|
|
571
|
+
//# debugId=E1547F90274E984D64756E2164756E21
|
|
569
572
|
//# sourceMappingURL=tls.js.map
|