@geekmidas/cli 10.0.0-alpha.16 → 10.0.0-alpha.18
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/{bundler-BeNDZdHQ.cjs → bundler-mjqGAvu1.cjs} +2 -2
- package/dist/{bundler-BeNDZdHQ.cjs.map → bundler-mjqGAvu1.cjs.map} +1 -1
- package/dist/{bundler-BodTL4kf.mjs → bundler-mwH7WOb1.mjs} +2 -2
- package/dist/{bundler-BodTL4kf.mjs.map → bundler-mwH7WOb1.mjs.map} +1 -1
- package/dist/{config-Q1rvKHvw.cjs → config-DSXI480r.cjs} +2 -2
- package/dist/{config-Q1rvKHvw.cjs.map → config-DSXI480r.cjs.map} +1 -1
- package/dist/{config-pqGD3hF9.mjs → config-DdKkoUE1.mjs} +2 -2
- package/dist/{config-pqGD3hF9.mjs.map → config-DdKkoUE1.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{credentials-Q6F0uqJq.cjs → credentials-C6Pfn6GY.cjs} +31 -9
- package/dist/credentials-C6Pfn6GY.cjs.map +1 -0
- package/dist/{credentials-BekkKtY_.mjs → credentials-lbv-j0Be.mjs} +32 -10
- package/dist/{credentials-BekkKtY_.mjs.map → credentials-lbv-j0Be.mjs.map} +1 -1
- package/dist/discover-BERGFg8v.cjs.map +1 -1
- package/dist/discover-CK-n3w6b.mjs.map +1 -1
- package/dist/{github-CRwnxKmD.mjs → github-DlIrXPee.mjs} +21 -8
- package/dist/github-DlIrXPee.mjs.map +1 -0
- package/dist/{github-BfflNPVe.cjs → github-n7N9yOff.cjs} +22 -9
- package/dist/github-n7N9yOff.cjs.map +1 -0
- package/dist/{index-C-VLwB_Y.d.mts → index-B4H4XF7A.d.mts} +2 -2
- package/dist/{index-C-HkOEBt.d.cts.map → index-B4H4XF7A.d.mts.map} +1 -1
- package/dist/{index-C-HkOEBt.d.cts → index-RCUizGxL.d.cts} +2 -2
- package/dist/{index-C-VLwB_Y.d.mts.map → index-RCUizGxL.d.cts.map} +1 -1
- package/dist/index.cjs +164 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +159 -167
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-Dcbme-Us.cjs → openapi-B8bu_OXN.cjs} +2 -2
- package/dist/{openapi-Dcbme-Us.cjs.map → openapi-B8bu_OXN.cjs.map} +1 -1
- package/dist/{openapi-vy4JzhUv.mjs → openapi-DfjbhrsM.mjs} +2 -2
- package/dist/{openapi-vy4JzhUv.mjs.map → openapi-DfjbhrsM.mjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{fullstack-secrets-DXsWO-5m.cjs → reconcile-Cq4cO83R.cjs} +45 -1
- package/dist/reconcile-Cq4cO83R.cjs.map +1 -0
- package/dist/{fullstack-secrets-BET8gqwK.mjs → reconcile-fmoluZmZ.mjs} +34 -2
- package/dist/reconcile-fmoluZmZ.mjs.map +1 -0
- package/dist/ssm-BKN1Cg4z.mjs +60 -0
- package/dist/ssm-BKN1Cg4z.mjs.map +1 -0
- package/dist/ssm-FxeSAt35.cjs +60 -0
- package/dist/ssm-FxeSAt35.cjs.map +1 -0
- package/dist/{storage-C-30BD1e.cjs → storage-DgypfSD6.cjs} +83 -4
- package/dist/storage-DgypfSD6.cjs.map +1 -0
- package/dist/{storage-P_b3w7uj.mjs → storage-TP0x118M.mjs} +77 -4
- package/dist/storage-TP0x118M.mjs.map +1 -0
- package/dist/transfer-2aRVVJ6I.mjs +122 -0
- package/dist/transfer-2aRVVJ6I.mjs.map +1 -0
- package/dist/transfer-C3WZVmj_.cjs +151 -0
- package/dist/transfer-C3WZVmj_.cjs.map +1 -0
- package/dist/{types-RhXmHR-e.d.cts → types-7R81hu0i.d.mts} +138 -1
- package/dist/types-7R81hu0i.d.mts.map +1 -0
- package/dist/{types-kvBQjpPo.d.mts → types-N7c2O2m7.d.cts} +140 -1
- package/dist/types-N7c2O2m7.d.cts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CgwmXEtc.mjs → workspace-Bu3xNCej.mjs} +14 -3
- package/dist/{workspace-CgwmXEtc.mjs.map → workspace-Bu3xNCej.mjs.map} +1 -1
- package/dist/{workspace-mFpI_ky_.cjs → workspace-CRD9YinV.cjs} +19 -2
- package/dist/{workspace-mFpI_ky_.cjs.map → workspace-CRD9YinV.cjs.map} +1 -1
- package/package.json +11 -11
- package/dist/credentials-Q6F0uqJq.cjs.map +0 -1
- package/dist/fullstack-secrets-BET8gqwK.mjs.map +0 -1
- package/dist/fullstack-secrets-DXsWO-5m.cjs.map +0 -1
- package/dist/github-BfflNPVe.cjs.map +0 -1
- package/dist/github-CRwnxKmD.mjs.map +0 -1
- package/dist/keystore-B4PRlbgE.mjs +0 -81
- package/dist/keystore-B4PRlbgE.mjs.map +0 -1
- package/dist/keystore-DgGRj2Yy.cjs +0 -98
- package/dist/keystore-DgGRj2Yy.cjs.map +0 -1
- package/dist/reconcile-Dd2iyILf.cjs +0 -34
- package/dist/reconcile-Dd2iyILf.cjs.map +0 -1
- package/dist/reconcile-DhQ00DpU.mjs +0 -34
- package/dist/reconcile-DhQ00DpU.mjs.map +0 -1
- package/dist/storage-C-30BD1e.cjs.map +0 -1
- package/dist/storage-P_b3w7uj.mjs.map +0 -1
- package/dist/sync-B3xxXtJT.mjs +0 -88
- package/dist/sync-B3xxXtJT.mjs.map +0 -1
- package/dist/sync-D7uTIIEi.cjs +0 -111
- package/dist/sync-D7uTIIEi.cjs.map +0 -1
- package/dist/types-RhXmHR-e.d.cts.map +0 -1
- package/dist/types-kvBQjpPo.d.mts.map +0 -1
package/dist/openapi.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const require_openapi = require("./openapi-
|
|
3
|
+
const require_openapi = require("./openapi-B8bu_OXN.cjs");
|
|
4
4
|
exports.OPENAPI_OUTPUT_PATH = require_openapi.OPENAPI_OUTPUT_PATH;
|
|
5
5
|
exports.generateOpenApi = require_openapi.generateOpenApi;
|
|
6
6
|
exports.generateOpenApiFrom = require_openapi.generateOpenApiFrom;
|
package/dist/openapi.d.cts
CHANGED
package/dist/openapi.d.mts
CHANGED
package/dist/openapi.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
|
-
import { a as openapiCommand, i as openApiPathFor, n as generateOpenApi, o as resolveOpenApiConfig, r as generateOpenApiFrom, t as OPENAPI_OUTPUT_PATH } from "./openapi-
|
|
2
|
+
import { a as openapiCommand, i as openApiPathFor, n as generateOpenApi, o as resolveOpenApiConfig, r as generateOpenApiFrom, t as OPENAPI_OUTPUT_PATH } from "./openapi-DfjbhrsM.mjs";
|
|
3
3
|
export { OPENAPI_OUTPUT_PATH, generateOpenApi, generateOpenApiFrom, openApiPathFor, openapiCommand, resolveOpenApiConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as EmailBackend, t as CacheBackend } from "../types-
|
|
1
|
+
import { n as EmailBackend, t as CacheBackend } from "../types-N7c2O2m7.cjs";
|
|
2
2
|
import { ConstructManifest, Declaration, RouteInfo } from "@geekmidas/manifest";
|
|
3
3
|
//#region src/reconcile/discover.d.ts
|
|
4
4
|
/** The construct face discovery needs: an id, and the ability to declare. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as EmailBackend, t as CacheBackend } from "../types-
|
|
1
|
+
import { n as EmailBackend, t as CacheBackend } from "../types-7R81hu0i.mjs";
|
|
2
2
|
import { ConstructManifest, Declaration, RouteInfo } from "@geekmidas/manifest";
|
|
3
3
|
//#region src/reconcile/discover.d.ts
|
|
4
4
|
/** The construct face discovery needs: an id, and the ability to declare. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
1
2
|
const require_generator = require("./generator-Wdfp0jCS.cjs");
|
|
2
3
|
let node_fs_promises = require("node:fs/promises");
|
|
3
4
|
let node_path = require("node:path");
|
|
@@ -76,12 +77,55 @@ ${dbPasswordEntries.map(([key, value]) => `${key}=${value}`).join("\n")}
|
|
|
76
77
|
await (0, node_fs_promises.writeFile)(envPath, envContent);
|
|
77
78
|
}
|
|
78
79
|
//#endregion
|
|
80
|
+
//#region src/secrets/reconcile.ts
|
|
81
|
+
var reconcile_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ reconcileMissingSecrets: () => reconcileMissingSecrets });
|
|
82
|
+
/**
|
|
83
|
+
* Reconcile missing custom secrets for a workspace.
|
|
84
|
+
*
|
|
85
|
+
* Compares current secrets against what generateFullstackCustomSecrets()
|
|
86
|
+
* would produce and backfills any missing keys without overwriting
|
|
87
|
+
* existing values.
|
|
88
|
+
*
|
|
89
|
+
* @returns ReconcileResult if keys were added, null if secrets are up-to-date
|
|
90
|
+
*/
|
|
91
|
+
function reconcileMissingSecrets(secrets, workspace, containers = []) {
|
|
92
|
+
if (!(Object.keys(workspace.apps).length > 1)) return null;
|
|
93
|
+
const expectedCustom = generateFullstackCustomSecrets(workspace, containers);
|
|
94
|
+
const addedKeys = [];
|
|
95
|
+
const mergedCustom = { ...secrets.custom };
|
|
96
|
+
for (const [key, value] of Object.entries(expectedCustom)) if (!(key in mergedCustom)) {
|
|
97
|
+
mergedCustom[key] = value;
|
|
98
|
+
addedKeys.push(key);
|
|
99
|
+
}
|
|
100
|
+
if (addedKeys.length === 0) return null;
|
|
101
|
+
return {
|
|
102
|
+
secrets: {
|
|
103
|
+
...secrets,
|
|
104
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
+
custom: mergedCustom
|
|
106
|
+
},
|
|
107
|
+
addedKeys
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
79
111
|
Object.defineProperty(exports, "generateFullstackCustomSecrets", {
|
|
80
112
|
enumerable: true,
|
|
81
113
|
get: function() {
|
|
82
114
|
return generateFullstackCustomSecrets;
|
|
83
115
|
}
|
|
84
116
|
});
|
|
117
|
+
Object.defineProperty(exports, "reconcileMissingSecrets", {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
get: function() {
|
|
120
|
+
return reconcileMissingSecrets;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
Object.defineProperty(exports, "reconcile_exports", {
|
|
124
|
+
enumerable: true,
|
|
125
|
+
get: function() {
|
|
126
|
+
return reconcile_exports;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
85
129
|
Object.defineProperty(exports, "writeDockerEnvFromSecrets", {
|
|
86
130
|
enumerable: true,
|
|
87
131
|
get: function() {
|
|
@@ -89,4 +133,4 @@ Object.defineProperty(exports, "writeDockerEnvFromSecrets", {
|
|
|
89
133
|
}
|
|
90
134
|
});
|
|
91
135
|
|
|
92
|
-
//# sourceMappingURL=
|
|
136
|
+
//# sourceMappingURL=reconcile-Cq4cO83R.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile-Cq4cO83R.cjs","names":["generateSecurePassword","join","mkdir","dirname","writeFile"],"sources":["../src/setup/fullstack-secrets.ts","../src/secrets/reconcile.ts"],"sourcesContent":["import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { generateSecurePassword } from '../secrets/generator.js';\nimport type { StageSecrets } from '../secrets/types.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\n/**\n * Generate a secure random password for database users.\n * Uses a combination of timestamp and random bytes for uniqueness.\n */\nexport function generateDbPassword(): string {\n\treturn `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Generate database URL for an app.\n * All apps connect to the same database, but use different users/schemas.\n */\nexport function generateDbUrl(\n\tappName: string,\n\tpassword: string,\n\tprojectName: string,\n\thost = 'localhost',\n\tport = 5432,\n): string {\n\tconst userName = appName.replace(/-/g, '_');\n\tconst dbName = `${projectName.replace(/-/g, '_')}_dev`;\n\treturn `postgresql://${userName}:${password}@${host}:${port}/${dbName}`;\n}\n\n/**\n * Generate fullstack-aware custom secrets for a workspace.\n *\n * Generates:\n * - Common secrets: NODE_ENV, PORT, LOG_LEVEL, JWT_SECRET\n * - Per-app database passwords and URLs for backend apps with db service\n * - Better-auth secrets for apps using the better-auth framework\n */\nexport function generateFullstackCustomSecrets(\n\tworkspace: NormalizedWorkspace,\n\tcontainers: readonly string[] = [],\n): Record<string, string> {\n\t// A Postgres exists because something declared a database, not because\n\t// config carried a flag saying so.\n\tconst hasDb = containers.includes('postgres');\n\tconst customs: Record<string, string> = {\n\t\tNODE_ENV: 'development',\n\t\tPORT: '3000',\n\t\tLOG_LEVEL: 'debug',\n\t\tJWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n\t};\n\n\tif (!hasDb) {\n\t\treturn customs;\n\t}\n\n\t// Collect all frontend ports for trusted origins\n\tconst frontendPorts: number[] = [];\n\n\tfor (const [appName, appConfig] of Object.entries(workspace.apps)) {\n\t\tif (appConfig.type === 'web') {\n\t\t\tfrontendPorts.push(appConfig.port);\n\t\t\tconst upperName = appName.toUpperCase();\n\t\t\tcustoms[`${upperName}_URL`] = `http://localhost:${appConfig.port}`;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Mobile apps (Expo, etc.) don't run on a host port the backend\n\t\t// cares about for CORS, and don't need DB credentials.\n\t\tif (appConfig.type === 'mobile') {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Backend apps with database: generate per-app DB passwords and URLs\n\t\tconst password = generateDbPassword();\n\t\tconst upperName = appName.toUpperCase();\n\n\t\tcustoms[`${upperName}_DATABASE_URL`] = generateDbUrl(\n\t\t\tappName,\n\t\t\tpassword,\n\t\t\tworkspace.name,\n\t\t);\n\t\tcustoms[`${upperName}_DB_PASSWORD`] = password;\n\n\t\t// Better-auth framework secrets\n\t\tif (appConfig.framework === 'better-auth') {\n\t\t\tcustoms.AUTH_PORT = String(appConfig.port);\n\t\t\tcustoms.AUTH_URL = `http://localhost:${appConfig.port}`;\n\t\t\tcustoms.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${generateSecurePassword(16)}`;\n\t\t\tcustoms.BETTER_AUTH_URL = `http://localhost:${appConfig.port}`;\n\t\t}\n\t}\n\n\t// Generate trusted origins for better-auth (all app ports)\n\tif (customs.BETTER_AUTH_SECRET) {\n\t\tconst allPorts = Object.values(workspace.apps).map((a) => a.port);\n\t\tcustoms.BETTER_AUTH_TRUSTED_ORIGINS = allPorts\n\t\t\t.map((p) => `http://localhost:${p}`)\n\t\t\t.join(',');\n\t}\n\n\treturn customs;\n}\n\n/**\n * Extract *_DB_PASSWORD keys from secrets and write docker/.env.\n *\n * The docker/.env file contains database passwords that the PostgreSQL\n * init script reads to create per-app database users.\n */\nexport async function writeDockerEnvFromSecrets(\n\tsecrets: StageSecrets,\n\tworkspaceRoot: string,\n): Promise<void> {\n\tconst dbPasswordEntries = Object.entries(secrets.custom).filter(([key]) =>\n\t\tkey.endsWith('_DB_PASSWORD'),\n\t);\n\n\t// Always include pgboss password when pgboss credentials exist\n\tif (\n\t\tsecrets.services.pgboss &&\n\t\t!dbPasswordEntries.some(([key]) => key === 'PGBOSS_DB_PASSWORD')\n\t) {\n\t\tdbPasswordEntries.push([\n\t\t\t'PGBOSS_DB_PASSWORD',\n\t\t\tsecrets.services.pgboss.password,\n\t\t]);\n\t}\n\n\tif (dbPasswordEntries.length === 0) {\n\t\treturn;\n\t}\n\n\tconst envContent = `# Auto-generated docker environment file\n# Contains database passwords for docker-compose postgres init\n# This file is gitignored - do not commit to version control\n${dbPasswordEntries.map(([key, value]) => `${key}=${value}`).join('\\n')}\n`;\n\n\tconst envPath = join(workspaceRoot, 'docker', '.env');\n\tawait mkdir(dirname(envPath), { recursive: true });\n\tawait writeFile(envPath, envContent);\n}\n","import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n\tcontainers: readonly string[] = [],\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace, containers);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
1
2
|
import { i as generateSecurePassword } from "./generator-LfZU7NlU.mjs";
|
|
2
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -76,6 +77,37 @@ ${dbPasswordEntries.map(([key, value]) => `${key}=${value}`).join("\n")}
|
|
|
76
77
|
await writeFile(envPath, envContent);
|
|
77
78
|
}
|
|
78
79
|
//#endregion
|
|
79
|
-
|
|
80
|
+
//#region src/secrets/reconcile.ts
|
|
81
|
+
var reconcile_exports = /* @__PURE__ */ __exportAll({ reconcileMissingSecrets: () => reconcileMissingSecrets });
|
|
82
|
+
/**
|
|
83
|
+
* Reconcile missing custom secrets for a workspace.
|
|
84
|
+
*
|
|
85
|
+
* Compares current secrets against what generateFullstackCustomSecrets()
|
|
86
|
+
* would produce and backfills any missing keys without overwriting
|
|
87
|
+
* existing values.
|
|
88
|
+
*
|
|
89
|
+
* @returns ReconcileResult if keys were added, null if secrets are up-to-date
|
|
90
|
+
*/
|
|
91
|
+
function reconcileMissingSecrets(secrets, workspace, containers = []) {
|
|
92
|
+
if (!(Object.keys(workspace.apps).length > 1)) return null;
|
|
93
|
+
const expectedCustom = generateFullstackCustomSecrets(workspace, containers);
|
|
94
|
+
const addedKeys = [];
|
|
95
|
+
const mergedCustom = { ...secrets.custom };
|
|
96
|
+
for (const [key, value] of Object.entries(expectedCustom)) if (!(key in mergedCustom)) {
|
|
97
|
+
mergedCustom[key] = value;
|
|
98
|
+
addedKeys.push(key);
|
|
99
|
+
}
|
|
100
|
+
if (addedKeys.length === 0) return null;
|
|
101
|
+
return {
|
|
102
|
+
secrets: {
|
|
103
|
+
...secrets,
|
|
104
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
+
custom: mergedCustom
|
|
106
|
+
},
|
|
107
|
+
addedKeys
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { writeDockerEnvFromSecrets as i, reconcile_exports as n, generateFullstackCustomSecrets as r, reconcileMissingSecrets as t };
|
|
80
112
|
|
|
81
|
-
//# sourceMappingURL=
|
|
113
|
+
//# sourceMappingURL=reconcile-fmoluZmZ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile-fmoluZmZ.mjs","names":[],"sources":["../src/setup/fullstack-secrets.ts","../src/secrets/reconcile.ts"],"sourcesContent":["import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { generateSecurePassword } from '../secrets/generator.js';\nimport type { StageSecrets } from '../secrets/types.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\n/**\n * Generate a secure random password for database users.\n * Uses a combination of timestamp and random bytes for uniqueness.\n */\nexport function generateDbPassword(): string {\n\treturn `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Generate database URL for an app.\n * All apps connect to the same database, but use different users/schemas.\n */\nexport function generateDbUrl(\n\tappName: string,\n\tpassword: string,\n\tprojectName: string,\n\thost = 'localhost',\n\tport = 5432,\n): string {\n\tconst userName = appName.replace(/-/g, '_');\n\tconst dbName = `${projectName.replace(/-/g, '_')}_dev`;\n\treturn `postgresql://${userName}:${password}@${host}:${port}/${dbName}`;\n}\n\n/**\n * Generate fullstack-aware custom secrets for a workspace.\n *\n * Generates:\n * - Common secrets: NODE_ENV, PORT, LOG_LEVEL, JWT_SECRET\n * - Per-app database passwords and URLs for backend apps with db service\n * - Better-auth secrets for apps using the better-auth framework\n */\nexport function generateFullstackCustomSecrets(\n\tworkspace: NormalizedWorkspace,\n\tcontainers: readonly string[] = [],\n): Record<string, string> {\n\t// A Postgres exists because something declared a database, not because\n\t// config carried a flag saying so.\n\tconst hasDb = containers.includes('postgres');\n\tconst customs: Record<string, string> = {\n\t\tNODE_ENV: 'development',\n\t\tPORT: '3000',\n\t\tLOG_LEVEL: 'debug',\n\t\tJWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n\t};\n\n\tif (!hasDb) {\n\t\treturn customs;\n\t}\n\n\t// Collect all frontend ports for trusted origins\n\tconst frontendPorts: number[] = [];\n\n\tfor (const [appName, appConfig] of Object.entries(workspace.apps)) {\n\t\tif (appConfig.type === 'web') {\n\t\t\tfrontendPorts.push(appConfig.port);\n\t\t\tconst upperName = appName.toUpperCase();\n\t\t\tcustoms[`${upperName}_URL`] = `http://localhost:${appConfig.port}`;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Mobile apps (Expo, etc.) don't run on a host port the backend\n\t\t// cares about for CORS, and don't need DB credentials.\n\t\tif (appConfig.type === 'mobile') {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Backend apps with database: generate per-app DB passwords and URLs\n\t\tconst password = generateDbPassword();\n\t\tconst upperName = appName.toUpperCase();\n\n\t\tcustoms[`${upperName}_DATABASE_URL`] = generateDbUrl(\n\t\t\tappName,\n\t\t\tpassword,\n\t\t\tworkspace.name,\n\t\t);\n\t\tcustoms[`${upperName}_DB_PASSWORD`] = password;\n\n\t\t// Better-auth framework secrets\n\t\tif (appConfig.framework === 'better-auth') {\n\t\t\tcustoms.AUTH_PORT = String(appConfig.port);\n\t\t\tcustoms.AUTH_URL = `http://localhost:${appConfig.port}`;\n\t\t\tcustoms.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${generateSecurePassword(16)}`;\n\t\t\tcustoms.BETTER_AUTH_URL = `http://localhost:${appConfig.port}`;\n\t\t}\n\t}\n\n\t// Generate trusted origins for better-auth (all app ports)\n\tif (customs.BETTER_AUTH_SECRET) {\n\t\tconst allPorts = Object.values(workspace.apps).map((a) => a.port);\n\t\tcustoms.BETTER_AUTH_TRUSTED_ORIGINS = allPorts\n\t\t\t.map((p) => `http://localhost:${p}`)\n\t\t\t.join(',');\n\t}\n\n\treturn customs;\n}\n\n/**\n * Extract *_DB_PASSWORD keys from secrets and write docker/.env.\n *\n * The docker/.env file contains database passwords that the PostgreSQL\n * init script reads to create per-app database users.\n */\nexport async function writeDockerEnvFromSecrets(\n\tsecrets: StageSecrets,\n\tworkspaceRoot: string,\n): Promise<void> {\n\tconst dbPasswordEntries = Object.entries(secrets.custom).filter(([key]) =>\n\t\tkey.endsWith('_DB_PASSWORD'),\n\t);\n\n\t// Always include pgboss password when pgboss credentials exist\n\tif (\n\t\tsecrets.services.pgboss &&\n\t\t!dbPasswordEntries.some(([key]) => key === 'PGBOSS_DB_PASSWORD')\n\t) {\n\t\tdbPasswordEntries.push([\n\t\t\t'PGBOSS_DB_PASSWORD',\n\t\t\tsecrets.services.pgboss.password,\n\t\t]);\n\t}\n\n\tif (dbPasswordEntries.length === 0) {\n\t\treturn;\n\t}\n\n\tconst envContent = `# Auto-generated docker environment file\n# Contains database passwords for docker-compose postgres init\n# This file is gitignored - do not commit to version control\n${dbPasswordEntries.map(([key, value]) => `${key}=${value}`).join('\\n')}\n`;\n\n\tconst envPath = join(workspaceRoot, 'docker', '.env');\n\tawait mkdir(dirname(envPath), { recursive: true });\n\tawait writeFile(envPath, envContent);\n}\n","import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n\tcontainers: readonly string[] = [],\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace, containers);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { GetParameterCommand, ParameterNotFound, PutParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
2
|
+
//#region src/secrets/ssm.ts
|
|
3
|
+
/**
|
|
4
|
+
* A stage's secrets in SSM Parameter Store, in the stage's own AWS account.
|
|
5
|
+
*
|
|
6
|
+
* One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —
|
|
7
|
+
* encrypted at rest by KMS, so nothing about it needs a key file anywhere.
|
|
8
|
+
* Which account it is in is whichever credentials are active: the stage's
|
|
9
|
+
* OIDC role in a deploy job, or `--profile` on a developer's machine. With
|
|
10
|
+
* staging and production in different accounts, each stage's secrets sit
|
|
11
|
+
* beside its infrastructure, and a staging role cannot read production's.
|
|
12
|
+
*/
|
|
13
|
+
/** The parameter a stage's secrets are kept in. */
|
|
14
|
+
function secretsParameterName(project, stage) {
|
|
15
|
+
return `/gkm/${project}/${stage}/secrets`;
|
|
16
|
+
}
|
|
17
|
+
var SsmSecretsStore = class {
|
|
18
|
+
options;
|
|
19
|
+
client;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
async ssm() {
|
|
24
|
+
if (this.client) return this.client;
|
|
25
|
+
const config = { region: this.options.region };
|
|
26
|
+
if (this.options.profile) {
|
|
27
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
28
|
+
config.credentials = fromIni({ profile: this.options.profile });
|
|
29
|
+
}
|
|
30
|
+
if (this.options.endpoint) config.endpoint = this.options.endpoint;
|
|
31
|
+
this.client = new SSMClient(config);
|
|
32
|
+
return this.client;
|
|
33
|
+
}
|
|
34
|
+
async pull(stage) {
|
|
35
|
+
const ssm = await this.ssm();
|
|
36
|
+
try {
|
|
37
|
+
const { Parameter } = await ssm.send(new GetParameterCommand({
|
|
38
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
39
|
+
WithDecryption: true
|
|
40
|
+
}));
|
|
41
|
+
return Parameter?.Value ? JSON.parse(Parameter.Value) : null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof ParameterNotFound) return null;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async push(stage, secrets) {
|
|
48
|
+
await (await this.ssm()).send(new PutParameterCommand({
|
|
49
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
50
|
+
Value: JSON.stringify(secrets),
|
|
51
|
+
Type: "SecureString",
|
|
52
|
+
Overwrite: true,
|
|
53
|
+
Description: `gkm secrets for ${this.options.project}/${stage}`
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
export { SsmSecretsStore };
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=ssm-BKN1Cg4z.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssm-BKN1Cg4z.mjs","names":[],"sources":["../src/secrets/ssm.ts"],"sourcesContent":["/**\n * A stage's secrets in SSM Parameter Store, in the stage's own AWS account.\n *\n * One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —\n * encrypted at rest by KMS, so nothing about it needs a key file anywhere.\n * Which account it is in is whichever credentials are active: the stage's\n * OIDC role in a deploy job, or `--profile` on a developer's machine. With\n * staging and production in different accounts, each stage's secrets sit\n * beside its infrastructure, and a staging role cannot read production's.\n */\n\nimport {\n\tGetParameterCommand,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface SsmSecretsStoreOptions {\n\t/** The workspace name, which scopes the parameter path. */\n\tproject: string;\n\tregion: string;\n\t/**\n\t * A named AWS profile, resolved with `fromIni` only. The default chain\n\t * reads AWS_* from the environment first, so exported staging keys would\n\t * win over `--profile prod` and put production's secrets in staging.\n\t */\n\tprofile?: string;\n\t/** For tests: an endpoint such as the AWS emulator. */\n\tendpoint?: string;\n}\n\n/** The parameter a stage's secrets are kept in. */\nexport function secretsParameterName(project: string, stage: string): string {\n\treturn `/gkm/${project}/${stage}/secrets`;\n}\n\nexport class SsmSecretsStore implements SecretsStore {\n\tprivate client?: SSMClient;\n\n\tconstructor(private readonly options: SsmSecretsStoreOptions) {}\n\n\tprivate async ssm(): Promise<SSMClient> {\n\t\tif (this.client) return this.client;\n\n\t\tconst config: SSMClientConfig = { region: this.options.region };\n\t\tif (this.options.profile) {\n\t\t\tconst { fromIni } = await import('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: this.options.profile });\n\t\t}\n\t\tif (this.options.endpoint) {\n\t\t\tconfig.endpoint = this.options.endpoint;\n\t\t}\n\n\t\tthis.client = new SSMClient(config);\n\t\treturn this.client;\n\t}\n\n\tasync pull(stage: string): Promise<StageSecrets | null> {\n\t\tconst ssm = await this.ssm();\n\n\t\ttry {\n\t\t\tconst { Parameter } = await ssm.send(\n\t\t\t\tnew GetParameterCommand({\n\t\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\t\tWithDecryption: true,\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Parameter?.Value\n\t\t\t\t? (JSON.parse(Parameter.Value) as StageSecrets)\n\t\t\t\t: null;\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync push(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst ssm = await this.ssm();\n\n\t\tawait ssm.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\tValue: JSON.stringify(secrets),\n\t\t\t\tType: 'SecureString',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription: `gkm secrets for ${this.options.project}/${stage}`,\n\t\t\t}),\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;AAoCA,SAAgB,qBAAqB,SAAiB,OAAuB;CAC5E,OAAO,QAAQ,QAAQ,GAAG,MAAM;AACjC;AAEA,IAAa,kBAAb,MAAqD;CAGvB;CAF7B;CAEA,YAAY,SAAkD;EAAjC,KAAA,UAAA;CAAkC;CAE/D,MAAc,MAA0B;EACvC,IAAI,KAAK,QAAQ,OAAO,KAAK;EAE7B,MAAM,SAA0B,EAAE,QAAQ,KAAK,QAAQ,OAAO;EAC9D,IAAI,KAAK,QAAQ,SAAS;GACzB,MAAM,EAAE,YAAY,MAAM,OAAO;GACjC,OAAO,cAAc,QAAQ,EAAE,SAAS,KAAK,QAAQ,QAAQ,CAAC;EAC/D;EACA,IAAI,KAAK,QAAQ,UAChB,OAAO,WAAW,KAAK,QAAQ;EAGhC,KAAK,SAAS,IAAI,UAAU,MAAM;EAClC,OAAO,KAAK;CACb;CAEA,MAAM,KAAK,OAA6C;EACvD,MAAM,MAAM,MAAM,KAAK,IAAI;EAE3B,IAAI;GACH,MAAM,EAAE,cAAc,MAAM,IAAI,KAC/B,IAAI,oBAAoB;IACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;IACtD,gBAAgB;GACjB,CAAC,CACF;GACA,OAAO,WAAW,QACd,KAAK,MAAM,UAAU,KAAK,IAC3B;EACJ,SAAS,OAAO;GACf,IAAI,iBAAiB,mBAAmB,OAAO;GAC/C,MAAM;EACP;CACD;CAEA,MAAM,KAAK,OAAe,SAAsC;EAG/D,OAAM,MAFY,KAAK,IAAI,EAAA,CAEjB,KACT,IAAI,oBAAoB;GACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;GACtD,OAAO,KAAK,UAAU,OAAO;GAC7B,MAAM;GACN,WAAW;GACX,aAAa,mBAAmB,KAAK,QAAQ,QAAQ,GAAG;EACzD,CAAC,CACF;CACD;AACD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
|
|
2
|
+
//#region src/secrets/ssm.ts
|
|
3
|
+
/**
|
|
4
|
+
* A stage's secrets in SSM Parameter Store, in the stage's own AWS account.
|
|
5
|
+
*
|
|
6
|
+
* One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —
|
|
7
|
+
* encrypted at rest by KMS, so nothing about it needs a key file anywhere.
|
|
8
|
+
* Which account it is in is whichever credentials are active: the stage's
|
|
9
|
+
* OIDC role in a deploy job, or `--profile` on a developer's machine. With
|
|
10
|
+
* staging and production in different accounts, each stage's secrets sit
|
|
11
|
+
* beside its infrastructure, and a staging role cannot read production's.
|
|
12
|
+
*/
|
|
13
|
+
/** The parameter a stage's secrets are kept in. */
|
|
14
|
+
function secretsParameterName(project, stage) {
|
|
15
|
+
return `/gkm/${project}/${stage}/secrets`;
|
|
16
|
+
}
|
|
17
|
+
var SsmSecretsStore = class {
|
|
18
|
+
options;
|
|
19
|
+
client;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
async ssm() {
|
|
24
|
+
if (this.client) return this.client;
|
|
25
|
+
const config = { region: this.options.region };
|
|
26
|
+
if (this.options.profile) {
|
|
27
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
28
|
+
config.credentials = fromIni({ profile: this.options.profile });
|
|
29
|
+
}
|
|
30
|
+
if (this.options.endpoint) config.endpoint = this.options.endpoint;
|
|
31
|
+
this.client = new _aws_sdk_client_ssm.SSMClient(config);
|
|
32
|
+
return this.client;
|
|
33
|
+
}
|
|
34
|
+
async pull(stage) {
|
|
35
|
+
const ssm = await this.ssm();
|
|
36
|
+
try {
|
|
37
|
+
const { Parameter } = await ssm.send(new _aws_sdk_client_ssm.GetParameterCommand({
|
|
38
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
39
|
+
WithDecryption: true
|
|
40
|
+
}));
|
|
41
|
+
return Parameter?.Value ? JSON.parse(Parameter.Value) : null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof _aws_sdk_client_ssm.ParameterNotFound) return null;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async push(stage, secrets) {
|
|
48
|
+
await (await this.ssm()).send(new _aws_sdk_client_ssm.PutParameterCommand({
|
|
49
|
+
Name: secretsParameterName(this.options.project, stage),
|
|
50
|
+
Value: JSON.stringify(secrets),
|
|
51
|
+
Type: "SecureString",
|
|
52
|
+
Overwrite: true,
|
|
53
|
+
Description: `gkm secrets for ${this.options.project}/${stage}`
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
exports.SsmSecretsStore = SsmSecretsStore;
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=ssm-FxeSAt35.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssm-FxeSAt35.cjs","names":["SSMClient","GetParameterCommand","ParameterNotFound","PutParameterCommand"],"sources":["../src/secrets/ssm.ts"],"sourcesContent":["/**\n * A stage's secrets in SSM Parameter Store, in the stage's own AWS account.\n *\n * One parameter per stage, `/gkm/<project>/<stage>/secrets`, a SecureString —\n * encrypted at rest by KMS, so nothing about it needs a key file anywhere.\n * Which account it is in is whichever credentials are active: the stage's\n * OIDC role in a deploy job, or `--profile` on a developer's machine. With\n * staging and production in different accounts, each stage's secrets sit\n * beside its infrastructure, and a staging role cannot read production's.\n */\n\nimport {\n\tGetParameterCommand,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { SecretsStore } from './store.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface SsmSecretsStoreOptions {\n\t/** The workspace name, which scopes the parameter path. */\n\tproject: string;\n\tregion: string;\n\t/**\n\t * A named AWS profile, resolved with `fromIni` only. The default chain\n\t * reads AWS_* from the environment first, so exported staging keys would\n\t * win over `--profile prod` and put production's secrets in staging.\n\t */\n\tprofile?: string;\n\t/** For tests: an endpoint such as the AWS emulator. */\n\tendpoint?: string;\n}\n\n/** The parameter a stage's secrets are kept in. */\nexport function secretsParameterName(project: string, stage: string): string {\n\treturn `/gkm/${project}/${stage}/secrets`;\n}\n\nexport class SsmSecretsStore implements SecretsStore {\n\tprivate client?: SSMClient;\n\n\tconstructor(private readonly options: SsmSecretsStoreOptions) {}\n\n\tprivate async ssm(): Promise<SSMClient> {\n\t\tif (this.client) return this.client;\n\n\t\tconst config: SSMClientConfig = { region: this.options.region };\n\t\tif (this.options.profile) {\n\t\t\tconst { fromIni } = await import('@aws-sdk/credential-providers');\n\t\t\tconfig.credentials = fromIni({ profile: this.options.profile });\n\t\t}\n\t\tif (this.options.endpoint) {\n\t\t\tconfig.endpoint = this.options.endpoint;\n\t\t}\n\n\t\tthis.client = new SSMClient(config);\n\t\treturn this.client;\n\t}\n\n\tasync pull(stage: string): Promise<StageSecrets | null> {\n\t\tconst ssm = await this.ssm();\n\n\t\ttry {\n\t\t\tconst { Parameter } = await ssm.send(\n\t\t\t\tnew GetParameterCommand({\n\t\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\t\tWithDecryption: true,\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Parameter?.Value\n\t\t\t\t? (JSON.parse(Parameter.Value) as StageSecrets)\n\t\t\t\t: null;\n\t\t} catch (error) {\n\t\t\tif (error instanceof ParameterNotFound) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync push(stage: string, secrets: StageSecrets): Promise<void> {\n\t\tconst ssm = await this.ssm();\n\n\t\tawait ssm.send(\n\t\t\tnew PutParameterCommand({\n\t\t\t\tName: secretsParameterName(this.options.project, stage),\n\t\t\t\tValue: JSON.stringify(secrets),\n\t\t\t\tType: 'SecureString',\n\t\t\t\tOverwrite: true,\n\t\t\t\tDescription: `gkm secrets for ${this.options.project}/${stage}`,\n\t\t\t}),\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;AAoCA,SAAgB,qBAAqB,SAAiB,OAAuB;CAC5E,OAAO,QAAQ,QAAQ,GAAG,MAAM;AACjC;AAEA,IAAa,kBAAb,MAAqD;CAGvB;CAF7B;CAEA,YAAY,SAAkD;EAAjC,KAAA,UAAA;CAAkC;CAE/D,MAAc,MAA0B;EACvC,IAAI,KAAK,QAAQ,OAAO,KAAK;EAE7B,MAAM,SAA0B,EAAE,QAAQ,KAAK,QAAQ,OAAO;EAC9D,IAAI,KAAK,QAAQ,SAAS;GACzB,MAAM,EAAE,YAAY,MAAM,OAAO;GACjC,OAAO,cAAc,QAAQ,EAAE,SAAS,KAAK,QAAQ,QAAQ,CAAC;EAC/D;EACA,IAAI,KAAK,QAAQ,UAChB,OAAO,WAAW,KAAK,QAAQ;EAGhC,KAAK,SAAS,IAAIA,oBAAAA,UAAU,MAAM;EAClC,OAAO,KAAK;CACb;CAEA,MAAM,KAAK,OAA6C;EACvD,MAAM,MAAM,MAAM,KAAK,IAAI;EAE3B,IAAI;GACH,MAAM,EAAE,cAAc,MAAM,IAAI,KAC/B,IAAIC,oBAAAA,oBAAoB;IACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;IACtD,gBAAgB;GACjB,CAAC,CACF;GACA,OAAO,WAAW,QACd,KAAK,MAAM,UAAU,KAAK,IAC3B;EACJ,SAAS,OAAO;GACf,IAAI,iBAAiBC,oBAAAA,mBAAmB,OAAO;GAC/C,MAAM;EACP;CACD;CAEA,MAAM,KAAK,OAAe,SAAsC;EAG/D,OAAM,MAFY,KAAK,IAAI,EAAA,CAEjB,KACT,IAAIC,oBAAAA,oBAAoB;GACvB,MAAM,qBAAqB,KAAK,QAAQ,SAAS,KAAK;GACtD,OAAO,KAAK,UAAU,OAAO;GAC7B,MAAM;GACN,WAAW;GACX,aAAa,mBAAmB,KAAK,QAAQ,QAAQ,GAAG;EACzD,CAAC,CACF;CACD;AACD"}
|
|
@@ -1,9 +1,82 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const require_keystore = require("./keystore-DgGRj2Yy.cjs");
|
|
3
2
|
let node_fs = require("node:fs");
|
|
4
3
|
let node_fs_promises = require("node:fs/promises");
|
|
4
|
+
let node_os = require("node:os");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
|
+
//#region src/secrets/keystore.ts
|
|
8
|
+
/** Key length for AES-256 encryption */
|
|
9
|
+
const KEY_LENGTH = 32;
|
|
10
|
+
/**
|
|
11
|
+
* Get the keystore directory for a project.
|
|
12
|
+
* Keys are stored at ~/.gkm/{project-name}/
|
|
13
|
+
*
|
|
14
|
+
* @param projectName - Name of the project (defaults to current directory name)
|
|
15
|
+
* @returns Path to the keystore directory
|
|
16
|
+
*/
|
|
17
|
+
function getKeystoreDir(projectName) {
|
|
18
|
+
const name = projectName ?? (0, node_path.basename)(process.cwd());
|
|
19
|
+
return (0, node_path.join)((0, node_os.homedir)(), ".gkm", name);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the path to a stage's encryption key.
|
|
23
|
+
*
|
|
24
|
+
* @param stage - Stage name (e.g., 'development', 'production')
|
|
25
|
+
* @param projectName - Name of the project (defaults to current directory name)
|
|
26
|
+
* @returns Path to the key file
|
|
27
|
+
*/
|
|
28
|
+
function getKeyPath(stage, projectName) {
|
|
29
|
+
return (0, node_path.join)(getKeystoreDir(projectName), `${stage}.key`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate a new encryption key for a stage.
|
|
33
|
+
* The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.
|
|
34
|
+
*
|
|
35
|
+
* @param stage - Stage name
|
|
36
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
37
|
+
* @returns The generated key as a hex string
|
|
38
|
+
*/
|
|
39
|
+
async function generateKey(stage, projectName) {
|
|
40
|
+
const keyDir = getKeystoreDir(projectName);
|
|
41
|
+
const keyPath = getKeyPath(stage, projectName);
|
|
42
|
+
await (0, node_fs_promises.mkdir)(keyDir, {
|
|
43
|
+
recursive: true,
|
|
44
|
+
mode: 448
|
|
45
|
+
});
|
|
46
|
+
const key = (0, node_crypto.randomBytes)(KEY_LENGTH).toString("hex");
|
|
47
|
+
await (0, node_fs_promises.writeFile)(keyPath, key, {
|
|
48
|
+
mode: 384,
|
|
49
|
+
encoding: "utf-8"
|
|
50
|
+
});
|
|
51
|
+
await (0, node_fs_promises.chmod)(keyPath, 384);
|
|
52
|
+
return key;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read an encryption key for a stage.
|
|
56
|
+
*
|
|
57
|
+
* @param stage - Stage name
|
|
58
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
59
|
+
* @returns The key as a hex string, or null if not found
|
|
60
|
+
*/
|
|
61
|
+
async function readKey(stage, projectName) {
|
|
62
|
+
const keyPath = getKeyPath(stage, projectName);
|
|
63
|
+
if (!(0, node_fs.existsSync)(keyPath)) return null;
|
|
64
|
+
return (await (0, node_fs_promises.readFile)(keyPath, "utf-8")).trim();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get or create a key for a stage.
|
|
68
|
+
* If the key already exists, it is returned. Otherwise, a new key is generated.
|
|
69
|
+
*
|
|
70
|
+
* @param stage - Stage name
|
|
71
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
72
|
+
* @returns The key as a hex string
|
|
73
|
+
*/
|
|
74
|
+
async function getOrCreateKey(stage, projectName) {
|
|
75
|
+
const existingKey = await readKey(stage, projectName);
|
|
76
|
+
if (existingKey) return existingKey;
|
|
77
|
+
return generateKey(stage, projectName);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
7
80
|
//#region src/secrets/storage.ts
|
|
8
81
|
var storage_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
9
82
|
getSecretsDir: () => getSecretsDir,
|
|
@@ -97,7 +170,7 @@ async function readStageSecrets(stage, cwd = process.cwd()) {
|
|
|
97
170
|
const data = JSON.parse(content);
|
|
98
171
|
if (data.version === 1 && data.encrypted && data.iv) {
|
|
99
172
|
const projectName = (0, node_path.basename)(cwd);
|
|
100
|
-
const key = await
|
|
173
|
+
const key = await readKey(stage, projectName);
|
|
101
174
|
if (!key) throw new Error(`Decryption key not found for stage "${stage}". Expected key at: ~/.gkm/${projectName}/${stage}.key`);
|
|
102
175
|
return decryptSecretsData(data, key);
|
|
103
176
|
}
|
|
@@ -112,7 +185,7 @@ async function writeStageSecrets(secrets, cwd = process.cwd()) {
|
|
|
112
185
|
const path = getSecretsPath(secrets.stage, cwd);
|
|
113
186
|
const projectName = (0, node_path.basename)(cwd);
|
|
114
187
|
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
115
|
-
const encrypted = encryptSecretsData(secrets, await
|
|
188
|
+
const encrypted = encryptSecretsData(secrets, await getOrCreateKey(secrets.stage, projectName));
|
|
116
189
|
await (0, node_fs_promises.writeFile)(path, JSON.stringify(encrypted, null, 2), "utf-8");
|
|
117
190
|
}
|
|
118
191
|
/**
|
|
@@ -226,6 +299,12 @@ function validateEnvironmentVariables(requiredVars, secrets) {
|
|
|
226
299
|
};
|
|
227
300
|
}
|
|
228
301
|
//#endregion
|
|
302
|
+
Object.defineProperty(exports, "getKeyPath", {
|
|
303
|
+
enumerable: true,
|
|
304
|
+
get: function() {
|
|
305
|
+
return getKeyPath;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
229
308
|
Object.defineProperty(exports, "maskPassword", {
|
|
230
309
|
enumerable: true,
|
|
231
310
|
get: function() {
|
|
@@ -269,4 +348,4 @@ Object.defineProperty(exports, "writeStageSecrets", {
|
|
|
269
348
|
}
|
|
270
349
|
});
|
|
271
350
|
|
|
272
|
-
//# sourceMappingURL=storage-
|
|
351
|
+
//# sourceMappingURL=storage-DgypfSD6.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-DgypfSD6.cjs","names":["basename","join","homedir","mkdir","randomBytes","writeFile","chmod","existsSync","readFile","join","existsSync","randomBytes","createCipheriv","createDecipheriv","readFile","basename","mkdir","writeFile"],"sources":["../src/secrets/keystore.ts","../src/secrets/storage.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, join } from 'node:path';\n\n/** Key length for AES-256 encryption */\nconst KEY_LENGTH = 32; // 256 bits\n\n/**\n * Get the keystore directory for a project.\n * Keys are stored at ~/.gkm/{project-name}/\n *\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the keystore directory\n */\nexport function getKeystoreDir(projectName?: string): string {\n\tconst name = projectName ?? basename(process.cwd());\n\treturn join(homedir(), '.gkm', name);\n}\n\n/**\n * Get the path to a stage's encryption key.\n *\n * @param stage - Stage name (e.g., 'development', 'production')\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the key file\n */\nexport function getKeyPath(stage: string, projectName?: string): string {\n\treturn join(getKeystoreDir(projectName), `${stage}.key`);\n}\n\n/**\n * Check if a key exists for a stage.\n */\nexport function keyExists(stage: string, projectName?: string): boolean {\n\treturn existsSync(getKeyPath(stage, projectName));\n}\n\n/**\n * Generate a new encryption key for a stage.\n * The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The generated key as a hex string\n */\nexport async function generateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst keyDir = getKeystoreDir(projectName);\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\t// Ensure keystore directory exists with restricted permissions\n\tawait mkdir(keyDir, { recursive: true, mode: 0o700 });\n\n\t// Generate random key\n\tconst key = randomBytes(KEY_LENGTH).toString('hex');\n\n\t// Write key with restricted permissions (owner read/write only)\n\tawait writeFile(keyPath, key, { mode: 0o600, encoding: 'utf-8' });\n\n\t// Ensure permissions are set correctly (in case file existed)\n\tawait chmod(keyPath, 0o600);\n\n\treturn key;\n}\n\n/**\n * Read an encryption key for a stage.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string, or null if not found\n */\nexport async function readKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string | null> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (!existsSync(keyPath)) {\n\t\treturn null;\n\t}\n\n\tconst key = await readFile(keyPath, 'utf-8');\n\treturn key.trim();\n}\n\n/**\n * Read an encryption key for a stage, throwing if not found.\n */\nexport async function requireKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst key = await readKey(stage, projectName);\n\n\tif (!key) {\n\t\tconst name = projectName ?? basename(process.cwd());\n\t\tthrow new Error(\n\t\t\t`Encryption key not found for stage \"${stage}\" in project \"${name}\". ` +\n\t\t\t\t`Expected key at: ${getKeyPath(stage, projectName)}`,\n\t\t);\n\t}\n\n\treturn key;\n}\n\n/**\n * Delete a key for a stage.\n */\nexport async function deleteKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<void> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (existsSync(keyPath)) {\n\t\tawait rm(keyPath);\n\t}\n}\n\n/**\n * Get or create a key for a stage.\n * If the key already exists, it is returned. Otherwise, a new key is generated.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string\n */\nexport async function getOrCreateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst existingKey = await readKey(stage, projectName);\n\n\tif (existingKey) {\n\t\treturn existingKey;\n\t}\n\n\treturn generateKey(stage, projectName);\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport { getOrCreateKey, readKey } from './keystore';\nimport type { EmbeddableSecrets, StageSecrets } from './types';\n\n/** Default secrets directory relative to project root */\nconst SECRETS_DIR = '.gkm/secrets';\n\n/** AES-256-GCM configuration */\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst AUTH_TAG_LENGTH = 16; // 128 bits\n\n/** Encrypted secrets file structure */\ninterface EncryptedSecretsFile {\n\t/** Version for future format changes */\n\tversion: 1;\n\t/** Base64 encoded encrypted data (ciphertext + auth tag) */\n\tencrypted: string;\n\t/** Hex encoded IV */\n\tiv: string;\n}\n\n/**\n * Get the secrets directory path.\n */\nexport function getSecretsDir(cwd = process.cwd()): string {\n\treturn join(cwd, SECRETS_DIR);\n}\n\n/**\n * Get the secrets file path for a stage.\n */\nexport function getSecretsPath(stage: string, cwd = process.cwd()): string {\n\treturn join(getSecretsDir(cwd), `${stage}.json`);\n}\n\n/**\n * Check if secrets exist for a stage.\n */\nexport function secretsExist(stage: string, cwd = process.cwd()): boolean {\n\treturn existsSync(getSecretsPath(stage, cwd));\n}\n\n/**\n * Initialize an empty StageSecrets object for a stage.\n */\nexport function initStageSecrets(stage: string): StageSecrets {\n\tconst now = new Date().toISOString();\n\treturn {\n\t\tstage,\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t\tservices: {},\n\t\turls: {},\n\t\tcustom: {},\n\t};\n}\n\n/**\n * Encrypt secrets using a key.\n */\nfunction encryptSecretsData(\n\tsecrets: StageSecrets,\n\tkeyHex: string,\n): EncryptedSecretsFile {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst iv = randomBytes(IV_LENGTH);\n\n\t// Serialize secrets to JSON\n\tconst plaintext = JSON.stringify(secrets);\n\n\t// Encrypt\n\tconst cipher = createCipheriv(ALGORITHM, key, iv);\n\tconst ciphertext = Buffer.concat([\n\t\tcipher.update(plaintext, 'utf-8'),\n\t\tcipher.final(),\n\t]);\n\n\t// Get auth tag\n\tconst authTag = cipher.getAuthTag();\n\n\t// Combine ciphertext + auth tag\n\tconst combined = Buffer.concat([ciphertext, authTag]);\n\n\treturn {\n\t\tversion: 1,\n\t\tencrypted: combined.toString('base64'),\n\t\tiv: iv.toString('hex'),\n\t};\n}\n\n/**\n * Decrypt secrets using a key.\n */\nfunction decryptSecretsData(\n\tdata: EncryptedSecretsFile,\n\tkeyHex: string,\n): StageSecrets {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst ivBuffer = Buffer.from(data.iv, 'hex');\n\tconst combined = Buffer.from(data.encrypted, 'base64');\n\n\t// Split ciphertext and auth tag\n\tconst ciphertext = combined.subarray(0, -AUTH_TAG_LENGTH);\n\tconst authTag = combined.subarray(-AUTH_TAG_LENGTH);\n\n\t// Decrypt\n\tconst decipher = createDecipheriv(ALGORITHM, key, ivBuffer);\n\tdecipher.setAuthTag(authTag);\n\n\tconst plaintext = Buffer.concat([\n\t\tdecipher.update(ciphertext),\n\t\tdecipher.final(),\n\t]);\n\n\treturn JSON.parse(plaintext.toString('utf-8')) as StageSecrets;\n}\n\n/**\n * Read secrets for a stage (encrypted).\n * Requires the decryption key to be present at ~/.gkm/{project}/{stage}.key\n *\n * @returns StageSecrets or null if not found\n */\nexport async function readStageSecrets(\n\tstage: string,\n\tcwd = process.cwd(),\n): Promise<StageSecrets | null> {\n\tconst path = getSecretsPath(stage, cwd);\n\n\tif (!existsSync(path)) {\n\t\treturn null;\n\t}\n\n\tconst content = await readFile(path, 'utf-8');\n\tconst data = JSON.parse(content);\n\n\t// Check if this is an encrypted file (has version field)\n\tif (data.version === 1 && data.encrypted && data.iv) {\n\t\tconst projectName = basename(cwd);\n\t\tconst key = await readKey(stage, projectName);\n\n\t\tif (!key) {\n\t\t\tthrow new Error(\n\t\t\t\t`Decryption key not found for stage \"${stage}\". ` +\n\t\t\t\t\t`Expected key at: ~/.gkm/${projectName}/${stage}.key`,\n\t\t\t);\n\t\t}\n\n\t\treturn decryptSecretsData(data as EncryptedSecretsFile, key);\n\t}\n\n\t// Legacy: unencrypted format (for backwards compatibility)\n\treturn data as StageSecrets;\n}\n\n/**\n * Write secrets for a stage (encrypted).\n * Creates or uses existing encryption key at ~/.gkm/{project}/{stage}.key\n */\nexport async function writeStageSecrets(\n\tsecrets: StageSecrets,\n\tcwd = process.cwd(),\n): Promise<void> {\n\tconst dir = getSecretsDir(cwd);\n\tconst path = getSecretsPath(secrets.stage, cwd);\n\tconst projectName = basename(cwd);\n\n\t// Ensure directory exists\n\tawait mkdir(dir, { recursive: true });\n\n\t// Get or create encryption key\n\tconst key = await getOrCreateKey(secrets.stage, projectName);\n\n\t// Encrypt and write\n\tconst encrypted = encryptSecretsData(secrets, key);\n\tawait writeFile(path, JSON.stringify(encrypted, null, 2), 'utf-8');\n}\n\n/**\n * Convert StageSecrets to embeddable format (flat key-value pairs).\n * This is what gets encrypted and embedded in the bundle.\n */\nexport function toEmbeddableSecrets(secrets: StageSecrets): EmbeddableSecrets {\n\treturn {\n\t\t...secrets.urls,\n\t\t...secrets.custom,\n\t\t// Also include individual service credentials if needed\n\t\t...(secrets.services.postgres && {\n\t\t\tPOSTGRES_USER: secrets.services.postgres.username,\n\t\t\tPOSTGRES_PASSWORD: secrets.services.postgres.password,\n\t\t\tPOSTGRES_DB: secrets.services.postgres.database ?? 'app',\n\t\t\tPOSTGRES_HOST: secrets.services.postgres.host,\n\t\t\tPOSTGRES_PORT: String(secrets.services.postgres.port),\n\t\t}),\n\t\t...(secrets.services.redis && {\n\t\t\tREDIS_PASSWORD: secrets.services.redis.password,\n\t\t\tREDIS_HOST: secrets.services.redis.host,\n\t\t\tREDIS_PORT: String(secrets.services.redis.port),\n\t\t}),\n\t\t...(secrets.services.rabbitmq && {\n\t\t\tRABBITMQ_USER: secrets.services.rabbitmq.username,\n\t\t\tRABBITMQ_PASSWORD: secrets.services.rabbitmq.password,\n\t\t\tRABBITMQ_HOST: secrets.services.rabbitmq.host,\n\t\t\tRABBITMQ_PORT: String(secrets.services.rabbitmq.port),\n\t\t\tRABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? '/',\n\t\t}),\n\t\t...(secrets.services.minio && {\n\t\t\tSTORAGE_ACCESS_KEY_ID: secrets.services.minio.username,\n\t\t\tSTORAGE_SECRET_ACCESS_KEY: secrets.services.minio.password,\n\t\t\tSTORAGE_BUCKET: secrets.services.minio.bucket ?? 'app',\n\t\t\tSTORAGE_REGION: 'eu-west-1',\n\t\t\tSTORAGE_FORCE_PATH_STYLE: 'true',\n\t\t}),\n\t\t...(secrets.services.mailpit && {\n\t\t\tSMTP_HOST: secrets.services.mailpit.host,\n\t\t\tSMTP_PORT: String(secrets.services.mailpit.port),\n\t\t\tSMTP_USER: secrets.services.mailpit.username,\n\t\t\tSMTP_PASS: secrets.services.mailpit.password,\n\t\t\tSMTP_SECURE: 'false',\n\t\t\tMAIL_FROM: 'noreply@localhost',\n\t\t}),\n\t\t...(secrets.services.localstack && {\n\t\t\tAWS_ACCESS_KEY_ID:\n\t\t\t\tsecrets.services.localstack.accessKeyId ??\n\t\t\t\tsecrets.services.localstack.username,\n\t\t\tAWS_SECRET_ACCESS_KEY: secrets.services.localstack.password,\n\t\t\tAWS_REGION: secrets.services.localstack.region ?? 'us-east-1',\n\t\t\tAWS_ENDPOINT_URL: `http://${secrets.services.localstack.host}:${secrets.services.localstack.port}`,\n\t\t}),\n\t\t...(secrets.services.pgboss && {\n\t\t\tPGBOSS_DB_USER: secrets.services.pgboss.username,\n\t\t\tPGBOSS_DB_PASSWORD: secrets.services.pgboss.password,\n\t\t}),\n\t};\n}\n\n/**\n * Update a custom secret in the secrets file.\n */\nexport async function setCustomSecret(\n\tstage: string,\n\tkey: string,\n\tvalue: string,\n\tcwd = process.cwd(),\n): Promise<StageSecrets> {\n\tconst secrets = await readStageSecrets(stage, cwd);\n\n\tif (!secrets) {\n\t\tthrow new Error(\n\t\t\t`Secrets not found for stage \"${stage}\". Run \"gkm secrets:init --stage ${stage}\" first.`,\n\t\t);\n\t}\n\n\tconst updated: StageSecrets = {\n\t\t...secrets,\n\t\tupdatedAt: new Date().toISOString(),\n\t\tcustom: {\n\t\t\t...secrets.custom,\n\t\t\t[key]: value,\n\t\t},\n\t};\n\n\tawait writeStageSecrets(updated, cwd);\n\treturn updated;\n}\n\n/**\n * Mask a password for display (show first 4 and last 2 chars).\n */\nexport function maskPassword(password: string): string {\n\tif (password.length <= 8) {\n\t\treturn '********';\n\t}\n\treturn `${password.slice(0, 4)}${'*'.repeat(password.length - 6)}${password.slice(-2)}`;\n}\n\n/**\n * Result of environment variable validation.\n */\nexport interface EnvValidationResult {\n\t/** Whether all required environment variables are present */\n\tvalid: boolean;\n\t/** List of missing environment variable names */\n\tmissing: string[];\n\t/** List of environment variables that are provided */\n\tprovided: string[];\n\t/** List of environment variables that were required */\n\trequired: string[];\n}\n\n/**\n * Validate that all required environment variables are present in secrets.\n *\n * @param requiredVars - Array of environment variable names required by the application\n * @param secrets - Stage secrets to validate against\n * @returns Validation result with missing and provided variables\n *\n * @example\n * ```typescript\n * const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];\n * const secrets = await readStageSecrets('production');\n * const result = validateEnvironmentVariables(required, secrets);\n *\n * if (!result.valid) {\n * console.error(`Missing environment variables: ${result.missing.join(', ')}`);\n * }\n * ```\n */\nexport function validateEnvironmentVariables(\n\trequiredVars: string[],\n\tsecrets: StageSecrets,\n): EnvValidationResult {\n\tconst embeddable = toEmbeddableSecrets(secrets);\n\tconst availableVars = new Set(Object.keys(embeddable));\n\n\tconst missing: string[] = [];\n\tconst provided: string[] = [];\n\n\tfor (const varName of requiredVars) {\n\t\tif (availableVars.has(varName)) {\n\t\t\tprovided.push(varName);\n\t\t} else {\n\t\t\tmissing.push(varName);\n\t\t}\n\t}\n\n\treturn {\n\t\tvalid: missing.length === 0,\n\t\tmissing: missing.sort(),\n\t\tprovided: provided.sort(),\n\t\trequired: [...requiredVars].sort(),\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,82 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
2
|
-
import { n as getOrCreateKey, r as readKey } from "./keystore-B4PRlbgE.mjs";
|
|
3
2
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
7
|
+
//#region src/secrets/keystore.ts
|
|
8
|
+
/** Key length for AES-256 encryption */
|
|
9
|
+
const KEY_LENGTH = 32;
|
|
10
|
+
/**
|
|
11
|
+
* Get the keystore directory for a project.
|
|
12
|
+
* Keys are stored at ~/.gkm/{project-name}/
|
|
13
|
+
*
|
|
14
|
+
* @param projectName - Name of the project (defaults to current directory name)
|
|
15
|
+
* @returns Path to the keystore directory
|
|
16
|
+
*/
|
|
17
|
+
function getKeystoreDir(projectName) {
|
|
18
|
+
const name = projectName ?? basename(process.cwd());
|
|
19
|
+
return join(homedir(), ".gkm", name);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the path to a stage's encryption key.
|
|
23
|
+
*
|
|
24
|
+
* @param stage - Stage name (e.g., 'development', 'production')
|
|
25
|
+
* @param projectName - Name of the project (defaults to current directory name)
|
|
26
|
+
* @returns Path to the key file
|
|
27
|
+
*/
|
|
28
|
+
function getKeyPath(stage, projectName) {
|
|
29
|
+
return join(getKeystoreDir(projectName), `${stage}.key`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate a new encryption key for a stage.
|
|
33
|
+
* The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.
|
|
34
|
+
*
|
|
35
|
+
* @param stage - Stage name
|
|
36
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
37
|
+
* @returns The generated key as a hex string
|
|
38
|
+
*/
|
|
39
|
+
async function generateKey(stage, projectName) {
|
|
40
|
+
const keyDir = getKeystoreDir(projectName);
|
|
41
|
+
const keyPath = getKeyPath(stage, projectName);
|
|
42
|
+
await mkdir(keyDir, {
|
|
43
|
+
recursive: true,
|
|
44
|
+
mode: 448
|
|
45
|
+
});
|
|
46
|
+
const key = randomBytes(KEY_LENGTH).toString("hex");
|
|
47
|
+
await writeFile(keyPath, key, {
|
|
48
|
+
mode: 384,
|
|
49
|
+
encoding: "utf-8"
|
|
50
|
+
});
|
|
51
|
+
await chmod(keyPath, 384);
|
|
52
|
+
return key;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read an encryption key for a stage.
|
|
56
|
+
*
|
|
57
|
+
* @param stage - Stage name
|
|
58
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
59
|
+
* @returns The key as a hex string, or null if not found
|
|
60
|
+
*/
|
|
61
|
+
async function readKey(stage, projectName) {
|
|
62
|
+
const keyPath = getKeyPath(stage, projectName);
|
|
63
|
+
if (!existsSync(keyPath)) return null;
|
|
64
|
+
return (await readFile(keyPath, "utf-8")).trim();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get or create a key for a stage.
|
|
68
|
+
* If the key already exists, it is returned. Otherwise, a new key is generated.
|
|
69
|
+
*
|
|
70
|
+
* @param stage - Stage name
|
|
71
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
72
|
+
* @returns The key as a hex string
|
|
73
|
+
*/
|
|
74
|
+
async function getOrCreateKey(stage, projectName) {
|
|
75
|
+
const existingKey = await readKey(stage, projectName);
|
|
76
|
+
if (existingKey) return existingKey;
|
|
77
|
+
return generateKey(stage, projectName);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
7
80
|
//#region src/secrets/storage.ts
|
|
8
81
|
var storage_exports = /* @__PURE__ */ __exportAll({
|
|
9
82
|
getSecretsDir: () => getSecretsDir,
|
|
@@ -226,6 +299,6 @@ function validateEnvironmentVariables(requiredVars, secrets) {
|
|
|
226
299
|
};
|
|
227
300
|
}
|
|
228
301
|
//#endregion
|
|
229
|
-
export { storage_exports as a, setCustomSecret as i, readStageSecrets as n, toEmbeddableSecrets as o, secretsExist as r, writeStageSecrets as s, maskPassword as t };
|
|
302
|
+
export { storage_exports as a, getKeyPath as c, setCustomSecret as i, readStageSecrets as n, toEmbeddableSecrets as o, secretsExist as r, writeStageSecrets as s, maskPassword as t };
|
|
230
303
|
|
|
231
|
-
//# sourceMappingURL=storage-
|
|
304
|
+
//# sourceMappingURL=storage-TP0x118M.mjs.map
|