@caelo-cms/provisioning 0.10.55 → 0.10.58
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/agent-db.js +97 -24
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/compose.js +5 -0
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +66 -0
- package/dist/converge-checkpoint.d.ts.map +1 -0
- package/dist/converge-checkpoint.js +61 -0
- package/dist/converge-checkpoint.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +46 -0
- package/dist/converge-job-gcp.d.ts.map +1 -0
- package/dist/converge-job-gcp.js +368 -0
- package/dist/converge-job-gcp.js.map +1 -0
- package/dist/converge-job-setup.d.ts +62 -0
- package/dist/converge-job-setup.d.ts.map +1 -0
- package/dist/converge-job-setup.js +228 -0
- package/dist/converge-job-setup.js.map +1 -0
- package/dist/converge-job.d.ts +141 -0
- package/dist/converge-job.d.ts.map +1 -0
- package/dist/converge-job.js +325 -0
- package/dist/converge-job.js.map +1 -0
- package/dist/gcp-names.d.ts +9 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +13 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +33 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +11 -3
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +22 -12
- package/dist/install-job.js.map +1 -1
- package/dist/lifecycle.d.ts +13 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +115 -58
- package/dist/lifecycle.js.map +1 -1
- package/dist/operator-access.d.ts +4 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +43 -7
- package/dist/operator-access.js.map +1 -1
- package/dist/release-contract.d.ts +3 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +5 -2
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts +2 -0
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp-edge.d.ts +114 -0
- package/dist/remote-mcp-edge.d.ts.map +1 -0
- package/dist/remote-mcp-edge.js +116 -0
- package/dist/remote-mcp-edge.js.map +1 -0
- package/dist/remote-mcp.d.ts +139 -0
- package/dist/remote-mcp.d.ts.map +1 -0
- package/dist/remote-mcp.js +434 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/runtime-secrets.d.ts +12 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +32 -4
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/seal-key-rotation.d.ts +38 -0
- package/dist/seal-key-rotation.d.ts.map +1 -0
- package/dist/seal-key-rotation.js +206 -0
- package/dist/seal-key-rotation.js.map +1 -0
- package/dist/stack-contract.d.ts +206 -3
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +219 -4
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +53 -1
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +124 -4
- package/dist/stack-converge.js.map +1 -1
- package/dist/state-lock-lease.d.ts +142 -0
- package/dist/state-lock-lease.d.ts.map +1 -0
- package/dist/state-lock-lease.js +347 -0
- package/dist/state-lock-lease.js.map +1 -0
- package/dist/state-store.d.ts +66 -9
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +249 -30
- package/dist/state-store.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +36 -7
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +53 -19
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/audit-job-roll.d.ts.map +1 -1
- package/dist/upgrade/audit-job-roll.js +3 -1
- package/dist/upgrade/audit-job-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +125 -12
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +27 -10
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +46 -7
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/record-rolled.d.ts +40 -0
- package/dist/upgrade/record-rolled.d.ts.map +1 -0
- package/dist/upgrade/record-rolled.js +46 -0
- package/dist/upgrade/record-rolled.js.map +1 -0
- package/dist/upgrade/upgrade.d.ts +45 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +60 -17
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +15 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +36 -6
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +15 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +42 -7
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +123 -0
- package/dist/upgrader-iam-preflight.d.ts.map +1 -0
- package/dist/upgrader-iam-preflight.js +414 -0
- package/dist/upgrader-iam-preflight.js.map +1 -0
- package/dist/upgrader-job.d.ts +33 -9
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +169 -27
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +58 -0
- package/dist/upgrader-records.d.ts.map +1 -0
- package/dist/upgrader-records.js +87 -0
- package/dist/upgrader-records.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +3 -2
- package/dist/upgrader-reporter.d.ts.map +1 -1
- package/dist/upgrader-reporter.js +10 -2
- package/dist/upgrader-reporter.js.map +1 -1
- package/dist/upgrader-setup.d.ts +25 -3
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +123 -11
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +11 -3
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +6 -0
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +14 -2
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +106 -4
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/agent/run.test.ts +14 -1
- package/src/audit-job-setup.test.ts +7 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +4 -1
- package/src/compose.ts +5 -0
- package/src/converge-checkpoint.ts +75 -0
- package/src/converge-entrypoint.test.ts +213 -0
- package/src/converge-job-gcp.test.ts +222 -0
- package/src/converge-job-gcp.ts +443 -0
- package/src/converge-job-setup.test.ts +152 -0
- package/src/converge-job-setup.ts +268 -0
- package/src/converge-job.test.ts +486 -0
- package/src/converge-job.ts +542 -0
- package/src/gateway-env-isolation.test.ts +8 -1
- package/src/gcp-names.ts +15 -0
- package/src/gcp-url-map.test.ts +9 -1
- package/src/index.ts +8 -0
- package/src/infra-contract.test.ts +8 -4
- package/src/infra-contract.ts +33 -1
- package/src/install-job.test.ts +3 -1
- package/src/install-job.ts +31 -14
- package/src/lifecycle.ts +137 -71
- package/src/operator-access.test.ts +24 -1
- package/src/operator-access.ts +44 -6
- package/src/release-contract.ts +5 -2
- package/src/release-manifest.test.ts +42 -0
- package/src/release-manifest.ts +2 -0
- package/src/remote-mcp-edge.ts +137 -0
- package/src/remote-mcp.test.ts +690 -0
- package/src/remote-mcp.ts +524 -0
- package/src/runtime-secrets.test.ts +31 -6
- package/src/runtime-secrets.ts +41 -3
- package/src/seal-key-rotation.test.ts +131 -0
- package/src/seal-key-rotation.ts +258 -0
- package/src/stack-contract-parity.test.ts +20 -3
- package/src/stack-contract.test.ts +32 -2
- package/src/stack-contract.ts +363 -4
- package/src/stack-converge-secrets.test.ts +4 -0
- package/src/stack-converge.test.ts +21 -5
- package/src/stack-converge.ts +162 -7
- package/src/state-attach.test.ts +7 -1
- package/src/state-lock-lease.test.ts +302 -0
- package/src/state-lock-lease.ts +423 -0
- package/src/state-lock.test.ts +568 -0
- package/src/state-store.ts +347 -27
- package/src/upgrade/app-roll.ts +102 -21
- package/src/upgrade/audit-job-roll.ts +3 -1
- package/src/upgrade/converge.ts +148 -10
- package/src/upgrade/gcp-lookup.test.ts +80 -0
- package/src/upgrade/gcp-lookup.ts +65 -8
- package/src/upgrade/record-rolled.test.ts +105 -0
- package/src/upgrade/record-rolled.ts +91 -0
- package/src/upgrade/upgrade.test.ts +203 -3
- package/src/upgrade/upgrade.ts +120 -24
- package/src/upgrader-assess.test.ts +92 -3
- package/src/upgrader-assess.ts +58 -9
- package/src/upgrader-gcp.ts +47 -8
- package/src/upgrader-iam-preflight.test.ts +393 -0
- package/src/upgrader-iam-preflight.ts +510 -0
- package/src/upgrader-job.test.ts +586 -15
- package/src/upgrader-job.ts +237 -53
- package/src/upgrader-records.ts +151 -0
- package/src/upgrader-reporter.test.ts +6 -0
- package/src/upgrader-reporter.ts +7 -2
- package/src/upgrader-setup.test.ts +3 -3
- package/src/upgrader-setup.ts +128 -11
- package/src/wizards/gcp-cost.ts +12 -3
- package/src/wizards/gcp-pulumi.test.ts +15 -0
- package/src/wizards/gcp-pulumi.ts +18 -2
- package/src/wizards/gcp.ts +134 -4
- package/stacks/aws/index.ts +10 -0
- package/stacks/azure/index.ts +9 -0
- package/stacks/gcp/index.ts +47 -7
- package/stacks/gcp-firebase/index.ts +38 -6
package/src/stack-contract.ts
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import type { SupportedProvider } from "./adapter.js";
|
|
38
38
|
import {
|
|
39
39
|
auditJobResource,
|
|
40
|
+
convergeJobResource,
|
|
40
41
|
gatewayServiceAccountEmail,
|
|
41
42
|
gcpBucketName,
|
|
42
43
|
gcpNamePrefix,
|
|
@@ -170,6 +171,13 @@ export const OPERATOR_ACCESS_JOB_ENV_VAR = "CAELO_OPERATOR_ACCESS_JOB";
|
|
|
170
171
|
*/
|
|
171
172
|
export const UPGRADER_JOB_ENV_VAR = "CAELO_UPGRADER_JOB";
|
|
172
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The admin env var naming the converge job (#819) — what "Install update"
|
|
176
|
+
* starts for a release that changes the cloud setup. `upgrade` leaves it
|
|
177
|
+
* untouched when it could not set the job up.
|
|
178
|
+
*/
|
|
179
|
+
export const CONVERGE_JOB_ENV_VAR = "CAELO_CONVERGE_JOB";
|
|
180
|
+
|
|
173
181
|
/**
|
|
174
182
|
* #875 — the admin env vars naming the parallel quality-audit job and the
|
|
175
183
|
* bucket its tasks write results into (admin-core quality/
|
|
@@ -251,7 +259,22 @@ export type RuntimeSecret =
|
|
|
251
259
|
| "secret-kek"
|
|
252
260
|
| "internal-secret"
|
|
253
261
|
| "tool-approval-secret"
|
|
254
|
-
| "staging-gate-secret"
|
|
262
|
+
| "staging-gate-secret"
|
|
263
|
+
/**
|
|
264
|
+
* #914 — the install's Google sign-in client: JSON
|
|
265
|
+
* `{"client_id","client_secret"}`, `{}` while it is not set up. One client
|
|
266
|
+
* for IAP browser login, IAP programmatic access and the remote-MCP broker
|
|
267
|
+
* (#806 introduced it as `mcp-oauth-client`). The Owner writes it from
|
|
268
|
+
* Settings → Google sign-in (the admin may add versions, never read them);
|
|
269
|
+
* the gateway reads it from a file, the operator-access sync job reads it
|
|
270
|
+
* to hand it to IAP.
|
|
271
|
+
*/
|
|
272
|
+
| "operator-oauth-client"
|
|
273
|
+
/**
|
|
274
|
+
* #806 — the broker's AES-256-GCM key (base64, 32 bytes) that seals its
|
|
275
|
+
* stateless OAuth state, codes and refresh tokens. Gateway only.
|
|
276
|
+
*/
|
|
277
|
+
| "mcp-oauth-seal-key";
|
|
255
278
|
|
|
256
279
|
/** The Postgres roles Caelo's services log in as, with their passwords. */
|
|
257
280
|
export type DatabaseRole = "admin_role" | "public_role";
|
|
@@ -285,8 +308,55 @@ export const CLI_GENERATED_SECRETS = [
|
|
|
285
308
|
// session cookies, derives the crawler credentials. Read by the admin
|
|
286
309
|
// (issues) and the gateway (verifies).
|
|
287
310
|
"staging-gate-secret",
|
|
311
|
+
// #914 / #806 — the Google sign-in client starts as the `{}` placeholder
|
|
312
|
+
// (not configured; the Owner fills it under Settings → Google sign-in),
|
|
313
|
+
// the broker's seal key as 32 random bytes in base64
|
|
314
|
+
// ({@link GENERATED_SECRET_FORMAT}).
|
|
315
|
+
"operator-oauth-client",
|
|
316
|
+
"mcp-oauth-seal-key",
|
|
288
317
|
] as const satisfies readonly RuntimeSecret[];
|
|
289
318
|
|
|
319
|
+
/** A CLI-generated secret's first value: random hex, random base64, or a fixed placeholder. */
|
|
320
|
+
export type GeneratedSecretFormat =
|
|
321
|
+
| { readonly kind: "hex32" }
|
|
322
|
+
| { readonly kind: "base64-32" }
|
|
323
|
+
| { readonly kind: "placeholder"; readonly value: string };
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* How each CLI-generated secret's first version is made. Every secret not
|
|
327
|
+
* listed is 32 random bytes as hex — the shape the stacks always generated.
|
|
328
|
+
*/
|
|
329
|
+
export const GENERATED_SECRET_FORMAT: Readonly<
|
|
330
|
+
Record<(typeof CLI_GENERATED_SECRETS)[number], GeneratedSecretFormat>
|
|
331
|
+
> = {
|
|
332
|
+
"internal-secret": { kind: "hex32" },
|
|
333
|
+
"tool-approval-secret": { kind: "hex32" },
|
|
334
|
+
"public-role-password": { kind: "hex32" },
|
|
335
|
+
"staging-gate-secret": { kind: "hex32" },
|
|
336
|
+
// The gateway's seal key contract (#806): base64 of exactly 32 bytes.
|
|
337
|
+
"mcp-oauth-seal-key": { kind: "base64-32" },
|
|
338
|
+
// "Not configured" until the Owner pastes a client (#914). A version must
|
|
339
|
+
// exist from the start: Cloud Run refuses a revision whose secret volume
|
|
340
|
+
// points at a secret without an enabled version.
|
|
341
|
+
"operator-oauth-client": { kind: "placeholder", value: "{}" },
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Secrets an older contract created that nothing reads any more. Upgrade
|
|
346
|
+
* deletes each once both services rolled onto revisions that no longer
|
|
347
|
+
* reference it (stack-converge.ts `deleteRetiredSecrets`); deleting a
|
|
348
|
+
* secret drops its IAM bindings with it. Pre-1.0 delete-don't-migrate: a
|
|
349
|
+
* renamed secret is created fresh, never copied.
|
|
350
|
+
*/
|
|
351
|
+
export const RETIRED_RUNTIME_SECRETS: readonly { readonly name: string; readonly why: string }[] = [
|
|
352
|
+
{
|
|
353
|
+
// #914 — renamed to `operator-oauth-client`; nobody had configured
|
|
354
|
+
// remote MCP when the rename shipped, so there is nothing to carry over.
|
|
355
|
+
name: "mcp-oauth-client",
|
|
356
|
+
why: "the Google sign-in client moved to operator-oauth-client (#914)",
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
|
|
290
360
|
/**
|
|
291
361
|
* Each service's Secret Manager-backed env vars and the secret each reads.
|
|
292
362
|
* The services' run SAs get `secretAccessor` on exactly these secrets
|
|
@@ -316,12 +386,119 @@ export const SERVICE_SECRET_ENV: Readonly<
|
|
|
316
386
|
PUBLIC_DATABASE_PASSWORD: "public-role-password",
|
|
317
387
|
// #650 — verifies staging-host requests (api-gateway staging-proxy.ts).
|
|
318
388
|
CAELO_STAGING_GATE_SECRET: "staging-gate-secret",
|
|
389
|
+
// #806 — seals the remote-MCP broker's state, codes and refresh tokens.
|
|
390
|
+
CAELO_MCP_OAUTH_SEAL_KEY: "mcp-oauth-seal-key",
|
|
319
391
|
},
|
|
320
392
|
};
|
|
321
393
|
|
|
322
|
-
|
|
394
|
+
// ===========================================================================
|
|
395
|
+
// Secret files (#806)
|
|
396
|
+
// ===========================================================================
|
|
397
|
+
|
|
398
|
+
/** Env var naming the file that holds the install's Google sign-in client (#914). */
|
|
399
|
+
export const OPERATOR_OAUTH_CLIENT_FILE_ENV = "CAELO_OPERATOR_OAUTH_CLIENT_FILE";
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* A Secret Manager secret mounted into a service as one file, at `latest`.
|
|
403
|
+
*
|
|
404
|
+
* Why a file and not an env var: Cloud Run resolves a secret env var once,
|
|
405
|
+
* at instance start, but reads a secret volume from Secret Manager each
|
|
406
|
+
* time the file is read ("When reading a volume, Cloud Run always fetches
|
|
407
|
+
* the secret value from the Secret Manager to use the value with the latest
|
|
408
|
+
* version" — cloud.google.com/run/docs/configuring/services/secrets). So the
|
|
409
|
+
* client the Owner saves under Settings → Google sign-in reaches the running gateway
|
|
410
|
+
* (which re-reads it within 60 s) without a new revision, and the admin
|
|
411
|
+
* needs no right to deploy the gateway.
|
|
412
|
+
*/
|
|
413
|
+
export interface SecretFileContract {
|
|
414
|
+
readonly secret: RuntimeSecret;
|
|
415
|
+
/** Volume name the stacks give it (gcloud names the ones upgrade adds itself). */
|
|
416
|
+
readonly volumeName: string;
|
|
417
|
+
/** Directory the volume is mounted at; must not exist in the image. */
|
|
418
|
+
readonly mountDir: string;
|
|
419
|
+
readonly fileName: string;
|
|
420
|
+
/** Always `latest`: a pinned version would never see the Owner's new client. */
|
|
421
|
+
readonly version: "latest";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** The gateway's Google sign-in client file, read by the remote-MCP broker (#806, #914). */
|
|
425
|
+
export const OPERATOR_OAUTH_CLIENT_FILE: SecretFileContract = {
|
|
426
|
+
secret: "operator-oauth-client",
|
|
427
|
+
volumeName: "operator-oauth-client",
|
|
428
|
+
mountDir: "/secrets/operator-oauth-client",
|
|
429
|
+
fileName: "client.json",
|
|
430
|
+
version: "latest",
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/** Full path of a secret file inside the container. */
|
|
434
|
+
export function secretFilePath(file: SecretFileContract): string {
|
|
435
|
+
return `${file.mountDir}/${file.fileName}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Each service's secret files. The admin has none. */
|
|
439
|
+
export const SERVICE_SECRET_FILES: Readonly<Record<CloudRunSlug, readonly SecretFileContract[]>> = {
|
|
440
|
+
admin: [],
|
|
441
|
+
gateway: [OPERATOR_OAUTH_CLIENT_FILE],
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/** A secret file an older contract mounted; upgrade unmounts it in the roll. */
|
|
445
|
+
export interface RetiredSecretFile {
|
|
446
|
+
readonly mountDir: string;
|
|
447
|
+
readonly fileName: string;
|
|
448
|
+
readonly why: string;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Secret files older contracts mounted that nothing reads any more, per
|
|
453
|
+
* service. The roll removes each mount (`--remove-secrets=<path>`) in the
|
|
454
|
+
* same revision that mounts its replacement.
|
|
455
|
+
*/
|
|
456
|
+
export const RETIRED_SECRET_FILES: Readonly<Record<CloudRunSlug, readonly RetiredSecretFile[]>> = {
|
|
457
|
+
admin: [],
|
|
458
|
+
gateway: [
|
|
459
|
+
{
|
|
460
|
+
mountDir: "/secrets/mcp-oauth-client",
|
|
461
|
+
fileName: "client.json",
|
|
462
|
+
why: "the Google sign-in client is mounted from operator-oauth-client now (#914)",
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The secret files of `service` in the shape `gcp.cloudrunv2.Service`
|
|
469
|
+
* takes: `volumes` on the template, `volumeMounts` on the container.
|
|
470
|
+
*/
|
|
471
|
+
export function secretFileVolumesTemplate(
|
|
472
|
+
service: CloudRunSlug,
|
|
473
|
+
env: string,
|
|
474
|
+
): {
|
|
475
|
+
readonly volumes: {
|
|
476
|
+
name: string;
|
|
477
|
+
secret: { secret: string; items: { version: string; path: string }[] };
|
|
478
|
+
}[];
|
|
479
|
+
readonly volumeMounts: { name: string; mountPath: string }[];
|
|
480
|
+
} {
|
|
481
|
+
const files = SERVICE_SECRET_FILES[service];
|
|
482
|
+
return {
|
|
483
|
+
volumes: files.map((f) => ({
|
|
484
|
+
name: f.volumeName,
|
|
485
|
+
secret: {
|
|
486
|
+
secret: gcpSecretId(env, f.secret),
|
|
487
|
+
items: [{ version: f.version, path: f.fileName }],
|
|
488
|
+
},
|
|
489
|
+
})),
|
|
490
|
+
volumeMounts: files.map((f) => ({ name: f.volumeName, mountPath: f.mountDir })),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The secrets `service` reads, as env or as files (deduplicated, table order). */
|
|
323
495
|
export function serviceSecrets(service: CloudRunSlug): RuntimeSecret[] {
|
|
324
|
-
return [
|
|
496
|
+
return [
|
|
497
|
+
...new Set([
|
|
498
|
+
...Object.values(SERVICE_SECRET_ENV[service]),
|
|
499
|
+
...SERVICE_SECRET_FILES[service].map((f) => f.secret),
|
|
500
|
+
]),
|
|
501
|
+
];
|
|
325
502
|
}
|
|
326
503
|
|
|
327
504
|
/**
|
|
@@ -332,8 +509,11 @@ export function serviceSecrets(service: CloudRunSlug): RuntimeSecret[] {
|
|
|
332
509
|
* credential is in its env (apps/api-gateway `adminDatabaseAccessInEnv`).
|
|
333
510
|
*/
|
|
334
511
|
export const RETIRED_SERVICE_ENV: Readonly<Record<CloudRunSlug, readonly string[]>> = {
|
|
335
|
-
|
|
512
|
+
// #914 — the sign-in client's store was renamed with its secret.
|
|
513
|
+
admin: ["CAELO_MCP_OAUTH_CLIENT_STORE"],
|
|
336
514
|
gateway: [
|
|
515
|
+
// #914 — renamed to CAELO_OPERATOR_OAUTH_CLIENT_FILE with its secret.
|
|
516
|
+
"CAELO_MCP_OAUTH_CLIENT_FILE",
|
|
337
517
|
"CAELO_SECRET_KEK",
|
|
338
518
|
"ADMIN_DATABASE_URL",
|
|
339
519
|
"ADMIN_DATABASE_PASSWORD",
|
|
@@ -411,10 +591,159 @@ export function gatewayEnvContract<V>(inputs: RuntimeEnvInputs<V>): CloudRunEnvV
|
|
|
411
591
|
...commonEnv(inputs),
|
|
412
592
|
{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS[inputs.provider]) },
|
|
413
593
|
{ name: "PUBLIC_DATABASE_URL", value: inputs.databaseUrls.public },
|
|
594
|
+
// #806 — where the remote-MCP broker reads its Google OAuth client
|
|
595
|
+
// (the secret volume of SERVICE_SECRET_FILES).
|
|
596
|
+
{ name: OPERATOR_OAUTH_CLIENT_FILE_ENV, value: secretFilePath(OPERATOR_OAUTH_CLIENT_FILE) },
|
|
414
597
|
...secretEnv("gateway", inputs.env),
|
|
415
598
|
];
|
|
416
599
|
}
|
|
417
600
|
|
|
601
|
+
/**
|
|
602
|
+
* #806 — the gateway's request timeout, in seconds: Cloud Run's maximum.
|
|
603
|
+
* The gateway proxies remote-MCP requests to the admin, and a `caelo_chat`
|
|
604
|
+
* turn streams for as long as the admin's own chat stream may (the admin
|
|
605
|
+
* runs at 3600 s for the same reason). Visitor endpoints are unaffected: a
|
|
606
|
+
* timeout is a ceiling, not a reservation, and Cloud Run bills request time,
|
|
607
|
+
* not the allowed maximum. On `gcp` the load balancer in front adds no
|
|
608
|
+
* shorter limit — the backend-service timeout does not apply to serverless
|
|
609
|
+
* NEGs, which get a fixed 60 minutes
|
|
610
|
+
* (cloud.google.com/load-balancing/docs/negs/serverless-neg-concepts).
|
|
611
|
+
*/
|
|
612
|
+
export const GATEWAY_TIMEOUT_SECONDS = 3600;
|
|
613
|
+
|
|
614
|
+
/** Admin env var naming the Secret Manager secret the Owner writes the Google sign-in client to. */
|
|
615
|
+
export const OPERATOR_OAUTH_CLIENT_STORE_ENV = "CAELO_OPERATOR_OAUTH_CLIENT_STORE";
|
|
616
|
+
|
|
617
|
+
/** `projects/<project>/secrets/<id>` of the Google sign-in client secret (#914). */
|
|
618
|
+
export function operatorOAuthClientSecretResource(projectId: string, env: string): string {
|
|
619
|
+
return `projects/${projectId}/secrets/${gcpSecretId(env, "operator-oauth-client")}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ===========================================================================
|
|
623
|
+
// Remote MCP identity env (#806)
|
|
624
|
+
// ===========================================================================
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The remote-MCP env vars that name generated identities of the install:
|
|
628
|
+
* the admin's URL, the gateway's public origin, and the audience of the IAP
|
|
629
|
+
* JWTs the admin verifies.
|
|
630
|
+
*
|
|
631
|
+
* They are not part of {@link adminEnvContract} / {@link gatewayEnvContract}
|
|
632
|
+
* because the stacks cannot declare them: on `gcp-firebase` each service's
|
|
633
|
+
* value names a Cloud Run service Pulumi auto-names (the admin's audience is
|
|
634
|
+
* its own name; the gateway's public URL is its own URL; the admin's URL is
|
|
635
|
+
* a service created after the gateway), and on `gcp` the audience is the
|
|
636
|
+
* numeric id of a backend service created after the admin. A resource
|
|
637
|
+
* cannot take its own (or a later resource's) output as input. So the CLI
|
|
638
|
+
* reads the live identities after deploy and applies these vars with gcloud
|
|
639
|
+
* — the wizard after every `pulumi up`, `upgrade` after every roll
|
|
640
|
+
* (remote-mcp.ts `ensureRemoteMcpIdentityEnv`). A roll that does not set
|
|
641
|
+
* them leaves them as they are (planEnvUpdate never removes a var the
|
|
642
|
+
* contract does not own).
|
|
643
|
+
*
|
|
644
|
+
* #914 adds two the admin's Google sign-in needs: its own URL (the
|
|
645
|
+
* self-check opens it signed out) and the project's Google organisation
|
|
646
|
+
* domain, which the admin's service account cannot read (organisation
|
|
647
|
+
* metadata needs a grant on the organisation) but the operator's identity
|
|
648
|
+
* usually can.
|
|
649
|
+
*/
|
|
650
|
+
export interface RemoteMcpIdentity {
|
|
651
|
+
/** Where the gateway proxies `/api/mcp/<surface>` to (IAP in front). */
|
|
652
|
+
readonly adminUrl: string;
|
|
653
|
+
/** The broker's public origin; no trailing slash. */
|
|
654
|
+
readonly mcpPublicUrl: string;
|
|
655
|
+
/** The `aud` of the admin's IAP JWTs. */
|
|
656
|
+
readonly iapAudience: string;
|
|
657
|
+
/**
|
|
658
|
+
* #914 — the project's Google organisation domain, `none` when the project
|
|
659
|
+
* is in no organisation, undefined when it could not be read (the var is
|
|
660
|
+
* then left as it is and the admin reports the organisation as unknown).
|
|
661
|
+
*/
|
|
662
|
+
readonly googleOrgDomain?: string;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Env var names of {@link RemoteMcpIdentity}. */
|
|
666
|
+
export const REMOTE_MCP_ENV = {
|
|
667
|
+
adminUrl: "CAELO_ADMIN_URL",
|
|
668
|
+
mcpPublicUrl: "CAELO_MCP_PUBLIC_URL",
|
|
669
|
+
iapAudience: "CAELO_IAP_AUDIENCE",
|
|
670
|
+
googleOrgDomain: "CAELO_GOOGLE_ORG_DOMAIN",
|
|
671
|
+
} as const;
|
|
672
|
+
|
|
673
|
+
/** What {@link remoteMcpIdentity} derives the identity from — all read from the live install. */
|
|
674
|
+
export type RemoteMcpIdentityInputs =
|
|
675
|
+
| {
|
|
676
|
+
readonly provider: "gcp";
|
|
677
|
+
readonly domain: string;
|
|
678
|
+
readonly projectNumber: string;
|
|
679
|
+
/** Numeric `id` of the admin's IAP-enabled backend service. */
|
|
680
|
+
readonly adminBackendServiceId: string;
|
|
681
|
+
}
|
|
682
|
+
| {
|
|
683
|
+
readonly provider: "gcp-firebase";
|
|
684
|
+
readonly projectNumber: string;
|
|
685
|
+
readonly region: string;
|
|
686
|
+
readonly adminServiceName: string;
|
|
687
|
+
/** The services' `status.url` (`https://<name>-<hash>-<r>.a.run.app`). */
|
|
688
|
+
readonly adminServiceUrl: string;
|
|
689
|
+
readonly gatewayServiceUrl: string;
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* The install's remote-MCP identity.
|
|
694
|
+
*
|
|
695
|
+
* - `gcp`: the broker answers on the site's own host through the load
|
|
696
|
+
* balancer (`/api/*` and the `/.well-known/oauth-*` path rules), the admin
|
|
697
|
+
* on `admin.<domain>` behind the LB's IAP; the IAP audience is
|
|
698
|
+
* `/projects/<number>/global/backendServices/<id>`.
|
|
699
|
+
* - `gcp-firebase`: the broker answers on the gateway's own Cloud Run URL
|
|
700
|
+
* (Firebase Hosting rewrites cut requests at 60 s and buffer streams), the
|
|
701
|
+
* admin on its Cloud Run URL behind native IAP — whatever custom domain
|
|
702
|
+
* is mapped to it, IAP and its audience are the service's; the audience
|
|
703
|
+
* is `/projects/<number>/locations/<region>/services/<name>`.
|
|
704
|
+
*
|
|
705
|
+
* Audience formats: cloud.google.com/iap/docs/signed-headers-howto.
|
|
706
|
+
*/
|
|
707
|
+
export function remoteMcpIdentity(i: RemoteMcpIdentityInputs): RemoteMcpIdentity {
|
|
708
|
+
const origin = (url: string) => url.replace(/\/+$/, "");
|
|
709
|
+
if (i.provider === "gcp") {
|
|
710
|
+
return {
|
|
711
|
+
adminUrl: `https://admin.${i.domain}`,
|
|
712
|
+
mcpPublicUrl: publicSiteUrl(i.domain),
|
|
713
|
+
iapAudience: `/projects/${i.projectNumber}/global/backendServices/${i.adminBackendServiceId}`,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
adminUrl: origin(i.adminServiceUrl),
|
|
718
|
+
mcpPublicUrl: origin(i.gatewayServiceUrl),
|
|
719
|
+
iapAudience: `/projects/${i.projectNumber}/locations/${i.region}/services/${i.adminServiceName}`,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** {@link RemoteMcpIdentity} as each service's env vars. */
|
|
724
|
+
export function remoteMcpIdentityEnv(
|
|
725
|
+
id: RemoteMcpIdentity,
|
|
726
|
+
): Record<CloudRunSlug, CloudRunEnvVar[]> {
|
|
727
|
+
return {
|
|
728
|
+
admin: [
|
|
729
|
+
{ name: REMOTE_MCP_ENV.iapAudience, value: id.iapAudience },
|
|
730
|
+
// The setup pages show the redirect URI and the MCP URLs from it.
|
|
731
|
+
{ name: REMOTE_MCP_ENV.mcpPublicUrl, value: id.mcpPublicUrl },
|
|
732
|
+
// #914 — Google sign-in's self-check opens the admin signed out.
|
|
733
|
+
{ name: REMOTE_MCP_ENV.adminUrl, value: id.adminUrl },
|
|
734
|
+
// #914 — who IAP admits without the Google sign-in client
|
|
735
|
+
// (admin-core security/identity-gate.ts).
|
|
736
|
+
...(id.googleOrgDomain
|
|
737
|
+
? [{ name: REMOTE_MCP_ENV.googleOrgDomain, value: id.googleOrgDomain }]
|
|
738
|
+
: []),
|
|
739
|
+
],
|
|
740
|
+
gateway: [
|
|
741
|
+
{ name: REMOTE_MCP_ENV.adminUrl, value: id.adminUrl },
|
|
742
|
+
{ name: REMOTE_MCP_ENV.mcpPublicUrl, value: id.mcpPublicUrl },
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
418
747
|
/**
|
|
419
748
|
* The env of the admin service.
|
|
420
749
|
*
|
|
@@ -452,10 +781,20 @@ export function adminEnvContract<V>(inputs: AdminEnvInputs<V>): CloudRunEnvVar<V
|
|
|
452
781
|
// #696 — the job "Install update" starts (upgrader-setup.ts); the admin
|
|
453
782
|
// may only run it, never override it.
|
|
454
783
|
{ name: UPGRADER_JOB_ENV_VAR, value: upgraderJobResource(projectId, inputs.region, env) },
|
|
784
|
+
// #819 — the job "Install update" starts for a release that changes the
|
|
785
|
+
// cloud setup (converge-job-setup.ts); start-only as well.
|
|
786
|
+
{ name: CONVERGE_JOB_ENV_VAR, value: convergeJobResource(projectId, inputs.region, env) },
|
|
455
787
|
// #875 — the job the quality audit fans its Lighthouse runs out to, and
|
|
456
788
|
// where the tasks write their results (audit-job-setup.ts).
|
|
457
789
|
{ name: AUDIT_JOB_ENV_VAR, value: auditJobResource(projectId, inputs.region, env) },
|
|
458
790
|
{ name: AUDIT_RESULTS_BUCKET_ENV_VAR, value: gcpBucketName(projectId, env, "audit-results") },
|
|
791
|
+
// #914 — where Settings → Google sign-in writes the install's Google
|
|
792
|
+
// sign-in client (add-version only, see the run SA's secretVersionAdder
|
|
793
|
+
// invariant).
|
|
794
|
+
{
|
|
795
|
+
name: OPERATOR_OAUTH_CLIENT_STORE_ENV,
|
|
796
|
+
value: operatorOAuthClientSecretResource(projectId, env),
|
|
797
|
+
},
|
|
459
798
|
];
|
|
460
799
|
if (inputs.provider === "gcp") {
|
|
461
800
|
// v0.2.78 — the GCS StaticPublisher: Stage uploads to staging,
|
|
@@ -542,6 +881,7 @@ export interface IamInvariant {
|
|
|
542
881
|
}
|
|
543
882
|
|
|
544
883
|
const SECRET_ACCESSOR = "roles/secretmanager.secretAccessor";
|
|
884
|
+
const SECRET_VERSION_ADDER = "roles/secretmanager.secretVersionAdder";
|
|
545
885
|
|
|
546
886
|
/** One `secretAccessor` binding a stack declares for a service's run SA. */
|
|
547
887
|
export interface RuntimeSecretBinding {
|
|
@@ -577,6 +917,24 @@ function secretAccessors(): IamInvariant[] {
|
|
|
577
917
|
}));
|
|
578
918
|
}
|
|
579
919
|
|
|
920
|
+
/** Bindings both GCP stacks declare beyond the secret accessors and telemetry. */
|
|
921
|
+
const GCP_COMMON: readonly IamInvariant[] = [
|
|
922
|
+
{
|
|
923
|
+
stackResource: "admin-operator-oauth-client-version-adder",
|
|
924
|
+
// Why the admin may write a secret at all: the Owner pastes the
|
|
925
|
+
// install's Google sign-in client under Settings → Google sign-in, and
|
|
926
|
+
// the admin stores it where the gateway and the operator-access sync job
|
|
927
|
+
// read it. secretVersionAdder is add-version only (no access, no
|
|
928
|
+
// destroy, no IAM), bound on this one secret only — the admin can
|
|
929
|
+
// replace the client, never read it or any other secret.
|
|
930
|
+
role: SECRET_VERSION_ADDER,
|
|
931
|
+
member: "run-sa",
|
|
932
|
+
target: { kind: "secret", name: "operator-oauth-client" },
|
|
933
|
+
onFailure: "warn",
|
|
934
|
+
why: "the Owner's Google sign-in setup (Settings → Google sign-in) stores the OAuth client; without it only that setup fails",
|
|
935
|
+
},
|
|
936
|
+
];
|
|
937
|
+
|
|
580
938
|
/** v0.6.6 — a custom runtime SA gets no telemetry roles implicitly. */
|
|
581
939
|
const TELEMETRY: readonly IamInvariant[] = [
|
|
582
940
|
{
|
|
@@ -743,6 +1101,7 @@ export const RETIRED_IAM_BINDINGS: readonly RetiredIamBinding[] = [
|
|
|
743
1101
|
export function stackIamInvariants(provider: GcpProvider): IamInvariant[] {
|
|
744
1102
|
return [
|
|
745
1103
|
...secretAccessors(),
|
|
1104
|
+
...GCP_COMMON,
|
|
746
1105
|
...(provider === "gcp" ? GCP_ONLY : GCP_FIREBASE_ONLY),
|
|
747
1106
|
...TELEMETRY,
|
|
748
1107
|
];
|
|
@@ -342,6 +342,10 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
|
|
|
342
342
|
expect(plan.ok && plan.services.gateway.changes.map((c) => c.name).sort()).toEqual([
|
|
343
343
|
"ADMIN_DATABASE_PASSWORD",
|
|
344
344
|
"ADMIN_DATABASE_URL",
|
|
345
|
+
// #806 — the remote-MCP broker's seal key and (#914) the Google
|
|
346
|
+
// sign-in client file path.
|
|
347
|
+
"CAELO_MCP_OAUTH_SEAL_KEY",
|
|
348
|
+
"CAELO_OPERATOR_OAUTH_CLIENT_FILE",
|
|
345
349
|
// #650 — the staging gate key arrives in the same roll.
|
|
346
350
|
"CAELO_STAGING_GATE_SECRET",
|
|
347
351
|
// #845 — and which forwarded address is the visitor.
|
|
@@ -312,13 +312,14 @@ describe("planContractEnv", () => {
|
|
|
312
312
|
// #650 — the same roll converges the protected staging domain's host
|
|
313
313
|
// and gate key onto an install that predates it.
|
|
314
314
|
expect(plan.services.admin.flags).toEqual([
|
|
315
|
-
"--update-env-vars=CAELO_SITE_URL=https://acme.com,MEDIA_ROOT_DIR=/app/apps/admin/data/media,CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@acme.iam.gserviceaccount.com,CAELO_OPERATOR_ACCESS_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync,CAELO_UPGRADER_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-upgrader,CAELO_AUDIT_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-audit,CAELO_AUDIT_RESULTS_BUCKET=acme-caelo-production-audit-results,CAELO_STAGING_HOST=staging.acme.com",
|
|
315
|
+
"--update-env-vars=CAELO_SITE_URL=https://acme.com,MEDIA_ROOT_DIR=/app/apps/admin/data/media,CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@acme.iam.gserviceaccount.com,CAELO_OPERATOR_ACCESS_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync,CAELO_UPGRADER_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-upgrader,CAELO_CONVERGE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-converge,CAELO_AUDIT_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-audit,CAELO_AUDIT_RESULTS_BUCKET=acme-caelo-production-audit-results,CAELO_OPERATOR_OAUTH_CLIENT_STORE=projects/acme/secrets/caelo-production-operator-oauth-client,CAELO_STAGING_HOST=staging.acme.com",
|
|
316
316
|
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
|
|
317
317
|
]);
|
|
318
318
|
expect(plan.services.gateway.flags).toEqual([
|
|
319
319
|
// The gateway learns which forwarded address is the visitor (#845).
|
|
320
|
-
|
|
321
|
-
"--update-
|
|
320
|
+
// #806 — and where the remote-MCP broker's client file and seal key are.
|
|
321
|
+
"--update-env-vars=CAELO_TRUSTED_PROXY_HOPS=1,CAELO_OPERATOR_OAUTH_CLIENT_FILE=/secrets/operator-oauth-client/client.json",
|
|
322
|
+
"--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest,CAELO_MCP_OAUTH_SEAL_KEY=caelo-production-mcp-oauth-seal-key:latest",
|
|
322
323
|
]);
|
|
323
324
|
});
|
|
324
325
|
|
|
@@ -474,7 +475,11 @@ describe("ensureStackInvariants", () => {
|
|
|
474
475
|
]),
|
|
475
476
|
],
|
|
476
477
|
"secrets get-iam-policy": [
|
|
477
|
-
policy([
|
|
478
|
+
policy([
|
|
479
|
+
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
|
|
480
|
+
// #806 — the admin adds remote-MCP OAuth client versions.
|
|
481
|
+
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
482
|
+
]),
|
|
478
483
|
],
|
|
479
484
|
"storage buckets get-iam-policy": [
|
|
480
485
|
policy([{ role: "roles/storage.objectAdmin", members: [RUN_SA] }]),
|
|
@@ -673,7 +678,10 @@ describe("ensureStackInvariants", () => {
|
|
|
673
678
|
]),
|
|
674
679
|
],
|
|
675
680
|
"secrets get-iam-policy": [
|
|
676
|
-
policy([
|
|
681
|
+
policy([
|
|
682
|
+
{ role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
|
|
683
|
+
{ role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
|
|
684
|
+
]),
|
|
677
685
|
],
|
|
678
686
|
"storage buckets get-iam-policy": [
|
|
679
687
|
policy([
|
|
@@ -779,6 +787,14 @@ describe("ensureStackInvariants", () => {
|
|
|
779
787
|
expect(gatewayGrants).toEqual([
|
|
780
788
|
`secrets add-iam-policy-binding caelo-production-public-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
781
789
|
`secrets add-iam-policy-binding caelo-production-staging-gate-secret --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
790
|
+
// #806 — the remote-MCP broker's seal key and OAuth client file.
|
|
791
|
+
`secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
792
|
+
`secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
|
|
793
|
+
]);
|
|
794
|
+
// #806 — the admin may add versions to the OAuth client secret, and to no other.
|
|
795
|
+
const adders = calls.filter((c) => c.includes("roles/secretmanager.secretVersionAdder"));
|
|
796
|
+
expect(adders).toEqual([
|
|
797
|
+
`secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=${RUN_SA} --role=roles/secretmanager.secretVersionAdder --quiet --format=none`,
|
|
782
798
|
]);
|
|
783
799
|
for (const secret of ["internal-secret", "tool-approval-secret", "secret-kek"]) {
|
|
784
800
|
expect(calls).toContain(
|