@geekmidas/cli 10.0.0-alpha.45 → 10.0.0-alpha.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +144 -183
  2. package/dist/{OpenApiTsGenerator-B4Kj_qNU.cjs → OpenApiTsGenerator-BN0PIZxd.cjs} +17 -71
  3. package/dist/OpenApiTsGenerator-BN0PIZxd.cjs.map +1 -0
  4. package/dist/{OpenApiTsGenerator-CEh3EKN3.mjs → OpenApiTsGenerator-BzIGy8KE.mjs} +17 -71
  5. package/dist/OpenApiTsGenerator-BzIGy8KE.mjs.map +1 -0
  6. package/dist/backends-B9V2XD2M.cjs +82 -0
  7. package/dist/backends-B9V2XD2M.cjs.map +1 -0
  8. package/dist/backends-CSuLl0YP.mjs +59 -0
  9. package/dist/backends-CSuLl0YP.mjs.map +1 -0
  10. package/dist/{config-CJ-PAb7M.cjs → config-CsYSyUya.cjs} +3 -3
  11. package/dist/{config-CJ-PAb7M.cjs.map → config-CsYSyUya.cjs.map} +1 -1
  12. package/dist/{config-5watB1D0.mjs → config-D8J5rrTb.mjs} +3 -3
  13. package/dist/{config-5watB1D0.mjs.map → config-D8J5rrTb.mjs.map} +1 -1
  14. package/dist/config.cjs +2 -2
  15. package/dist/config.d.cts +2 -2
  16. package/dist/config.d.mts +2 -2
  17. package/dist/config.mjs +2 -2
  18. package/dist/{discover-BLhf7V_J.mjs → discover-Cf2jAcgF.mjs} +4 -56
  19. package/dist/discover-Cf2jAcgF.mjs.map +1 -0
  20. package/dist/{discover-B0f_kuNN.cjs → discover-DXNviAh-.cjs} +3 -79
  21. package/dist/discover-DXNviAh-.cjs.map +1 -0
  22. package/dist/{emit-Bzq7JsMP.mjs → emit-C1e8qD08.mjs} +12 -43
  23. package/dist/emit-C1e8qD08.mjs.map +1 -0
  24. package/dist/{emit-BdoPFjZf.cjs → emit-Dmc1nvdS.cjs} +11 -54
  25. package/dist/emit-Dmc1nvdS.cjs.map +1 -0
  26. package/dist/encryption-C7OGMve_.cjs +91 -0
  27. package/dist/encryption-C7OGMve_.cjs.map +1 -0
  28. package/dist/encryption-DyQnPsZN.mjs +90 -0
  29. package/dist/encryption-DyQnPsZN.mjs.map +1 -0
  30. package/dist/{github-D5MthUlR.mjs → github-BfpadQWQ.mjs} +3 -3
  31. package/dist/{github-D5MthUlR.mjs.map → github-BfpadQWQ.mjs.map} +1 -1
  32. package/dist/{github-BAr8WGkR.cjs → github-BrI1tv6u.cjs} +3 -3
  33. package/dist/{github-BAr8WGkR.cjs.map → github-BrI1tv6u.cjs.map} +1 -1
  34. package/dist/{index-B3PeGOQp.d.cts → index-BQhJXpaQ.d.mts} +2 -2
  35. package/dist/{index-B3PeGOQp.d.cts.map → index-BQhJXpaQ.d.mts.map} +1 -1
  36. package/dist/{index-BOyaGcCm.d.mts → index-DHJePE0y.d.cts} +2 -2
  37. package/dist/{index-BOyaGcCm.d.mts.map → index-DHJePE0y.d.cts.map} +1 -1
  38. package/dist/index.cjs +444 -670
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.mjs +442 -668
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/{migrate-PJi4b1ga.cjs → migrate-BgEY9pdP.cjs} +5 -5
  43. package/dist/{migrate-PJi4b1ga.cjs.map → migrate-BgEY9pdP.cjs.map} +1 -1
  44. package/dist/{migrate-BRklCm7b.mjs → migrate-DhxTIA0z.mjs} +5 -5
  45. package/dist/{migrate-BRklCm7b.mjs.map → migrate-DhxTIA0z.mjs.map} +1 -1
  46. package/dist/openapi.cjs +2 -2
  47. package/dist/openapi.d.cts +1 -1
  48. package/dist/openapi.d.mts +1 -1
  49. package/dist/openapi.mjs +2 -2
  50. package/dist/{ready-ZqOuNvKD.mjs → ready-WfT-eWlm.mjs} +50 -8
  51. package/dist/ready-WfT-eWlm.mjs.map +1 -0
  52. package/dist/{ready-3RfHxdA0.cjs → ready-lr2Fm-ep.cjs} +68 -8
  53. package/dist/ready-lr2Fm-ep.cjs.map +1 -0
  54. package/dist/reconcile/public.cjs +2 -4
  55. package/dist/reconcile/public.d.cts +6 -10
  56. package/dist/reconcile/public.d.cts.map +1 -1
  57. package/dist/reconcile/public.d.mts +6 -10
  58. package/dist/reconcile/public.d.mts.map +1 -1
  59. package/dist/reconcile/public.mjs +3 -3
  60. package/dist/{types-0R124hrY.d.mts → types-BbrNu83u.d.mts} +14 -109
  61. package/dist/{types-0R124hrY.d.mts.map → types-BbrNu83u.d.mts.map} +1 -1
  62. package/dist/{types-DfdYe31u.d.cts → types-DNqEsctD.d.cts} +14 -109
  63. package/dist/{types-DfdYe31u.d.cts.map → types-DNqEsctD.d.cts.map} +1 -1
  64. package/dist/vitest.cjs +1 -1
  65. package/dist/vitest.mjs +1 -1
  66. package/dist/workspace/index.cjs +1 -1
  67. package/dist/workspace/index.d.cts +2 -2
  68. package/dist/workspace/index.d.mts +2 -2
  69. package/dist/workspace/index.mjs +1 -1
  70. package/dist/{workspace-RYSYtHP7.cjs → workspace-DEibcoBe.cjs} +11 -25
  71. package/dist/{workspace-RYSYtHP7.cjs.map → workspace-DEibcoBe.cjs.map} +1 -1
  72. package/dist/{workspace-DOmIhiau.mjs → workspace-DNzWgM1J.mjs} +11 -25
  73. package/dist/{workspace-DOmIhiau.mjs.map → workspace-DNzWgM1J.mjs.map} +1 -1
  74. package/dist/{workspace-BKytVCzb.mjs → workspace-cb6U29EL.mjs} +59 -81
  75. package/dist/workspace-cb6U29EL.mjs.map +1 -0
  76. package/dist/{workspace-dV6f-3vv.cjs → workspace-vafrzZTC.cjs} +65 -105
  77. package/dist/workspace-vafrzZTC.cjs.map +1 -0
  78. package/package.json +14 -14
  79. package/dist/OpenApiTsGenerator-B4Kj_qNU.cjs.map +0 -1
  80. package/dist/OpenApiTsGenerator-CEh3EKN3.mjs.map +0 -1
  81. package/dist/discover-B0f_kuNN.cjs.map +0 -1
  82. package/dist/discover-BLhf7V_J.mjs.map +0 -1
  83. package/dist/emit-BdoPFjZf.cjs.map +0 -1
  84. package/dist/emit-Bzq7JsMP.mjs.map +0 -1
  85. package/dist/ready-3RfHxdA0.cjs.map +0 -1
  86. package/dist/ready-ZqOuNvKD.mjs.map +0 -1
  87. package/dist/workspace-BKytVCzb.mjs.map +0 -1
  88. package/dist/workspace-dV6f-3vv.cjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover-DXNviAh-.cjs","names":["fg","kebabCase","relative","fg","canonicalId","relative"],"sources":["../src/generators/Generator.ts","../src/reconcile/discover.ts"],"sourcesContent":["import { relative } from 'node:path';\nimport type { Construct } from '@geekmidas/constructs';\nimport fg from 'fast-glob';\nimport kebabCase from 'lodash.kebabcase';\nimport type { BuildContext } from '../build/types';\nimport type { MainProvider, Routes } from '../types';\n\n/**\n * Zod v4 maintains a process-wide registry of schemas registered via\n * `.meta({ id })`. When we cache-bust user module imports (by appending a\n * `?t=timestamp` query), those modules re-execute and try to register the\n * same ids again, which throws `ID X already exists in the registry`.\n *\n * Clearing the registry in the CLI process is safe: the user's running\n * server lives in a subprocess and has its own isolated registry.\n *\n * Exported for tests.\n */\nexport function clearZodGlobalRegistry(): void {\n\tconst registry = (\n\t\tglobalThis as { __zod_globalRegistry?: { clear?: () => void } }\n\t).__zod_globalRegistry;\n\tif (registry && typeof registry.clear === 'function') {\n\t\tregistry.clear();\n\t}\n}\n\nexport interface GeneratorOptions {\n\t/** Where the build deploys: one Lambda per construct, or one process. */\n\ttarget?: MainProvider;\n\t/**\n\t * What a handler path in the manifest is relative to — the workspace root,\n\t * which is where `sst.config.ts` runs and resolves it from.\n\t */\n\troot?: string;\n\t[key: string]: any;\n}\n\nexport abstract class ConstructGenerator<T extends Construct, R = void> {\n\tabstract isConstruct(value: any): value is T;\n\n\tstatic async build<T extends Construct, R = void>(\n\t\tcontext: BuildContext,\n\t\toutputDir: string,\n\t\tgenerator: ConstructGenerator<T, R>,\n\t\tpatterns?: Routes,\n\t\toptions?: GeneratorOptions,\n\t): Promise<R> {\n\t\tconst constructs = await generator.load(patterns);\n\t\treturn generator.build(context, constructs, outputDir, options);\n\t}\n\n\tabstract build(\n\t\tcontext: BuildContext,\n\t\tconstructs: GeneratedConstruct<T>[],\n\t\toutputDir: string,\n\t\toptions?: GeneratorOptions,\n\t): Promise<R>;\n\n\tasync load(\n\t\tpatterns?: Routes,\n\t\tcwd = process.cwd(),\n\t\tbustCache = false,\n\t): Promise<GeneratedConstruct<T>[]> {\n\t\tconst logger = console;\n\n\t\tconst globPatterns = Array.isArray(patterns)\n\t\t\t? patterns\n\t\t\t: patterns\n\t\t\t\t? [patterns]\n\t\t\t\t: [];\n\n\t\t// Find all files\n\t\tconst files = fg.stream(globPatterns, {\n\t\t\tcwd,\n\t\t\tabsolute: true,\n\t\t});\n\n\t\t// Cache-busted re-imports re-execute user modules, which would try to\n\t\t// re-register Zod schemas with the same `.meta({ id })`. Clear the\n\t\t// global registry once per load to prevent duplicate-id errors.\n\t\tif (bustCache) {\n\t\t\tclearZodGlobalRegistry();\n\t\t}\n\n\t\t// Load constructs\n\t\tconst constructs: GeneratedConstruct<T>[] = [];\n\n\t\tfor await (const f of files) {\n\t\t\ttry {\n\t\t\t\tconst file = f.toString();\n\t\t\t\t// Append cache-busting query param to force re-import of changed modules\n\t\t\t\tconst importPath = bustCache ? `${file}?t=${Date.now()}` : file;\n\t\t\t\tconst module = await import(importPath);\n\n\t\t\t\t// Check all exports for constructs\n\t\t\t\tfor (const [key, construct] of Object.entries(module)) {\n\t\t\t\t\tif (this.isConstruct(construct)) {\n\t\t\t\t\t\tconstructs.push({\n\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\tname: kebabCase(key),\n\t\t\t\t\t\t\tconstruct,\n\t\t\t\t\t\t\tpath: {\n\t\t\t\t\t\t\t\tabsolute: file,\n\t\t\t\t\t\t\t\trelative: relative(process.cwd(), file),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst err = error as Error;\n\t\t\t\tlogger.error(`Failed to load ${f}: ${err.message}`);\n\t\t\t\tif (err.stack) {\n\t\t\t\t\tlogger.error(err.stack);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\treturn constructs;\n\t}\n}\n\nexport interface GeneratedConstruct<T extends Construct> {\n\tkey: string;\n\tname: string;\n\tconstruct: T;\n\tpath: {\n\t\tabsolute: string;\n\t\trelative: string;\n\t};\n}\n","/**\n * Discovery — one glob, one structural test.\n *\n * A glob per kind is the same specialness the construct model removes, and\n * resources have no kind to be listed under: a declared `ObjectStorage` would\n * never be found by `routes:` or `crons:`. So there is one `constructs` glob,\n * every export of every matching module is inspected, and the per-kind\n * `isConstruct` checks collapse into a single question — does it have an id and\n * can it declare?\n *\n * Structural rather than `instanceof` on purpose. A construct from a different\n * copy of `@geekmidas/constructs` — a linked workspace, two versions in a\n * lockfile — is still a construct, and `instanceof` is exactly the check that\n * says otherwise.\n */\n\nimport { relative } from 'node:path';\nimport {\n\tassertDerivations,\n\ttype ConstructManifest,\n\tcanonicalId,\n\ttype Declaration,\n} from '@geekmidas/manifest';\nimport fg from 'fast-glob';\nimport { clearZodGlobalRegistry } from '../generators/Generator';\n\n/** Matches the rest of the CLI, which logs through `console` directly. */\nconst logger = console;\n\n/** The construct face discovery needs: an id, and the ability to declare. */\ninterface Declarable {\n\tid: string;\n\tdeclare(): Declaration[];\n}\n\n/**\n * Whether a module export is a construct.\n *\n * Deliberately loose about what else it carries — a construct that also has a\n * `service` is still a construct, and requiring one would exclude the kinds that\n * own no address.\n */\nexport function isDeclarable(value: unknown): value is Declarable {\n\tif (typeof value !== 'object' || value === null) return false;\n\n\tconst candidate = value as Partial<Declarable>;\n\n\treturn (\n\t\ttypeof candidate.id === 'string' &&\n\t\tcandidate.id.length > 0 &&\n\t\ttypeof candidate.declare === 'function'\n\t);\n}\n\nexport interface DiscoverOptions {\n\t/** The glob(s) to search. One pattern, not one per kind. */\n\tpatterns: string | readonly string[];\n\tcwd?: string;\n\t/**\n\t * Re-import changed modules, for the dev watcher.\n\t *\n\t * A single glob makes discovery more central, not less: a new construct file\n\t * has to be picked up on `add`, not only on `change`.\n\t */\n\tbustCache?: boolean;\n\t/**\n\t * Filled in with where each construct was found, when provided.\n\t *\n\t * Discovery is the only thing that knows: it globbed the files and imported\n\t * them. Everything downstream that needs to *write* an import — the build,\n\t * generating a server entry that reads a surface's logger — would otherwise\n\t * have to be told the path in config, which is the string this removes.\n\t *\n\t * An out-parameter rather than a second return value so that every existing\n\t * caller, which wants the manifest and nothing else, is unchanged.\n\t */\n\tsources?: Record<string, ConstructSource>;\n\t/**\n\t * Filled in, when provided, with the ids each owner's runnables declared an\n\t * edge to — keyed by the surface an endpoint was built from, or the worker\n\t * a cron or subscriber was.\n\t *\n\t * A runnable is not a declaration: a surface's endpoints are found by the\n\t * glob, not listed by the surface, so its node says `endpoints: []` until a\n\t * build folds the routes in. Anything deciding what an app's process reaches\n\t * — which keys its container is given — needs those edges before any build,\n\t * and discovery is already importing the files they are in.\n\t */\n\trunnables?: Record<string, string[]>;\n}\n\n/** Where a construct was declared, and under what name. */\nexport interface ConstructSource {\n\t/** Absolute path to the module that exported it. */\n\tfile: string;\n\t/** The name it was exported as — what an import statement has to say. */\n\texportName: string;\n\t/**\n\t * The exported construct itself.\n\t *\n\t * What a declaration cannot say — whether it serves itself — the object can\n\t * be asked, and discovery already imported it.\n\t */\n\tconstruct: unknown;\n}\n\n/**\n * Build the construct manifest for a project.\n *\n * Runs the manifest's own validation before returning, so an app is wrong here\n * — where the error names a file — rather than at deploy.\n *\n * @throws {DuplicateConstruct} when two exports claim one id.\n */\nexport async function discover(\n\toptions: DiscoverOptions,\n): Promise<ConstructManifest> {\n\tconst {\n\t\tpatterns,\n\t\tcwd = process.cwd(),\n\t\tbustCache = false,\n\t\tsources: out,\n\t} = options;\n\n\t// Re-importing user modules re-executes them, and Zod v4 throws on a\n\t// re-registered `.meta({ id })`. Same reason the generators clear it.\n\tif (bustCache) clearZodGlobalRegistry();\n\n\tconst manifest: Record<string, Declaration> = {};\n\t/** Which file declared each id, for the error when two of them do. */\n\tconst sources: Record<string, string> = {};\n\t/**\n\t * Constructs already seen, by identity.\n\t *\n\t * A re-export is the *same object* reached through a second file — ESM\n\t * bindings are live, so `export * from './database.js'` hands back the\n\t * binding rather than a copy. Without this, a barrel file made every\n\t * construct in it appear declared twice, and `constructs/index.ts` is the\n\t * first thing anyone writes in a shared folder.\n\t *\n\t * Identity rather than file is also the more honest rule: what may not\n\t * happen twice is two *different* constructs claiming one id, and that is\n\t * still an error below.\n\t */\n\tconst seen = new WeakSet<object>();\n\n\tconst globs = Array.isArray(patterns) ? [...patterns] : [patterns as string];\n\tconst files = fg.stream(globs, { cwd, absolute: true });\n\n\t/** Matched files, for the diagnostic below. */\n\tlet matched = 0;\n\n\tfor await (const found of files) {\n\t\tmatched++;\n\t\tconst file = found.toString();\n\t\tconst module = await import(bustCache ? `${file}?t=${Date.now()}` : file);\n\n\t\tfor (const [exportName, exported] of Object.entries(module)) {\n\t\t\tif (!isDeclarable(exported)) {\n\t\t\t\tif (options.runnables) recordRunnable(options.runnables, exported);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// The same construct, reached again through a re-export. Skipped\n\t\t\t// rather than re-declared: it has already claimed its id, from the\n\t\t\t// file that defined it.\n\t\t\tif (seen.has(exported as object)) continue;\n\t\t\tseen.add(exported as object);\n\n\t\t\t// A construct that is also something a process runs — a queue and its\n\t\t\t// consumer, built from a worker — reaches what it depends on through\n\t\t\t// that process, exactly as an endpoint or a cron does.\n\t\t\tif (\n\t\t\t\toptions.runnables &&\n\t\t\t\ttypeof (exported as { owner?: unknown }).owner === 'string'\n\t\t\t) {\n\t\t\t\trecordRunnable(options.runnables, exported);\n\t\t\t}\n\n\t\t\tfor (const declaration of exported.declare()) {\n\t\t\t\t// Canonicalise here too: a construct built by hand rather than through\n\t\t\t\t// a constructor is still subject to the same identity rule.\n\t\t\t\tconst id = canonicalId(declaration.id);\n\t\t\t\tconst source = relative(cwd, file);\n\n\t\t\t\tconst claimed = sources[id];\n\t\t\t\tif (claimed !== undefined && claimed !== source) {\n\t\t\t\t\tthrow new DuplicateConstruct(id, [claimed, source]);\n\t\t\t\t}\n\n\t\t\t\tmanifest[id] = { ...declaration, id };\n\t\t\t\tsources[id] = source;\n\t\t\t\tif (out) out[id] = { file, exportName, construct: exported };\n\t\t\t}\n\t\t}\n\t}\n\n\t// Reference integrity before anyone reads it — a schema tenant naming a\n\t// database that was deleted is a manifest error, not a reconcile failure.\n\tassertDerivations(manifest);\n\n\twarnIfNothingFound(globs, matched, Object.keys(manifest).length);\n\n\treturn manifest;\n}\n\n/**\n * Record a runnable's edges under the construct that owns it — the surface an\n * endpoint was built from, or the worker a cron or subscriber was. Anything\n * with no owner, or that is not a runnable at all, is left alone.\n */\nfunction recordRunnable(\n\trunnables: Record<string, string[]>,\n\texported: unknown,\n): void {\n\tif (typeof exported !== 'object' || exported === null) return;\n\n\tconst runnable = exported as {\n\t\tsurface?: { id?: unknown };\n\t\towner?: unknown;\n\t\tconstructs?: unknown;\n\t};\n\tconst owner =\n\t\ttypeof runnable.surface?.id === 'string'\n\t\t\t? runnable.surface.id\n\t\t\t: typeof runnable.owner === 'string'\n\t\t\t\t? runnable.owner\n\t\t\t\t: undefined;\n\tif (!owner || !Array.isArray(runnable.constructs)) return;\n\n\tconst key = canonicalId(owner);\n\tconst edges = runnables[key] ?? [];\n\trunnables[key] = edges;\n\tfor (const id of runnable.constructs) {\n\t\tif (typeof id !== 'string') continue;\n\t\tconst target = canonicalId(id);\n\t\tif (!edges.includes(target)) edges.push(target);\n\t}\n}\n\n/**\n * Say so when a glob was configured and found nothing.\n *\n * The glob is not a filter over declarations already known — it is *how they\n * are found*, by importing what it matches. So a file it misses is a\n * declaration that does not exist, and every symptom of that appears somewhere\n * other than the cause: no container starts, no env key is written, and the\n * application fails on first use against a resource it can see in its own\n * source.\n *\n * Nothing reported it before, because an unmatched file is indistinguishable\n * from a file nobody wrote. These two cases are the ones that are *not*\n * ambiguous — a glob was configured, so something was expected.\n *\n * Silent when no glob is configured at all: that is a project which has not\n * adopted constructs, and has nothing to be missing.\n */\nfunction warnIfNothingFound(\n\tglobs: readonly string[],\n\tmatched: number,\n\tdeclared: number,\n): void {\n\tif (globs.length === 0) return;\n\n\tif (matched === 0) {\n\t\tlogger.warn(\n\t\t\t`\\n⚠️ The constructs glob matched no files, so nothing was declared.\\n` +\n\t\t\t\t` Patterns: ${globs.join(', ')}\\n` +\n\t\t\t\t` Nothing will be provisioned — no containers, no env keys, no ` +\n\t\t\t\t`resources.\\n` +\n\t\t\t\t` A common cause is depth: '*.ts' matches one level, '**/*.ts' ` +\n\t\t\t\t`matches any.`,\n\t\t);\n\t\treturn;\n\t}\n\n\tif (declared === 0) {\n\t\tlogger.warn(\n\t\t\t`\\n⚠️ The constructs glob matched ${matched} file(s) but none declared ` +\n\t\t\t\t`anything.\\n` +\n\t\t\t\t` Patterns: ${globs.join(', ')}\\n` +\n\t\t\t\t` Discovery keeps exports with an 'id' that can 'declare()'. A ` +\n\t\t\t\t`construct that is\\n` +\n\t\t\t\t` built but never exported is invisible to it.`,\n\t\t);\n\t}\n}\n\n/** Two constructs claiming one id. */\nexport class DuplicateConstruct extends Error {\n\t/** The id claimed twice. */\n\treadonly id: string;\n\t/** The files that claimed it. */\n\treadonly sources: readonly string[];\n\n\tconstructor(id: string, sources: readonly string[]) {\n\t\tsuper('Two constructs declare the same id');\n\t\tthis.name = 'DuplicateConstruct';\n\t\tthis.id = id;\n\t\tthis.sources = sources;\n\t}\n}\n"],"mappings":"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"}
@@ -1,29 +1,6 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
1
  import { canonicalId, provideKey } from "@geekmidas/manifest";
4
2
  //#region src/reconcile/emit.ts
5
3
  /**
6
- * The manifest, written out as a module.
7
- *
8
- * Discovery imports the application's own code — that is how it finds
9
- * constructs — so anything calling it evaluates the whole runtime graph: React
10
- * email templates, database clients, modules that read env at import time. That
11
- * is fine inside `gkm dev`, which is already running the app. It is not fine
12
- * inside a deploy config, where it fails on the first thing the deploy toolchain
13
- * does not expect and would keep failing on the next.
14
- *
15
- * So discovery happens once, in the build, and everything downstream reads what
16
- * it wrote. The same split `RestApi` makes by naming a routes glob rather than
17
- * importing route modules.
18
- *
19
- * A module rather than JSON, because a manifest is not just data to this
20
- * codebase — `as const satisfies ConstructManifest` is what keeps every id,
21
- * kind and provided key a *literal*, so `IdsOf`, `ProvidedKeys` and the rest
22
- * can select out of it. `JSON.parse` returns `any` and gives all of that up.
23
- */
24
- /** Where the build writes it, relative to the app root. */
25
- const MANIFEST_PATH = ".gkm/manifest.ts";
26
- /**
27
4
  * Fold the build's generated routes into the surface that serves them.
28
5
  *
29
6
  * The routes already exist: `gkm build` generates one handler per endpoint and
@@ -37,11 +14,10 @@ const MANIFEST_PATH = ".gkm/manifest.ts";
37
14
  * already knows where it wrote each handler. Asking the declaration to restate
38
15
  * any of that is how the two come to disagree.
39
16
  *
40
- * Attribution is by exclusion — routes go to the surface that declared none of
41
- * its own. A surface with static endpoints (an auth server's single wildcard)
42
- * keeps them. That is exact while an app has one API of its own, and needs a
43
- * real answer the day it has two; the alternative today would be inventing one
44
- * before anything needs it.
17
+ * Attribution is to the surface the build served — the one whose `path` is
18
+ * the app being built. Without one, routes go to the surface that declared none
19
+ * of its own; a surface with static endpoints (an auth server's single
20
+ * wildcard) keeps them.
45
21
  *
46
22
  * **Only from a provider that generates one handler per route.** The `server`
47
23
  * provider generates a single catch-all — `ALL *` pointing at the Hono app that
@@ -53,7 +29,7 @@ const MANIFEST_PATH = ".gkm/manifest.ts";
53
29
  */
54
30
  function withRoutes(manifest, routes, options) {
55
31
  if (routes.length === 0 || !options.perRoute) return manifest;
56
- const target = Object.entries(manifest).find(([, declaration]) => declaration.kind === "rest-api" && declaration.endpoints.length === 0);
32
+ const target = Object.entries(manifest).find(([id, declaration]) => options.surface ? id === options.surface : declaration.kind === "rest-api" && declaration.endpoints.length === 0);
57
33
  if (!target) return manifest;
58
34
  const [id, declaration] = target;
59
35
  if (declaration.kind !== "rest-api") return manifest;
@@ -162,7 +138,7 @@ function asEndpoint(manifest, surface, route) {
162
138
  */
163
139
  function manifestModule(manifest, backends = {}) {
164
140
  return `${HEADER}
165
- export const manifest = ${serialise(manifest)} as const satisfies ConstructManifest;
141
+ export const constructs = ${serialise(manifest)} as const satisfies ConstructManifest;
166
142
 
167
143
  /**
168
144
  * Where the backends that are *config* rather than declaration resolved to.
@@ -176,24 +152,17 @@ export const manifest = ${serialise(manifest)} as const satisfies ConstructManif
176
152
  export const backends = ${serialise(backends)} as const;
177
153
 
178
154
  // Derived types
179
- export type Ids = IdsOf<typeof manifest>;
180
- export type Construct<Id extends Ids> = DeclarationOf<typeof manifest, Id>;
155
+ export type Ids = IdsOf<typeof constructs>;
156
+ export type Construct<Id extends Ids> = DeclarationOf<typeof constructs, Id>;
181
157
  export type Kind = Construct<Ids>['kind'];
182
158
 
183
159
  // Useful union types
184
- export type ProvidedKeys = AllProvidedKeys<typeof manifest>;
185
- export type Surfaces = IdsOfKind<typeof manifest, 'rest-api'>;
160
+ export type ProvidedKeys = AllProvidedKeys<typeof constructs>;
161
+ export type Surfaces = IdsOfKind<typeof constructs, 'rest-api'>;
186
162
  export type CacheBackend = (typeof backends)['cache'];
187
163
  export type EmailBackend = (typeof backends)['email'];
188
164
  `;
189
165
  }
190
- /** Write it where the deploy config expects to import it from. */
191
- async function writeManifestModule(manifest, appRoot, backends = {}) {
192
- const path = join(appRoot, MANIFEST_PATH);
193
- await mkdir(dirname(path), { recursive: true });
194
- await writeFile(path, manifestModule(manifest, backends), "utf8");
195
- return path;
196
- }
197
166
  const HEADER = `// Generated by \`gkm build\`. Do not edit.
198
167
  //
199
168
  // Discovery imports application code, so it runs once here rather than in every
@@ -219,6 +188,6 @@ function serialise(value) {
219
188
  return JSON.stringify(value, null, " ").replace(/^(\t*)"([A-Za-z_$][\w$]*)":/gm, "$1$2:").replace(/"/g, "'");
220
189
  }
221
190
  //#endregion
222
- export { writeManifestModule as a, withRoutes as i, manifestModule as n, withCompute as r, MANIFEST_PATH as t };
191
+ export { withCompute as n, withRoutes as r, manifestModule as t };
223
192
 
224
- //# sourceMappingURL=emit-Bzq7JsMP.mjs.map
193
+ //# sourceMappingURL=emit-C1e8qD08.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-C1e8qD08.mjs","names":[],"sources":["../src/reconcile/emit.ts"],"sourcesContent":["/**\n * The manifest, written out as a module.\n *\n * Discovery imports the application's own code — that is how it finds\n * constructs — so anything calling it evaluates the whole runtime graph: React\n * email templates, database clients, modules that read env at import time. That\n * is fine inside `gkm dev`, which is already running the app. It is not fine\n * inside a deploy config, where it fails on the first thing the deploy toolchain\n * does not expect and would keep failing on the next.\n *\n * So discovery happens once, in the build, and everything downstream reads what\n * it wrote. The same split `RestApi` makes by naming a routes glob rather than\n * importing route modules.\n *\n * A module rather than JSON, because a manifest is not just data to this\n * codebase — `as const satisfies ConstructManifest` is what keeps every id,\n * kind and provided key a *literal*, so `IdsOf`, `ProvidedKeys` and the rest\n * can select out of it. `JSON.parse` returns `any` and gives all of that up.\n */\n\nimport type {\n\tConstructManifest,\n\tCronInfo,\n\tDeclaration,\n\tDependency,\n\tFunctionInfo,\n\tQueueInfo,\n\tRestApiEndpoint,\n\tRouteInfo,\n\tSubscriberInfo,\n} from '@geekmidas/manifest';\nimport { canonicalId, provideKey } from '@geekmidas/manifest';\nimport type { CacheBackend, EmailBackend } from '../types.js';\n\n/**\n * Fold the build's generated routes into the surface that serves them.\n *\n * The routes already exist: `gkm build` generates one handler per endpoint and\n * records its path, method and environment. What was missing is that the\n * *construct* manifest never learned about them, so a `rest-api` node said\n * `endpoints: []` while five handlers sat in `.gkm/`. Two documents describing\n * the same routes, one of them wrong.\n *\n * Merging here rather than making the construct name a glob or import its\n * endpoints: the build is the thing that already walked the filesystem and\n * already knows where it wrote each handler. Asking the declaration to restate\n * any of that is how the two come to disagree.\n *\n * Attribution is to the surface the build served — the one whose `path` is\n * the app being built. Without one, routes go to the surface that declared none\n * of its own; a surface with static endpoints (an auth server's single\n * wildcard) keeps them.\n *\n * **Only from a provider that generates one handler per route.** The `server`\n * provider generates a single catch-all — `ALL *` pointing at the Hono app that\n * routes in-process — which is a true description of that build and a false\n * description of the application. Merging it wrote `endpoints: [{ method:\n * 'ALL', path: '*' }]` into a manifest for an app with five routes, and made the\n * document's contents depend on which `--provider` ran last. A manifest must not\n * do that.\n */\nexport function withRoutes(\n\tmanifest: ConstructManifest,\n\troutes: readonly RouteInfo[],\n\toptions: { perRoute: boolean; surface?: string },\n): ConstructManifest {\n\tif (routes.length === 0 || !options.perRoute) return manifest;\n\n\tconst target = Object.entries(manifest).find(([id, declaration]) =>\n\t\toptions.surface\n\t\t\t? id === options.surface\n\t\t\t: declaration.kind === 'rest-api' && declaration.endpoints.length === 0,\n\t);\n\n\tif (!target) return manifest;\n\n\tconst [id, declaration] = target;\n\tif (declaration.kind !== 'rest-api') return manifest;\n\n\treturn {\n\t\t...manifest,\n\t\t[id]: {\n\t\t\t...declaration,\n\t\t\tendpoints: routes.map((route) => asEndpoint(manifest, id, route)),\n\t\t},\n\t};\n}\n\n/**\n * Fold in the compute the build generated: functions, crons, queue workers and\n * topic subscribers.\n *\n * All four are named by their *export*, the same way an endpoint's handler is —\n * `CronGenerator` uses the export key as the name — so none of them can declare\n * themselves and all of them are the build's to add. That is why a `Function`\n * has no `declare()` and never will.\n *\n * A worker and a subscriber **nest** inside the queue or topic that triggers\n * them, because position carries the trigger. A function and a cron are\n * top-level, because nothing triggers them but a caller or a clock.\n */\nexport function withCompute(\n\tmanifest: ConstructManifest,\n\tcompute: {\n\t\tfunctions?: readonly FunctionInfo[];\n\t\tcrons?: readonly CronInfo[];\n\t\tqueues?: readonly QueueInfo[];\n\t\tsubscribers?: readonly SubscriberInfo[];\n\t},\n): ConstructManifest {\n\tconst next: Record<string, Declaration> = { ...manifest };\n\n\tfor (const fn of compute.functions ?? []) {\n\t\tnext[fn.name] = {\n\t\t\tkind: 'function',\n\t\t\tid: fn.name,\n\t\t\thandler: fn.handler,\n\t\t\tdependencies: edgesTo(manifest, fn.dependencies),\n\t\t\tprovides: [provideKey(fn.name, 'url')],\n\t\t};\n\t}\n\n\tfor (const cron of compute.crons ?? []) {\n\t\tnext[cron.name] = {\n\t\t\tkind: 'cron',\n\t\t\tid: cron.name,\n\t\t\thandler: cron.handler,\n\t\t\tschedule: cron.schedule,\n\t\t\tdependencies: edgesTo(manifest, cron.dependencies),\n\t\t};\n\t}\n\n\t// A worker drains exactly one queue, so it is matched by the queue it names\n\t// rather than by anything about the handler.\n\tfor (const worker of compute.queues ?? []) {\n\t\tconst queue = next[canonicalId(worker.name)];\n\t\tif (queue?.kind !== 'queue') continue;\n\n\t\tnext[queue.id] = {\n\t\t\t...queue,\n\t\t\tworker: {\n\t\t\t\tid: `${queue.id}Worker`,\n\t\t\t\thandler: worker.handler,\n\t\t\t\tdependencies: edgesTo(manifest, worker.dependencies),\n\t\t\t},\n\t\t};\n\t}\n\n\tfor (const subscriber of compute.subscribers ?? []) {\n\t\tconst topicId = subscriber.topic && canonicalId(subscriber.topic);\n\t\tconst topic = topicId ? next[topicId] : undefined;\n\t\tif (topic?.kind !== 'topic') continue;\n\n\t\tnext[topic.id] = {\n\t\t\t...topic,\n\t\t\tsubscribers: [\n\t\t\t\t...topic.subscribers,\n\t\t\t\t{\n\t\t\t\t\tid: subscriber.name,\n\t\t\t\t\thandler: subscriber.handler,\n\t\t\t\t\tevents: subscriber.subscribedEvents,\n\t\t\t\t\tdependencies: edgesTo(manifest, subscriber.dependencies),\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\treturn next;\n}\n\n/**\n * Construct ids as manifest edges.\n *\n * A `Dependency` carries the target's kind as well as its id, and the kind is\n * read off the manifest rather than recorded beside the edge — the same rule\n * `edgeTo` follows, for the same reason: a hand-written kind is one fact stated\n * twice, and the copy is what goes stale.\n *\n * An id that names nothing is dropped rather than thrown on. A handler can\n * depend on a construct that declares no node of its own — a lifted `Service`\n * is exactly that — and a build is the wrong place to refuse it; what a target\n * needs from this list is the subset it can actually grant.\n */\nfunction edgesTo(\n\tmanifest: ConstructManifest,\n\tids: readonly string[] = [],\n): Dependency[] {\n\treturn ids.flatMap((target) => {\n\t\tconst declaration = manifest[target];\n\n\t\treturn declaration ? [{ target, kind: declaration.kind }] : [];\n\t});\n}\n\n/** One generated route as the manifest's shape. */\nfunction asEndpoint(\n\tmanifest: ConstructManifest,\n\tsurface: string,\n\troute: RouteInfo,\n): RestApiEndpoint {\n\treturn {\n\t\t// The handler path is already unique per route and is what a target needs\n\t\t// to point a function at, so it is also the natural id.\n\t\tid: `${surface}${route.method}${route.path}`,\n\t\thandler: route.handler,\n\t\tmethod: route.method,\n\t\tpath: route.path,\n\t\t// The edges the handler declared, which is what a target reads to grant\n\t\t// this one function access to exactly those constructs. `environment` is\n\t\t// the same edges' shadow — the keys read — and cannot be turned back into\n\t\t// them, which is why the ids are carried rather than inferred.\n\t\tdependencies: edgesTo(manifest, route.dependencies),\n\t\t...(route.authorizer ? { authorizer: route.authorizer } : {}),\n\t};\n}\n\n/**\n * The manifest as TypeScript source.\n *\n * Pure, so what gets written can be asserted without touching a filesystem.\n */\nexport function manifestModule(\n\tmanifest: ConstructManifest,\n\tbackends: Backends = {},\n): string {\n\treturn `${HEADER}\nexport const constructs = ${serialise(manifest)} as const satisfies ConstructManifest;\n\n/**\n * Where the backends that are *config* rather than declaration resolved to.\n *\n * Recorded because they were being answered twice: once by the build, which\n * registers exactly one cache driver, and once by a deploy config, which told\n * the provisioner something else. The result was a deployed URL the running code\n * had no driver for — two documents disagreeing about one fact, which is what\n * this manifest exists to stop.\n */\nexport const backends = ${serialise(backends)} as const;\n\n// Derived types\nexport type Ids = IdsOf<typeof constructs>;\nexport type Construct<Id extends Ids> = DeclarationOf<typeof constructs, Id>;\nexport type Kind = Construct<Ids>['kind'];\n\n// Useful union types\nexport type ProvidedKeys = AllProvidedKeys<typeof constructs>;\nexport type Surfaces = IdsOfKind<typeof constructs, 'rest-api'>;\nexport type CacheBackend = (typeof backends)['cache'];\nexport type EmailBackend = (typeof backends)['email'];\n`;\n}\n\n/** The choices that are deployment config rather than declaration. */\nexport interface Backends {\n\tcache?: CacheBackend;\n\temail?: EmailBackend;\n}\n\nconst HEADER = `// Generated by \\`gkm build\\`. Do not edit.\n//\n// Discovery imports application code, so it runs once here rather than in every\n// consumer — a deploy config that discovered for itself would evaluate the whole\n// runtime graph inside its own toolchain. This is the result, as literals.\n\nimport type {\n\tAllProvidedKeys,\n\tConstructManifest,\n\tDeclarationOf,\n\tIdsOf,\n\tIdsOfKind,\n} from '@geekmidas/manifest';\n`;\n\n/**\n * JSON, re-indented with tabs and with keys left unquoted where they can be.\n *\n * Cosmetic, and worth it: this file is read by people trying to work out what\n * their app declares, and a wall of quoted keys reads as output rather than as\n * something anyone is expected to understand.\n */\nfunction serialise(value: object): string {\n\treturn JSON.stringify(value, null, '\\t')\n\t\t.replace(/^(\\t*)\"([A-Za-z_$][\\w$]*)\":/gm, '$1$2:')\n\t\t.replace(/\"/g, \"'\");\n}\n"],"mappings":"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"}
@@ -1,29 +1,6 @@
1
- let node_fs_promises = require("node:fs/promises");
2
- let node_path = require("node:path");
3
1
  let _geekmidas_manifest = require("@geekmidas/manifest");
4
2
  //#region src/reconcile/emit.ts
5
3
  /**
6
- * The manifest, written out as a module.
7
- *
8
- * Discovery imports the application's own code — that is how it finds
9
- * constructs — so anything calling it evaluates the whole runtime graph: React
10
- * email templates, database clients, modules that read env at import time. That
11
- * is fine inside `gkm dev`, which is already running the app. It is not fine
12
- * inside a deploy config, where it fails on the first thing the deploy toolchain
13
- * does not expect and would keep failing on the next.
14
- *
15
- * So discovery happens once, in the build, and everything downstream reads what
16
- * it wrote. The same split `RestApi` makes by naming a routes glob rather than
17
- * importing route modules.
18
- *
19
- * A module rather than JSON, because a manifest is not just data to this
20
- * codebase — `as const satisfies ConstructManifest` is what keeps every id,
21
- * kind and provided key a *literal*, so `IdsOf`, `ProvidedKeys` and the rest
22
- * can select out of it. `JSON.parse` returns `any` and gives all of that up.
23
- */
24
- /** Where the build writes it, relative to the app root. */
25
- const MANIFEST_PATH = ".gkm/manifest.ts";
26
- /**
27
4
  * Fold the build's generated routes into the surface that serves them.
28
5
  *
29
6
  * The routes already exist: `gkm build` generates one handler per endpoint and
@@ -37,11 +14,10 @@ const MANIFEST_PATH = ".gkm/manifest.ts";
37
14
  * already knows where it wrote each handler. Asking the declaration to restate
38
15
  * any of that is how the two come to disagree.
39
16
  *
40
- * Attribution is by exclusion — routes go to the surface that declared none of
41
- * its own. A surface with static endpoints (an auth server's single wildcard)
42
- * keeps them. That is exact while an app has one API of its own, and needs a
43
- * real answer the day it has two; the alternative today would be inventing one
44
- * before anything needs it.
17
+ * Attribution is to the surface the build served — the one whose `path` is
18
+ * the app being built. Without one, routes go to the surface that declared none
19
+ * of its own; a surface with static endpoints (an auth server's single
20
+ * wildcard) keeps them.
45
21
  *
46
22
  * **Only from a provider that generates one handler per route.** The `server`
47
23
  * provider generates a single catch-all — `ALL *` pointing at the Hono app that
@@ -53,7 +29,7 @@ const MANIFEST_PATH = ".gkm/manifest.ts";
53
29
  */
54
30
  function withRoutes(manifest, routes, options) {
55
31
  if (routes.length === 0 || !options.perRoute) return manifest;
56
- const target = Object.entries(manifest).find(([, declaration]) => declaration.kind === "rest-api" && declaration.endpoints.length === 0);
32
+ const target = Object.entries(manifest).find(([id, declaration]) => options.surface ? id === options.surface : declaration.kind === "rest-api" && declaration.endpoints.length === 0);
57
33
  if (!target) return manifest;
58
34
  const [id, declaration] = target;
59
35
  if (declaration.kind !== "rest-api") return manifest;
@@ -162,7 +138,7 @@ function asEndpoint(manifest, surface, route) {
162
138
  */
163
139
  function manifestModule(manifest, backends = {}) {
164
140
  return `${HEADER}
165
- export const manifest = ${serialise(manifest)} as const satisfies ConstructManifest;
141
+ export const constructs = ${serialise(manifest)} as const satisfies ConstructManifest;
166
142
 
167
143
  /**
168
144
  * Where the backends that are *config* rather than declaration resolved to.
@@ -176,24 +152,17 @@ export const manifest = ${serialise(manifest)} as const satisfies ConstructManif
176
152
  export const backends = ${serialise(backends)} as const;
177
153
 
178
154
  // Derived types
179
- export type Ids = IdsOf<typeof manifest>;
180
- export type Construct<Id extends Ids> = DeclarationOf<typeof manifest, Id>;
155
+ export type Ids = IdsOf<typeof constructs>;
156
+ export type Construct<Id extends Ids> = DeclarationOf<typeof constructs, Id>;
181
157
  export type Kind = Construct<Ids>['kind'];
182
158
 
183
159
  // Useful union types
184
- export type ProvidedKeys = AllProvidedKeys<typeof manifest>;
185
- export type Surfaces = IdsOfKind<typeof manifest, 'rest-api'>;
160
+ export type ProvidedKeys = AllProvidedKeys<typeof constructs>;
161
+ export type Surfaces = IdsOfKind<typeof constructs, 'rest-api'>;
186
162
  export type CacheBackend = (typeof backends)['cache'];
187
163
  export type EmailBackend = (typeof backends)['email'];
188
164
  `;
189
165
  }
190
- /** Write it where the deploy config expects to import it from. */
191
- async function writeManifestModule(manifest, appRoot, backends = {}) {
192
- const path = (0, node_path.join)(appRoot, MANIFEST_PATH);
193
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
194
- await (0, node_fs_promises.writeFile)(path, manifestModule(manifest, backends), "utf8");
195
- return path;
196
- }
197
166
  const HEADER = `// Generated by \`gkm build\`. Do not edit.
198
167
  //
199
168
  // Discovery imports application code, so it runs once here rather than in every
@@ -219,12 +188,6 @@ function serialise(value) {
219
188
  return JSON.stringify(value, null, " ").replace(/^(\t*)"([A-Za-z_$][\w$]*)":/gm, "$1$2:").replace(/"/g, "'");
220
189
  }
221
190
  //#endregion
222
- Object.defineProperty(exports, "MANIFEST_PATH", {
223
- enumerable: true,
224
- get: function() {
225
- return MANIFEST_PATH;
226
- }
227
- });
228
191
  Object.defineProperty(exports, "manifestModule", {
229
192
  enumerable: true,
230
193
  get: function() {
@@ -243,11 +206,5 @@ Object.defineProperty(exports, "withRoutes", {
243
206
  return withRoutes;
244
207
  }
245
208
  });
246
- Object.defineProperty(exports, "writeManifestModule", {
247
- enumerable: true,
248
- get: function() {
249
- return writeManifestModule;
250
- }
251
- });
252
209
 
253
- //# sourceMappingURL=emit-BdoPFjZf.cjs.map
210
+ //# sourceMappingURL=emit-Dmc1nvdS.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-Dmc1nvdS.cjs","names":["provideKey","canonicalId"],"sources":["../src/reconcile/emit.ts"],"sourcesContent":["/**\n * The manifest, written out as a module.\n *\n * Discovery imports the application's own code — that is how it finds\n * constructs — so anything calling it evaluates the whole runtime graph: React\n * email templates, database clients, modules that read env at import time. That\n * is fine inside `gkm dev`, which is already running the app. It is not fine\n * inside a deploy config, where it fails on the first thing the deploy toolchain\n * does not expect and would keep failing on the next.\n *\n * So discovery happens once, in the build, and everything downstream reads what\n * it wrote. The same split `RestApi` makes by naming a routes glob rather than\n * importing route modules.\n *\n * A module rather than JSON, because a manifest is not just data to this\n * codebase — `as const satisfies ConstructManifest` is what keeps every id,\n * kind and provided key a *literal*, so `IdsOf`, `ProvidedKeys` and the rest\n * can select out of it. `JSON.parse` returns `any` and gives all of that up.\n */\n\nimport type {\n\tConstructManifest,\n\tCronInfo,\n\tDeclaration,\n\tDependency,\n\tFunctionInfo,\n\tQueueInfo,\n\tRestApiEndpoint,\n\tRouteInfo,\n\tSubscriberInfo,\n} from '@geekmidas/manifest';\nimport { canonicalId, provideKey } from '@geekmidas/manifest';\nimport type { CacheBackend, EmailBackend } from '../types.js';\n\n/**\n * Fold the build's generated routes into the surface that serves them.\n *\n * The routes already exist: `gkm build` generates one handler per endpoint and\n * records its path, method and environment. What was missing is that the\n * *construct* manifest never learned about them, so a `rest-api` node said\n * `endpoints: []` while five handlers sat in `.gkm/`. Two documents describing\n * the same routes, one of them wrong.\n *\n * Merging here rather than making the construct name a glob or import its\n * endpoints: the build is the thing that already walked the filesystem and\n * already knows where it wrote each handler. Asking the declaration to restate\n * any of that is how the two come to disagree.\n *\n * Attribution is to the surface the build served — the one whose `path` is\n * the app being built. Without one, routes go to the surface that declared none\n * of its own; a surface with static endpoints (an auth server's single\n * wildcard) keeps them.\n *\n * **Only from a provider that generates one handler per route.** The `server`\n * provider generates a single catch-all — `ALL *` pointing at the Hono app that\n * routes in-process — which is a true description of that build and a false\n * description of the application. Merging it wrote `endpoints: [{ method:\n * 'ALL', path: '*' }]` into a manifest for an app with five routes, and made the\n * document's contents depend on which `--provider` ran last. A manifest must not\n * do that.\n */\nexport function withRoutes(\n\tmanifest: ConstructManifest,\n\troutes: readonly RouteInfo[],\n\toptions: { perRoute: boolean; surface?: string },\n): ConstructManifest {\n\tif (routes.length === 0 || !options.perRoute) return manifest;\n\n\tconst target = Object.entries(manifest).find(([id, declaration]) =>\n\t\toptions.surface\n\t\t\t? id === options.surface\n\t\t\t: declaration.kind === 'rest-api' && declaration.endpoints.length === 0,\n\t);\n\n\tif (!target) return manifest;\n\n\tconst [id, declaration] = target;\n\tif (declaration.kind !== 'rest-api') return manifest;\n\n\treturn {\n\t\t...manifest,\n\t\t[id]: {\n\t\t\t...declaration,\n\t\t\tendpoints: routes.map((route) => asEndpoint(manifest, id, route)),\n\t\t},\n\t};\n}\n\n/**\n * Fold in the compute the build generated: functions, crons, queue workers and\n * topic subscribers.\n *\n * All four are named by their *export*, the same way an endpoint's handler is —\n * `CronGenerator` uses the export key as the name — so none of them can declare\n * themselves and all of them are the build's to add. That is why a `Function`\n * has no `declare()` and never will.\n *\n * A worker and a subscriber **nest** inside the queue or topic that triggers\n * them, because position carries the trigger. A function and a cron are\n * top-level, because nothing triggers them but a caller or a clock.\n */\nexport function withCompute(\n\tmanifest: ConstructManifest,\n\tcompute: {\n\t\tfunctions?: readonly FunctionInfo[];\n\t\tcrons?: readonly CronInfo[];\n\t\tqueues?: readonly QueueInfo[];\n\t\tsubscribers?: readonly SubscriberInfo[];\n\t},\n): ConstructManifest {\n\tconst next: Record<string, Declaration> = { ...manifest };\n\n\tfor (const fn of compute.functions ?? []) {\n\t\tnext[fn.name] = {\n\t\t\tkind: 'function',\n\t\t\tid: fn.name,\n\t\t\thandler: fn.handler,\n\t\t\tdependencies: edgesTo(manifest, fn.dependencies),\n\t\t\tprovides: [provideKey(fn.name, 'url')],\n\t\t};\n\t}\n\n\tfor (const cron of compute.crons ?? []) {\n\t\tnext[cron.name] = {\n\t\t\tkind: 'cron',\n\t\t\tid: cron.name,\n\t\t\thandler: cron.handler,\n\t\t\tschedule: cron.schedule,\n\t\t\tdependencies: edgesTo(manifest, cron.dependencies),\n\t\t};\n\t}\n\n\t// A worker drains exactly one queue, so it is matched by the queue it names\n\t// rather than by anything about the handler.\n\tfor (const worker of compute.queues ?? []) {\n\t\tconst queue = next[canonicalId(worker.name)];\n\t\tif (queue?.kind !== 'queue') continue;\n\n\t\tnext[queue.id] = {\n\t\t\t...queue,\n\t\t\tworker: {\n\t\t\t\tid: `${queue.id}Worker`,\n\t\t\t\thandler: worker.handler,\n\t\t\t\tdependencies: edgesTo(manifest, worker.dependencies),\n\t\t\t},\n\t\t};\n\t}\n\n\tfor (const subscriber of compute.subscribers ?? []) {\n\t\tconst topicId = subscriber.topic && canonicalId(subscriber.topic);\n\t\tconst topic = topicId ? next[topicId] : undefined;\n\t\tif (topic?.kind !== 'topic') continue;\n\n\t\tnext[topic.id] = {\n\t\t\t...topic,\n\t\t\tsubscribers: [\n\t\t\t\t...topic.subscribers,\n\t\t\t\t{\n\t\t\t\t\tid: subscriber.name,\n\t\t\t\t\thandler: subscriber.handler,\n\t\t\t\t\tevents: subscriber.subscribedEvents,\n\t\t\t\t\tdependencies: edgesTo(manifest, subscriber.dependencies),\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\treturn next;\n}\n\n/**\n * Construct ids as manifest edges.\n *\n * A `Dependency` carries the target's kind as well as its id, and the kind is\n * read off the manifest rather than recorded beside the edge — the same rule\n * `edgeTo` follows, for the same reason: a hand-written kind is one fact stated\n * twice, and the copy is what goes stale.\n *\n * An id that names nothing is dropped rather than thrown on. A handler can\n * depend on a construct that declares no node of its own — a lifted `Service`\n * is exactly that — and a build is the wrong place to refuse it; what a target\n * needs from this list is the subset it can actually grant.\n */\nfunction edgesTo(\n\tmanifest: ConstructManifest,\n\tids: readonly string[] = [],\n): Dependency[] {\n\treturn ids.flatMap((target) => {\n\t\tconst declaration = manifest[target];\n\n\t\treturn declaration ? [{ target, kind: declaration.kind }] : [];\n\t});\n}\n\n/** One generated route as the manifest's shape. */\nfunction asEndpoint(\n\tmanifest: ConstructManifest,\n\tsurface: string,\n\troute: RouteInfo,\n): RestApiEndpoint {\n\treturn {\n\t\t// The handler path is already unique per route and is what a target needs\n\t\t// to point a function at, so it is also the natural id.\n\t\tid: `${surface}${route.method}${route.path}`,\n\t\thandler: route.handler,\n\t\tmethod: route.method,\n\t\tpath: route.path,\n\t\t// The edges the handler declared, which is what a target reads to grant\n\t\t// this one function access to exactly those constructs. `environment` is\n\t\t// the same edges' shadow — the keys read — and cannot be turned back into\n\t\t// them, which is why the ids are carried rather than inferred.\n\t\tdependencies: edgesTo(manifest, route.dependencies),\n\t\t...(route.authorizer ? { authorizer: route.authorizer } : {}),\n\t};\n}\n\n/**\n * The manifest as TypeScript source.\n *\n * Pure, so what gets written can be asserted without touching a filesystem.\n */\nexport function manifestModule(\n\tmanifest: ConstructManifest,\n\tbackends: Backends = {},\n): string {\n\treturn `${HEADER}\nexport const constructs = ${serialise(manifest)} as const satisfies ConstructManifest;\n\n/**\n * Where the backends that are *config* rather than declaration resolved to.\n *\n * Recorded because they were being answered twice: once by the build, which\n * registers exactly one cache driver, and once by a deploy config, which told\n * the provisioner something else. The result was a deployed URL the running code\n * had no driver for — two documents disagreeing about one fact, which is what\n * this manifest exists to stop.\n */\nexport const backends = ${serialise(backends)} as const;\n\n// Derived types\nexport type Ids = IdsOf<typeof constructs>;\nexport type Construct<Id extends Ids> = DeclarationOf<typeof constructs, Id>;\nexport type Kind = Construct<Ids>['kind'];\n\n// Useful union types\nexport type ProvidedKeys = AllProvidedKeys<typeof constructs>;\nexport type Surfaces = IdsOfKind<typeof constructs, 'rest-api'>;\nexport type CacheBackend = (typeof backends)['cache'];\nexport type EmailBackend = (typeof backends)['email'];\n`;\n}\n\n/** The choices that are deployment config rather than declaration. */\nexport interface Backends {\n\tcache?: CacheBackend;\n\temail?: EmailBackend;\n}\n\nconst HEADER = `// Generated by \\`gkm build\\`. Do not edit.\n//\n// Discovery imports application code, so it runs once here rather than in every\n// consumer — a deploy config that discovered for itself would evaluate the whole\n// runtime graph inside its own toolchain. This is the result, as literals.\n\nimport type {\n\tAllProvidedKeys,\n\tConstructManifest,\n\tDeclarationOf,\n\tIdsOf,\n\tIdsOfKind,\n} from '@geekmidas/manifest';\n`;\n\n/**\n * JSON, re-indented with tabs and with keys left unquoted where they can be.\n *\n * Cosmetic, and worth it: this file is read by people trying to work out what\n * their app declares, and a wall of quoted keys reads as output rather than as\n * something anyone is expected to understand.\n */\nfunction serialise(value: object): string {\n\treturn JSON.stringify(value, null, '\\t')\n\t\t.replace(/^(\\t*)\"([A-Za-z_$][\\w$]*)\":/gm, '$1$2:')\n\t\t.replace(/\"/g, \"'\");\n}\n"],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ const require_config = require("./config-CsYSyUya.cjs");
2
+ const require_workspace = require("./workspace-DEibcoBe.cjs");
3
+ const require_storage = require("./storage-CSGoC3bK.cjs");
4
+ const require_store = require("./store-DQgxT_WR.cjs");
5
+ const require_backends = require("./backends-B9V2XD2M.cjs");
6
+ let _geekmidas_manifest = require("@geekmidas/manifest");
7
+ let _geekmidas_constructs_encryption = require("@geekmidas/constructs/encryption");
8
+ //#region src/encryption/index.ts
9
+ /**
10
+ * `gkm encryption:rotate` and `gkm encryption:retire` — the keyring of an
11
+ * `Encryption` construct on a server stage.
12
+ *
13
+ * Rotating adds a key in front and keeps every older one, so what they wrote
14
+ * still opens. Retiring removes one, and is the half that makes rotating worth
15
+ * doing: until a key is gone, it still opens everything it ever wrote. It is
16
+ * never automatic, because the only thing that knows nothing is left under a
17
+ * key is the sweep that re-encrypted the column — so it is done after one,
18
+ * deliberately, by name.
19
+ *
20
+ * Neither applies everywhere, and each says so rather than doing something
21
+ * that looks right: a local stage's keyring is derived and would be replaced on
22
+ * the next `gkm dev`, and on AWS the key is KMS's, which rotates it itself and
23
+ * never strands a value.
24
+ */
25
+ const logger = console;
26
+ var LocalKeyringIsDerived = class extends Error {
27
+ construct;
28
+ stage;
29
+ constructor(construct, stage) {
30
+ super(`'${stage}' is the local stage, where ${construct}'s keyring is derived from the project and stage — nothing stored there would be read. Rotate a deployed stage: gkm encryption:rotate ${construct} --stage <stage>.`);
31
+ this.construct = construct;
32
+ this.stage = stage;
33
+ this.name = "LocalKeyringIsDerived";
34
+ }
35
+ };
36
+ var KmsRotatesItself = class extends Error {
37
+ construct;
38
+ constructor(construct) {
39
+ super(`${construct} is a KMS key on AWS: KMS rotates it yearly and keeps every version, so no value is ever stranded and there is no key to retire. Nothing to do here.`);
40
+ this.construct = construct;
41
+ this.name = "KmsRotatesItself";
42
+ }
43
+ };
44
+ var NoKeyringYet = class extends Error {
45
+ construct;
46
+ stage;
47
+ key;
48
+ constructor(construct, stage, key) {
49
+ super(`'${stage}' has no ${key} yet. The first deploy generates ${construct}'s keyring: gkm deploy --stage ${stage}.`);
50
+ this.construct = construct;
51
+ this.stage = stage;
52
+ this.key = key;
53
+ this.name = "NoKeyringYet";
54
+ }
55
+ };
56
+ async function encryptionRotateCommand(construct, options, context = {}) {
57
+ return updateKeyring(construct, options, context, (id, url) => {
58
+ const next = (0, _geekmidas_constructs_encryption.rotateKeyring)(id, url);
59
+ const [current, ...older] = (0, _geekmidas_constructs_encryption.parseKeyring)(id, next).keys.map(({ id }) => id);
60
+ logger.log(`\n✓ ${id} on '${options.stage}' now writes with '${current}'.`);
61
+ logger.log(` Still opening: ${older.join(", ")}. Redeploy to start writing with '${current}', reencrypt what is stored, then: gkm encryption:retire ${id} <key> --stage ${options.stage}`);
62
+ return next;
63
+ });
64
+ }
65
+ async function encryptionRetireCommand(construct, keyId, options, context = {}) {
66
+ return updateKeyring(construct, options, context, (id, url) => {
67
+ const next = (0, _geekmidas_constructs_encryption.retireKey)(id, url, keyId);
68
+ logger.log(`\n✓ ${id} on '${options.stage}' no longer holds '${keyId}'. Anything still under it cannot be read after the next deploy.`);
69
+ return next;
70
+ });
71
+ }
72
+ async function updateKeyring(construct, { stage }, context, update) {
73
+ const workspace = context.workspace ?? await require_config.loadWorkspaceSettings();
74
+ const id = (0, _geekmidas_manifest.canonicalId)(construct);
75
+ if (stage === workspace.stages.local) throw new LocalKeyringIsDerived(id, stage);
76
+ require_workspace.assertDeployedStage(workspace.stages, stage);
77
+ if (require_backends.providerOf(workspace) === "aws") throw new KmsRotatesItself(id);
78
+ const store = context.store ?? await require_store.secretsStoreFor(workspace, stage);
79
+ const key = (0, _geekmidas_manifest.provideKey)(id, "url");
80
+ const secrets = await store.read(stage);
81
+ const url = secrets?.custom[key];
82
+ if (!secrets || !url) throw new NoKeyringYet(id, stage, key);
83
+ const next = update(id, url);
84
+ await store.write(stage, require_storage.withCustomSecret(secrets, key, next));
85
+ return next;
86
+ }
87
+ //#endregion
88
+ exports.encryptionRetireCommand = encryptionRetireCommand;
89
+ exports.encryptionRotateCommand = encryptionRotateCommand;
90
+
91
+ //# sourceMappingURL=encryption-C7OGMve_.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption-C7OGMve_.cjs","names":["rotateKeyring","parseKeyring","retireKey","loadWorkspaceSettings","canonicalId","providerOf","secretsStoreFor","provideKey","withCustomSecret"],"sources":["../src/encryption/index.ts"],"sourcesContent":["/**\n * `gkm encryption:rotate` and `gkm encryption:retire` — the keyring of an\n * `Encryption` construct on a server stage.\n *\n * Rotating adds a key in front and keeps every older one, so what they wrote\n * still opens. Retiring removes one, and is the half that makes rotating worth\n * doing: until a key is gone, it still opens everything it ever wrote. It is\n * never automatic, because the only thing that knows nothing is left under a\n * key is the sweep that re-encrypted the column — so it is done after one,\n * deliberately, by name.\n *\n * Neither applies everywhere, and each says so rather than doing something\n * that looks right: a local stage's keyring is derived and would be replaced on\n * the next `gkm dev`, and on AWS the key is KMS's, which rotates it itself and\n * never strands a value.\n */\n\nimport {\n\tparseKeyring,\n\tretireKey,\n\trotateKeyring,\n} from '@geekmidas/constructs/encryption';\nimport { canonicalId, provideKey } from '@geekmidas/manifest';\nimport { loadWorkspaceSettings } from '../config';\nimport { withCustomSecret } from '../secrets/storage.js';\nimport { type SecretsStore, secretsStoreFor } from '../secrets/store.js';\nimport { providerOf } from '../workspace/backends.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\nconst logger = console;\n\nexport class LocalKeyringIsDerived extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly stage: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' is the local stage, where ${construct}'s keyring is derived from the project and stage — nothing stored there would be read. Rotate a deployed stage: gkm encryption:rotate ${construct} --stage <stage>.`,\n\t\t);\n\t\tthis.name = 'LocalKeyringIsDerived';\n\t}\n}\n\nexport class KmsRotatesItself extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} is a KMS key on AWS: KMS rotates it yearly and keeps every version, so no value is ever stranded and there is no key to retire. Nothing to do here.`,\n\t\t);\n\t\tthis.name = 'KmsRotatesItself';\n\t}\n}\n\nexport class NoKeyringYet extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly stage: string,\n\t\treadonly key: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' has no ${key} yet. The first deploy generates ${construct}'s keyring: gkm deploy --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'NoKeyringYet';\n\t}\n}\n\nexport interface EncryptionCommandOptions {\n\tstage: string;\n}\n\n/** Where a command reads and writes; the real ones unless a test says. */\nexport interface EncryptionCommandContext {\n\tworkspace?: NormalizedWorkspace;\n\tstore?: SecretsStore;\n}\n\nexport async function encryptionRotateCommand(\n\tconstruct: string,\n\toptions: EncryptionCommandOptions,\n\tcontext: EncryptionCommandContext = {},\n): Promise<string> {\n\treturn updateKeyring(construct, options, context, (id, url) => {\n\t\tconst next = rotateKeyring(id, url);\n\t\tconst [current, ...older] = parseKeyring(id, next).keys.map(({ id }) => id);\n\n\t\tlogger.log(`\\n✓ ${id} on '${options.stage}' now writes with '${current}'.`);\n\t\tlogger.log(\n\t\t\t` Still opening: ${older.join(', ')}. Redeploy to start writing with '${current}', reencrypt what is stored, then: gkm encryption:retire ${id} <key> --stage ${options.stage}`,\n\t\t);\n\t\treturn next;\n\t});\n}\n\nexport async function encryptionRetireCommand(\n\tconstruct: string,\n\tkeyId: string,\n\toptions: EncryptionCommandOptions,\n\tcontext: EncryptionCommandContext = {},\n): Promise<string> {\n\treturn updateKeyring(construct, options, context, (id, url) => {\n\t\tconst next = retireKey(id, url, keyId);\n\n\t\tlogger.log(\n\t\t\t`\\n✓ ${id} on '${options.stage}' no longer holds '${keyId}'. Anything still under it cannot be read after the next deploy.`,\n\t\t);\n\t\treturn next;\n\t});\n}\n\nasync function updateKeyring(\n\tconstruct: string,\n\t{ stage }: EncryptionCommandOptions,\n\tcontext: EncryptionCommandContext,\n\tupdate: (id: string, url: string) => string,\n): Promise<string> {\n\tconst workspace = context.workspace ?? (await loadWorkspaceSettings());\n\tconst id = canonicalId(construct);\n\n\tif (stage === workspace.stages.local) {\n\t\tthrow new LocalKeyringIsDerived(id, stage);\n\t}\n\tassertDeployedStage(workspace.stages, stage);\n\tif (providerOf(workspace) === 'aws') throw new KmsRotatesItself(id);\n\n\tconst store = context.store ?? (await secretsStoreFor(workspace, stage));\n\tconst key = provideKey(id, 'url');\n\tconst secrets = await store.read(stage);\n\tconst url = secrets?.custom[key];\n\tif (!secrets || !url) throw new NoKeyringYet(id, stage, key);\n\n\tconst next = update(id, url);\n\tawait store.write(stage, withCustomSecret(secrets, key, next));\n\treturn next;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,SAAS;AAEf,IAAa,wBAAb,cAA2C,MAAM;CAEtC;CACA;CAFV,YACC,WACA,OACC;EACD,MACC,IAAI,MAAM,8BAA8B,UAAU,wIAAwI,UAAU,kBACrM;EALS,KAAA,YAAA;EACA,KAAA,QAAA;EAKT,KAAK,OAAO;CACb;AACD;AAEA,IAAa,mBAAb,cAAsC,MAAM;CACtB;CAArB,YAAY,WAA4B;EACvC,MACC,GAAG,UAAU,qJACd;EAHoB,KAAA,YAAA;EAIpB,KAAK,OAAO;CACb;AACD;AAEA,IAAa,eAAb,cAAkC,MAAM;CAE7B;CACA;CACA;CAHV,YACC,WACA,OACA,KACC;EACD,MACC,IAAI,MAAM,WAAW,IAAI,mCAAmC,UAAU,iCAAiC,MAAM,EAC9G;EANS,KAAA,YAAA;EACA,KAAA,QAAA;EACA,KAAA,MAAA;EAKT,KAAK,OAAO;CACb;AACD;AAYA,eAAsB,wBACrB,WACA,SACA,UAAoC,CAAC,GACnB;CAClB,OAAO,cAAc,WAAW,SAAS,UAAU,IAAI,QAAQ;EAC9D,MAAM,QAAA,GAAOA,iCAAAA,cAAAA,CAAc,IAAI,GAAG;EAClC,MAAM,CAAC,SAAS,GAAG,UAAA,GAASC,iCAAAA,aAAAA,CAAa,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK,EAAE,SAAS,EAAE;EAE1E,OAAO,IAAI,OAAO,GAAG,OAAO,QAAQ,MAAM,qBAAqB,QAAQ,GAAG;EAC1E,OAAO,IACN,oBAAoB,MAAM,KAAK,IAAI,EAAE,oCAAoC,QAAQ,2DAA2D,GAAG,iBAAiB,QAAQ,OACzK;EACA,OAAO;CACR,CAAC;AACF;AAEA,eAAsB,wBACrB,WACA,OACA,SACA,UAAoC,CAAC,GACnB;CAClB,OAAO,cAAc,WAAW,SAAS,UAAU,IAAI,QAAQ;EAC9D,MAAM,QAAA,GAAOC,iCAAAA,UAAAA,CAAU,IAAI,KAAK,KAAK;EAErC,OAAO,IACN,OAAO,GAAG,OAAO,QAAQ,MAAM,qBAAqB,MAAM,iEAC3D;EACA,OAAO;CACR,CAAC;AACF;AAEA,eAAe,cACd,WACA,EAAE,SACF,SACA,QACkB;CAClB,MAAM,YAAY,QAAQ,aAAc,MAAMC,eAAAA,sBAAsB;CACpE,MAAM,MAAA,GAAKC,oBAAAA,YAAAA,CAAY,SAAS;CAEhC,IAAI,UAAU,UAAU,OAAO,OAC9B,MAAM,IAAI,sBAAsB,IAAI,KAAK;CAE1C,kBAAA,oBAAoB,UAAU,QAAQ,KAAK;CAC3C,IAAIC,iBAAAA,WAAW,SAAS,MAAM,OAAO,MAAM,IAAI,iBAAiB,EAAE;CAElE,MAAM,QAAQ,QAAQ,SAAU,MAAMC,cAAAA,gBAAgB,WAAW,KAAK;CACtE,MAAM,OAAA,GAAMC,oBAAAA,WAAAA,CAAW,IAAI,KAAK;CAChC,MAAM,UAAU,MAAM,MAAM,KAAK,KAAK;CACtC,MAAM,MAAM,SAAS,OAAO;CAC5B,IAAI,CAAC,WAAW,CAAC,KAAK,MAAM,IAAI,aAAa,IAAI,OAAO,GAAG;CAE3D,MAAM,OAAO,OAAO,IAAI,GAAG;CAC3B,MAAM,MAAM,MAAM,OAAOC,gBAAAA,iBAAiB,SAAS,KAAK,IAAI,CAAC;CAC7D,OAAO;AACR"}
@@ -0,0 +1,90 @@
1
+ import { u as loadWorkspaceSettings } from "./config-D8J5rrTb.mjs";
2
+ import { D as assertDeployedStage } from "./workspace-DNzWgM1J.mjs";
3
+ import { a as withCustomSecret } from "./storage-BzaUxmqe.mjs";
4
+ import { n as secretsStoreFor } from "./store-DeGATCS-.mjs";
5
+ import { r as providerOf } from "./backends-CSuLl0YP.mjs";
6
+ import { canonicalId, provideKey } from "@geekmidas/manifest";
7
+ import { parseKeyring, retireKey, rotateKeyring } from "@geekmidas/constructs/encryption";
8
+ //#region src/encryption/index.ts
9
+ /**
10
+ * `gkm encryption:rotate` and `gkm encryption:retire` — the keyring of an
11
+ * `Encryption` construct on a server stage.
12
+ *
13
+ * Rotating adds a key in front and keeps every older one, so what they wrote
14
+ * still opens. Retiring removes one, and is the half that makes rotating worth
15
+ * doing: until a key is gone, it still opens everything it ever wrote. It is
16
+ * never automatic, because the only thing that knows nothing is left under a
17
+ * key is the sweep that re-encrypted the column — so it is done after one,
18
+ * deliberately, by name.
19
+ *
20
+ * Neither applies everywhere, and each says so rather than doing something
21
+ * that looks right: a local stage's keyring is derived and would be replaced on
22
+ * the next `gkm dev`, and on AWS the key is KMS's, which rotates it itself and
23
+ * never strands a value.
24
+ */
25
+ const logger = console;
26
+ var LocalKeyringIsDerived = class extends Error {
27
+ construct;
28
+ stage;
29
+ constructor(construct, stage) {
30
+ super(`'${stage}' is the local stage, where ${construct}'s keyring is derived from the project and stage — nothing stored there would be read. Rotate a deployed stage: gkm encryption:rotate ${construct} --stage <stage>.`);
31
+ this.construct = construct;
32
+ this.stage = stage;
33
+ this.name = "LocalKeyringIsDerived";
34
+ }
35
+ };
36
+ var KmsRotatesItself = class extends Error {
37
+ construct;
38
+ constructor(construct) {
39
+ super(`${construct} is a KMS key on AWS: KMS rotates it yearly and keeps every version, so no value is ever stranded and there is no key to retire. Nothing to do here.`);
40
+ this.construct = construct;
41
+ this.name = "KmsRotatesItself";
42
+ }
43
+ };
44
+ var NoKeyringYet = class extends Error {
45
+ construct;
46
+ stage;
47
+ key;
48
+ constructor(construct, stage, key) {
49
+ super(`'${stage}' has no ${key} yet. The first deploy generates ${construct}'s keyring: gkm deploy --stage ${stage}.`);
50
+ this.construct = construct;
51
+ this.stage = stage;
52
+ this.key = key;
53
+ this.name = "NoKeyringYet";
54
+ }
55
+ };
56
+ async function encryptionRotateCommand(construct, options, context = {}) {
57
+ return updateKeyring(construct, options, context, (id, url) => {
58
+ const next = rotateKeyring(id, url);
59
+ const [current, ...older] = parseKeyring(id, next).keys.map(({ id }) => id);
60
+ logger.log(`\n✓ ${id} on '${options.stage}' now writes with '${current}'.`);
61
+ logger.log(` Still opening: ${older.join(", ")}. Redeploy to start writing with '${current}', reencrypt what is stored, then: gkm encryption:retire ${id} <key> --stage ${options.stage}`);
62
+ return next;
63
+ });
64
+ }
65
+ async function encryptionRetireCommand(construct, keyId, options, context = {}) {
66
+ return updateKeyring(construct, options, context, (id, url) => {
67
+ const next = retireKey(id, url, keyId);
68
+ logger.log(`\n✓ ${id} on '${options.stage}' no longer holds '${keyId}'. Anything still under it cannot be read after the next deploy.`);
69
+ return next;
70
+ });
71
+ }
72
+ async function updateKeyring(construct, { stage }, context, update) {
73
+ const workspace = context.workspace ?? await loadWorkspaceSettings();
74
+ const id = canonicalId(construct);
75
+ if (stage === workspace.stages.local) throw new LocalKeyringIsDerived(id, stage);
76
+ assertDeployedStage(workspace.stages, stage);
77
+ if (providerOf(workspace) === "aws") throw new KmsRotatesItself(id);
78
+ const store = context.store ?? await secretsStoreFor(workspace, stage);
79
+ const key = provideKey(id, "url");
80
+ const secrets = await store.read(stage);
81
+ const url = secrets?.custom[key];
82
+ if (!secrets || !url) throw new NoKeyringYet(id, stage, key);
83
+ const next = update(id, url);
84
+ await store.write(stage, withCustomSecret(secrets, key, next));
85
+ return next;
86
+ }
87
+ //#endregion
88
+ export { encryptionRetireCommand, encryptionRotateCommand };
89
+
90
+ //# sourceMappingURL=encryption-DyQnPsZN.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption-DyQnPsZN.mjs","names":[],"sources":["../src/encryption/index.ts"],"sourcesContent":["/**\n * `gkm encryption:rotate` and `gkm encryption:retire` — the keyring of an\n * `Encryption` construct on a server stage.\n *\n * Rotating adds a key in front and keeps every older one, so what they wrote\n * still opens. Retiring removes one, and is the half that makes rotating worth\n * doing: until a key is gone, it still opens everything it ever wrote. It is\n * never automatic, because the only thing that knows nothing is left under a\n * key is the sweep that re-encrypted the column — so it is done after one,\n * deliberately, by name.\n *\n * Neither applies everywhere, and each says so rather than doing something\n * that looks right: a local stage's keyring is derived and would be replaced on\n * the next `gkm dev`, and on AWS the key is KMS's, which rotates it itself and\n * never strands a value.\n */\n\nimport {\n\tparseKeyring,\n\tretireKey,\n\trotateKeyring,\n} from '@geekmidas/constructs/encryption';\nimport { canonicalId, provideKey } from '@geekmidas/manifest';\nimport { loadWorkspaceSettings } from '../config';\nimport { withCustomSecret } from '../secrets/storage.js';\nimport { type SecretsStore, secretsStoreFor } from '../secrets/store.js';\nimport { providerOf } from '../workspace/backends.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\nconst logger = console;\n\nexport class LocalKeyringIsDerived extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly stage: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' is the local stage, where ${construct}'s keyring is derived from the project and stage — nothing stored there would be read. Rotate a deployed stage: gkm encryption:rotate ${construct} --stage <stage>.`,\n\t\t);\n\t\tthis.name = 'LocalKeyringIsDerived';\n\t}\n}\n\nexport class KmsRotatesItself extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} is a KMS key on AWS: KMS rotates it yearly and keeps every version, so no value is ever stranded and there is no key to retire. Nothing to do here.`,\n\t\t);\n\t\tthis.name = 'KmsRotatesItself';\n\t}\n}\n\nexport class NoKeyringYet extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly stage: string,\n\t\treadonly key: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' has no ${key} yet. The first deploy generates ${construct}'s keyring: gkm deploy --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'NoKeyringYet';\n\t}\n}\n\nexport interface EncryptionCommandOptions {\n\tstage: string;\n}\n\n/** Where a command reads and writes; the real ones unless a test says. */\nexport interface EncryptionCommandContext {\n\tworkspace?: NormalizedWorkspace;\n\tstore?: SecretsStore;\n}\n\nexport async function encryptionRotateCommand(\n\tconstruct: string,\n\toptions: EncryptionCommandOptions,\n\tcontext: EncryptionCommandContext = {},\n): Promise<string> {\n\treturn updateKeyring(construct, options, context, (id, url) => {\n\t\tconst next = rotateKeyring(id, url);\n\t\tconst [current, ...older] = parseKeyring(id, next).keys.map(({ id }) => id);\n\n\t\tlogger.log(`\\n✓ ${id} on '${options.stage}' now writes with '${current}'.`);\n\t\tlogger.log(\n\t\t\t` Still opening: ${older.join(', ')}. Redeploy to start writing with '${current}', reencrypt what is stored, then: gkm encryption:retire ${id} <key> --stage ${options.stage}`,\n\t\t);\n\t\treturn next;\n\t});\n}\n\nexport async function encryptionRetireCommand(\n\tconstruct: string,\n\tkeyId: string,\n\toptions: EncryptionCommandOptions,\n\tcontext: EncryptionCommandContext = {},\n): Promise<string> {\n\treturn updateKeyring(construct, options, context, (id, url) => {\n\t\tconst next = retireKey(id, url, keyId);\n\n\t\tlogger.log(\n\t\t\t`\\n✓ ${id} on '${options.stage}' no longer holds '${keyId}'. Anything still under it cannot be read after the next deploy.`,\n\t\t);\n\t\treturn next;\n\t});\n}\n\nasync function updateKeyring(\n\tconstruct: string,\n\t{ stage }: EncryptionCommandOptions,\n\tcontext: EncryptionCommandContext,\n\tupdate: (id: string, url: string) => string,\n): Promise<string> {\n\tconst workspace = context.workspace ?? (await loadWorkspaceSettings());\n\tconst id = canonicalId(construct);\n\n\tif (stage === workspace.stages.local) {\n\t\tthrow new LocalKeyringIsDerived(id, stage);\n\t}\n\tassertDeployedStage(workspace.stages, stage);\n\tif (providerOf(workspace) === 'aws') throw new KmsRotatesItself(id);\n\n\tconst store = context.store ?? (await secretsStoreFor(workspace, stage));\n\tconst key = provideKey(id, 'url');\n\tconst secrets = await store.read(stage);\n\tconst url = secrets?.custom[key];\n\tif (!secrets || !url) throw new NoKeyringYet(id, stage, key);\n\n\tconst next = update(id, url);\n\tawait store.write(stage, withCustomSecret(secrets, key, next));\n\treturn next;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,SAAS;AAEf,IAAa,wBAAb,cAA2C,MAAM;CAEtC;CACA;CAFV,YACC,WACA,OACC;EACD,MACC,IAAI,MAAM,8BAA8B,UAAU,wIAAwI,UAAU,kBACrM;EALS,KAAA,YAAA;EACA,KAAA,QAAA;EAKT,KAAK,OAAO;CACb;AACD;AAEA,IAAa,mBAAb,cAAsC,MAAM;CACtB;CAArB,YAAY,WAA4B;EACvC,MACC,GAAG,UAAU,qJACd;EAHoB,KAAA,YAAA;EAIpB,KAAK,OAAO;CACb;AACD;AAEA,IAAa,eAAb,cAAkC,MAAM;CAE7B;CACA;CACA;CAHV,YACC,WACA,OACA,KACC;EACD,MACC,IAAI,MAAM,WAAW,IAAI,mCAAmC,UAAU,iCAAiC,MAAM,EAC9G;EANS,KAAA,YAAA;EACA,KAAA,QAAA;EACA,KAAA,MAAA;EAKT,KAAK,OAAO;CACb;AACD;AAYA,eAAsB,wBACrB,WACA,SACA,UAAoC,CAAC,GACnB;CAClB,OAAO,cAAc,WAAW,SAAS,UAAU,IAAI,QAAQ;EAC9D,MAAM,OAAO,cAAc,IAAI,GAAG;EAClC,MAAM,CAAC,SAAS,GAAG,SAAS,aAAa,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK,EAAE,SAAS,EAAE;EAE1E,OAAO,IAAI,OAAO,GAAG,OAAO,QAAQ,MAAM,qBAAqB,QAAQ,GAAG;EAC1E,OAAO,IACN,oBAAoB,MAAM,KAAK,IAAI,EAAE,oCAAoC,QAAQ,2DAA2D,GAAG,iBAAiB,QAAQ,OACzK;EACA,OAAO;CACR,CAAC;AACF;AAEA,eAAsB,wBACrB,WACA,OACA,SACA,UAAoC,CAAC,GACnB;CAClB,OAAO,cAAc,WAAW,SAAS,UAAU,IAAI,QAAQ;EAC9D,MAAM,OAAO,UAAU,IAAI,KAAK,KAAK;EAErC,OAAO,IACN,OAAO,GAAG,OAAO,QAAQ,MAAM,qBAAqB,MAAM,iEAC3D;EACA,OAAO;CACR,CAAC;AACF;AAEA,eAAe,cACd,WACA,EAAE,SACF,SACA,QACkB;CAClB,MAAM,YAAY,QAAQ,aAAc,MAAM,sBAAsB;CACpE,MAAM,KAAK,YAAY,SAAS;CAEhC,IAAI,UAAU,UAAU,OAAO,OAC9B,MAAM,IAAI,sBAAsB,IAAI,KAAK;CAE1C,oBAAoB,UAAU,QAAQ,KAAK;CAC3C,IAAI,WAAW,SAAS,MAAM,OAAO,MAAM,IAAI,iBAAiB,EAAE;CAElE,MAAM,QAAQ,QAAQ,SAAU,MAAM,gBAAgB,WAAW,KAAK;CACtE,MAAM,MAAM,WAAW,IAAI,KAAK;CAChC,MAAM,UAAU,MAAM,MAAM,KAAK,KAAK;CACtC,MAAM,MAAM,SAAS,OAAO;CAC5B,IAAI,CAAC,WAAW,CAAC,KAAK,MAAM,IAAI,aAAa,IAAI,OAAO,GAAG;CAE3D,MAAM,OAAO,OAAO,IAAI,GAAG;CAC3B,MAAM,MAAM,MAAM,OAAO,iBAAiB,SAAS,KAAK,IAAI,CAAC;CAC7D,OAAO;AACR"}
@@ -1,5 +1,5 @@
1
- import { l as loadWorkspaceConfig } from "./config-5watB1D0.mjs";
2
- import { D as assertDeployedStage } from "./workspace-DOmIhiau.mjs";
1
+ import { l as loadWorkspaceConfig } from "./config-D8J5rrTb.mjs";
2
+ import { D as assertDeployedStage } from "./workspace-DNzWgM1J.mjs";
3
3
  import { a as getKeyPath, n as secretsStoreFor, t as isRemoteStore } from "./store-DeGATCS-.mjs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { execFileSync } from "node:child_process";
@@ -210,4 +210,4 @@ var SsoSessionExpired = class extends Error {
210
210
  //#endregion
211
211
  export { deployGithubCommand };
212
212
 
213
- //# sourceMappingURL=github-D5MthUlR.mjs.map
213
+ //# sourceMappingURL=github-BfpadQWQ.mjs.map