@caelo-cms/provisioning 0.10.50 → 0.10.51

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 (74) hide show
  1. package/dist/adapter.d.ts +4 -6
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/admin-internal.d.ts +41 -0
  4. package/dist/admin-internal.d.ts.map +1 -0
  5. package/dist/admin-internal.js +169 -0
  6. package/dist/admin-internal.js.map +1 -0
  7. package/dist/agent-db.js +89 -20
  8. package/dist/aws-task-roles.d.ts +27 -0
  9. package/dist/aws-task-roles.d.ts.map +1 -0
  10. package/dist/aws-task-roles.js +35 -0
  11. package/dist/aws-task-roles.js.map +1 -0
  12. package/dist/cli.js +34 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/compose.js +2 -2
  15. package/dist/compose.js.map +1 -1
  16. package/dist/deployed-release.d.ts +3 -2
  17. package/dist/deployed-release.d.ts.map +1 -1
  18. package/dist/deployed-release.js +12 -5
  19. package/dist/deployed-release.js.map +1 -1
  20. package/dist/gateway-credentials.d.ts.map +1 -1
  21. package/dist/gateway-credentials.js +6 -3
  22. package/dist/gateway-credentials.js.map +1 -1
  23. package/dist/gcloud.d.ts +2 -2
  24. package/dist/gcloud.d.ts.map +1 -1
  25. package/dist/gcloud.js +3 -3
  26. package/dist/gcloud.js.map +1 -1
  27. package/dist/infra-contract.d.ts +1 -1
  28. package/dist/infra-contract.d.ts.map +1 -1
  29. package/dist/infra-contract.js +6 -1
  30. package/dist/infra-contract.js.map +1 -1
  31. package/dist/owner-reset.d.ts +37 -0
  32. package/dist/owner-reset.d.ts.map +1 -0
  33. package/dist/owner-reset.js +80 -0
  34. package/dist/owner-reset.js.map +1 -0
  35. package/dist/stack-contract.d.ts +9 -10
  36. package/dist/stack-contract.d.ts.map +1 -1
  37. package/dist/stack-contract.js +21 -10
  38. package/dist/stack-contract.js.map +1 -1
  39. package/dist/staging-admin.d.ts +12 -20
  40. package/dist/staging-admin.d.ts.map +1 -1
  41. package/dist/staging-admin.js +7 -159
  42. package/dist/staging-admin.js.map +1 -1
  43. package/dist/wizards/gcp-cost.js +1 -1
  44. package/package.json +2 -2
  45. package/src/adapter.ts +4 -6
  46. package/src/admin-internal.ts +212 -0
  47. package/src/aws-task-roles.ts +58 -0
  48. package/src/checkpoint-secrets.test.ts +0 -1
  49. package/src/cli.ts +41 -1
  50. package/src/compose.ts +2 -2
  51. package/src/deployed-release.test.ts +22 -4
  52. package/src/deployed-release.ts +14 -7
  53. package/src/gateway-credentials.ts +6 -3
  54. package/src/gateway-env-isolation.test.ts +212 -0
  55. package/src/gcloud.ts +5 -3
  56. package/src/index.test.ts +6 -6
  57. package/src/infra-contract.test.ts +4 -3
  58. package/src/infra-contract.ts +6 -1
  59. package/src/owner-reset.test.ts +165 -0
  60. package/src/owner-reset.ts +106 -0
  61. package/src/runtime-secrets.test.ts +25 -8
  62. package/src/stack-contract.test.ts +9 -18
  63. package/src/stack-contract.ts +25 -16
  64. package/src/stack-converge-secrets.test.ts +45 -10
  65. package/src/stack-converge.test.ts +19 -10
  66. package/src/staging-admin.ts +18 -181
  67. package/src/wizards/gcp-cost.ts +1 -1
  68. package/stacks/aws/README.md +1 -1
  69. package/stacks/aws/index.ts +55 -43
  70. package/stacks/azure/README.md +1 -1
  71. package/stacks/azure/index.ts +8 -14
  72. package/stacks/gcp/README.md +1 -1
  73. package/stacks/gcp/index.ts +2 -5
  74. package/stacks/gcp-firebase/index.ts +0 -3
