@caelo-cms/provisioning 0.10.49 → 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.
- package/caddy/Caddyfile.production +4 -0
- package/caddy/Caddyfile.staging +4 -0
- package/dist/adapter.d.ts +4 -6
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin-internal.d.ts +41 -0
- package/dist/admin-internal.d.ts.map +1 -0
- package/dist/admin-internal.js +169 -0
- package/dist/admin-internal.js.map +1 -0
- package/dist/agent-db.js +93 -20
- package/dist/aws-task-roles.d.ts +27 -0
- package/dist/aws-task-roles.d.ts.map +1 -0
- package/dist/aws-task-roles.js +35 -0
- package/dist/aws-task-roles.js.map +1 -0
- package/dist/caddy-redirects.d.ts +15 -0
- package/dist/caddy-redirects.d.ts.map +1 -0
- package/dist/caddy-redirects.js +65 -0
- package/dist/caddy-redirects.js.map +1 -0
- package/dist/caddy.d.ts +22 -2
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +66 -17
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.d.ts.map +1 -1
- package/dist/cli-routing.js +7 -0
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +282 -255
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +54 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +88 -34
- package/dist/compose.js.map +1 -1
- package/dist/deployed-release.d.ts +3 -2
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +12 -5
- package/dist/deployed-release.js.map +1 -1
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +6 -3
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/gcloud.d.ts +2 -2
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +3 -3
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +12 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +21 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +27 -0
- package/dist/gcp-url-map.d.ts.map +1 -0
- package/dist/gcp-url-map.js +62 -0
- package/dist/gcp-url-map.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +20 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +44 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/owner-reset.d.ts +37 -0
- package/dist/owner-reset.d.ts.map +1 -0
- package/dist/owner-reset.js +80 -0
- package/dist/owner-reset.js.map +1 -0
- package/dist/public-role-isolation.d.ts +19 -0
- package/dist/public-role-isolation.d.ts.map +1 -0
- package/dist/public-role-isolation.js +69 -0
- package/dist/public-role-isolation.js.map +1 -0
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/self-hosted-backup.d.ts +86 -0
- package/dist/self-hosted-backup.d.ts.map +1 -0
- package/dist/self-hosted-backup.js +350 -0
- package/dist/self-hosted-backup.js.map +1 -0
- package/dist/self-hosted-release.d.ts +12 -3
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -4
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-start.d.ts +50 -0
- package/dist/self-hosted-start.d.ts.map +1 -0
- package/dist/self-hosted-start.js +59 -0
- package/dist/self-hosted-start.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +30 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +29 -7
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +9 -10
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +21 -10
- package/dist/stack-contract.js.map +1 -1
- package/dist/staging-admin.d.ts +12 -20
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-admin.js +7 -159
- package/dist/staging-admin.js.map +1 -1
- package/dist/tar-entries.d.ts +14 -0
- package/dist/tar-entries.d.ts.map +1 -0
- package/dist/tar-entries.js +104 -0
- package/dist/tar-entries.js.map +1 -0
- package/dist/upgrade/converge.d.ts +2 -2
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +44 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +2 -1
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/url-map-redirects-access.d.ts +15 -0
- package/dist/url-map-redirects-access.d.ts.map +1 -0
- package/dist/url-map-redirects-access.js +81 -0
- package/dist/url-map-redirects-access.js.map +1 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -1
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.js +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +45 -0
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +4 -6
- package/src/admin-internal.ts +212 -0
- package/src/admin-memory.test.ts +2 -2
- package/src/aws-task-roles.ts +58 -0
- package/src/caddy-redirects.docker.test.ts +255 -0
- package/src/caddy-redirects.test.ts +115 -0
- package/src/caddy-redirects.ts +68 -0
- package/src/caddy-validate.test.ts +119 -0
- package/src/caddy.test.ts +108 -0
- package/src/caddy.ts +89 -20
- package/src/checkpoint-secrets.test.ts +0 -1
- package/src/cli-routing.test.ts +12 -0
- package/src/cli-routing.ts +7 -0
- package/src/cli.ts +340 -288
- package/src/compose.ts +108 -36
- package/src/deployed-release.test.ts +22 -4
- package/src/deployed-release.ts +14 -7
- package/src/gateway-credentials.ts +6 -3
- package/src/gateway-env-isolation.test.ts +212 -0
- package/src/gcloud.ts +5 -3
- package/src/gcp-names.ts +23 -0
- package/src/gcp-url-map.test.ts +150 -0
- package/src/gcp-url-map.ts +81 -0
- package/src/index.test.ts +24 -152
- package/src/index.ts +0 -9
- package/src/infra-contract.test.ts +40 -0
- package/src/infra-contract.ts +47 -1
- package/src/owner-reset.test.ts +165 -0
- package/src/owner-reset.ts +106 -0
- package/src/public-role-isolation.test.ts +89 -0
- package/src/public-role-isolation.ts +83 -0
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +12 -0
- package/src/release-manifest.ts +2 -1
- package/src/runtime-secrets.test.ts +25 -8
- package/src/self-hosted-backup.test.ts +344 -0
- package/src/self-hosted-backup.ts +480 -0
- package/src/self-hosted-compose.test.ts +88 -4
- package/src/self-hosted-release.ts +22 -7
- package/src/self-hosted-start.test.ts +68 -0
- package/src/self-hosted-start.ts +123 -0
- package/src/self-hosted-upgrade.ts +35 -8
- package/src/stack-contract.test.ts +11 -20
- package/src/stack-contract.ts +25 -16
- package/src/stack-converge-secrets.test.ts +45 -10
- package/src/stack-converge.test.ts +19 -10
- package/src/staging-admin.ts +18 -181
- package/src/tar-entries.test.ts +52 -0
- package/src/tar-entries.ts +116 -0
- package/src/upgrade/converge.ts +51 -4
- package/src/upgrade/upgrade.test.ts +78 -1
- package/src/upgrader-assess.test.ts +22 -0
- package/src/upgrader-assess.ts +2 -1
- package/src/url-map-redirects-access.test.ts +88 -0
- package/src/url-map-redirects-access.ts +89 -0
- package/src/wizard.ts +3 -1
- package/src/wizards/gcp-cost.ts +1 -1
- package/src/wizards/gcp.ts +48 -0
- package/stacks/aws/README.md +1 -1
- package/stacks/aws/index.ts +89 -45
- package/stacks/azure/README.md +1 -1
- package/stacks/azure/index.ts +21 -14
- package/stacks/gcp/README.md +1 -1
- package/stacks/gcp/index.ts +26 -22
- package/stacks/gcp-firebase/index.ts +7 -3
- package/stacks/self-hosted/Pulumi.yaml +2 -2
- package/stacks/self-hosted/README.md +3 -3
- package/stacks/self-hosted/index.ts +45 -22
- package/dist/redirects-emit.d.ts +0 -52
- package/dist/redirects-emit.d.ts.map +0 -1
- package/dist/redirects-emit.js +0 -111
- package/dist/redirects-emit.js.map +0 -1
- package/src/redirects-emit.ts +0 -175
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
:80 {
|
|
12
12
|
root * /srv/current
|
|
13
|
+
# #587 — the redirects table of the build served here (the generator
|
|
14
|
+
# writes it; `*` so a fresh checkout without a build still starts).
|
|
15
|
+
# docker-compose.yml reloads Caddy when it changes (caddy-redirects.ts).
|
|
16
|
+
import /srv/current/_redirects.caddy*
|
|
13
17
|
# Content-hashed build outputs (fonts, plugin bundles, media) never
|
|
14
18
|
# change under the same URL — cache them for a year. Keep this pattern in
|
|
15
19
|
# sync with CONTENT_HASHED_PATH_PATTERN in @caelo-cms/shared
|
package/caddy/Caddyfile.staging
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
:80 {
|
|
16
16
|
root * /srv/current
|
|
17
|
+
# #587 — the redirects table of the build served here (the generator
|
|
18
|
+
# writes it; `*` so a fresh checkout without a build still starts).
|
|
19
|
+
# docker-compose.yml reloads Caddy when it changes (caddy-redirects.ts).
|
|
20
|
+
import /srv/current/_redirects.caddy*
|
|
17
21
|
# Content-hashed build outputs (fonts, plugin bundles, media) never
|
|
18
22
|
# change under the same URL — cache them for a year. Keep this pattern in
|
|
19
23
|
# sync with CONTENT_HASHED_PATH_PATTERN in @caelo-cms/shared
|
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
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
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. */
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -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
|
|
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":"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"}
|
package/dist/agent-db.js
CHANGED
|
@@ -8083,6 +8083,10 @@ var pageLogEntrySchema = object({
|
|
|
8083
8083
|
// ../shared/src/proposal-status.ts
|
|
8084
8084
|
var PROPOSAL_STATUSES = ["pending", "applied", "rejected", "superseded"];
|
|
8085
8085
|
var proposalStatus = _enum(PROPOSAL_STATUSES);
|
|
8086
|
+
// ../shared/src/redirect-emit-cloudfront.ts
|
|
8087
|
+
var CLOUDFRONT_KVS_MAX_TOTAL_BYTES = 5 * 1024 * 1024;
|
|
8088
|
+
// ../shared/src/redirect-plan.ts
|
|
8089
|
+
var REDIRECT_STATUSES = new Set([301, 302, 307, 308]);
|
|
8086
8090
|
// ../shared/src/release-manifest.ts
|
|
8087
8091
|
var RELEASE_CHANNELS = ["stable", "rc", "beta", "alpha"];
|
|
8088
8092
|
var IMAGE_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
@@ -17773,6 +17777,13 @@ function isRlsDenial(error) {
|
|
|
17773
17777
|
// ../query-api/src/adapter.ts
|
|
17774
17778
|
var SQL3 = globalThis.Bun?.SQL;
|
|
17775
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
|
+
}
|
|
17776
17787
|
function resolvePoolMax(explicit) {
|
|
17777
17788
|
if (explicit !== undefined)
|
|
17778
17789
|
return validatePoolMax(explicit, "poolMax option");
|
|
@@ -17796,19 +17807,41 @@ class DatabaseAdapter {
|
|
|
17796
17807
|
#skipVerify;
|
|
17797
17808
|
#roles;
|
|
17798
17809
|
#verifyPromise = null;
|
|
17810
|
+
#projectionPublisher = null;
|
|
17799
17811
|
constructor(config) {
|
|
17800
|
-
|
|
17801
|
-
if (
|
|
17802
|
-
|
|
17803
|
-
this.#publicRaw = new SQL3(config.publicDatabaseUrl);
|
|
17804
|
-
} else {
|
|
17805
|
-
this.#adminRaw = new SQL3(config.adminDatabaseUrl, { max: poolMax });
|
|
17806
|
-
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");
|
|
17807
17815
|
}
|
|
17808
|
-
|
|
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);
|
|
17809
17821
|
this.#public = drizzle(this.#publicRaw);
|
|
17810
17822
|
this.#skipVerify = config.skipRoleVerification === true;
|
|
17811
|
-
|
|
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;
|
|
17812
17845
|
}
|
|
17813
17846
|
verifyRoles() {
|
|
17814
17847
|
if (this.#verifyPromise !== null)
|
|
@@ -17820,9 +17853,11 @@ class DatabaseAdapter {
|
|
|
17820
17853
|
return this.#verifyPromise;
|
|
17821
17854
|
}
|
|
17822
17855
|
async#verifyRolesOnce() {
|
|
17823
|
-
|
|
17824
|
-
|
|
17825
|
-
|
|
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
|
+
}
|
|
17826
17861
|
}
|
|
17827
17862
|
const pub = await this.#identity(this.#publicRaw);
|
|
17828
17863
|
if (pub.database !== "cms_public") {
|
|
@@ -17842,9 +17877,11 @@ class DatabaseAdapter {
|
|
|
17842
17877
|
async runOperation(op, ctx, validatedInput) {
|
|
17843
17878
|
if (!this.#skipVerify)
|
|
17844
17879
|
await this.verifyRoles();
|
|
17845
|
-
const db = op.database === "cms_admin" ? this.#
|
|
17880
|
+
const db = op.database === "cms_admin" ? this.#adminDb(`operation ${op.name}`) : this.#public;
|
|
17881
|
+
let publishAfterCommit = false;
|
|
17882
|
+
let result;
|
|
17846
17883
|
try {
|
|
17847
|
-
|
|
17884
|
+
result = await db.transaction(async (tx) => {
|
|
17848
17885
|
await tx.execute(sql`SELECT set_config('caelo.actor_id', ${ctx.actorId}, true)`);
|
|
17849
17886
|
await tx.execute(sql`SELECT set_config('caelo.actor_kind', ${ctx.actorKind}, true)`);
|
|
17850
17887
|
await tx.execute(sql`SELECT set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true)`);
|
|
@@ -17855,7 +17892,10 @@ class DatabaseAdapter {
|
|
|
17855
17892
|
if (denied !== null)
|
|
17856
17893
|
return err(branchNotFound(op.name, denied));
|
|
17857
17894
|
}
|
|
17858
|
-
|
|
17895
|
+
const out = await op.handler(ctx, validatedInput, tx);
|
|
17896
|
+
if (op.database === "cms_admin")
|
|
17897
|
+
publishAfterCommit = await this.#projectionDirty(tx);
|
|
17898
|
+
return out;
|
|
17859
17899
|
});
|
|
17860
17900
|
} catch (thrown) {
|
|
17861
17901
|
if (thrown instanceof OperationAbortError) {
|
|
@@ -17876,14 +17916,28 @@ class DatabaseAdapter {
|
|
|
17876
17916
|
...pgDetail ? { pgDetail } : {}
|
|
17877
17917
|
});
|
|
17878
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;
|
|
17879
17931
|
}
|
|
17880
17932
|
async close() {
|
|
17881
|
-
await Promise.all([this.#adminRaw
|
|
17933
|
+
await Promise.all([this.#adminRaw?.end(), this.#publicRaw.end()]);
|
|
17882
17934
|
}
|
|
17883
17935
|
async withAdminTransaction(ctx, fn) {
|
|
17936
|
+
const admin = this.#adminDb("withAdminTransaction");
|
|
17884
17937
|
if (!this.#skipVerify)
|
|
17885
17938
|
await this.verifyRoles();
|
|
17886
|
-
|
|
17939
|
+
let publishAfterCommit = false;
|
|
17940
|
+
const out = await admin.transaction(async (tx) => {
|
|
17887
17941
|
await tx.execute(sql`SELECT set_config('caelo.actor_id', ${ctx.actorId}, true)`);
|
|
17888
17942
|
await tx.execute(sql`SELECT set_config('caelo.actor_kind', ${ctx.actorKind}, true)`);
|
|
17889
17943
|
await tx.execute(sql`SELECT set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true)`);
|
|
@@ -17893,10 +17947,27 @@ class DatabaseAdapter {
|
|
|
17893
17947
|
if (denied !== null) {
|
|
17894
17948
|
throw new OperationAbortError(branchNotFound("withAdminTransaction", denied));
|
|
17895
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)`);
|
|
17896
17965
|
return await fn(tx);
|
|
17897
17966
|
});
|
|
17898
17967
|
}
|
|
17899
17968
|
rawAdmin() {
|
|
17969
|
+
if (this.#adminRaw === null)
|
|
17970
|
+
throw new AdminDatabaseUnavailableError("rawAdmin");
|
|
17900
17971
|
return this.#adminRaw;
|
|
17901
17972
|
}
|
|
17902
17973
|
rawPublic() {
|
|
@@ -17912,21 +17983,23 @@ class DatabaseAdapter {
|
|
|
17912
17983
|
});
|
|
17913
17984
|
}
|
|
17914
17985
|
async provisionPluginAdminSchema(opts) {
|
|
17986
|
+
const admin = this.#adminDb("provisionPluginAdminSchema");
|
|
17915
17987
|
if (!this.#skipVerify)
|
|
17916
17988
|
await this.verifyRoles();
|
|
17917
|
-
await
|
|
17989
|
+
await admin.transaction(async (tx) => {
|
|
17918
17990
|
await tx.execute(sql.raw("SELECT set_config('caelo.actor_kind', 'system', true)"));
|
|
17919
17991
|
await tx.execute(sql.raw(`SELECT set_config('caelo.plugin_id', '${opts.pluginId}', true)`));
|
|
17920
17992
|
await tx.execute(sql.raw(opts.sql));
|
|
17921
17993
|
});
|
|
17922
17994
|
}
|
|
17923
17995
|
async dropPluginAdminSchema(opts) {
|
|
17996
|
+
const admin = this.#adminDb("dropPluginAdminSchema");
|
|
17924
17997
|
if (!this.#skipVerify)
|
|
17925
17998
|
await this.verifyRoles();
|
|
17926
17999
|
if (!/^plugin_[a-z][a-z0-9_]*$/.test(opts.schemaName)) {
|
|
17927
18000
|
throw new Error(`dropPluginAdminSchema: refusing to drop schema "${opts.schemaName}" \u2014 name doesn't match plugin_<slug> pattern`);
|
|
17928
18001
|
}
|
|
17929
|
-
await
|
|
18002
|
+
await admin.transaction(async (tx) => {
|
|
17930
18003
|
await tx.execute(sql.raw("SELECT set_config('caelo.actor_kind', 'system', true)"));
|
|
17931
18004
|
await tx.execute(sql.raw(`DROP SCHEMA IF EXISTS "${opts.schemaName}" CASCADE`));
|
|
17932
18005
|
});
|
|
@@ -17943,7 +18016,7 @@ class DatabaseAdapter {
|
|
|
17943
18016
|
});
|
|
17944
18017
|
}
|
|
17945
18018
|
get admin() {
|
|
17946
|
-
return this.#admin;
|
|
18019
|
+
return this.#adminDb("admin pool");
|
|
17947
18020
|
}
|
|
17948
18021
|
get public() {
|
|
17949
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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The container's shell script: Caddy in the foreground of the container's
|
|
3
|
+
* lifetime, reloaded when any file matching `snippetGlob` changes.
|
|
4
|
+
*
|
|
5
|
+
* @param snippetGlob Shell glob of the imported snippets, e.g.
|
|
6
|
+
* `/srv/caelo/output/*\/current/_redirects.caddy`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function caddyRedirectReloadScript(snippetGlob: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* A Compose `command:` line running {@link caddyRedirectReloadScript}. `$` is
|
|
11
|
+
* doubled because Compose interpolates `$VAR` in the file; the JSON string is
|
|
12
|
+
* a valid YAML double-quoted scalar.
|
|
13
|
+
*/
|
|
14
|
+
export declare function caddyRedirectReloadCommand(snippetGlob: string): string;
|
|
15
|
+
//# sourceMappingURL=caddy-redirects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caddy-redirects.d.ts","sourceRoot":"","sources":["../src/caddy-redirects.ts"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAuBrE;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAGtE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* How the self-hosted Caddy picks up a deploy's redirect snippet (#587).
|
|
4
|
+
*
|
|
5
|
+
* Each public site block imports `<site root>/_redirects.caddy`, which the
|
|
6
|
+
* static generator rewrites on every build. Caddy reads imports only when it
|
|
7
|
+
* loads a config, so something must reload it after a deploy. The container
|
|
8
|
+
* does that itself: its command runs Caddy plus a small loop that watches a
|
|
9
|
+
* checksum of the snippets and runs `caddy reload` (against Caddy's own
|
|
10
|
+
* localhost admin endpoint, inside the container) when they change.
|
|
11
|
+
*
|
|
12
|
+
* Why not the alternatives:
|
|
13
|
+
* - `caddy run --watch` stops watching for good after the first config that
|
|
14
|
+
* fails to load — a snippet read while a deploy is still copying it would
|
|
15
|
+
* freeze the redirects until Caddy restarts.
|
|
16
|
+
* - The admin reloading Caddy through the admin API would need a channel
|
|
17
|
+
* from the admin into Caddy and would post a file's content over HTTP.
|
|
18
|
+
*
|
|
19
|
+
* The loop waits until the checksum is stable across two reads before
|
|
20
|
+
* reloading (a deploy may be mid-copy), and a reload Caddy rejects only
|
|
21
|
+
* logs: Caddy keeps the previous config, and the next change retries.
|
|
22
|
+
*/
|
|
23
|
+
/** Seconds between checksum polls. */
|
|
24
|
+
const POLL_SECONDS = 2;
|
|
25
|
+
/**
|
|
26
|
+
* The container's shell script: Caddy in the foreground of the container's
|
|
27
|
+
* lifetime, reloaded when any file matching `snippetGlob` changes.
|
|
28
|
+
*
|
|
29
|
+
* @param snippetGlob Shell glob of the imported snippets, e.g.
|
|
30
|
+
* `/srv/caelo/output/*\/current/_redirects.caddy`.
|
|
31
|
+
*/
|
|
32
|
+
export function caddyRedirectReloadScript(snippetGlob) {
|
|
33
|
+
if (/[\s'"`$\\;&|<>]/.test(snippetGlob)) {
|
|
34
|
+
throw new Error(`caddy-redirects: unsafe snippet glob ${JSON.stringify(snippetGlob)}`);
|
|
35
|
+
}
|
|
36
|
+
const config = "--config /etc/caddy/Caddyfile --adapter caddyfile";
|
|
37
|
+
return [
|
|
38
|
+
`sum() { cat ${snippetGlob} 2>/dev/null | md5sum; }`,
|
|
39
|
+
"last=$(sum)",
|
|
40
|
+
`caddy run ${config} &`,
|
|
41
|
+
"pid=$!",
|
|
42
|
+
`trap 'kill -TERM "$pid" 2>/dev/null; wait "$pid"; exit $?' TERM INT`,
|
|
43
|
+
`while kill -0 "$pid" 2>/dev/null; do`,
|
|
44
|
+
` sleep ${POLL_SECONDS}`,
|
|
45
|
+
" now=$(sum)",
|
|
46
|
+
` [ "$now" = "$last" ] && continue`,
|
|
47
|
+
// A deploy may still be copying the file: reload only a stable one.
|
|
48
|
+
" sleep 1",
|
|
49
|
+
` [ "$(sum)" = "$now" ] || continue`,
|
|
50
|
+
" last=$now",
|
|
51
|
+
` caddy reload ${config} || echo "caelo: redirects changed but Caddy rejected the reload; still serving the previous redirects" >&2`,
|
|
52
|
+
"done",
|
|
53
|
+
`wait "$pid"`,
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A Compose `command:` line running {@link caddyRedirectReloadScript}. `$` is
|
|
58
|
+
* doubled because Compose interpolates `$VAR` in the file; the JSON string is
|
|
59
|
+
* a valid YAML double-quoted scalar.
|
|
60
|
+
*/
|
|
61
|
+
export function caddyRedirectReloadCommand(snippetGlob) {
|
|
62
|
+
const script = caddyRedirectReloadScript(snippetGlob).replaceAll("$", () => "$$");
|
|
63
|
+
return `command: ["/bin/sh", "-c", ${JSON.stringify(script)}]`;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=caddy-redirects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caddy-redirects.js","sourceRoot":"","sources":["../src/caddy-redirects.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAEnC;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,sCAAsC;AACtC,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,WAAmB;IAC3D,IAAI,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACzF,CAAC;IACD,MAAM,MAAM,GAAG,mDAAmD,CAAC;IACnE,OAAO;QACL,eAAe,WAAW,0BAA0B;QACpD,aAAa;QACb,aAAa,MAAM,IAAI;QACvB,QAAQ;QACR,qEAAqE;QACrE,sCAAsC;QACtC,WAAW,YAAY,EAAE;QACzB,cAAc;QACd,oCAAoC;QACpC,oEAAoE;QACpE,WAAW;QACX,qCAAqC;QACrC,aAAa;QACb,kBAAkB,MAAM,6GAA6G;QACrI,MAAM;QACN,aAAa;KACd,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CAAC,WAAmB;IAC5D,MAAM,MAAM,GAAG,yBAAyB,CAAC,WAAW,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAClF,OAAO,8BAA8B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC;AACjE,CAAC"}
|