package/dist/adapter.d.ts CHANGED
@@ -49,14 +49,12 @@ export interface DnsRecord {
49
49
  export interface CloudAdapterOutputs {
50
50
  /** DSN for cms_admin role (encrypted in Pulumi state). */
51
51
  readonly adminDatabaseUrl: string;
52
- /** DSN for public_role on cms_public (encrypted in Pulumi state). */
53
- readonly publicDatabaseUrl: string;
54
52
  /**
55
- * DSN for gateway_role on cms_admin — the API gateway's own narrow login
56
- * (#613). The gateway gets this and `publicDatabaseUrl`, never
57
- * `adminDatabaseUrl` (CLAUDE.md §2).
53
+ * DSN for public_role on cms_public (encrypted in Pulumi state) — the API
54
+ * gateway's only database login. It never gets a cms_admin DSN of any
55
+ * role (CLAUDE.md §2, #635).
58
56
  */
59
- readonly gatewayDatabaseUrl: string;
57
+ readonly publicDatabaseUrl: string;
60
58
  /** Provider-native blob URL (s3://, gs://, https://<account>.blob.core.windows.net/<container>). */
61
59
  readonly mediaStorageUrl: string;
62
60
  /** Public-facing URL the admin reaches for media reads. */
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,SAAS,GAAG,YAAY,CAAC;AAE3D,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAClD,gFAAgF;IAChF,QAAQ,CAAC,qBAAqB,CAAC,EAAE;QAC/B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,0DAA0D;IAC1D,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,qEAAqE;IACrE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,oGAAoG;IACpG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,2DAA2D;IAC3D,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACtD;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;CACnC;AAED,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,KAAK,GAAG,cAAc,GAAG,KAAK,GAAG,OAAO,CAAC;AAEzF;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,SAAS,GAAG,YAAY,CAAC;AAE3D,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAClD,gFAAgF;IAChF,QAAQ,CAAC,qBAAqB,CAAC,EAAE;QAC/B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,0DAA0D;IAC1D,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,oGAAoG;IACpG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,2DAA2D;IAC3D,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACtD;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;CACnC;AAED,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,KAAK,GAAG,cAAc,GAAG,KAAK,GAAG,OAAO,CAAC;AAEzF;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,41 @@
1
+ import { type GcloudResult } from "./gcloud.js";
2
+ type Run = (args: string[]) => Promise<GcloudResult>;
3
+ export interface AdminInternalTarget {
4
+ readonly projectId: string;
5
+ readonly region: string;
6
+ }
7
+ export interface AdminInternalDeps {
8
+ readonly run?: Run;
9
+ readonly fetch?: typeof fetch;
10
+ /** Seconds since epoch (IAP JWT); tests pin it. */
11
+ readonly now?: () => number;
12
+ }
13
+ /** One endpoint call: where, under which token scope, with what body. */
14
+ export interface AdminInternalCall {
15
+ /** Path under the admin origin, e.g. `/api/internal/owner-reset`. */
16
+ readonly endpoint: string;
17
+ /** Scope the endpoint's `requireInternalAuth` checks. */
18
+ readonly scope: string;
19
+ readonly body: Record<string, unknown>;
20
+ /** The issue that added the endpoint, named when an older admin answers 404. */
21
+ readonly introducedBy: string;
22
+ }
23
+ export type AdminInternalResult<T> = {
24
+ ok: true;
25
+ value: T;
26
+ } | {
27
+ ok: false;
28
+ error: string;
29
+ };
30
+ /**
31
+ * POST `call.body` to the endpoint and return the admin's JSON answer and
32
+ * the admin URL it was reached at (to build links the operator opens).
33
+ * Every failure names the fix: an IAP refusal or a pre-endpoint admin points
34
+ * at `cms-provision upgrade`, a refusal carries the admin's own message.
35
+ */
36
+ export declare function callAdminInternal<T>(target: AdminInternalTarget, call: AdminInternalCall, deps?: AdminInternalDeps): Promise<AdminInternalResult<{
37
+ answer: T;
38
+ adminUrl: string;
39
+ }>>;
40
+ export {};
41
+ //# sourceMappingURL=admin-internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-internal.d.ts","sourceRoot":"","sources":["../src/admin-internal.ts"],"names":[],"mappings":"AAuBA,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAIzE,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAKrD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,iBAAiB,EACvB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,mBAAmB,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAkG/D"}
@@ -0,0 +1,169 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ /**
3
+ * The CLI's channel to the admin's `/api/internal/*` endpoints on a
4
+ * gcp-firebase install, for commands that need an answer BACK from the admin
5
+ * (a generated password, a reset link). A one-shot Cloud Run job could only
6
+ * report through Cloud Logging, which would put that secret into the
7
+ * project's logs; this call answers over TLS straight to the CLI process, and
8
+ * nothing is written anywhere (CLAUDE.md §7, #659).
9
+ *
10
+ * Getting there, as the operator's own gcloud identity — the same path the
11
+ * Power-MCP shim takes (packages/mcp-server/src/ingress-auth.ts):
12
+ *
13
+ * 1. the admin Cloud Run service and its URL;
14
+ * 2. the install's internal secret from Secret Manager (in memory only),
15
+ * which signs a 5-minute token for the endpoint's scope;
16
+ * 3. a JWT signed as `caelo-mcp@<project>` through the IAM Credentials
17
+ * API — the admin's IAP lets that service account in, and every
18
+ * IAP-allowlisted operator may sign as it (`cms-provision upgrade`
19
+ * grants it) — so the request passes IAP first, then Caelo's token.
20
+ */
21
+ import { findService } from "./admin-domain.js";
22
+ import { gcloud as defaultGcloud } from "./gcloud.js";
23
+ import { GCP_STACK_ENV, gcpSecretId, mcpIapServiceAccountEmail } from "./gcp-names.js";
24
+ import { mintInternalToken } from "./internal-token.js";
25
+ /** Signed JWTs live at most 1 hour; one call needs a few seconds. */
26
+ const IAP_JWT_LIFETIME_S = 600;
27
+ /**
28
+ * POST `call.body` to the endpoint and return the admin's JSON answer and
29
+ * the admin URL it was reached at (to build links the operator opens).
30
+ * Every failure names the fix: an IAP refusal or a pre-endpoint admin points
31
+ * at `cms-provision upgrade`, a refusal carries the admin's own message.
32
+ */
33
+ export async function callAdminInternal(target, call, deps = {}) {
34
+ const run = deps.run ?? defaultGcloud;
35
+ const doFetch = deps.fetch ?? fetch;
36
+ const scope = [`--region=${target.region}`, `--project=${target.projectId}`];
37
+ const found = await findService(run, scope, "admin");
38
+ if (!found.ok)
39
+ return { ok: false, error: found.error };
40
+ const url = await run([
41
+ "run",
42
+ "services",
43
+ "describe",
44
+ found.service,
45
+ ...scope,
46
+ "--format=value(status.url)",
47
+ ]);
48
+ const adminUrl = url.stdout.trim();
49
+ if (!url.ok || !adminUrl) {
50
+ return { ok: false, error: `read the admin's URL: ${url.stderr.trim() || "empty"}` };
51
+ }
52
+ const secretId = gcpSecretId(GCP_STACK_ENV, "internal-secret");
53
+ const secret = await run([
54
+ "secrets",
55
+ "versions",
56
+ "access",
57
+ "latest",
58
+ `--secret=${secretId}`,
59
+ `--project=${target.projectId}`,
60
+ ]);
61
+ const internalSecret = secret.stdout.trim();
62
+ if (!secret.ok || internalSecret.length < 32) {
63
+ return {
64
+ ok: false,
65
+ error: `read the install's internal secret (${secretId}) from Secret Manager: ${secret.stderr.trim() || "missing or too short"}. Your gcloud account needs secretmanager.versions.access on the project.`,
66
+ };
67
+ }
68
+ // Installs from before #678 may not have the IAM Credentials API on;
69
+ // enabling an enabled API is a no-op (codex review on #681).
70
+ const api = await run([
71
+ "services",
72
+ "enable",
73
+ "iamcredentials.googleapis.com",
74
+ `--project=${target.projectId}`,
75
+ ]);
76
+ if (!api.ok) {
77
+ return {
78
+ ok: false,
79
+ error: `enable the IAM Credentials API (iamcredentials.googleapis.com), needed to pass the admin's IAP: ${api.stderr.trim()}`,
80
+ };
81
+ }
82
+ const iap = await signIapJwt(run, doFetch, target.projectId, adminUrl, deps.now);
83
+ if (!iap.ok)
84
+ return iap;
85
+ const token = await mintInternalToken(internalSecret, call.scope);
86
+ let res;
87
+ try {
88
+ res = await doFetch(`${adminUrl}${call.endpoint}`, {
89
+ method: "POST",
90
+ headers: {
91
+ authorization: `Bearer ${iap.value}`,
92
+ "x-caelo-internal-token": token,
93
+ "content-type": "application/json",
94
+ },
95
+ body: JSON.stringify(call.body),
96
+ });
97
+ }
98
+ catch (e) {
99
+ return { ok: false, error: `reach the admin at ${adminUrl}: ${String(e)}` };
100
+ }
101
+ const text = await res.text();
102
+ if (res.headers.get("x-goog-iap-generated-response") === "true") {
103
+ return {
104
+ ok: false,
105
+ error: `Google IAP refused the request (HTTP ${res.status}: ${text.trim().slice(0, 200)}) — ${mcpIapServiceAccountEmail(target.projectId)} is not allowed on the admin's IAP yet. Run \`cms-provision upgrade\` (it allowlists that service account), then run this command again.`,
106
+ };
107
+ }
108
+ if (res.status === 404) {
109
+ return {
110
+ ok: false,
111
+ error: `the admin has no ${call.endpoint} yet (it runs a release from before ${call.introducedBy}). Run \`cms-provision upgrade\`, then run this command again.`,
112
+ };
113
+ }
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(text);
117
+ }
118
+ catch {
119
+ return {
120
+ ok: false,
121
+ error: `the admin answered HTTP ${res.status} with a non-JSON body: ${text.slice(0, 200)}`,
122
+ };
123
+ }
124
+ if (!res.ok || parsed.ok !== true) {
125
+ return {
126
+ ok: false,
127
+ error: `the admin refused (HTTP ${res.status}): ${parsed.error ?? text.slice(0, 300)}`,
128
+ };
129
+ }
130
+ return { ok: true, value: { answer: parsed, adminUrl } };
131
+ }
132
+ /** A JWT signed as the install's MCP service account, for the admin's IAP. */
133
+ async function signIapJwt(run, doFetch, projectId, adminUrl, now) {
134
+ const access = await run(["auth", "print-access-token"]);
135
+ const accessToken = access.stdout.trim();
136
+ if (!access.ok || !accessToken) {
137
+ return {
138
+ ok: false,
139
+ error: `get an access token from gcloud: ${access.stderr.trim() || "empty"}. Run \`gcloud auth login\` with the account you provisioned with.`,
140
+ };
141
+ }
142
+ const serviceAccount = mcpIapServiceAccountEmail(projectId);
143
+ const iat = now ? now() : Math.floor(Date.now() / 1000);
144
+ const res = await doFetch(`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(serviceAccount)}:signJwt`, {
145
+ method: "POST",
146
+ headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
147
+ body: JSON.stringify({
148
+ payload: JSON.stringify({
149
+ iss: serviceAccount,
150
+ sub: serviceAccount,
151
+ aud: `${new URL(adminUrl).origin}/*`,
152
+ iat,
153
+ exp: iat + IAP_JWT_LIFETIME_S,
154
+ }),
155
+ }),
156
+ });
157
+ if (!res.ok) {
158
+ const detail = (await res.text().catch(() => "")).slice(0, 300);
159
+ return {
160
+ ok: false,
161
+ error: `sign an IAP token as ${serviceAccount} (HTTP ${res.status}): ${detail} — your account needs roles/iam.serviceAccountTokenCreator on it; \`cms-provision upgrade\` grants it to everyone the admin's IAP lets in.`,
162
+ };
163
+ }
164
+ const { signedJwt } = (await res.json());
165
+ if (!signedJwt)
166
+ return { ok: false, error: "IAM signJwt answered without a signedJwt" };
167
+ return { ok: true, value: signedJwt };
168
+ }
169
+ //# sourceMappingURL=admin-internal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-internal.js","sourceRoot":"","sources":["../src/admin-internal.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,IAAI,aAAa,EAAqB,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACvF,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAIxD,qEAAqE;AACrE,MAAM,kBAAkB,GAAG,GAAG,CAAC;AA2B/B;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAA2B,EAC3B,IAAuB,EACvB,OAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,aAAa,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC;IACpC,MAAM,KAAK,GAAG,CAAC,YAAY,MAAM,CAAC,MAAM,EAAE,EAAE,aAAa,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;IAE7E,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACrD,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;IACxD,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC;QACpB,KAAK;QACL,UAAU;QACV,UAAU;QACV,KAAK,CAAC,OAAO;QACb,GAAG,KAAK;QACR,4BAA4B;KAC7B,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,OAAO,EAAE,EAAE,CAAC;IACvF,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC;QACvB,SAAS;QACT,UAAU;QACV,QAAQ;QACR,QAAQ;QACR,YAAY,QAAQ,EAAE;QACtB,aAAa,MAAM,CAAC,SAAS,EAAE;KAChC,CAAC,CAAC;IACH,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,cAAc,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QAC7C,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,uCAAuC,QAAQ,0BAA0B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,sBAAsB,2EAA2E;SAC1M,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,6DAA6D;IAC7D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC;QACpB,UAAU;QACV,QAAQ;QACR,+BAA+B;QAC/B,aAAa,MAAM,CAAC,SAAS,EAAE;KAChC,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,mGAAmG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE;SAC9H,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACjF,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC;IAExB,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAClE,IAAI,GAAa,CAAC;IAClB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE;YACjD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,GAAG,CAAC,KAAK,EAAE;gBACpC,wBAAwB,EAAE,KAAK;gBAC/B,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;SAChC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAC9E,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,KAAK,MAAM,EAAE,CAAC;QAChE,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,wCAAwC,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,yBAAyB,CAAC,MAAM,CAAC,SAAS,CAAC,0IAA0I;SACpR,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,oBAAoB,IAAI,CAAC,QAAQ,uCAAuC,IAAI,CAAC,YAAY,gEAAgE;SACjK,CAAC;IACJ,CAAC;IACD,IAAI,MAAkE,CAAC;IACvE,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,2BAA2B,GAAG,CAAC,MAAM,0BAA0B,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;SAC3F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;QAClC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,2BAA2B,GAAG,CAAC,MAAM,MAAM,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;SACvF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAsB,EAAE,QAAQ,EAAE,EAAE,CAAC;AAC3E,CAAC;AAED,8EAA8E;AAC9E,KAAK,UAAU,UAAU,CACvB,GAAQ,EACR,OAAqB,EACrB,SAAiB,EACjB,QAAgB,EAChB,GAA+B;IAE/B,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,oCAAoC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,OAAO,oEAAoE;SAC/I,CAAC;IACJ,CAAC;IACD,MAAM,cAAc,GAAG,yBAAyB,CAAC,SAAS,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,MAAM,OAAO,CACvB,uEAAuE,kBAAkB,CAAC,cAAc,CAAC,UAAU,EACnH;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QACvF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;gBACtB,GAAG,EAAE,cAAc;gBACnB,GAAG,EAAE,cAAc;gBACnB,GAAG,EAAE,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,IAAI;gBACpC,GAAG;gBACH,GAAG,EAAE,GAAG,GAAG,kBAAkB;aAC9B,CAAC;SACH,CAAC;KACH,CACF,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAChE,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,wBAAwB,cAAc,UAAU,GAAG,CAAC,MAAM,MAAM,MAAM,4IAA4I;SAC1N,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA2B,CAAC;IACnE,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,0CAA0C,EAAE,CAAC;IACxF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AACxC,CAAC"}
package/dist/agent-db.js CHANGED
@@ -17777,6 +17777,13 @@ function isRlsDenial(error) {
17777
17777
  // ../query-api/src/adapter.ts
17778
17778
  var SQL3 = globalThis.Bun?.SQL;
17779
17779
  var DEFAULT_ROLES = { admin: "admin_role", public: ["admin_role", "public_role"] };
17780
+
17781
+ class AdminDatabaseUnavailableError extends Error {
17782
+ name = "AdminDatabaseUnavailableError";
17783
+ constructor(what) {
17784
+ super(`${what}: this process has no cms_admin connection by design (public side, CLAUDE.md \xA72). Read what the admin published into cms_public instead.`);
17785
+ }
17786
+ }
17780
17787
  function resolvePoolMax(explicit) {
17781
17788
  if (explicit !== undefined)
17782
17789
  return validatePoolMax(explicit, "poolMax option");
@@ -17800,19 +17807,41 @@ class DatabaseAdapter {
17800
17807
  #skipVerify;
17801
17808
  #roles;
17802
17809
  #verifyPromise = null;
17810
+ #projectionPublisher = null;
17803
17811
  constructor(config) {
17804
- const poolMax = resolvePoolMax(config.poolMax);
17805
- if (poolMax === undefined) {
17806
- this.#adminRaw = new SQL3(config.adminDatabaseUrl);
17807
- this.#publicRaw = new SQL3(config.publicDatabaseUrl);
17808
- } else {
17809
- this.#adminRaw = new SQL3(config.adminDatabaseUrl, { max: poolMax });
17810
- this.#publicRaw = new SQL3(config.publicDatabaseUrl, { max: poolMax });
17812
+ this.#roles = config.expectedRoles ?? DEFAULT_ROLES;
17813
+ if (config.adminDatabaseUrl === null !== (this.#roles.admin === null)) {
17814
+ throw new Error("DatabaseAdapter: a public-only adapter (adminDatabaseUrl: null) must expect no admin role, and an adapter with an admin URL must expect one");
17811
17815
  }
17812
- this.#admin = drizzle(this.#adminRaw);
17816
+ const poolMax = resolvePoolMax(config.poolMax);
17817
+ const open = (url) => poolMax === undefined ? new SQL3(url) : new SQL3(url, { max: poolMax });
17818
+ this.#adminRaw = config.adminDatabaseUrl === null ? null : open(config.adminDatabaseUrl);
17819
+ this.#publicRaw = open(config.publicDatabaseUrl);
17820
+ this.#admin = this.#adminRaw === null ? null : drizzle(this.#adminRaw);
17813
17821
  this.#public = drizzle(this.#publicRaw);
17814
17822
  this.#skipVerify = config.skipRoleVerification === true;
17815
- this.#roles = config.expectedRoles ?? DEFAULT_ROLES;
17823
+ }
17824
+ get hasAdminDatabase() {
17825
+ return this.#admin !== null;
17826
+ }
17827
+ setProjectionPublisher(publish) {
17828
+ this.#projectionPublisher = publish;
17829
+ }
17830
+ async#projectionDirty(tx) {
17831
+ if (this.#projectionPublisher === null)
17832
+ return false;
17833
+ const found = await tx.execute(sql`SELECT current_setting('caelo.projection_dirty', true) AS v`);
17834
+ return found[0]?.v === "on";
17835
+ }
17836
+ async#publishAfterCommit() {
17837
+ const publish = this.#projectionPublisher;
17838
+ if (publish !== null)
17839
+ await publish();
17840
+ }
17841
+ #adminDb(what) {
17842
+ if (this.#admin === null)
17843
+ throw new AdminDatabaseUnavailableError(what);
17844
+ return this.#admin;
17816
17845
  }
17817
17846
  verifyRoles() {
17818
17847
  if (this.#verifyPromise !== null)
@@ -17824,9 +17853,11 @@ class DatabaseAdapter {
17824
17853
  return this.#verifyPromise;
17825
17854
  }
17826
17855
  async#verifyRolesOnce() {
17827
- const admin = await this.#identity(this.#adminRaw);
17828
- if (admin.user !== this.#roles.admin || admin.database !== "cms_admin") {
17829
- throw new Error(`DatabaseAdapter admin pool expected (${this.#roles.admin}, cms_admin) but connected as (${admin.user}, ${admin.database}). Check ADMIN_DATABASE_URL.`);
17856
+ if (this.#adminRaw !== null) {
17857
+ const admin = await this.#identity(this.#adminRaw);
17858
+ if (admin.user !== this.#roles.admin || admin.database !== "cms_admin") {
17859
+ throw new Error(`DatabaseAdapter admin pool expected (${this.#roles.admin}, cms_admin) but connected as (${admin.user}, ${admin.database}). Check ADMIN_DATABASE_URL.`);
17860
+ }
17830
17861
  }
17831
17862
  const pub = await this.#identity(this.#publicRaw);
17832
17863
  if (pub.database !== "cms_public") {
@@ -17846,9 +17877,11 @@ class DatabaseAdapter {
17846
17877
  async runOperation(op, ctx, validatedInput) {
17847
17878
  if (!this.#skipVerify)
17848
17879
  await this.verifyRoles();
17849
- const db = op.database === "cms_admin" ? this.#admin : this.#public;
17880
+ const db = op.database === "cms_admin" ? this.#adminDb(`operation ${op.name}`) : this.#public;
17881
+ let publishAfterCommit = false;
17882
+ let result;
17850
17883
  try {
17851
- return await db.transaction(async (tx) => {
17884
+ result = await db.transaction(async (tx) => {
17852
17885
  await tx.execute(sql`SELECT set_config('caelo.actor_id', ${ctx.actorId}, true)`);
17853
17886
  await tx.execute(sql`SELECT set_config('caelo.actor_kind', ${ctx.actorKind}, true)`);
17854
17887
  await tx.execute(sql`SELECT set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true)`);
@@ -17859,7 +17892,10 @@ class DatabaseAdapter {
17859
17892
  if (denied !== null)
17860
17893
  return err(branchNotFound(op.name, denied));
17861
17894
  }
17862
- return await op.handler(ctx, validatedInput, tx);
17895
+ const out = await op.handler(ctx, validatedInput, tx);
17896
+ if (op.database === "cms_admin")
17897
+ publishAfterCommit = await this.#projectionDirty(tx);
17898
+ return out;
17863
17899
  });
17864
17900
  } catch (thrown) {
17865
17901
  if (thrown instanceof OperationAbortError) {
@@ -17880,14 +17916,28 @@ class DatabaseAdapter {
17880
17916
  ...pgDetail ? { pgDetail } : {}
17881
17917
  });
17882
17918
  }
17919
+ if (publishAfterCommit) {
17920
+ try {
17921
+ await this.#publishAfterCommit();
17922
+ } catch (thrown) {
17923
+ return err({
17924
+ kind: "HandlerError",
17925
+ operation: op.name,
17926
+ message: `The change is saved, but the public site does not serve it yet: publishing it failed (${thrown instanceof Error ? thrown.message : String(thrown)}). Do not repeat the change; it is published automatically within seconds once the public database answers.`
17927
+ });
17928
+ }
17929
+ }
17930
+ return result;
17883
17931
  }
17884
17932
  async close() {
17885
- await Promise.all([this.#adminRaw.end(), this.#publicRaw.end()]);
17933
+ await Promise.all([this.#adminRaw?.end(), this.#publicRaw.end()]);
17886
17934
  }
17887
17935
  async withAdminTransaction(ctx, fn) {
17936
+ const admin = this.#adminDb("withAdminTransaction");
17888
17937
  if (!this.#skipVerify)
17889
17938
  await this.verifyRoles();
17890
- return await this.#admin.transaction(async (tx) => {
17939
+ let publishAfterCommit = false;
17940
+ const out = await admin.transaction(async (tx) => {
17891
17941
  await tx.execute(sql`SELECT set_config('caelo.actor_id', ${ctx.actorId}, true)`);
17892
17942
  await tx.execute(sql`SELECT set_config('caelo.actor_kind', ${ctx.actorKind}, true)`);
17893
17943
  await tx.execute(sql`SELECT set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true)`);
@@ -17897,10 +17947,27 @@ class DatabaseAdapter {
17897
17947
  if (denied !== null) {
17898
17948
  throw new OperationAbortError(branchNotFound("withAdminTransaction", denied));
17899
17949
  }
17950
+ const result = await fn(tx);
17951
+ publishAfterCommit = await this.#projectionDirty(tx);
17952
+ return result;
17953
+ });
17954
+ if (publishAfterCommit)
17955
+ await this.#publishAfterCommit();
17956
+ return out;
17957
+ }
17958
+ async withPublicTransaction(ctx, fn) {
17959
+ if (!this.#skipVerify)
17960
+ await this.verifyRoles();
17961
+ return await this.#public.transaction(async (tx) => {
17962
+ await tx.execute(sql`SELECT set_config('caelo.actor_id', ${ctx.actorId}, true)`);
17963
+ await tx.execute(sql`SELECT set_config('caelo.actor_kind', ${ctx.actorKind}, true)`);
17964
+ await tx.execute(sql`SELECT set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true)`);
17900
17965
  return await fn(tx);
17901
17966
  });
17902
17967
  }
17903
17968
  rawAdmin() {
17969
+ if (this.#adminRaw === null)
17970
+ throw new AdminDatabaseUnavailableError("rawAdmin");
17904
17971
  return this.#adminRaw;
17905
17972
  }
17906
17973
  rawPublic() {
@@ -17916,21 +17983,23 @@ class DatabaseAdapter {
17916
17983
  });
17917
17984
  }
17918
17985
  async provisionPluginAdminSchema(opts) {
17986
+ const admin = this.#adminDb("provisionPluginAdminSchema");
17919
17987
  if (!this.#skipVerify)
17920
17988
  await this.verifyRoles();
17921
- await this.#admin.transaction(async (tx) => {
17989
+ await admin.transaction(async (tx) => {
17922
17990
  await tx.execute(sql.raw("SELECT set_config('caelo.actor_kind', 'system', true)"));
17923
17991
  await tx.execute(sql.raw(`SELECT set_config('caelo.plugin_id', '${opts.pluginId}', true)`));
17924
17992
  await tx.execute(sql.raw(opts.sql));
17925
17993
  });
17926
17994
  }
17927
17995
  async dropPluginAdminSchema(opts) {
17996
+ const admin = this.#adminDb("dropPluginAdminSchema");
17928
17997
  if (!this.#skipVerify)
17929
17998
  await this.verifyRoles();
17930
17999
  if (!/^plugin_[a-z][a-z0-9_]*$/.test(opts.schemaName)) {
17931
18000
  throw new Error(`dropPluginAdminSchema: refusing to drop schema "${opts.schemaName}" \u2014 name doesn't match plugin_<slug> pattern`);
17932
18001
  }
17933
- await this.#admin.transaction(async (tx) => {
18002
+ await admin.transaction(async (tx) => {
17934
18003
  await tx.execute(sql.raw("SELECT set_config('caelo.actor_kind', 'system', true)"));
17935
18004
  await tx.execute(sql.raw(`DROP SCHEMA IF EXISTS "${opts.schemaName}" CASCADE`));
17936
18005
  });
@@ -17947,7 +18016,7 @@ class DatabaseAdapter {
17947
18016
  });
17948
18017
  }
17949
18018
  get admin() {
17950
- return this.#admin;
18019
+ return this.#adminDb("admin pool");
17951
18020
  }
17952
18021
  get public() {
17953
18022
  return this.#public;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The AWS stack's per-service ECS task roles (#635 review): which Secrets
3
+ * Manager entries each service's task role may read. One role per service,
4
+ * so the API gateway's identity cannot read a secret that unlocks a
5
+ * cms_admin login (CLAUDE.md §2: the public side has no path to cms_admin),
6
+ * mirroring the GCP gateway SA's bindings (stack-contract.ts
7
+ * `SERVICE_SECRET_ENV`). Kept apart from the Pulumi program so the
8
+ * provisioning tests can assert it.
9
+ */
10
+ /** The Secrets Manager entries the AWS stack creates (`caelo-<env>-<name>`). */
11
+ export type AwsSecret = "postgres-password" | "public-role-password" | "gateway-role-password" | "csrf-secret" | "cookie-secret" | "anthropic-api-key" | "resend-api-key";
12
+ /** A service with its own ECS task role. */
13
+ export type AwsTaskService = "admin" | "gateway";
14
+ /**
15
+ * The secrets each service's task role may read. The admin side (admin and
16
+ * its in-process workers) reads all of them; the gateway reads only
17
+ * public_role's password — its one database login.
18
+ */
19
+ export declare const AWS_SERVICE_SECRETS: Readonly<Record<AwsTaskService, readonly AwsSecret[]>>;
20
+ /**
21
+ * The IAM policy document letting a task role read exactly `secretArns`.
22
+ *
23
+ * @throws on an empty list: a policy with no resource is malformed, and a
24
+ * service that needs no secret should get no secrets policy at all.
25
+ */
26
+ export declare function secretsReadPolicy(secretArns: readonly string[]): string;
27
+ //# sourceMappingURL=aws-task-roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aws-task-roles.d.ts","sourceRoot":"","sources":["../src/aws-task-roles.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GACjB,mBAAmB,GACnB,sBAAsB,GACtB,uBAAuB,GACvB,aAAa,GACb,eAAe,GACf,mBAAmB,GACnB,gBAAgB,CAAC;AAErB,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,SAAS,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,SAAS,SAAS,EAAE,CAAC,CAWtF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAQvE"}
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ /**
3
+ * The secrets each service's task role may read. The admin side (admin and
4
+ * its in-process workers) reads all of them; the gateway reads only
5
+ * public_role's password — its one database login.
6
+ */
7
+ export const AWS_SERVICE_SECRETS = {
8
+ admin: [
9
+ "postgres-password",
10
+ "public-role-password",
11
+ "gateway-role-password",
12
+ "csrf-secret",
13
+ "cookie-secret",
14
+ "anthropic-api-key",
15
+ "resend-api-key",
16
+ ],
17
+ gateway: ["public-role-password"],
18
+ };
19
+ /**
20
+ * The IAM policy document letting a task role read exactly `secretArns`.
21
+ *
22
+ * @throws on an empty list: a policy with no resource is malformed, and a
23
+ * service that needs no secret should get no secrets policy at all.
24
+ */
25
+ export function secretsReadPolicy(secretArns) {
26
+ if (secretArns.length === 0)
27
+ throw new Error("secretsReadPolicy: no secret ARNs");
28
+ return JSON.stringify({
29
+ Version: "2012-10-17",
30
+ Statement: [
31
+ { Effect: "Allow", Action: "secretsmanager:GetSecretValue", Resource: [...secretArns] },
32
+ ],
33
+ });
34
+ }
35
+ //# sourceMappingURL=aws-task-roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aws-task-roles.js","sourceRoot":"","sources":["../src/aws-task-roles.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAyBnC;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAA2D;IACzF,KAAK,EAAE;QACL,mBAAmB;QACnB,sBAAsB;QACtB,uBAAuB;QACvB,aAAa;QACb,eAAe;QACf,mBAAmB;QACnB,gBAAgB;KACjB;IACD,OAAO,EAAE,CAAC,sBAAsB,CAAC;CAClC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA6B;IAC7D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAClF,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,OAAO,EAAE,YAAY;QACrB,SAAS,EAAE;YACT,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,+BAA+B,EAAE,QAAQ,EAAE,CAAC,GAAG,UAAU,CAAC,EAAE;SACxF;KACF,CAAC,CAAC;AACL,CAAC"}
package/dist/cli.js CHANGED
@@ -925,6 +925,7 @@ const handlers = {
925
925
  truncate: lifecycleTruncate,
926
926
  "admin-domain": adminDomain,
927
927
  "staging-domain": stagingDomain,
928
+ "owner-reset": ownerReset,
928
929
  // #696 — Pulumi state in the install's own project.
929
930
  state: async () => (await import("./state-command.js")).stateCommand(process.argv),
930
931
  attach: async () => (await import("./state-command.js")).attachCommand(process.argv),
@@ -997,6 +998,38 @@ async function lifecycleTruncate() {
997
998
  const { truncateCommand } = await import("./lifecycle.js");
998
999
  await truncateCommand();
999
1000
  }
1001
+ /**
1002
+ * #769 `owner-reset [--install <id>] [--email <addr>]` — print a one-time
1003
+ * reset link for a locked-out Owner, for installs whose admin cannot email
1004
+ * one (see owner-reset.ts for why the operator's cloud identity may).
1005
+ */
1006
+ async function ownerReset() {
1007
+ const { installFlag, listInstalls, selectInstall } = await import("./install-state.js");
1008
+ const selected = selectInstall(listInstalls(), installFlag(process.argv));
1009
+ if (!selected.ok) {
1010
+ console.error(selected.message);
1011
+ process.exit(1);
1012
+ }
1013
+ const meta = selected.meta;
1014
+ if (meta.provider !== "gcp-firebase") {
1015
+ console.error(`owner-reset works on gcp-firebase installs so far; this one is ${meta.provider} (issue #770). ` +
1016
+ "Until then, another Owner can set a new password under Settings -> Users, or set up email under Settings -> Email so /forgot can send the link.");
1017
+ process.exit(1);
1018
+ }
1019
+ if (!meta.projectId || !meta.region) {
1020
+ console.error(`install ${meta.installId} has no project or region recorded in install.json; re-run the wizard to record it.`);
1021
+ process.exit(1);
1022
+ }
1023
+ const email = arg("email");
1024
+ const { describeOwnerReset, requestOwnerReset } = await import("./owner-reset.js");
1025
+ const r = await requestOwnerReset({ projectId: meta.projectId, region: meta.region }, email ? { email } : {});
1026
+ if (!r.ok) {
1027
+ console.error(`owner-reset: ${r.error}`);
1028
+ process.exit(1);
1029
+ }
1030
+ for (const line of describeOwnerReset(r.value))
1031
+ console.log(line);
1032
+ }
1000
1033
  /**
1001
1034
  * `admin-domain enable` — bind admin.<domain> to a gcp-firebase admin
1002
1035
  * (see admin-domain.ts for why this runs as the operator, not Pulumi).
@@ -1247,7 +1280,7 @@ if (route.kind === "handler" || route.kind === "wizard") {
1247
1280
  }
1248
1281
  }
1249
1282
  else {
1250
- console.log("Usage: cms-provision [wizard] [--provider <name> --domain <d> [--staging-domain <host>] --owner-email <e> --region <r>] [--local|--install-job] | <init|up|migrate|status|upgrade|backup|restore|rotate-secret|truncate|destroy|regenerate-caddy|admin-domain|staging-domain|state migrate|attach|pulumi-output-sync|version> [options]\n" +
1283
+ console.log("Usage: cms-provision [wizard] [--provider <name> --domain <d> [--staging-domain <host>] --owner-email <e> --region <r>] [--local|--install-job] | <init|up|migrate|status|upgrade|backup|restore|rotate-secret|truncate|destroy|regenerate-caddy|admin-domain|staging-domain|owner-reset|state migrate|attach|pulumi-output-sync|version> [options]\n" +
1251
1284
  "With several installs on this machine, pick one with --install <install-id or domain> (status, upgrade and backup also take --install all); in a terminal you are asked otherwise.\n" +
1252
1285
  "Pass --no-wizard with no sub-command to print this usage instead of the wizard.");
1253
1286
  process.exit(cmd && !cmd.startsWith("-") ? 2 : 0);