@wildo-ai/platform-infra-providers 1.1.7 → 1.1.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"aws-s3.infra.module.d.ts","sourceRoot":"","sources":["../../../src/aws-s3/aws-s3.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAIL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAEvC,eAAO,MAAM,wBAAwB,EAAE,mBA2DtC,CAAC"}
1
+ {"version":3,"file":"aws-s3.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/aws-s3/aws-s3.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAIL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAEvC,eAAO,MAAM,wBAAwB,EAAE,mBA2DtC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"aws-s3.infra.module.js","sourceRoot":"","sources":["../../../src/aws-s3/aws-s3.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EACL,eAAe,EACf,0BAA0B,EAC1B,aAAa,GAGd,MAAM,+BAA+B,CAAC;AAEvC,MAAM,CAAC,MAAM,wBAAwB,GAAwB;IAC3D,QAAQ,EAAE,aAAa,CAAC,MAAM;IAC9B,qBAAqB,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC;IAEvD;;;;OAIG;IACH,gBAAgB,EACd,iGAAiG;UAC/F,4CAA4C;IAEhD;;;;OAIG;IACH,2BAA2B,EAAE,EAAE;IAE/B,IAAI;QACF,OAAO;YACL,4CAA4C;YAC5C,iCAAiC;YACjC,aAAa,EAAE;gBACb,OAAO,EAAE;oBACP,gBAAgB,EAAE,IAAI;oBACtB,6DAA6D;oBAC7D,yDAAyD;oBACzD,8CAA8C;oBAC9C,eAAe,EAAE,MAAM;iBACxB;gBACD,aAAa,EAAE;oBACb,oBAAoB;oBACpB,oBAAoB;oBACpB,uDAAuD;oBACvD,yDAAyD;oBACzD,yDAAyD;oBACzD,2DAA2D;oBAC3D,qDAAqD;oBACrD,iCAAiC;oBACjC,gBAAgB;iBACjB;gBACD,SAAS,EAAE;oBACT,0BAA0B,CAAC,YAAY;oBACvC,0BAA0B,CAAC,SAAS;iBACrC;aACF;YACD,gEAAgE;YAChE,oEAAoE;YACpE,mEAAmE;YACnE,oEAAoE;SACrE,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,oEAAoE;IACpE,8EAA8E;IAC9E,0DAA0D;CAC3D,CAAC","sourcesContent":["/**\n * `AwsS3InfraProviderModule` — seventh concrete `InfraProviderModule`\n * (Slice 5.1 of `infra-provider-planner.md`).\n *\n * Implements the `AWS_S3` `InfraProvider`. Shares the one-bucket,\n * prefix-namespaced object layout of `MinioSelfInfraProviderModule` —\n * the only differences are (a) no compose / K8s contribution (S3 is\n * managed), and (b) the bucket endpoint is the regional AWS hostname\n * instead of the in-cluster `minio` service.\n *\n * ## ⚠️ NOT USABLE TODAY — selecting this provider leaves apps with no\n * storage credential (2026-08-10)\n *\n * The application-tier `STORAGE_ACCESS_KEY` / `STORAGE_SECRET_KEY` env\n * vars were removed **for every provider**: they were the object store's\n * ROOT pair, handed to every app, minion and worker. Applications now\n * obtain short-lived, prefix-confined credentials from apps-manager.\n *\n * That replacement is implemented for **MinIO only**. apps-manager's\n * `ObjectStorageStsBrokerService` speaks MinIO's STS `AssumeRole` with an\n * inline session policy; AWS STS requires a role ARN the broker does not\n * supply, so an `AWS_S3` deployment would fail at storage initialisation\n * with no credential and no path to one.\n *\n * No environment selects this provider today (every example uses\n * `BackingServiceSource.SELF` → MinIO), which is why this is recorded as\n * a gap rather than an outage. `s3: { source: MANAGED }` and\n * `provider: 'aws'` still RESOLVE here, but since #1693 such a\n * declaration is refused at authoring time: `unservableReason` below\n * takes this provider out of the servable set until the AWS STS path\n * exists. Tracked in\n * `.claude/plans/object-storage-root-credential-distribution.md`.\n *\n * The module still reuses the `STORAGE_*` env contract for the\n * NON-secret values (endpoint, region, bucket, provider); it is only the\n * credential half that is gone.\n *\n * **Why this module exists despite \"no compose / no mint\"**: it\n * gives the lockfile a stable digest entry (so a switch from\n * `minio-self` to `aws-s3` shows up as a `wildo lock check` drift)\n * AND it formalizes which env vars the operator must set for AWS\n * (`STORAGE_REGION` is mandatory; `STORAGE_ENDPOINT` is optional —\n * defaults to the AWS regional endpoint when unset).\n *\n * **No mint methods here** — and the reason is no longer the one this\n * block used to give. It said per-app credentials were out of scope\n * \"because the engine adapter uses a single shared SDK client\", which\n * stopped being true when the adapter moved to a credential provider that\n * rebuilds its client whenever the credential changes. Per-application\n * isolation exists; what is missing for AWS is only the broker's ability\n * to speak AWS STS.\n *\n * Nor should the missing piece be per-app IAM users or roles, which is\n * what this block previously anticipated. The MinIO path deliberately\n * rejected long-lived per-principal accounts in favour of `AssumeRole`\n * with an inline session policy — nothing long-lived exists to leak, and\n * credential-rotation safety dissolves instead of needing a design. The\n * AWS equivalent is the same shape: `AssumeRole` against a role ARN the\n * operator supplies, with a session policy confining the principal to\n * `applications/<applicationId>/`. That role ARN is the one input the\n * broker does not have today, and is the whole of the gap.\n */\n\nimport {\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n} from '@wildo-ai/platform-config-lib';\n\nexport const AwsS3InfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.AWS_S3,\n supportedCapabilities: [InfraCapability.OBJECT_STORAGE],\n\n /**\n * What the ⚠️ block above says, made mechanical: a declaration resolving here (`s3: { source:\n * MANAGED }`, or `provider: 'aws'`) is refused by `wildo config validate` / `config sync` /\n * `lock check` until this field is deleted — in the change that teaches the broker AWS STS (#14).\n */\n unservableReason:\n 'The object-storage credential broker speaks only MinIO AssumeRole, so an application on AWS S3 '\n + 'would receive no storage credential (#14).',\n\n /**\n * Shared bucket, shared access key — no per-app credential is minted, so there is nothing\n * per-principal to confine. Confinement for object storage is done by STS session policy at\n * request time, not by a secret in the application blob.\n */\n perPrincipalSecretBlobPaths: [],\n\n plan(): InfraProviderPlanContribution {\n return {\n // No compose service — S3 is fully managed.\n // No K8s manifest — same reason.\n platformAdmin: {\n envVars: {\n STORAGE_PROVIDER: 's3',\n // Endpoint and port have AWS defaults; operator may override\n // for region-specific or VPC-endpoint topologies via the\n // env's authored `backingServices.s3` config.\n STORAGE_USE_SSL: 'true',\n },\n secretEnvVars: [\n 'STORAGE_ACCESS_KEY',\n 'STORAGE_SECRET_KEY',\n // Region is sensitive-ish (deployment topology) and is\n // delivered via the secret env channel for symmetry with\n // the access keys, even though it's not strictly secret.\n // The legacy treatment was env-var, not secret-env-var; if\n // operators object, this can flip to `envVars` later\n // without breaking the contract.\n 'STORAGE_REGION',\n ],\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n // Same shared bucket as MinIO_Self. Apps receive the NON-secret\n // `STORAGE_*` values via the bootstrap env contract; the credential\n // half is no longer emitted for any provider — see the ⚠️ block in\n // this module's JSDoc for why that makes AWS_S3 currently unusable.\n };\n },\n\n // No `mintAppCredentials` / `revokeAppCredentials` — not because the model\n // is shared-bucket (it is, but that is not the reason), but because\n // per-application isolation is delivered by BROKERED STS SESSIONS rather than\n // by minted long-lived accounts. See this module's JSDoc.\n};\n"]}
1
+ {"version":3,"file":"aws-s3.infra.module.js","sourceRoot":"","sources":["../../../../src/aws-s3/aws-s3.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EACL,eAAe,EACf,0BAA0B,EAC1B,aAAa,GAGd,MAAM,+BAA+B,CAAC;AAEvC,MAAM,CAAC,MAAM,wBAAwB,GAAwB;IAC3D,QAAQ,EAAE,aAAa,CAAC,MAAM;IAC9B,qBAAqB,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC;IAEvD;;;;OAIG;IACH,gBAAgB,EACd,iGAAiG;UAC/F,4CAA4C;IAEhD;;;;OAIG;IACH,2BAA2B,EAAE,EAAE;IAE/B,IAAI;QACF,OAAO;YACL,4CAA4C;YAC5C,iCAAiC;YACjC,aAAa,EAAE;gBACb,OAAO,EAAE;oBACP,gBAAgB,EAAE,IAAI;oBACtB,6DAA6D;oBAC7D,yDAAyD;oBACzD,8CAA8C;oBAC9C,eAAe,EAAE,MAAM;iBACxB;gBACD,aAAa,EAAE;oBACb,oBAAoB;oBACpB,oBAAoB;oBACpB,uDAAuD;oBACvD,yDAAyD;oBACzD,yDAAyD;oBACzD,2DAA2D;oBAC3D,qDAAqD;oBACrD,iCAAiC;oBACjC,gBAAgB;iBACjB;gBACD,SAAS,EAAE;oBACT,0BAA0B,CAAC,YAAY;oBACvC,0BAA0B,CAAC,SAAS;iBACrC;aACF;YACD,gEAAgE;YAChE,oEAAoE;YACpE,mEAAmE;YACnE,oEAAoE;SACrE,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,oEAAoE;IACpE,8EAA8E;IAC9E,0DAA0D;CAC3D,CAAC","sourcesContent":["/**\n * `AwsS3InfraProviderModule` — seventh concrete `InfraProviderModule`\n * (Slice 5.1 of `infra-provider-planner.md`).\n *\n * Implements the `AWS_S3` `InfraProvider`. Shares the one-bucket,\n * prefix-namespaced object layout of `MinioSelfInfraProviderModule` —\n * the only differences are (a) no compose / K8s contribution (S3 is\n * managed), and (b) the bucket endpoint is the regional AWS hostname\n * instead of the in-cluster `minio` service.\n *\n * ## ⚠️ NOT USABLE TODAY — selecting this provider leaves apps with no\n * storage credential (2026-08-10)\n *\n * The application-tier `STORAGE_ACCESS_KEY` / `STORAGE_SECRET_KEY` env\n * vars were removed **for every provider**: they were the object store's\n * ROOT pair, handed to every app, minion and worker. Applications now\n * obtain short-lived, prefix-confined credentials from apps-manager.\n *\n * That replacement is implemented for **MinIO only**. apps-manager's\n * `ObjectStorageStsBrokerService` speaks MinIO's STS `AssumeRole` with an\n * inline session policy; AWS STS requires a role ARN the broker does not\n * supply, so an `AWS_S3` deployment would fail at storage initialisation\n * with no credential and no path to one.\n *\n * No environment selects this provider today (every example uses\n * `BackingServiceSource.SELF` → MinIO), which is why this is recorded as\n * a gap rather than an outage. `s3: { source: MANAGED }` and\n * `provider: 'aws'` still RESOLVE here, but since #1693 such a\n * declaration is refused at authoring time: `unservableReason` below\n * takes this provider out of the servable set until the AWS STS path\n * exists. Tracked in\n * `.claude/plans/object-storage-root-credential-distribution.md`.\n *\n * The module still reuses the `STORAGE_*` env contract for the\n * NON-secret values (endpoint, region, bucket, provider); it is only the\n * credential half that is gone.\n *\n * **Why this module exists despite \"no compose / no mint\"**: it\n * gives the lockfile a stable digest entry (so a switch from\n * `minio-self` to `aws-s3` shows up as a `wildo lock check` drift)\n * AND it formalizes which env vars the operator must set for AWS\n * (`STORAGE_REGION` is mandatory; `STORAGE_ENDPOINT` is optional —\n * defaults to the AWS regional endpoint when unset).\n *\n * **No mint methods here** — and the reason is no longer the one this\n * block used to give. It said per-app credentials were out of scope\n * \"because the engine adapter uses a single shared SDK client\", which\n * stopped being true when the adapter moved to a credential provider that\n * rebuilds its client whenever the credential changes. Per-application\n * isolation exists; what is missing for AWS is only the broker's ability\n * to speak AWS STS.\n *\n * Nor should the missing piece be per-app IAM users or roles, which is\n * what this block previously anticipated. The MinIO path deliberately\n * rejected long-lived per-principal accounts in favour of `AssumeRole`\n * with an inline session policy — nothing long-lived exists to leak, and\n * credential-rotation safety dissolves instead of needing a design. The\n * AWS equivalent is the same shape: `AssumeRole` against a role ARN the\n * operator supplies, with a session policy confining the principal to\n * `applications/<applicationId>/`. That role ARN is the one input the\n * broker does not have today, and is the whole of the gap.\n */\n\nimport {\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n} from '@wildo-ai/platform-config-lib';\n\nexport const AwsS3InfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.AWS_S3,\n supportedCapabilities: [InfraCapability.OBJECT_STORAGE],\n\n /**\n * What the ⚠️ block above says, made mechanical: a declaration resolving here (`s3: { source:\n * MANAGED }`, or `provider: 'aws'`) is refused by `wildo config validate` / `config sync` /\n * `lock check` until this field is deleted — in the change that teaches the broker AWS STS (#14).\n */\n unservableReason:\n 'The object-storage credential broker speaks only MinIO AssumeRole, so an application on AWS S3 '\n + 'would receive no storage credential (#14).',\n\n /**\n * Shared bucket, shared access key — no per-app credential is minted, so there is nothing\n * per-principal to confine. Confinement for object storage is done by STS session policy at\n * request time, not by a secret in the application blob.\n */\n perPrincipalSecretBlobPaths: [],\n\n plan(): InfraProviderPlanContribution {\n return {\n // No compose service — S3 is fully managed.\n // No K8s manifest — same reason.\n platformAdmin: {\n envVars: {\n STORAGE_PROVIDER: 's3',\n // Endpoint and port have AWS defaults; operator may override\n // for region-specific or VPC-endpoint topologies via the\n // env's authored `backingServices.s3` config.\n STORAGE_USE_SSL: 'true',\n },\n secretEnvVars: [\n 'STORAGE_ACCESS_KEY',\n 'STORAGE_SECRET_KEY',\n // Region is sensitive-ish (deployment topology) and is\n // delivered via the secret env channel for symmetry with\n // the access keys, even though it's not strictly secret.\n // The legacy treatment was env-var, not secret-env-var; if\n // operators object, this can flip to `envVars` later\n // without breaking the contract.\n 'STORAGE_REGION',\n ],\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n // Same shared bucket as MinIO_Self. Apps receive the NON-secret\n // `STORAGE_*` values via the bootstrap env contract; the credential\n // half is no longer emitted for any provider — see the ⚠️ block in\n // this module's JSDoc for why that makes AWS_S3 currently unusable.\n };\n },\n\n // No `mintAppCredentials` / `revokeAppCredentials` — not because the model\n // is shared-bucket (it is, but that is not the reason), but because\n // per-application isolation is delivered by BROKERED STS SESSIONS rather than\n // by minted long-lived accounts. See this module's JSDoc.\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"clamav-self.infra.module.d.ts","sourceRoot":"","sources":["../../../src/clamav-self/clamav-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAIL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAIvC,eAAO,MAAM,6BAA6B,EAAE,mBAiC3C,CAAC"}
1
+ {"version":3,"file":"clamav-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/clamav-self/clamav-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAIL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAIvC,eAAO,MAAM,6BAA6B,EAAE,mBAiC3C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"immudb-self.infra.module.d.ts","sourceRoot":"","sources":["../../../src/immudb-self/immudb-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAIH,OAAO,EASL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAKvC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAC/D,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAC/D,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAiB/D;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMnE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,oBAAoB,EAAE,MAAM,GAAG,MAAM,CAI5E;AAED,2GAA2G;AAC3G,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhE;AA0BD,eAAO,MAAM,6BAA6B,EAAE,mBA0E3C,CAAC"}
1
+ {"version":3,"file":"immudb-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/immudb-self/immudb-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAIH,OAAO,EASL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAKvC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAC/D,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAC/D,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAiB/D;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMnE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,oBAAoB,EAAE,MAAM,GAAG,MAAM,CAI5E;AAED,2GAA2G;AAC3G,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhE;AA0BD,eAAO,MAAM,6BAA6B,EAAE,mBA0E3C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"immudb-self.infra.module.js","sourceRoot":"","sources":["../../../src/immudb-self/immudb-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ImmudbSelfInfraProviderModule` — the self-hosted tamper-evident audit store (#1696).\n *\n * ## What it replaces\n *\n * Until #1696 the platform ran ONE immudb and every application's audit sink logged into it as the server's\n * built-in `immudb` SYSADMIN, into the same `defaultdb`: any application's backend held a superuser credential over\n * every application's tamper-evident audit data, and the server's admin password was copied from the OWNING\n * application's provider secret, so every hosted application had to be handed the owner's sysadmin password to write\n * at all. `IMMUDB_SELF` was servable only as an operator-credentialed exemption because nothing could mint anything\n * narrower.\n *\n * ## What it mints\n *\n * Per application: ONE immudb database and ONE user holding `READ_WRITE` on that database only — the permission an\n * audit sink needs to append and verify, and nothing that administers users, databases or the server. The values\n * reach the application in its platform secrets at `database.immudb.{username,password,database}`, the same paths the\n * engine's immudb provider reads first (`platformMintedSecretPath`), so the operator-authored `IMMUDB_USER` /\n * `IMMUDB_PASSWORD` / `IMMUDB_DATABASE` no longer decide anything on a self-hosted platform.\n *\n * The admin credential apps-manager mints with is a framework-managed PLATFORM secret (`immudb.adminPassword`),\n * delivered to apps-manager only (`IMMUDB_ADMIN_*`), and never to an application.\n *\n * ## Naming, and why it is this narrow\n *\n * immudb accepts only letters and digits in a database name (\"punctuation marks and symbols are not allowed in\n * database name\", immudb issue #1770) and `^[a-zA-Z0-9_]+$` up to 63 characters for a username. The application\n * namespace (`<slug>-<environment>`) carries hyphens, so the database name is its lowercased letters and digits plus\n * a short digest of the namespace itself: stripping punctuation alone would give `wonder-todos` and `wondertodos` the\n * SAME database, which is exactly the sharing this module exists to remove.\n * spec-cite: immudb v1.11.1 `pkg/auth/user.go` — `usernameRegex = ^[a-zA-Z0-9_]+$`, `maxUsernameLen = 63`\n * (https://raw.githubusercontent.com/codenotary/immudb/v1.11.1/pkg/auth/user.go, fetched 2026-09-29); the database\n * name rule is the server's error text quoted in https://github.com/codenotary/immudb/issues/1770.\n *\n * ## Runtime principals\n *\n * The APPLICATION BACKEND only. A minion's audit events go through the same application's audit trail, so it\n * receives the application's credential through the ordinary delivery (`perPrincipalSecretBlobPaths` is empty, so\n * nothing is stripped for it). A named principal passed here is refused rather than given a second database.\n *\n * ## Revocation\n *\n * `revokeAppCredentials` DEACTIVATES the user and revokes its permission — immudb has no user deletion, and the\n * database and its events are kept, because an append-only audit history must outlive the credential that wrote it\n * (#1307, #1562). `mintAppCredentials` re-activates and re-grants, which is what a restore runs.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport {\n ApplicationRuntimePrincipalKind,\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IImmudbAdminClient,\n ImmudbDatabasePermission,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n} from '@wildo-ai/platform-config-lib';\n\n/** immudb's gRPC port (matches `INFRA_DEFAULT_PORTS.IMMUDB`). */\nconst DEFAULT_IMMUDB_PORT = 3322;\n\n/**\n * The `Wildo_Application_Secrets` paths this module writes — the SAME strings the engine's immudb provider names as\n * `platformMintedSecretPath` (`IMMUDB_MINTED_SECRET_PATHS` in external-connectors-private). Each side is pinned to the\n * engine's `AppConfigurationSecrets.database.immudb` schema, which is where the two ends meet: these constants by\n * apps-manager's `immudb-minted-secret-paths` test, the engine's by saas-backend-lib's\n * `app-config-secrets.boundaries` test. The lifecycle imports these, never literals.\n */\nexport const IMMUDB_USERNAME_PATH = 'database.immudb.username';\nexport const IMMUDB_PASSWORD_PATH = 'database.immudb.password';\nexport const IMMUDB_DATABASE_PATH = 'database.immudb.database';\n\n/**\n * `applicationConfigInputs` key for the database name RECORDED at the first mint (apps-manager passes the stored\n * `database.immudb.database` back, so a revoke, a restore and a re-registration address the database the first mint\n * created even if the derivation ever changes). Not an authored value: `backingServices.immudb.database` is not read\n * for a self-hosted store.\n */\nconst DATABASE_NAME_INPUT_KEY = 'databaseName';\n\n/**\n * The longest database name whose `<name>_audit` user still fits immudb's 63-character username limit, and the\n * database name's own character rule (letters and digits). A recorded name outside either is refused rather than sent.\n */\nconst MAX_DATABASE_NAME_LENGTH = 63 - '_audit'.length;\nconst DATABASE_NAME_PATTERN = /^[a-zA-Z0-9]+$/;\n\n/**\n * immudb's password rule, checked here so a non-compliant value fails naming the application rather than as a bare\n * gRPC error from the server.\n * spec-cite: immudb v1.11.1 `pkg/auth/passwords.go` — \"password must have between 8 and 32 letters, digits and\n * special characters of which at least 1 uppercase letter, 1 digit and 1 special character\"\n * (https://raw.githubusercontent.com/codenotary/immudb/v1.11.1/pkg/auth/passwords.go, fetched 2026-09-29).\n */\nexport function isImmudbCompliantPassword(password: string): boolean {\n return password.length >= 8\n && password.length <= 32\n && /[A-Z]/.test(password)\n && /[0-9]/.test(password)\n && /[^A-Za-z0-9]/.test(password);\n}\n\n/**\n * The per-application database name: the namespace's lowercased letters and digits (capped) plus the first 8 hex of\n * its SHA-256, so two namespaces differing only in punctuation still get two databases. Deterministic, so a revoke,\n * a restore and a re-registration all address the database the first mint created.\n */\nexport function buildImmudbDatabaseName(applicationNamespace: string): string {\n const letters = applicationNamespace.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 40);\n const digest = createHash('sha256').update(applicationNamespace).digest('hex').slice(0, 8);\n return `${letters}${digest}`;\n}\n\n/** The per-application audit user: the database name plus a role suffix, inside immudb's username rule. */\nexport function buildImmudbUsername(databaseName: string): string {\n return `${databaseName}_audit`;\n}\n\nfunction refuseNamedPrincipal(context: AppCredentialMintingContext, verb: string): void {\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n throw new Error(\n `ImmudbSelfInfraProviderModule.${verb}: principal '${context.principal.kind}:${context.principal.name}' is not `\n + 'the application backend. An application has ONE audit database and ONE audit user; its runtimes write through '\n + 'the application\\'s credential, so a per-principal immudb credential is never minted or revoked.',\n );\n }\n}\n\nfunction resolveDatabaseName(context: AppCredentialMintingContext): string {\n if (!context.applicationNamespace) {\n throw new Error('ImmudbSelfInfraProviderModule: applicationNamespace is empty. The orchestrator MUST supply it.');\n }\n const recorded = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (recorded && (!DATABASE_NAME_PATTERN.test(recorded) || recorded.length > MAX_DATABASE_NAME_LENGTH)) {\n throw new Error(\n `ImmudbSelfInfraProviderModule: the recorded database name '${recorded}' is outside immudb's rules (letters and `\n + `digits only, at most ${MAX_DATABASE_NAME_LENGTH} characters so its audit user fits the 63-character limit).`,\n );\n }\n return recorded || buildImmudbDatabaseName(context.applicationNamespace);\n}\n\nexport const ImmudbSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.IMMUDB_SELF,\n supportedCapabilities: [InfraCapability.AUDIT_LOG_STORAGE],\n\n /**\n * Empty: minions write through the application's audit credential (see the module JSDoc), so the delivery must\n * not strip it from them.\n */\n perPrincipalSecretBlobPaths: [],\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['immudb'],\n k8sResources: ['templates/infra/k8s/internal/immudb/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n IMMUDB_ADMIN_HOST: 'immudb',\n IMMUDB_ADMIN_PORT: String(DEFAULT_IMMUDB_PORT),\n },\n // The server's built-in sysadmin password: a framework-managed platform secret (`immudb.adminPassword`),\n // written by the CLI's env builder into apps-manager's env only.\n secretEnvVars: ['IMMUDB_ADMIN_PASSWORD'],\n consumers: [InfraPlatformAdminConsumer.APPS_MANAGER],\n },\n appRuntime: {\n perAppSecretBlobPaths: [IMMUDB_USERNAME_PATH, IMMUDB_PASSWORD_PATH, IMMUDB_DATABASE_PATH],\n },\n };\n },\n\n // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — READ_WRITE on the application's own database only, never ADMIN or SYSADMIN.\n async mintAppCredentials(context: AppCredentialMintingContext): Promise<AppCredentialMintingResult> {\n refuseNamedPrincipal(context, 'mintAppCredentials');\n const password = context.secretInputs[IMMUDB_PASSWORD_PATH];\n if (!password) {\n throw new Error(\n `ImmudbSelfInfraProviderModule.mintAppCredentials: secretInputs is missing '${IMMUDB_PASSWORD_PATH}'. `\n + 'The orchestrator MUST supply the password — modules are upsert-only.',\n );\n }\n if (!isImmudbCompliantPassword(password)) {\n throw new Error(\n 'ImmudbSelfInfraProviderModule.mintAppCredentials: the supplied password does not meet immudb\\'s rule '\n + '(8 to 32 characters with at least one uppercase letter, one digit and one special character).',\n );\n }\n const databaseName = resolveDatabaseName(context);\n const username = buildImmudbUsername(databaseName);\n const adminClient = context.adminClient as IImmudbAdminClient;\n await adminClient.ensureDatabase({ databaseName });\n await adminClient.upsertDatabaseUser({\n username,\n password,\n databaseName,\n permission: ImmudbDatabasePermission.READ_WRITE,\n });\n return {\n secretBlobValues: {\n [IMMUDB_USERNAME_PATH]: username,\n [IMMUDB_PASSWORD_PATH]: password,\n [IMMUDB_DATABASE_PATH]: databaseName,\n },\n };\n },\n\n // assurance-control: WILDO.IDENTITY.CREDENTIAL_LIFECYCLE — a withdrawn application's audit user is deactivated and its permission revoked.\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n refuseNamedPrincipal(context, 'revokeAppCredentials');\n const databaseName = resolveDatabaseName(context);\n const adminClient = context.adminClient as IImmudbAdminClient;\n await adminClient.deactivateDatabaseUser({ username: buildImmudbUsername(databaseName), databaseName });\n },\n\n // No `mintPlatformServiceCredentials`: no platform service writes to the audit store.\n};\n"]}
1
+ {"version":3,"file":"immudb-self.infra.module.js","sourceRoot":"","sources":["../../../../src/immudb-self/immudb-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ImmudbSelfInfraProviderModule` — the self-hosted tamper-evident audit store (#1696).\n *\n * ## What it replaces\n *\n * Until #1696 the platform ran ONE immudb and every application's audit sink logged into it as the server's\n * built-in `immudb` SYSADMIN, into the same `defaultdb`: any application's backend held a superuser credential over\n * every application's tamper-evident audit data, and the server's admin password was copied from the OWNING\n * application's provider secret, so every hosted application had to be handed the owner's sysadmin password to write\n * at all. `IMMUDB_SELF` was servable only as an operator-credentialed exemption because nothing could mint anything\n * narrower.\n *\n * ## What it mints\n *\n * Per application: ONE immudb database and ONE user holding `READ_WRITE` on that database only — the permission an\n * audit sink needs to append and verify, and nothing that administers users, databases or the server. The values\n * reach the application in its platform secrets at `database.immudb.{username,password,database}`, the same paths the\n * engine's immudb provider reads first (`platformMintedSecretPath`), so the operator-authored `IMMUDB_USER` /\n * `IMMUDB_PASSWORD` / `IMMUDB_DATABASE` no longer decide anything on a self-hosted platform.\n *\n * The admin credential apps-manager mints with is a framework-managed PLATFORM secret (`immudb.adminPassword`),\n * delivered to apps-manager only (`IMMUDB_ADMIN_*`), and never to an application.\n *\n * ## Naming, and why it is this narrow\n *\n * immudb accepts only letters and digits in a database name (\"punctuation marks and symbols are not allowed in\n * database name\", immudb issue #1770) and `^[a-zA-Z0-9_]+$` up to 63 characters for a username. The application\n * namespace (`<slug>-<environment>`) carries hyphens, so the database name is its lowercased letters and digits plus\n * a short digest of the namespace itself: stripping punctuation alone would give `wonder-todos` and `wondertodos` the\n * SAME database, which is exactly the sharing this module exists to remove.\n * spec-cite: immudb v1.11.1 `pkg/auth/user.go` — `usernameRegex = ^[a-zA-Z0-9_]+$`, `maxUsernameLen = 63`\n * (https://raw.githubusercontent.com/codenotary/immudb/v1.11.1/pkg/auth/user.go, fetched 2026-09-29); the database\n * name rule is the server's error text quoted in https://github.com/codenotary/immudb/issues/1770.\n *\n * ## Runtime principals\n *\n * The APPLICATION BACKEND only. A minion's audit events go through the same application's audit trail, so it\n * receives the application's credential through the ordinary delivery (`perPrincipalSecretBlobPaths` is empty, so\n * nothing is stripped for it). A named principal passed here is refused rather than given a second database.\n *\n * ## Revocation\n *\n * `revokeAppCredentials` DEACTIVATES the user and revokes its permission — immudb has no user deletion, and the\n * database and its events are kept, because an append-only audit history must outlive the credential that wrote it\n * (#1307, #1562). `mintAppCredentials` re-activates and re-grants, which is what a restore runs.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport {\n ApplicationRuntimePrincipalKind,\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IImmudbAdminClient,\n ImmudbDatabasePermission,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n} from '@wildo-ai/platform-config-lib';\n\n/** immudb's gRPC port (matches `INFRA_DEFAULT_PORTS.IMMUDB`). */\nconst DEFAULT_IMMUDB_PORT = 3322;\n\n/**\n * The `Wildo_Application_Secrets` paths this module writes — the SAME strings the engine's immudb provider names as\n * `platformMintedSecretPath` (`IMMUDB_MINTED_SECRET_PATHS` in external-connectors-private). Each side is pinned to the\n * engine's `AppConfigurationSecrets.database.immudb` schema, which is where the two ends meet: these constants by\n * apps-manager's `immudb-minted-secret-paths` test, the engine's by saas-backend-lib's\n * `app-config-secrets.boundaries` test. The lifecycle imports these, never literals.\n */\nexport const IMMUDB_USERNAME_PATH = 'database.immudb.username';\nexport const IMMUDB_PASSWORD_PATH = 'database.immudb.password';\nexport const IMMUDB_DATABASE_PATH = 'database.immudb.database';\n\n/**\n * `applicationConfigInputs` key for the database name RECORDED at the first mint (apps-manager passes the stored\n * `database.immudb.database` back, so a revoke, a restore and a re-registration address the database the first mint\n * created even if the derivation ever changes). Not an authored value: `backingServices.immudb.database` is not read\n * for a self-hosted store.\n */\nconst DATABASE_NAME_INPUT_KEY = 'databaseName';\n\n/**\n * The longest database name whose `<name>_audit` user still fits immudb's 63-character username limit, and the\n * database name's own character rule (letters and digits). A recorded name outside either is refused rather than sent.\n */\nconst MAX_DATABASE_NAME_LENGTH = 63 - '_audit'.length;\nconst DATABASE_NAME_PATTERN = /^[a-zA-Z0-9]+$/;\n\n/**\n * immudb's password rule, checked here so a non-compliant value fails naming the application rather than as a bare\n * gRPC error from the server.\n * spec-cite: immudb v1.11.1 `pkg/auth/passwords.go` — \"password must have between 8 and 32 letters, digits and\n * special characters of which at least 1 uppercase letter, 1 digit and 1 special character\"\n * (https://raw.githubusercontent.com/codenotary/immudb/v1.11.1/pkg/auth/passwords.go, fetched 2026-09-29).\n */\nexport function isImmudbCompliantPassword(password: string): boolean {\n return password.length >= 8\n && password.length <= 32\n && /[A-Z]/.test(password)\n && /[0-9]/.test(password)\n && /[^A-Za-z0-9]/.test(password);\n}\n\n/**\n * The per-application database name: the namespace's lowercased letters and digits (capped) plus the first 8 hex of\n * its SHA-256, so two namespaces differing only in punctuation still get two databases. Deterministic, so a revoke,\n * a restore and a re-registration all address the database the first mint created.\n */\nexport function buildImmudbDatabaseName(applicationNamespace: string): string {\n const letters = applicationNamespace.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 40);\n const digest = createHash('sha256').update(applicationNamespace).digest('hex').slice(0, 8);\n return `${letters}${digest}`;\n}\n\n/** The per-application audit user: the database name plus a role suffix, inside immudb's username rule. */\nexport function buildImmudbUsername(databaseName: string): string {\n return `${databaseName}_audit`;\n}\n\nfunction refuseNamedPrincipal(context: AppCredentialMintingContext, verb: string): void {\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n throw new Error(\n `ImmudbSelfInfraProviderModule.${verb}: principal '${context.principal.kind}:${context.principal.name}' is not `\n + 'the application backend. An application has ONE audit database and ONE audit user; its runtimes write through '\n + 'the application\\'s credential, so a per-principal immudb credential is never minted or revoked.',\n );\n }\n}\n\nfunction resolveDatabaseName(context: AppCredentialMintingContext): string {\n if (!context.applicationNamespace) {\n throw new Error('ImmudbSelfInfraProviderModule: applicationNamespace is empty. The orchestrator MUST supply it.');\n }\n const recorded = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (recorded && (!DATABASE_NAME_PATTERN.test(recorded) || recorded.length > MAX_DATABASE_NAME_LENGTH)) {\n throw new Error(\n `ImmudbSelfInfraProviderModule: the recorded database name '${recorded}' is outside immudb's rules (letters and `\n + `digits only, at most ${MAX_DATABASE_NAME_LENGTH} characters so its audit user fits the 63-character limit).`,\n );\n }\n return recorded || buildImmudbDatabaseName(context.applicationNamespace);\n}\n\nexport const ImmudbSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.IMMUDB_SELF,\n supportedCapabilities: [InfraCapability.AUDIT_LOG_STORAGE],\n\n /**\n * Empty: minions write through the application's audit credential (see the module JSDoc), so the delivery must\n * not strip it from them.\n */\n perPrincipalSecretBlobPaths: [],\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['immudb'],\n k8sResources: ['templates/infra/k8s/internal/immudb/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n IMMUDB_ADMIN_HOST: 'immudb',\n IMMUDB_ADMIN_PORT: String(DEFAULT_IMMUDB_PORT),\n },\n // The server's built-in sysadmin password: a framework-managed platform secret (`immudb.adminPassword`),\n // written by the CLI's env builder into apps-manager's env only.\n secretEnvVars: ['IMMUDB_ADMIN_PASSWORD'],\n consumers: [InfraPlatformAdminConsumer.APPS_MANAGER],\n },\n appRuntime: {\n perAppSecretBlobPaths: [IMMUDB_USERNAME_PATH, IMMUDB_PASSWORD_PATH, IMMUDB_DATABASE_PATH],\n },\n };\n },\n\n // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — READ_WRITE on the application's own database only, never ADMIN or SYSADMIN.\n async mintAppCredentials(context: AppCredentialMintingContext): Promise<AppCredentialMintingResult> {\n refuseNamedPrincipal(context, 'mintAppCredentials');\n const password = context.secretInputs[IMMUDB_PASSWORD_PATH];\n if (!password) {\n throw new Error(\n `ImmudbSelfInfraProviderModule.mintAppCredentials: secretInputs is missing '${IMMUDB_PASSWORD_PATH}'. `\n + 'The orchestrator MUST supply the password — modules are upsert-only.',\n );\n }\n if (!isImmudbCompliantPassword(password)) {\n throw new Error(\n 'ImmudbSelfInfraProviderModule.mintAppCredentials: the supplied password does not meet immudb\\'s rule '\n + '(8 to 32 characters with at least one uppercase letter, one digit and one special character).',\n );\n }\n const databaseName = resolveDatabaseName(context);\n const username = buildImmudbUsername(databaseName);\n const adminClient = context.adminClient as IImmudbAdminClient;\n await adminClient.ensureDatabase({ databaseName });\n await adminClient.upsertDatabaseUser({\n username,\n password,\n databaseName,\n permission: ImmudbDatabasePermission.READ_WRITE,\n });\n return {\n secretBlobValues: {\n [IMMUDB_USERNAME_PATH]: username,\n [IMMUDB_PASSWORD_PATH]: password,\n [IMMUDB_DATABASE_PATH]: databaseName,\n },\n };\n },\n\n // assurance-control: WILDO.IDENTITY.CREDENTIAL_LIFECYCLE — a withdrawn application's audit user is deactivated and its permission revoked.\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n refuseNamedPrincipal(context, 'revokeAppCredentials');\n const databaseName = resolveDatabaseName(context);\n const adminClient = context.adminClient as IImmudbAdminClient;\n await adminClient.deactivateDatabaseUser({ username: buildImmudbUsername(databaseName), databaseName });\n },\n\n // No `mintPlatformServiceCredentials`: no platform service writes to the audit store.\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAaxF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,8BAA8B,EAAE,MAAM,0CAA0C,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,cAAc,wCAAwC,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CAYrE,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qCAAqC,EAAE,WAAW,CAAC,aAAa,CAA4B,CAAC;AAE1G;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,IAAI,WAAW,CAAC,aAAa,CAAC,CAM1E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kCAAkC,IAAI,WAAW,CAAC,MAAM,CAAC,CAQxE;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,WAAW,CAAC,aAAa,EAAE,mBAAmB,CAAC,CAY7F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAaxF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,8BAA8B,EAAE,MAAM,0CAA0C,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,cAAc,wCAAwC,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CAYrE,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qCAAqC,EAAE,WAAW,CAAC,aAAa,CAA4B,CAAC;AAE1G;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,IAAI,WAAW,CAAC,aAAa,CAAC,CAM1E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kCAAkC,IAAI,WAAW,CAAC,MAAM,CAAC,CAQxE;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,WAAW,CAAC,aAAa,EAAE,mBAAmB,CAAC,CAY7F"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `@wildo-ai/platform-infra-providers` — concrete `InfraProviderModule`\n * catalog (Slice 2.5c-1 of `infra-provider-planner.md`).\n *\n * Tier-portable: the modules import only `@wildo-ai/platform-config-lib`\n * (contracts) and have no DI / mongoose / axios runtime dependencies.\n * This lets both `platform-apps-manager` (orchestrator) and\n * `wildo-cli` (lockfile materializer) consume the same module\n * objects — apps-manager passes a real admin-client implementation\n * via `AppCredentialMintingContext.adminClient`; wildo-cli only\n * needs `module.plan()` for the lockfile digest.\n *\n * Each module is exported by name (no intermediary barrels per\n * platform/CLAUDE.md \"no intermediary index.ts barrels\"). The\n * `INFRA_PROVIDER_MODULES` map below is the canonical catalog —\n * consumers iterate it (or look up by `InfraProvider` value) instead\n * of importing modules one by one.\n */\n\nimport { InfraProvider, type InfraProviderModule } from '@wildo-ai/platform-config-lib';\n\nimport { RabbitmqSelfInfraProviderModule } from './rabbitmq-self/rabbitmq-self.infra.module';\nimport { RedisSelfInfraProviderModule } from './redis-self/redis-self.infra.module';\nimport { PostgresSelfInfraProviderModule } from './postgres-self/postgres-self.infra.module';\nimport { MongodbAtlasInfraProviderModule } from './mongodb-atlas/mongodb-atlas.infra.module';\nimport { MongodbSelfInfraProviderModule } from './mongodb-self/mongodb-self.infra.module';\n// Slice 5 of `infra-provider-planner.md` — object-storage + stateless infra.\nimport { MinioSelfInfraProviderModule } from './minio-self/minio-self.infra.module';\nimport { AwsS3InfraProviderModule } from './aws-s3/aws-s3.infra.module';\nimport { ClamavSelfInfraProviderModule } from './clamav-self/clamav-self.infra.module';\nimport { ImmudbSelfInfraProviderModule } from './immudb-self/immudb-self.infra.module';\n\nexport { RabbitmqSelfInfraProviderModule } from './rabbitmq-self/rabbitmq-self.infra.module';\nexport { RedisSelfInfraProviderModule } from './redis-self/redis-self.infra.module';\nexport { PostgresSelfInfraProviderModule } from './postgres-self/postgres-self.infra.module';\nexport { MongodbAtlasInfraProviderModule } from './mongodb-atlas/mongodb-atlas.infra.module';\nexport { MongodbSelfInfraProviderModule } from './mongodb-self/mongodb-self.infra.module';\nexport { MinioSelfInfraProviderModule } from './minio-self/minio-self.infra.module';\nexport { AwsS3InfraProviderModule } from './aws-s3/aws-s3.infra.module';\nexport { ClamavSelfInfraProviderModule } from './clamav-self/clamav-self.infra.module';\nexport * from './immudb-self/immudb-self.infra.module';\n\n/**\n * Canonical `InfraProvider` → `InfraProviderModule` catalog. Caller\n * (CLI, apps-manager) builds a `Map<InfraProvider, InfraProviderModule>`\n * from this constant when threading modules into\n * `materializeInfraSection`.\n *\n * **Why a const array instead of a `Map` literal**: arrays serialize\n * stably under tsc and roll up to a deterministic order. The\n * downstream `Map` is built once at program start; the catalog\n * itself is a value-level fixture.\n *\n * **Coverage** (Slice 5 baseline, 2026-05-10):\n * - `RABBITMQ_SELF`, `REDIS_SELF`, `POSTGRES_SELF`, `MONGODB_SELF`,\n * `MONGODB_ATLAS` — five stateful modules with mint/revoke\n * methods.\n * - `MINIO_SELF`, `AWS_S3` — object storage (shared-bucket model;\n * no per-app credentials). `AWS_S3` declares `unservableReason`\n * (no AWS STS path in the credential broker, #14), so it plans but is\n * not servable.\n * - `CLAMAV_SELF` — stateless infra (no auth, no per-app state;\n * `plan()` only).\n *\n * **NOT covered** (catalog entries without modules):\n * - `POSTGRES_MANAGED`, `POSTGRES_AWS_RDS`, `POSTGRES_GCP_CLOUD_SQL`,\n * `POSTGRES_AZURE_DATABASE`, `POSTGRES_SUPABASE`, `POSTGRES_NEON`,\n * `POSTGRES_RENDER` — managed-Postgres brand catalog entries\n * (Slice 4.B.0). No module yet; trigger = real customer.\n *\n * A declaration resolving to a provider outside\n * {@link collectServableInfraProviders} is REFUSED by `wildo config\n * validate`, `config sync` and `lock check` (#1693) — it no longer falls\n * through to an authored-entry digest.\n */\nexport const INFRA_PROVIDER_MODULES: ReadonlyArray<InfraProviderModule> = [\n RabbitmqSelfInfraProviderModule,\n RedisSelfInfraProviderModule,\n PostgresSelfInfraProviderModule,\n MongodbAtlasInfraProviderModule,\n MongodbSelfInfraProviderModule,\n MinioSelfInfraProviderModule,\n AwsS3InfraProviderModule,\n ClamavSelfInfraProviderModule,\n // #1696: one immudb database and one confined user per application, replacing the operator-credentialed\n // exemption under which every application logged in as the server's sysadmin.\n ImmudbSelfInfraProviderModule,\n];\n\n/**\n * Providers the platform serves WITHOUT a module in {@link INFRA_PROVIDER_MODULES}, because their\n * application credential is the operator's to supply rather than the catalogue's to mint.\n *\n * ## Why this is the one exception, and why it is a list of exemptions rather than of refusals\n *\n * The servable set is DERIVED from the catalogue ({@link collectServableInfraProviders}); a hand-kept\n * list of refused providers would silently admit every provider added later. So the only thing\n * written down by hand is the exception, and it must be argued for here, one entry at a time.\n *\n * EMPTY since #1696, and kept as the named place such an exemption would be argued. `IMMUDB_SELF` was the one entry:\n * the self-hosted audit store had no module, and every application on the server logged in as its sysadmin into\n * one shared database. Its module now mints one database and one confined user per application, which makes it\n * servable on its own — and REFUSED on a platform that holds no immudb admin credential, like every minting kind.\n */\nexport const OPERATOR_CREDENTIALED_INFRA_PROVIDERS: ReadonlySet<InfraProvider> = new Set<InfraProvider>();\n\n/**\n * The providers a `backingServices` declaration may resolve to — every module in the catalogue that\n * does not declare `unservableReason`, plus {@link OPERATOR_CREDENTIALED_INFRA_PROVIDERS}.\n *\n * The one source both lockfile projections (`wildo config sync`, `wildo lock check`) and\n * `wildo config validate` pass to the shared check in `@wildo-ai/platform-config-lib`\n * (`findUnservableBackingServiceDeclarations`), so all three refuse the same declarations (#1693).\n * Derived, so a module entering the catalogue makes its provider declarable the day it lands.\n */\nexport function collectServableInfraProviders(): ReadonlySet<InfraProvider> {\n const servable = new Set<InfraProvider>(OPERATOR_CREDENTIALED_INFRA_PROVIDERS);\n for (const module of INFRA_PROVIDER_MODULES) {\n if (module.unservableReason === undefined) servable.add(module.provider);\n }\n return servable;\n}\n\n/**\n * Every `Wildo_Application_Secrets` JSON path that ANY provider module can mint per runtime\n * principal — the union of the catalog's own static declarations.\n *\n * ## Why this is derived rather than written down\n *\n * The consumer is the secret-DELIVERY path in apps-manager: it strips exactly this set from the\n * application blob before overlaying a principal's own material, so a principal receives a\n * credential only where one was minted FOR IT. A hand-maintained copy at that consumer would be a\n * second source — add a provider that mints a new credential path and delivery keeps shipping the\n * APPLICATION's value for it, silently. That is the defect class this repo has paid for twice: the\n * party that CONSTRAINS is not the party that DECLARES.\n *\n * Derived from `INFRA_PROVIDER_MODULES`, so a new module is covered the day it enters the catalog.\n *\n * Deliberately the union across ALL modules, not the ones a given application selected: a principal\n * must not receive an application-wide credential for a provider it does not use either, and the\n * selected set is not knowable at the delivery call site without threading app config through it.\n * Stripping a path no module minted for this app is harmless — it is absent from the blob anyway.\n */\nexport function collectPerPrincipalSecretBlobPaths(): ReadonlySet<string> {\n const paths = new Set<string>();\n for (const module of INFRA_PROVIDER_MODULES) {\n for (const path of module.perPrincipalSecretBlobPaths) {\n paths.add(path);\n }\n }\n return paths;\n}\n\n/**\n * Build a `Map<InfraProvider, InfraProviderModule>` for the\n * canonical catalog. Callers store the result once at startup and\n * pass it to `materializeInfraSection` (digest derivation) or to\n * orchestrator paths that look up modules by `InfraProvider`.\n */\nexport function buildInfraProviderModuleMap(): ReadonlyMap<InfraProvider, InfraProviderModule> {\n const map = new Map<InfraProvider, InfraProviderModule>();\n for (const m of INFRA_PROVIDER_MODULES) {\n if (map.has(m.provider)) {\n throw new Error(\n `INFRA_PROVIDER_MODULES contains duplicate provider '${m.provider}' — ` +\n `each InfraProvider may have at most one module.`,\n );\n }\n map.set(m.provider, m);\n }\n return map;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,8BAA8B,EAAE,MAAM,0CAA0C,CAAC;AAC1F,6EAA6E;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AAEvF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,+BAA+B,EAAE,MAAM,4CAA4C,CAAC;AAC7F,OAAO,EAAE,8BAA8B,EAAE,MAAM,0CAA0C,CAAC;AAC1F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,cAAc,wCAAwC,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACxE,+BAA+B;IAC/B,4BAA4B;IAC5B,+BAA+B;IAC/B,+BAA+B;IAC/B,8BAA8B;IAC9B,4BAA4B;IAC5B,wBAAwB;IACxB,6BAA6B;IAC7B,wGAAwG;IACxG,8EAA8E;IAC9E,6BAA6B;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,qCAAqC,GAA+B,IAAI,GAAG,EAAiB,CAAC;AAE1G;;;;;;;;GAQG;AACH,MAAM,UAAU,6BAA6B;IAC3C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAgB,qCAAqC,CAAC,CAAC;IAC/E,KAAK,MAAM,MAAM,IAAI,sBAAsB,EAAE,CAAC;QAC5C,IAAI,MAAM,CAAC,gBAAgB,KAAK,SAAS;YAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,kCAAkC;IAChD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,MAAM,IAAI,sBAAsB,EAAE,CAAC;QAC5C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,2BAA2B,EAAE,CAAC;YACtD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B;IACzC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsC,CAAC;IAC1D,KAAK,MAAM,CAAC,IAAI,sBAAsB,EAAE,CAAC;QACvC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,uDAAuD,CAAC,CAAC,QAAQ,MAAM;gBACvE,iDAAiD,CAClD,CAAC;QACJ,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * `@wildo-ai/platform-infra-providers` — concrete `InfraProviderModule`\n * catalog (Slice 2.5c-1 of `infra-provider-planner.md`).\n *\n * Tier-portable: the modules import only `@wildo-ai/platform-config-lib`\n * (contracts) and have no DI / mongoose / axios runtime dependencies.\n * This lets both `platform-apps-manager` (orchestrator) and\n * `wildo-cli` (lockfile materializer) consume the same module\n * objects — apps-manager passes a real admin-client implementation\n * via `AppCredentialMintingContext.adminClient`; wildo-cli only\n * needs `module.plan()` for the lockfile digest.\n *\n * Each module is exported by name (no intermediary barrels per\n * platform/CLAUDE.md \"no intermediary index.ts barrels\"). The\n * `INFRA_PROVIDER_MODULES` map below is the canonical catalog —\n * consumers iterate it (or look up by `InfraProvider` value) instead\n * of importing modules one by one.\n */\n\nimport { InfraProvider, type InfraProviderModule } from '@wildo-ai/platform-config-lib';\n\nimport { RabbitmqSelfInfraProviderModule } from './rabbitmq-self/rabbitmq-self.infra.module';\nimport { RedisSelfInfraProviderModule } from './redis-self/redis-self.infra.module';\nimport { PostgresSelfInfraProviderModule } from './postgres-self/postgres-self.infra.module';\nimport { MongodbAtlasInfraProviderModule } from './mongodb-atlas/mongodb-atlas.infra.module';\nimport { MongodbSelfInfraProviderModule } from './mongodb-self/mongodb-self.infra.module';\n// Slice 5 of `infra-provider-planner.md` — object-storage + stateless infra.\nimport { MinioSelfInfraProviderModule } from './minio-self/minio-self.infra.module';\nimport { AwsS3InfraProviderModule } from './aws-s3/aws-s3.infra.module';\nimport { ClamavSelfInfraProviderModule } from './clamav-self/clamav-self.infra.module';\nimport { ImmudbSelfInfraProviderModule } from './immudb-self/immudb-self.infra.module';\n\nexport { RabbitmqSelfInfraProviderModule } from './rabbitmq-self/rabbitmq-self.infra.module';\nexport { RedisSelfInfraProviderModule } from './redis-self/redis-self.infra.module';\nexport { PostgresSelfInfraProviderModule } from './postgres-self/postgres-self.infra.module';\nexport { MongodbAtlasInfraProviderModule } from './mongodb-atlas/mongodb-atlas.infra.module';\nexport { MongodbSelfInfraProviderModule } from './mongodb-self/mongodb-self.infra.module';\nexport { MinioSelfInfraProviderModule } from './minio-self/minio-self.infra.module';\nexport { AwsS3InfraProviderModule } from './aws-s3/aws-s3.infra.module';\nexport { ClamavSelfInfraProviderModule } from './clamav-self/clamav-self.infra.module';\nexport * from './immudb-self/immudb-self.infra.module';\n\n/**\n * Canonical `InfraProvider` → `InfraProviderModule` catalog. Caller\n * (CLI, apps-manager) builds a `Map<InfraProvider, InfraProviderModule>`\n * from this constant when threading modules into\n * `materializeInfraSection`.\n *\n * **Why a const array instead of a `Map` literal**: arrays serialize\n * stably under tsc and roll up to a deterministic order. The\n * downstream `Map` is built once at program start; the catalog\n * itself is a value-level fixture.\n *\n * **Coverage** (Slice 5 baseline, 2026-05-10):\n * - `RABBITMQ_SELF`, `REDIS_SELF`, `POSTGRES_SELF`, `MONGODB_SELF`,\n * `MONGODB_ATLAS` — five stateful modules with mint/revoke\n * methods.\n * - `MINIO_SELF`, `AWS_S3` — object storage (shared-bucket model;\n * no per-app credentials). `AWS_S3` declares `unservableReason`\n * (no AWS STS path in the credential broker, #14), so it plans but is\n * not servable.\n * - `CLAMAV_SELF` — stateless infra (no auth, no per-app state;\n * `plan()` only).\n *\n * **NOT covered** (catalog entries without modules):\n * - `POSTGRES_MANAGED`, `POSTGRES_AWS_RDS`, `POSTGRES_GCP_CLOUD_SQL`,\n * `POSTGRES_AZURE_DATABASE`, `POSTGRES_SUPABASE`, `POSTGRES_NEON`,\n * `POSTGRES_RENDER` — managed-Postgres brand catalog entries\n * (Slice 4.B.0). No module yet; trigger = real customer.\n *\n * A declaration resolving to a provider outside\n * {@link collectServableInfraProviders} is REFUSED by `wildo config\n * validate`, `config sync` and `lock check` (#1693) — it no longer falls\n * through to an authored-entry digest.\n */\nexport const INFRA_PROVIDER_MODULES: ReadonlyArray<InfraProviderModule> = [\n RabbitmqSelfInfraProviderModule,\n RedisSelfInfraProviderModule,\n PostgresSelfInfraProviderModule,\n MongodbAtlasInfraProviderModule,\n MongodbSelfInfraProviderModule,\n MinioSelfInfraProviderModule,\n AwsS3InfraProviderModule,\n ClamavSelfInfraProviderModule,\n // #1696: one immudb database and one confined user per application, replacing the operator-credentialed\n // exemption under which every application logged in as the server's sysadmin.\n ImmudbSelfInfraProviderModule,\n];\n\n/**\n * Providers the platform serves WITHOUT a module in {@link INFRA_PROVIDER_MODULES}, because their\n * application credential is the operator's to supply rather than the catalogue's to mint.\n *\n * ## Why this is the one exception, and why it is a list of exemptions rather than of refusals\n *\n * The servable set is DERIVED from the catalogue ({@link collectServableInfraProviders}); a hand-kept\n * list of refused providers would silently admit every provider added later. So the only thing\n * written down by hand is the exception, and it must be argued for here, one entry at a time.\n *\n * EMPTY since #1696, and kept as the named place such an exemption would be argued. `IMMUDB_SELF` was the one entry:\n * the self-hosted audit store had no module, and every application on the server logged in as its sysadmin into\n * one shared database. Its module now mints one database and one confined user per application, which makes it\n * servable on its own — and REFUSED on a platform that holds no immudb admin credential, like every minting kind.\n */\nexport const OPERATOR_CREDENTIALED_INFRA_PROVIDERS: ReadonlySet<InfraProvider> = new Set<InfraProvider>();\n\n/**\n * The providers a `backingServices` declaration may resolve to — every module in the catalogue that\n * does not declare `unservableReason`, plus {@link OPERATOR_CREDENTIALED_INFRA_PROVIDERS}.\n *\n * The one source both lockfile projections (`wildo config sync`, `wildo lock check`) and\n * `wildo config validate` pass to the shared check in `@wildo-ai/platform-config-lib`\n * (`findUnservableBackingServiceDeclarations`), so all three refuse the same declarations (#1693).\n * Derived, so a module entering the catalogue makes its provider declarable the day it lands.\n */\nexport function collectServableInfraProviders(): ReadonlySet<InfraProvider> {\n const servable = new Set<InfraProvider>(OPERATOR_CREDENTIALED_INFRA_PROVIDERS);\n for (const module of INFRA_PROVIDER_MODULES) {\n if (module.unservableReason === undefined) servable.add(module.provider);\n }\n return servable;\n}\n\n/**\n * Every `Wildo_Application_Secrets` JSON path that ANY provider module can mint per runtime\n * principal — the union of the catalog's own static declarations.\n *\n * ## Why this is derived rather than written down\n *\n * The consumer is the secret-DELIVERY path in apps-manager: it strips exactly this set from the\n * application blob before overlaying a principal's own material, so a principal receives a\n * credential only where one was minted FOR IT. A hand-maintained copy at that consumer would be a\n * second source — add a provider that mints a new credential path and delivery keeps shipping the\n * APPLICATION's value for it, silently. That is the defect class this repo has paid for twice: the\n * party that CONSTRAINS is not the party that DECLARES.\n *\n * Derived from `INFRA_PROVIDER_MODULES`, so a new module is covered the day it enters the catalog.\n *\n * Deliberately the union across ALL modules, not the ones a given application selected: a principal\n * must not receive an application-wide credential for a provider it does not use either, and the\n * selected set is not knowable at the delivery call site without threading app config through it.\n * Stripping a path no module minted for this app is harmless — it is absent from the blob anyway.\n */\nexport function collectPerPrincipalSecretBlobPaths(): ReadonlySet<string> {\n const paths = new Set<string>();\n for (const module of INFRA_PROVIDER_MODULES) {\n for (const path of module.perPrincipalSecretBlobPaths) {\n paths.add(path);\n }\n }\n return paths;\n}\n\n/**\n * Build a `Map<InfraProvider, InfraProviderModule>` for the\n * canonical catalog. Callers store the result once at startup and\n * pass it to `materializeInfraSection` (digest derivation) or to\n * orchestrator paths that look up modules by `InfraProvider`.\n */\nexport function buildInfraProviderModuleMap(): ReadonlyMap<InfraProvider, InfraProviderModule> {\n const map = new Map<InfraProvider, InfraProviderModule>();\n for (const m of INFRA_PROVIDER_MODULES) {\n if (map.has(m.provider)) {\n throw new Error(\n `INFRA_PROVIDER_MODULES contains duplicate provider '${m.provider}' — ` +\n `each InfraProvider may have at most one module.`,\n );\n }\n map.set(m.provider, m);\n }\n return map;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"minio-self.infra.module.d.ts","sourceRoot":"","sources":["../../../src/minio-self/minio-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAEH,OAAO,EAIL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAIvC,eAAO,MAAM,4BAA4B,EAAE,mBA6C1C,CAAC"}
1
+ {"version":3,"file":"minio-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/minio-self/minio-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAEH,OAAO,EAIL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AAIvC,eAAO,MAAM,4BAA4B,EAAE,mBA6C1C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"mongodb-atlas.infra.module.d.ts","sourceRoot":"","sources":["../../../src/mongodb-atlas/mongodb-atlas.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,OAAO,EAQL,mBAAmB,EAKpB,MAAM,+BAA+B,CAAC;AA0IvC,eAAO,MAAM,+BAA+B,EAAE,mBA2K7C,CAAC"}
1
+ {"version":3,"file":"mongodb-atlas.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/mongodb-atlas/mongodb-atlas.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,OAAO,EAQL,mBAAmB,EAKpB,MAAM,+BAA+B,CAAC;AA0IvC,eAAO,MAAM,+BAA+B,EAAE,mBA2K7C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"mongodb-atlas.infra.module.js","sourceRoot":"","sources":["../../../src/mongodb-atlas/mongodb-atlas.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MongodbAtlasInfraProviderModule` — fourth concrete `InfraProviderModule`\n * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the Slice\n * 2 / 4.B.1 / 4.B.2 pattern to MongoDB Atlas).\n *\n * Implements the `MONGODB_ATLAS` `InfraProvider` over the\n * `IMongoAtlasClient` transport (Atlas REST API + digest auth, exposed\n * by `MongoAtlasManagerService` after the Slice 4.B.3 5C policy/\n * transport split). Owns the Atlas naming and grant policy; the USERNAME\n * policy itself is shared with the self-hosted module through\n * `buildMongodbPrincipalUsername` (`<applicationNamespace>_user` for the\n * application backend, `<namespace>_<kind>_<name>_user` for a principal).\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `applicationNamespace` + `principal` — the username source (above).\n * - `secretInputs['database.mongodb.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs['databaseName']` — required, the\n * per-app database name.\n * - `applicationConfigInputs['clusterName']` — required, the Atlas\n * cluster name. apps-manager resolves it once (`resolveAtlasClusterName`)\n * for the application user and every principal user, and a later\n * reconcile reads back the cluster recorded at registration.\n * - `resourceGrants` — required for a named principal: its declared\n * access, to which the engine floor is added here.\n * - `adminClient` — narrowed to `IMongoAtlasClient`.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin)**: Atlas API public/private key\n * pair + project id + base URL. Consumers: `apps-manager` only\n * (companion uses self-managed locally; Atlas is remote-only).\n *\n * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:\n * per-customer-app Atlas database user. Result threaded into\n * `Wildo_Application_Secrets.database.mongodb.{username,password}`\n * via `_merge_WildoApplicationSecrets`; the cluster's connection\n * string (its SRV address, or the `mongoURIWithOptions` seed list\n * when it has none, #1969) returned via `runtimeArtifacts.connectionString` for\n * transient app-init operations (NOT persisted — the persistence\n * shape is split between config (host/protocol/options/database)\n * and secrets (username/password)).\n *\n * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:\n * - **Audience-2 (platform-service-scoped)**: no platform service\n * uses Atlas for inter-service messaging today. Method omitted.\n *\n * **`revokeAppCredentials`**: for a runtime principal, apps-manager's\n * declaration reconcile calls it when a minion is removed. For the application\n * backend, apps-manager calls it when the application is withdrawn from the\n * platform (`deregisterApplication`, #1307): the database user is deleted and\n * the database is kept, so `restoreApplication` can re-mint it.\n *\n * **Runtime principals (minions), #1694**: each gets its OWN Atlas database\n * user holding ONE Atlas custom database role — the Atlas twin of the\n * self-hosted module's collection-scoped user:\n * - the username comes from `buildMongodbPrincipalUsername`, the SAME\n * derivation the self-hosted module uses, so it can never be the backend's\n * `<namespace>_user`, and the mint and the revoke cannot disagree about it;\n * - the role carries the principal's declared `resourceAccess` PLUS the engine\n * execution-history floor every enabled Node minion needs, derived through\n * `deriveMinionDatabaseGrants` → `buildMinionMongodbPrivileges`, the same\n * pair the self-hosted module uses, then translated into Atlas's action names;\n * - the user is scoped to the application's cluster alone;\n * - revoking deletes that user and that role, and nothing else.\n *\n * Before #1694 the module ignored `principal` and derived `<namespace>_user`\n * for every input, so a minion mint would have overwritten the backend's user\n * and a minion revoke would have deleted it; apps-manager refused the\n * combination at registration instead.\n */\n\nimport {\n ApplicationRuntimePrincipalKind,\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IMongoAtlasClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n deriveMinionDatabaseGrants,\n resolveGrantStorageNames,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\nimport {\n buildMinionMongodbPrivileges,\n buildMongodbPrincipalUsername,\n type MongodbCollectionPrivilege,\n} from '../mongodb-self/mongodb-self.infra.module';\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes\n * through `mintAppCredentials.secretBlobValues`. Declared once and\n * reused as `appRuntime.perAppSecretBlobPaths` in `plan()` AND as\n * the keys of the result, so a refactor that renames a path fails\n * loudly at one place.\n */\nconst MONGODB_USERNAME_PATH = 'database.mongodb.username';\nconst MONGODB_PASSWORD_PATH = 'database.mongodb.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [MONGODB_USERNAME_PATH, MONGODB_PASSWORD_PATH];\n\n\n/**\n * Key the module looks for in `secretInputs` for the orchestrator-\n * supplied password.\n */\nconst MONGODB_PASSWORD_INPUT_KEY = 'database.mongodb.password';\n\n/**\n * Required keys in `applicationConfigInputs`. The orchestrator\n * derives both before calling `mintAppCredentials`.\n */\nconst DATABASE_NAME_INPUT_KEY = 'databaseName';\nconst CLUSTER_NAME_INPUT_KEY = 'clusterName';\n\n/**\n * Key in `runtimeArtifacts` carrying the cluster's connection string — its SRV\n * address, or the `mongoURIWithOptions` seed list when it has none (#1969) — with\n * credentials embedded. Exposed for orchestrator transient use\n * (app-init seeding); deliberately NOT persisted.\n */\nconst CONNECTION_STRING_ARTIFACT_KEY = 'connectionString';\n\n/**\n * A named runtime principal (a minion) — anything but the application backend, which keeps the\n * application-level `<namespace>_user` and its database-wide role.\n */\nfunction isNamedPrincipal(principal: ApplicationRuntimePrincipal | undefined): principal is ApplicationRuntimePrincipal {\n return principal !== undefined && principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND;\n}\n\n/**\n * The Atlas custom role a principal's user holds — derived from the principal's username exactly as the\n * self-hosted module derives its role (`<username>_role`), so the mint and the revoke name the same role.\n *\n * Custom roles are PROJECT-wide in Atlas, not per database or cluster, so the name must be unique across\n * every application the project hosts. It is: the username embeds the application namespace.\n */\nfunction buildAtlasPrincipalRoleName(username: string): string {\n return `${username}_role`;\n}\n\n/**\n * Atlas's rule for a custom role name, checked before any request so a name Atlas would refuse fails here,\n * naming the principal, rather than as a bare HTTP 400 from the Admin API.\n *\n * spec-cite: https://www.mongodb.com/docs/atlas/security-add-mongodb-roles/ — \"The specified role name can\n * only contain letters, numbers, underscores or hyphens.\" (fetched 2026-09-27). The OpenAPI schema for\n * `UserCustomDBRole.roleName` states no pattern, so the prose page is the authority.\n */\nconst ATLAS_CUSTOM_ROLE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/**\n * MongoDB's privilege action (the vocabulary `buildMinionMongodbPrivileges` speaks, shared with the\n * self-hosted module) → the Atlas Admin API's name for the same action.\n *\n * semantic-literal-boundary: both sides are EXTERNAL vocabularies — MongoDB's `createRole` action names and\n * the Atlas Admin API's `DatabasePrivilegeAction.action` enum — and this table is the one place they meet.\n * spec-cite: Atlas Administration API v1.0 OpenAPI (github.com/mongodb/openapi, `openapi/v1-deprecated/v1.json`),\n * schema `DatabasePrivilegeAction`, enum `action`: `FIND`, `INSERT`, `REMOVE`, `UPDATE` (fetched 2026-09-27).\n *\n * Closed on purpose: an action with no entry is REFUSED rather than dropped (a silently narrower role reads\n * as a working grant until the minion fails) or passed through (a spelling Atlas does not know is a 400 at\n * best). Grants only ever reach Atlas through this table, so nothing can widen them on the way.\n */\nconst ATLAS_PRIVILEGE_ACTION_BY_MONGODB_ACTION: Readonly<Record<string, string>> = {\n find: 'FIND',\n insert: 'INSERT',\n update: 'UPDATE',\n remove: 'REMOVE',\n};\n\nfunction toAtlasPrivileges(privileges: readonly MongodbCollectionPrivilege[]): MongodbCollectionPrivilege[] {\n return privileges.map((privilege) => ({\n resource: privilege.resource,\n actions: privilege.actions.map((action) => {\n const atlasAction = ATLAS_PRIVILEGE_ACTION_BY_MONGODB_ACTION[action];\n if (!atlasAction) {\n throw new Error(\n `MongodbAtlasInfraProviderModule: MongoDB privilege action '${action}' has no Atlas Admin API equivalent `\n + `in ATLAS_PRIVILEGE_ACTION_BY_MONGODB_ACTION. Refusing to mint a role that would silently lack it.`,\n );\n }\n return atlasAction;\n }),\n }));\n}\n\n/** The principal's username and role, checked against Atlas's role-name rule. One derivation for mint and revoke. */\nfunction resolvePrincipalAtlasIdentity(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal,\n): { username: string; roleName: string } {\n const username = buildMongodbPrincipalUsername(applicationNamespace, principal);\n // The whole point of this path. `buildMongodbPrincipalUsername` returns the backend's name only for the\n // backend, but this is the one assertion whose failure would hand a minion the backend's user, so it is\n // made here rather than inferred from the helper.\n if (username === buildMongodbPrincipalUsername(applicationNamespace, undefined)) {\n throw new Error(\n `MongodbAtlasInfraProviderModule: principal '${principal.kind}:${principal.name ?? ''}' resolved to the `\n + `application backend's Atlas user '${username}'. Refusing: minting would overwrite it and revoking would delete it.`,\n );\n }\n const roleName = buildAtlasPrincipalRoleName(username);\n if (!ATLAS_CUSTOM_ROLE_NAME_PATTERN.test(roleName)) {\n throw new Error(\n `MongodbAtlasInfraProviderModule: the custom role name '${roleName}' for principal `\n + `'${principal.kind}:${principal.name ?? ''}' is not one Atlas accepts (letters, numbers, underscores or hyphens only).`,\n );\n }\n return { username, roleName };\n}\n\nexport const MongodbAtlasInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.MONGODB_ATLAS,\n supportedCapabilities: [InfraCapability.DOCUMENT_DATABASE],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n // Atlas is remote-only — no compose service / no K8s manifest.\n // The plan's `externalEndpoints` (in the wider envelope) carry\n // the Atlas host topology; this module's `plan()` covers only\n // admin-API access + per-app secret paths.\n platformAdmin: {\n envVars: {\n // ATLAS_API_BASE_URL is non-secret because it identifies\n // the Atlas API surface (default https://cloud.mongodb.com/api/atlas/v1.0).\n // ATLAS_PROJECT_ID + ATLAS_CLUSTER_NAME are non-secret\n // identifiers — public information about the customer's\n // Atlas project.\n ATLAS_API_BASE_URL: 'https://cloud.mongodb.com/api/atlas/v1.0',\n // PROJECT_ID + CLUSTER_NAME default to authored values; the\n // CLI's env-vars-builder writes them per-env from the\n // operator's `wildo.infra.<env>.config.ts > backingServices.mongodb`.\n ATLAS_PROJECT_ID: '',\n ATLAS_CLUSTER_NAME: 'Cluster0',\n },\n secretEnvVars: ['ATLAS_PUBLIC_KEY', 'ATLAS_PRIVATE_KEY'],\n // Atlas is admin-API only for apps-manager. Companion runs\n // locally against self-managed Mongo, NOT Atlas (no\n // companion consumer for the Atlas variant).\n consumers: [InfraPlatformAdminConsumer.APPS_MANAGER],\n },\n appRuntime: {\n // Static topology for the app pod. The actual host /\n // protocol / options for Atlas connections are derived\n // server-side at runtime from cluster info; the persistence\n // shape mirrors the legacy `_merge_WildoApplicationConfig`\n // mongodb section.\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IMongoAtlasClient;\n const password = context.secretInputs[MONGODB_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${MONGODB_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Mongo password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the per-app database name.`,\n );\n }\n const clusterName = context.applicationConfigInputs?.[CLUSTER_NAME_INPUT_KEY];\n if (!clusterName) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${CLUSTER_NAME_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Atlas cluster name.`,\n );\n }\n\n let username: string;\n let connectionString: string;\n if (isNamedPrincipal(context.principal)) {\n // A named principal gets a COLLECTION-SCOPED user on ONE custom role (#1694) — never the\n // application's `<namespace>_user`, which holds `readWrite` on the whole database.\n //\n // `resourceGrants` is REQUIRED and its absence is an error rather than a default, exactly as in the\n // self-hosted module: falling back to the application-wide upsert would hand a minion `readWrite` on\n // the whole database AND overwrite the backend's password, through a path that looks narrow.\n const grants = context.resourceGrants;\n if (!grants) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: `\n + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `\n + `Refusing to fall back to the database-wide application user. The orchestrator must pass `\n + `an explicit empty resource grant when only the mandatory runtime floor is required.`,\n );\n }\n const identity = resolvePrincipalAtlasIdentity(context.applicationNamespace, context.principal);\n username = identity.username;\n // Authored declaration + the engine floor, through the SAME derivation the self-hosted and PostgreSQL\n // adapters use, so the three cannot disagree about what every runtime principal needs.\n const databaseGrants = deriveMinionDatabaseGrants(grants);\n ({ connectionString } = await adminClient.upsertCollectionScopedDatabaseUser({\n clusterName,\n databaseName,\n username,\n password,\n roleName: identity.roleName,\n privileges: toAtlasPrivileges(buildMinionMongodbPrivileges({\n databaseName,\n readCollections: resolveGrantStorageNames(databaseGrants.read),\n writeCollections: resolveGrantStorageNames(databaseGrants.write),\n appendCollections: resolveGrantStorageNames(databaseGrants.append),\n maintainCollections: resolveGrantStorageNames(databaseGrants.maintain),\n })),\n }));\n } else {\n username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n // POST /databaseUsers (with PATCH-fallback) — idempotent upsert.\n // Returns the cluster's connection string (SRV address, or the seed list when it has none) with credentials embedded.\n ({ connectionString } = await adminClient.upsertApplicationDatabase({\n clusterName,\n databaseName,\n username,\n password,\n }));\n }\n\n return {\n secretBlobValues: {\n [MONGODB_USERNAME_PATH]: username,\n // Echo of input password — proves upsert-only.\n [MONGODB_PASSWORD_PATH]: password,\n },\n runtimeArtifacts: {\n // Transient only — orchestrator uses for app-init seeding,\n // does NOT persist.\n [CONNECTION_STRING_ARTIFACT_KEY]: connectionString,\n },\n };\n },\n\n /**\n * Tear down an Atlas database user. Idempotent.\n *\n * **Caller status**: for a runtime principal, apps-manager's declaration reconcile calls this when a\n * minion is removed. For the application backend (and every minion), apps-manager's withdrawal calls it\n * (`deregisterApplication`, #1307): the user is deleted, the database is kept, and `restoreApplication`\n * re-mints the user with the recorded password.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminClient = context.adminClient as IMongoAtlasClient;\n if (isNamedPrincipal(context.principal)) {\n // The principal's user AND its role, derived through the SAME function the mint uses. Before #1694\n // this derived `<namespace>_user` for every input, so revoking a minion would have deleted the\n // application backend's user — `invariant-call-site-enumeration.md` #18, on the Atlas side.\n await adminClient.dropCollectionScopedDatabaseUser(\n resolvePrincipalAtlasIdentity(context.applicationNamespace, context.principal),\n );\n return;\n }\n await adminClient.deleteDatabaseUser(buildMongodbPrincipalUsername(context.applicationNamespace, context.principal));\n },\n\n // No `mintPlatformServiceCredentials` (D-4).\n};\n"]}
1
+ {"version":3,"file":"mongodb-atlas.infra.module.js","sourceRoot":"","sources":["../../../../src/mongodb-atlas/mongodb-atlas.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MongodbAtlasInfraProviderModule` — fourth concrete `InfraProviderModule`\n * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the Slice\n * 2 / 4.B.1 / 4.B.2 pattern to MongoDB Atlas).\n *\n * Implements the `MONGODB_ATLAS` `InfraProvider` over the\n * `IMongoAtlasClient` transport (Atlas REST API + digest auth, exposed\n * by `MongoAtlasManagerService` after the Slice 4.B.3 5C policy/\n * transport split). Owns the Atlas naming and grant policy; the USERNAME\n * policy itself is shared with the self-hosted module through\n * `buildMongodbPrincipalUsername` (`<applicationNamespace>_user` for the\n * application backend, `<namespace>_<kind>_<name>_user` for a principal).\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `applicationNamespace` + `principal` — the username source (above).\n * - `secretInputs['database.mongodb.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs['databaseName']` — required, the\n * per-app database name.\n * - `applicationConfigInputs['clusterName']` — required, the Atlas\n * cluster name. apps-manager resolves it once (`resolveAtlasClusterName`)\n * for the application user and every principal user, and a later\n * reconcile reads back the cluster recorded at registration.\n * - `resourceGrants` — required for a named principal: its declared\n * access, to which the engine floor is added here.\n * - `adminClient` — narrowed to `IMongoAtlasClient`.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin)**: Atlas API public/private key\n * pair + project id + base URL. Consumers: `apps-manager` only\n * (companion uses self-managed locally; Atlas is remote-only).\n *\n * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:\n * per-customer-app Atlas database user. Result threaded into\n * `Wildo_Application_Secrets.database.mongodb.{username,password}`\n * via `_merge_WildoApplicationSecrets`; the cluster's connection\n * string (its SRV address, or the `mongoURIWithOptions` seed list\n * when it has none, #1969) returned via `runtimeArtifacts.connectionString` for\n * transient app-init operations (NOT persisted — the persistence\n * shape is split between config (host/protocol/options/database)\n * and secrets (username/password)).\n *\n * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:\n * - **Audience-2 (platform-service-scoped)**: no platform service\n * uses Atlas for inter-service messaging today. Method omitted.\n *\n * **`revokeAppCredentials`**: for a runtime principal, apps-manager's\n * declaration reconcile calls it when a minion is removed. For the application\n * backend, apps-manager calls it when the application is withdrawn from the\n * platform (`deregisterApplication`, #1307): the database user is deleted and\n * the database is kept, so `restoreApplication` can re-mint it.\n *\n * **Runtime principals (minions), #1694**: each gets its OWN Atlas database\n * user holding ONE Atlas custom database role — the Atlas twin of the\n * self-hosted module's collection-scoped user:\n * - the username comes from `buildMongodbPrincipalUsername`, the SAME\n * derivation the self-hosted module uses, so it can never be the backend's\n * `<namespace>_user`, and the mint and the revoke cannot disagree about it;\n * - the role carries the principal's declared `resourceAccess` PLUS the engine\n * execution-history floor every enabled Node minion needs, derived through\n * `deriveMinionDatabaseGrants` → `buildMinionMongodbPrivileges`, the same\n * pair the self-hosted module uses, then translated into Atlas's action names;\n * - the user is scoped to the application's cluster alone;\n * - revoking deletes that user and that role, and nothing else.\n *\n * Before #1694 the module ignored `principal` and derived `<namespace>_user`\n * for every input, so a minion mint would have overwritten the backend's user\n * and a minion revoke would have deleted it; apps-manager refused the\n * combination at registration instead.\n */\n\nimport {\n ApplicationRuntimePrincipalKind,\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IMongoAtlasClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n deriveMinionDatabaseGrants,\n resolveGrantStorageNames,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\nimport {\n buildMinionMongodbPrivileges,\n buildMongodbPrincipalUsername,\n type MongodbCollectionPrivilege,\n} from '../mongodb-self/mongodb-self.infra.module';\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes\n * through `mintAppCredentials.secretBlobValues`. Declared once and\n * reused as `appRuntime.perAppSecretBlobPaths` in `plan()` AND as\n * the keys of the result, so a refactor that renames a path fails\n * loudly at one place.\n */\nconst MONGODB_USERNAME_PATH = 'database.mongodb.username';\nconst MONGODB_PASSWORD_PATH = 'database.mongodb.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [MONGODB_USERNAME_PATH, MONGODB_PASSWORD_PATH];\n\n\n/**\n * Key the module looks for in `secretInputs` for the orchestrator-\n * supplied password.\n */\nconst MONGODB_PASSWORD_INPUT_KEY = 'database.mongodb.password';\n\n/**\n * Required keys in `applicationConfigInputs`. The orchestrator\n * derives both before calling `mintAppCredentials`.\n */\nconst DATABASE_NAME_INPUT_KEY = 'databaseName';\nconst CLUSTER_NAME_INPUT_KEY = 'clusterName';\n\n/**\n * Key in `runtimeArtifacts` carrying the cluster's connection string — its SRV\n * address, or the `mongoURIWithOptions` seed list when it has none (#1969) — with\n * credentials embedded. Exposed for orchestrator transient use\n * (app-init seeding); deliberately NOT persisted.\n */\nconst CONNECTION_STRING_ARTIFACT_KEY = 'connectionString';\n\n/**\n * A named runtime principal (a minion) — anything but the application backend, which keeps the\n * application-level `<namespace>_user` and its database-wide role.\n */\nfunction isNamedPrincipal(principal: ApplicationRuntimePrincipal | undefined): principal is ApplicationRuntimePrincipal {\n return principal !== undefined && principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND;\n}\n\n/**\n * The Atlas custom role a principal's user holds — derived from the principal's username exactly as the\n * self-hosted module derives its role (`<username>_role`), so the mint and the revoke name the same role.\n *\n * Custom roles are PROJECT-wide in Atlas, not per database or cluster, so the name must be unique across\n * every application the project hosts. It is: the username embeds the application namespace.\n */\nfunction buildAtlasPrincipalRoleName(username: string): string {\n return `${username}_role`;\n}\n\n/**\n * Atlas's rule for a custom role name, checked before any request so a name Atlas would refuse fails here,\n * naming the principal, rather than as a bare HTTP 400 from the Admin API.\n *\n * spec-cite: https://www.mongodb.com/docs/atlas/security-add-mongodb-roles/ — \"The specified role name can\n * only contain letters, numbers, underscores or hyphens.\" (fetched 2026-09-27). The OpenAPI schema for\n * `UserCustomDBRole.roleName` states no pattern, so the prose page is the authority.\n */\nconst ATLAS_CUSTOM_ROLE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/**\n * MongoDB's privilege action (the vocabulary `buildMinionMongodbPrivileges` speaks, shared with the\n * self-hosted module) → the Atlas Admin API's name for the same action.\n *\n * semantic-literal-boundary: both sides are EXTERNAL vocabularies — MongoDB's `createRole` action names and\n * the Atlas Admin API's `DatabasePrivilegeAction.action` enum — and this table is the one place they meet.\n * spec-cite: Atlas Administration API v1.0 OpenAPI (github.com/mongodb/openapi, `openapi/v1-deprecated/v1.json`),\n * schema `DatabasePrivilegeAction`, enum `action`: `FIND`, `INSERT`, `REMOVE`, `UPDATE` (fetched 2026-09-27).\n *\n * Closed on purpose: an action with no entry is REFUSED rather than dropped (a silently narrower role reads\n * as a working grant until the minion fails) or passed through (a spelling Atlas does not know is a 400 at\n * best). Grants only ever reach Atlas through this table, so nothing can widen them on the way.\n */\nconst ATLAS_PRIVILEGE_ACTION_BY_MONGODB_ACTION: Readonly<Record<string, string>> = {\n find: 'FIND',\n insert: 'INSERT',\n update: 'UPDATE',\n remove: 'REMOVE',\n};\n\nfunction toAtlasPrivileges(privileges: readonly MongodbCollectionPrivilege[]): MongodbCollectionPrivilege[] {\n return privileges.map((privilege) => ({\n resource: privilege.resource,\n actions: privilege.actions.map((action) => {\n const atlasAction = ATLAS_PRIVILEGE_ACTION_BY_MONGODB_ACTION[action];\n if (!atlasAction) {\n throw new Error(\n `MongodbAtlasInfraProviderModule: MongoDB privilege action '${action}' has no Atlas Admin API equivalent `\n + `in ATLAS_PRIVILEGE_ACTION_BY_MONGODB_ACTION. Refusing to mint a role that would silently lack it.`,\n );\n }\n return atlasAction;\n }),\n }));\n}\n\n/** The principal's username and role, checked against Atlas's role-name rule. One derivation for mint and revoke. */\nfunction resolvePrincipalAtlasIdentity(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal,\n): { username: string; roleName: string } {\n const username = buildMongodbPrincipalUsername(applicationNamespace, principal);\n // The whole point of this path. `buildMongodbPrincipalUsername` returns the backend's name only for the\n // backend, but this is the one assertion whose failure would hand a minion the backend's user, so it is\n // made here rather than inferred from the helper.\n if (username === buildMongodbPrincipalUsername(applicationNamespace, undefined)) {\n throw new Error(\n `MongodbAtlasInfraProviderModule: principal '${principal.kind}:${principal.name ?? ''}' resolved to the `\n + `application backend's Atlas user '${username}'. Refusing: minting would overwrite it and revoking would delete it.`,\n );\n }\n const roleName = buildAtlasPrincipalRoleName(username);\n if (!ATLAS_CUSTOM_ROLE_NAME_PATTERN.test(roleName)) {\n throw new Error(\n `MongodbAtlasInfraProviderModule: the custom role name '${roleName}' for principal `\n + `'${principal.kind}:${principal.name ?? ''}' is not one Atlas accepts (letters, numbers, underscores or hyphens only).`,\n );\n }\n return { username, roleName };\n}\n\nexport const MongodbAtlasInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.MONGODB_ATLAS,\n supportedCapabilities: [InfraCapability.DOCUMENT_DATABASE],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n // Atlas is remote-only — no compose service / no K8s manifest.\n // The plan's `externalEndpoints` (in the wider envelope) carry\n // the Atlas host topology; this module's `plan()` covers only\n // admin-API access + per-app secret paths.\n platformAdmin: {\n envVars: {\n // ATLAS_API_BASE_URL is non-secret because it identifies\n // the Atlas API surface (default https://cloud.mongodb.com/api/atlas/v1.0).\n // ATLAS_PROJECT_ID + ATLAS_CLUSTER_NAME are non-secret\n // identifiers — public information about the customer's\n // Atlas project.\n ATLAS_API_BASE_URL: 'https://cloud.mongodb.com/api/atlas/v1.0',\n // PROJECT_ID + CLUSTER_NAME default to authored values; the\n // CLI's env-vars-builder writes them per-env from the\n // operator's `wildo.infra.<env>.config.ts > backingServices.mongodb`.\n ATLAS_PROJECT_ID: '',\n ATLAS_CLUSTER_NAME: 'Cluster0',\n },\n secretEnvVars: ['ATLAS_PUBLIC_KEY', 'ATLAS_PRIVATE_KEY'],\n // Atlas is admin-API only for apps-manager. Companion runs\n // locally against self-managed Mongo, NOT Atlas (no\n // companion consumer for the Atlas variant).\n consumers: [InfraPlatformAdminConsumer.APPS_MANAGER],\n },\n appRuntime: {\n // Static topology for the app pod. The actual host /\n // protocol / options for Atlas connections are derived\n // server-side at runtime from cluster info; the persistence\n // shape mirrors the legacy `_merge_WildoApplicationConfig`\n // mongodb section.\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IMongoAtlasClient;\n const password = context.secretInputs[MONGODB_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${MONGODB_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Mongo password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the per-app database name.`,\n );\n }\n const clusterName = context.applicationConfigInputs?.[CLUSTER_NAME_INPUT_KEY];\n if (!clusterName) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${CLUSTER_NAME_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Atlas cluster name.`,\n );\n }\n\n let username: string;\n let connectionString: string;\n if (isNamedPrincipal(context.principal)) {\n // A named principal gets a COLLECTION-SCOPED user on ONE custom role (#1694) — never the\n // application's `<namespace>_user`, which holds `readWrite` on the whole database.\n //\n // `resourceGrants` is REQUIRED and its absence is an error rather than a default, exactly as in the\n // self-hosted module: falling back to the application-wide upsert would hand a minion `readWrite` on\n // the whole database AND overwrite the backend's password, through a path that looks narrow.\n const grants = context.resourceGrants;\n if (!grants) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.mintAppCredentials: `\n + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `\n + `Refusing to fall back to the database-wide application user. The orchestrator must pass `\n + `an explicit empty resource grant when only the mandatory runtime floor is required.`,\n );\n }\n const identity = resolvePrincipalAtlasIdentity(context.applicationNamespace, context.principal);\n username = identity.username;\n // Authored declaration + the engine floor, through the SAME derivation the self-hosted and PostgreSQL\n // adapters use, so the three cannot disagree about what every runtime principal needs.\n const databaseGrants = deriveMinionDatabaseGrants(grants);\n ({ connectionString } = await adminClient.upsertCollectionScopedDatabaseUser({\n clusterName,\n databaseName,\n username,\n password,\n roleName: identity.roleName,\n privileges: toAtlasPrivileges(buildMinionMongodbPrivileges({\n databaseName,\n readCollections: resolveGrantStorageNames(databaseGrants.read),\n writeCollections: resolveGrantStorageNames(databaseGrants.write),\n appendCollections: resolveGrantStorageNames(databaseGrants.append),\n maintainCollections: resolveGrantStorageNames(databaseGrants.maintain),\n })),\n }));\n } else {\n username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n // POST /databaseUsers (with PATCH-fallback) — idempotent upsert.\n // Returns the cluster's connection string (SRV address, or the seed list when it has none) with credentials embedded.\n ({ connectionString } = await adminClient.upsertApplicationDatabase({\n clusterName,\n databaseName,\n username,\n password,\n }));\n }\n\n return {\n secretBlobValues: {\n [MONGODB_USERNAME_PATH]: username,\n // Echo of input password — proves upsert-only.\n [MONGODB_PASSWORD_PATH]: password,\n },\n runtimeArtifacts: {\n // Transient only — orchestrator uses for app-init seeding,\n // does NOT persist.\n [CONNECTION_STRING_ARTIFACT_KEY]: connectionString,\n },\n };\n },\n\n /**\n * Tear down an Atlas database user. Idempotent.\n *\n * **Caller status**: for a runtime principal, apps-manager's declaration reconcile calls this when a\n * minion is removed. For the application backend (and every minion), apps-manager's withdrawal calls it\n * (`deregisterApplication`, #1307): the user is deleted, the database is kept, and `restoreApplication`\n * re-mints the user with the recorded password.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbAtlasInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminClient = context.adminClient as IMongoAtlasClient;\n if (isNamedPrincipal(context.principal)) {\n // The principal's user AND its role, derived through the SAME function the mint uses. Before #1694\n // this derived `<namespace>_user` for every input, so revoking a minion would have deleted the\n // application backend's user — `invariant-call-site-enumeration.md` #18, on the Atlas side.\n await adminClient.dropCollectionScopedDatabaseUser(\n resolvePrincipalAtlasIdentity(context.applicationNamespace, context.principal),\n );\n return;\n }\n await adminClient.deleteDatabaseUser(buildMongodbPrincipalUsername(context.applicationNamespace, context.principal));\n },\n\n // No `mintPlatformServiceCredentials` (D-4).\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"mongodb-self.infra.module.d.ts","sourceRoot":"","sources":["../../../src/mongodb-self/mongodb-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,EAOL,mBAAmB,EAKnB,KAAK,2BAA2B,EACjC,MAAM,+BAA+B,CAAC;AA2FvC,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,kGAAkG;IAClG,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC,GAAG,0BAA0B,EAAE,CAe/B;AA4BD;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAC3C,oBAAoB,EAAE,MAAM,EAC5B,SAAS,EAAE,2BAA2B,GAAG,SAAS,GACjD,MAAM,CAMR;AAED,eAAO,MAAM,8BAA8B,EAAE,mBAwM5C,CAAC"}
1
+ {"version":3,"file":"mongodb-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/mongodb-self/mongodb-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,EAOL,mBAAmB,EAKnB,KAAK,2BAA2B,EACjC,MAAM,+BAA+B,CAAC;AA2FvC,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,kGAAkG;IAClG,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC,GAAG,0BAA0B,EAAE,CAe/B;AA4BD;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAC3C,oBAAoB,EAAE,MAAM,EAC5B,SAAS,EAAE,2BAA2B,GAAG,SAAS,GACjD,MAAM,CAMR;AAED,eAAO,MAAM,8BAA8B,EAAE,mBAwM5C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"mongodb-self.infra.module.js","sourceRoot":"","sources":["../../../src/mongodb-self/mongodb-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MongodbSelfInfraProviderModule` — fifth concrete `InfraProviderModule`\n * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the\n * pattern to self-managed MongoDB).\n *\n * Implements the `MONGODB_SELF` `InfraProvider` over the\n * `IMongoSelfAdminClient` transport (admin-side mongoose connection,\n * exposed by `MongoSelfManagerService` after the Slice 4.B.3 5C\n * policy/transport split). Owns the MongoDB username policy\n * (`buildMongodbPrincipalUsername`, which the Atlas module also uses, so a\n * user has one name whichever provider holds the store) and\n * the per-app connection URL derivation (same scheme as the admin URL) by swapping admin credentials\n * with per-app credentials in the admin connect URL the orchestrator\n * reads from `MONGO_CONNECT_URL`.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `applicationNamespace` + `principal` — the username source\n * (`buildMongodbPrincipalUsername`: `${ns}_user` for the application\n * backend, `${ns}_<kind>_<name>_user` for a runtime principal).\n * - `resourceGrants` — required for a named principal: its declared\n * access, to which `deriveMinionDatabaseGrants` adds the engine floor.\n * - `secretInputs['database.mongodb.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs['databaseName']` — required.\n * - `applicationConfigInputs['adminConnectUrl']` — required, the\n * admin-scoped MongoDB connect URL the orchestrator reads from\n * `MONGO_CONNECT_URL` env at app-init time.\n * - `adminClient` — narrowed to `IMongoSelfAdminClient`.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin)**: cluster-wide Mongo admin\n * connect URL (`MONGO_CONNECT_URL`). Consumers: `apps-manager`\n * (mints per-app users via `mintAppCredentials`), `companion`\n * (mirrors apps-manager admin role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:\n * per-customer-app Mongo database user. Result threaded into\n * `Wildo_Application_Secrets.database.mongodb.{username,password}`\n * via `_merge_WildoApplicationSecrets`; the per-app connect URL\n * returned via `runtimeArtifacts.connectionString` for\n * transient app-init operations.\n *\n * **NOT served (D-4)**: audience-2 not applicable.\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IMongoSelfAdminClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipalKind,\n deriveMinionDatabaseGrants,\n resolveGrantStorageNames,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\nconst MONGODB_USERNAME_PATH = 'database.mongodb.username';\nconst MONGODB_PASSWORD_PATH = 'database.mongodb.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [MONGODB_USERNAME_PATH, MONGODB_PASSWORD_PATH];\n\n\nconst MONGODB_PASSWORD_INPUT_KEY = 'database.mongodb.password';\n\nconst DATABASE_NAME_INPUT_KEY = 'databaseName';\nconst ADMIN_CONNECT_URL_INPUT_KEY = 'adminConnectUrl';\n\nconst CONNECTION_STRING_ARTIFACT_KEY = 'connectionString';\n\n/**\n * The application backend's username (`${ns}_user`). Reached only through\n * `buildMongodbPrincipalUsername`, which both MongoDB modules call, so the\n * persistence shape is identical regardless of provider.\n */\nfunction buildMongodbUsername(applicationNamespace: string): string {\n return `${applicationNamespace}_user`;\n}\n\n/**\n * MongoDB privilege ACTIONS, per grant axis.\n *\n * Named here rather than inlined because these ARE the security semantics: what\n * \"read\" and \"write\" mean once they reach the database. Mongo's action\n * vocabulary is an external one, so it lives at this adapter — the module that\n * speaks Mongo — and never leaks into the platform's grant model.\n *\n * `find` alone for read: no `listCollections`, no `collStats`, nothing that\n * enumerates. A minion granted read on one resource should not be able to\n * discover what else exists.\n *\n * `insert`/`update`/`remove` for write, and deliberately NOT `find`. The\n * platform-level derivation refuses to widen write into read, and re-adding it\n * here would defeat that decision at the last hop — see\n * `deriveMinionResourceGrants`.\n */\nconst MONGODB_READ_ACTIONS: readonly string[] = ['find'];\nconst MONGODB_WRITE_ACTIONS: readonly string[] = ['insert', 'update', 'remove'];\n/**\n * Append-only: `insert` and `find`, WITHOUT `update` or `remove`. Carries the engine's audit-log\n * floor, where a principal must be able to record what it did and must never be able to unrecord it.\n *\n * `find` is not a widening — see {@link MONGODB_MAINTAIN_ACTIONS}, whose note covers both floors.\n */\nconst MONGODB_APPEND_ACTIONS: readonly string[] = ['insert', 'find'];\n/**\n * Durable runtime state may be inserted, read back and updated, never removed.\n *\n * ## Why the two FLOORS carry `find` and the author-declared write axis above does not\n *\n * Without it this axis could not perform the update the engine actually issues. Measured 2026-09-16\n * against the running replica set, as a throwaway role holding exactly these actions:\n *\n * | actions | operation | outcome |\n * |---|---|---|\n * | `insert`, `update` | `updateOne({_id}, …)` | works |\n * | `insert`, `update` | **`findOneAndUpdate`** | **Unauthorized** |\n * | `insert`, `find`, `update` | `findOneAndUpdate` | works; `deleteOne` still Unauthorized |\n *\n * The third row is the one that matters: `RepositoryResourceBackendService` updates through\n * `findOneAndUpdate`, so a role without `find` could not update at all — only a filter-less write no\n * caller issues. Note MongoDB and PostgreSQL differ here, which is why each was measured rather than\n * inferred from the other: Mongo's `update` action covers its own filter, while PostgreSQL requires\n * `SELECT` for the columns a `WHERE` reads.\n *\n * `find` on the APPEND axis was NOT broken on MongoDB — `insertOne` succeeds without it. It is added\n * so an axis means the same thing on both adapters: `deriveMinionDatabaseGrants` exists precisely so\n * the two cannot drift, and an append grant that carried read-back on one adapter and not the other\n * would make the same declaration yield two different authorities.\n *\n * **What these floors withhold is untouched.** Append still holds no `update` and no `remove`;\n * maintain still holds no `remove`. Both were re-probed after adding `find`, and `deleteOne` stayed\n * Unauthorized. The author-declared write axis keeps write-without-read deliberately, because there\n * the author has a remedy — declare the read — which `deriveMinionResourceGrants` argues for at\n * length. These two axes are engine-imposed and refuse any declaration, so no author action could\n * ever have repaired them.\n */\nconst MONGODB_MAINTAIN_ACTIONS: readonly string[] = ['insert', 'find', 'update'];\n\n/** One `createRole`/`updateRole` privilege entry. */\nexport interface MongodbCollectionPrivilege {\n readonly resource: { readonly db: string; readonly collection: string };\n readonly actions: readonly string[];\n}\n\n/**\n * Turn a minion's grants into collection-scoped MongoDB privileges (D14, R1).\n *\n * ## What this deliberately never produces\n *\n * No `dbAdmin` and no `readWrite` — the two roles the application backend\n * currently holds on the whole database. `dbAdmin` is banned outright by D14;\n * `readWrite` is database-WIDE, so granting it to a minion would make every\n * collection reachable regardless of what `resourceAccess` declared, which is\n * the containment this exists to create.\n *\n * No privilege on the database RESOURCE either (`{ db, collection: '' }`), which\n * is Mongo's way of saying \"every collection in this database\". Every entry\n * names one collection explicitly.\n *\n * ## Read and write are separate entries on purpose\n *\n * A collection granted both axes yields TWO privilege entries rather than one\n * merged action list. Mongo unions them identically, but the emitted role then\n * reads back as the declaration that produced it — which is what makes an\n * operator inspecting `db.getRole()` able to check the grant against the config\n * without re-deriving it.\n */\nexport function buildMinionMongodbPrivileges(input: {\n databaseName: string;\n readCollections: readonly string[];\n writeCollections: readonly string[];\n /** Insert-only — see {@link MONGODB_APPEND_ACTIONS}. Optional so existing pins read unchanged. */\n appendCollections?: readonly string[];\n maintainCollections?: readonly string[];\n}): MongodbCollectionPrivilege[] {\n const privileges: MongodbCollectionPrivilege[] = [];\n for (const collection of input.readCollections) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_READ_ACTIONS] });\n }\n for (const collection of input.writeCollections) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_WRITE_ACTIONS] });\n }\n for (const collection of input.appendCollections ?? []) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_APPEND_ACTIONS] });\n }\n for (const collection of input.maintainCollections ?? []) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_MAINTAIN_ACTIONS] });\n }\n return privileges;\n}\n\n/**\n * Derive the per-app connect URL by swapping admin credentials and\n * the database path in the authored `adminConnectUrl`. Mirrors the\n * legacy logic at\n * `wildo-application-lifecycle.service.ts:469-479`:\n *\n * const parsed = new URL(adminConnectUrl);\n * parsed.username = perAppUsername;\n * parsed.password = perAppPassword;\n * parsed.pathname = '/' + databaseName;\n *\n * URL-encoding handled automatically by the WHATWG URL class.\n */\nfunction buildPerAppConnectUrl(\n adminConnectUrl: string,\n databaseName: string,\n username: string,\n password: string,\n): string {\n const parsed = new URL(adminConnectUrl);\n parsed.username = encodeURIComponent(username);\n parsed.password = encodeURIComponent(password);\n parsed.pathname = '/' + databaseName;\n return parsed.toString();\n}\n\n/**\n * Per-principal Mongo username — the SAME derivation the revoke path must use.\n *\n * The application backend keeps `<namespace>_user` VERBATIM so an existing\n * deployment's user is never orphaned. Third time this shape appears (Redis,\n * RabbitMQ, now Mongo): a revoke deriving it differently deletes the BACKEND's\n * database user while believing it is removing a minion's.\n *\n * Exported because BOTH MongoDB modules name principals through it — the Atlas\n * module too (#1694) — so a principal's user has one name whichever provider\n * holds the store, and neither module can drift into the backend's name.\n */\nexport function buildMongodbPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildMongodbUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');\n return `${applicationNamespace}_${principal.kind}_${sanitizedName}_user`;\n}\n\nexport const MongodbSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.MONGODB_SELF,\n supportedCapabilities: [InfraCapability.DOCUMENT_DATABASE],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['mongodb'],\n k8sResources: ['templates/infra/k8s/internal/mongodb/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {},\n // The single MONGO_CONNECT_URL secret carries the admin\n // credentials embedded; legacy field name preserved.\n secretEnvVars: ['MONGO_CONNECT_URL'],\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Same persistence shape as Atlas — per-app username +\n // password go into secrets. The stored host/protocol are read\n // by apps-manager from the connection string this module\n // RETURNS when the application declares its provider (#1876),\n // and from BASE_MONGO_SERVER_URL only when it declares none.\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IMongoSelfAdminClient;\n const password = context.secretInputs[MONGODB_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${MONGODB_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Mongo password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the per-app database name.`,\n );\n }\n const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];\n if (!adminConnectUrl) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the admin Mongo connect URL.`,\n );\n }\n\n const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A named principal gets a COLLECTION-SCOPED user (Step 3 / R1 / D14):\n // one custom role carrying authored business grants plus the mandatory\n // runtime floor, and no inherited database-wide privileges.\n //\n // `resourceGrants` is REQUIRED here and its absence is an error rather\n // than a default. Falling back to the app-wide upsert would hand a minion\n // `readWrite` + `dbAdmin` on the whole database — the exact over-grant\n // this path exists to replace, arriving silently through a code path that\n // looks like it is doing the narrow thing.\n const grants = context.resourceGrants;\n if (!grants) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: `\n + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `\n + `Refusing to fall back to the database-wide application user. The orchestrator must pass `\n + `an explicit empty resource grant when only the mandatory runtime floor is required.`,\n );\n }\n const databaseGrants = deriveMinionDatabaseGrants(grants);\n\n await adminClient.upsertCollectionScopedDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n password,\n roleName: `${username}_role`,\n // Resource types → collection names HERE, at the adapter that speaks\n // Mongo. The grant contract itself stays in the platform vocabulary.\n // Authored declaration + the engine floor, through the SAME derivation\n // the PostgreSQL adapter uses — so the two cannot disagree about what\n // the framework itself requires of every runtime principal.\n privileges: buildMinionMongodbPrivileges({\n databaseName,\n readCollections: resolveGrantStorageNames(databaseGrants.read),\n writeCollections: resolveGrantStorageNames(databaseGrants.write),\n appendCollections: resolveGrantStorageNames(databaseGrants.append),\n maintainCollections: resolveGrantStorageNames(databaseGrants.maintain),\n }),\n });\n } else {\n // createUser-or-updateUser over an admin-scoped mongoose\n // connection — idempotent upsert.\n await adminClient.upsertApplicationDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n password,\n });\n }\n\n // Derive the per-app connection URL (the admin URL's own scheme) by swapping credentials + db path\n // in the authored admin URL. The orchestrator uses this for\n // transient app-init seeding; not persisted.\n const connectionString = buildPerAppConnectUrl(\n adminConnectUrl,\n databaseName,\n username,\n password,\n );\n\n return {\n secretBlobValues: {\n [MONGODB_USERNAME_PATH]: username,\n [MONGODB_PASSWORD_PATH]: password,\n },\n runtimeArtifacts: {\n [CONNECTION_STRING_ARTIFACT_KEY]: connectionString,\n },\n };\n },\n\n /**\n * Revoke a principal's Mongo database user (and a minion's custom role); the database\n * and its documents are kept.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation\n * calls this for a removed minion when its application has a MongoDB store, and\n * application withdrawal (`deregisterApplication`, #1307) for the backend and every\n * minion; `restoreApplication` re-mints them.\n * The named principal and database scope identify the grant being revoked.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];\n if (!adminConnectUrl) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the admin Mongo connect URL.`,\n );\n }\n const adminClient = context.adminClient as IMongoSelfAdminClient;\n // Derived through the SAME function the mint uses. Deriving it independently\n // here — which is what this line did before named principals existed — meant\n // revoking a minion left its credential live AND dropped the APPLICATION\n // BACKEND's database user, because the backend's name is what\n // `buildMongodbUsername` returns for every input.\n const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A scoped principal owns a user AND a custom role. Routing it through the\n // user-only door would leave a privileged role behind that the next mint\n // adopts without anybody declaring it.\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: `\n + `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}' for principal `\n + `'${context.principal.kind}:${context.principal.name}'. The role to drop lives IN that `\n + `database — revoking without it would drop the user and silently orphan its privileges.`,\n );\n }\n await adminClient.dropCollectionScopedDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n roleName: `${username}_role`,\n });\n return;\n }\n\n await adminClient.deleteApplicationDatabaseUser({ adminConnectUrl, username });\n },\n\n // No `mintPlatformServiceCredentials` (D-4).\n};\n"]}
1
+ {"version":3,"file":"mongodb-self.infra.module.js","sourceRoot":"","sources":["../../../../src/mongodb-self/mongodb-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MongodbSelfInfraProviderModule` — fifth concrete `InfraProviderModule`\n * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the\n * pattern to self-managed MongoDB).\n *\n * Implements the `MONGODB_SELF` `InfraProvider` over the\n * `IMongoSelfAdminClient` transport (admin-side mongoose connection,\n * exposed by `MongoSelfManagerService` after the Slice 4.B.3 5C\n * policy/transport split). Owns the MongoDB username policy\n * (`buildMongodbPrincipalUsername`, which the Atlas module also uses, so a\n * user has one name whichever provider holds the store) and\n * the per-app connection URL derivation (same scheme as the admin URL) by swapping admin credentials\n * with per-app credentials in the admin connect URL the orchestrator\n * reads from `MONGO_CONNECT_URL`.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `applicationNamespace` + `principal` — the username source\n * (`buildMongodbPrincipalUsername`: `${ns}_user` for the application\n * backend, `${ns}_<kind>_<name>_user` for a runtime principal).\n * - `resourceGrants` — required for a named principal: its declared\n * access, to which `deriveMinionDatabaseGrants` adds the engine floor.\n * - `secretInputs['database.mongodb.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs['databaseName']` — required.\n * - `applicationConfigInputs['adminConnectUrl']` — required, the\n * admin-scoped MongoDB connect URL the orchestrator reads from\n * `MONGO_CONNECT_URL` env at app-init time.\n * - `adminClient` — narrowed to `IMongoSelfAdminClient`.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin)**: cluster-wide Mongo admin\n * connect URL (`MONGO_CONNECT_URL`). Consumers: `apps-manager`\n * (mints per-app users via `mintAppCredentials`), `companion`\n * (mirrors apps-manager admin role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:\n * per-customer-app Mongo database user. Result threaded into\n * `Wildo_Application_Secrets.database.mongodb.{username,password}`\n * via `_merge_WildoApplicationSecrets`; the per-app connect URL\n * returned via `runtimeArtifacts.connectionString` for\n * transient app-init operations.\n *\n * **NOT served (D-4)**: audience-2 not applicable.\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IMongoSelfAdminClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipalKind,\n deriveMinionDatabaseGrants,\n resolveGrantStorageNames,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\nconst MONGODB_USERNAME_PATH = 'database.mongodb.username';\nconst MONGODB_PASSWORD_PATH = 'database.mongodb.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [MONGODB_USERNAME_PATH, MONGODB_PASSWORD_PATH];\n\n\nconst MONGODB_PASSWORD_INPUT_KEY = 'database.mongodb.password';\n\nconst DATABASE_NAME_INPUT_KEY = 'databaseName';\nconst ADMIN_CONNECT_URL_INPUT_KEY = 'adminConnectUrl';\n\nconst CONNECTION_STRING_ARTIFACT_KEY = 'connectionString';\n\n/**\n * The application backend's username (`${ns}_user`). Reached only through\n * `buildMongodbPrincipalUsername`, which both MongoDB modules call, so the\n * persistence shape is identical regardless of provider.\n */\nfunction buildMongodbUsername(applicationNamespace: string): string {\n return `${applicationNamespace}_user`;\n}\n\n/**\n * MongoDB privilege ACTIONS, per grant axis.\n *\n * Named here rather than inlined because these ARE the security semantics: what\n * \"read\" and \"write\" mean once they reach the database. Mongo's action\n * vocabulary is an external one, so it lives at this adapter — the module that\n * speaks Mongo — and never leaks into the platform's grant model.\n *\n * `find` alone for read: no `listCollections`, no `collStats`, nothing that\n * enumerates. A minion granted read on one resource should not be able to\n * discover what else exists.\n *\n * `insert`/`update`/`remove` for write, and deliberately NOT `find`. The\n * platform-level derivation refuses to widen write into read, and re-adding it\n * here would defeat that decision at the last hop — see\n * `deriveMinionResourceGrants`.\n */\nconst MONGODB_READ_ACTIONS: readonly string[] = ['find'];\nconst MONGODB_WRITE_ACTIONS: readonly string[] = ['insert', 'update', 'remove'];\n/**\n * Append-only: `insert` and `find`, WITHOUT `update` or `remove`. Carries the engine's audit-log\n * floor, where a principal must be able to record what it did and must never be able to unrecord it.\n *\n * `find` is not a widening — see {@link MONGODB_MAINTAIN_ACTIONS}, whose note covers both floors.\n */\nconst MONGODB_APPEND_ACTIONS: readonly string[] = ['insert', 'find'];\n/**\n * Durable runtime state may be inserted, read back and updated, never removed.\n *\n * ## Why the two FLOORS carry `find` and the author-declared write axis above does not\n *\n * Without it this axis could not perform the update the engine actually issues. Measured 2026-09-16\n * against the running replica set, as a throwaway role holding exactly these actions:\n *\n * | actions | operation | outcome |\n * |---|---|---|\n * | `insert`, `update` | `updateOne({_id}, …)` | works |\n * | `insert`, `update` | **`findOneAndUpdate`** | **Unauthorized** |\n * | `insert`, `find`, `update` | `findOneAndUpdate` | works; `deleteOne` still Unauthorized |\n *\n * The third row is the one that matters: `RepositoryResourceBackendService` updates through\n * `findOneAndUpdate`, so a role without `find` could not update at all — only a filter-less write no\n * caller issues. Note MongoDB and PostgreSQL differ here, which is why each was measured rather than\n * inferred from the other: Mongo's `update` action covers its own filter, while PostgreSQL requires\n * `SELECT` for the columns a `WHERE` reads.\n *\n * `find` on the APPEND axis was NOT broken on MongoDB — `insertOne` succeeds without it. It is added\n * so an axis means the same thing on both adapters: `deriveMinionDatabaseGrants` exists precisely so\n * the two cannot drift, and an append grant that carried read-back on one adapter and not the other\n * would make the same declaration yield two different authorities.\n *\n * **What these floors withhold is untouched.** Append still holds no `update` and no `remove`;\n * maintain still holds no `remove`. Both were re-probed after adding `find`, and `deleteOne` stayed\n * Unauthorized. The author-declared write axis keeps write-without-read deliberately, because there\n * the author has a remedy — declare the read — which `deriveMinionResourceGrants` argues for at\n * length. These two axes are engine-imposed and refuse any declaration, so no author action could\n * ever have repaired them.\n */\nconst MONGODB_MAINTAIN_ACTIONS: readonly string[] = ['insert', 'find', 'update'];\n\n/** One `createRole`/`updateRole` privilege entry. */\nexport interface MongodbCollectionPrivilege {\n readonly resource: { readonly db: string; readonly collection: string };\n readonly actions: readonly string[];\n}\n\n/**\n * Turn a minion's grants into collection-scoped MongoDB privileges (D14, R1).\n *\n * ## What this deliberately never produces\n *\n * No `dbAdmin` and no `readWrite` — the two roles the application backend\n * currently holds on the whole database. `dbAdmin` is banned outright by D14;\n * `readWrite` is database-WIDE, so granting it to a minion would make every\n * collection reachable regardless of what `resourceAccess` declared, which is\n * the containment this exists to create.\n *\n * No privilege on the database RESOURCE either (`{ db, collection: '' }`), which\n * is Mongo's way of saying \"every collection in this database\". Every entry\n * names one collection explicitly.\n *\n * ## Read and write are separate entries on purpose\n *\n * A collection granted both axes yields TWO privilege entries rather than one\n * merged action list. Mongo unions them identically, but the emitted role then\n * reads back as the declaration that produced it — which is what makes an\n * operator inspecting `db.getRole()` able to check the grant against the config\n * without re-deriving it.\n */\nexport function buildMinionMongodbPrivileges(input: {\n databaseName: string;\n readCollections: readonly string[];\n writeCollections: readonly string[];\n /** Insert-only — see {@link MONGODB_APPEND_ACTIONS}. Optional so existing pins read unchanged. */\n appendCollections?: readonly string[];\n maintainCollections?: readonly string[];\n}): MongodbCollectionPrivilege[] {\n const privileges: MongodbCollectionPrivilege[] = [];\n for (const collection of input.readCollections) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_READ_ACTIONS] });\n }\n for (const collection of input.writeCollections) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_WRITE_ACTIONS] });\n }\n for (const collection of input.appendCollections ?? []) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_APPEND_ACTIONS] });\n }\n for (const collection of input.maintainCollections ?? []) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_MAINTAIN_ACTIONS] });\n }\n return privileges;\n}\n\n/**\n * Derive the per-app connect URL by swapping admin credentials and\n * the database path in the authored `adminConnectUrl`. Mirrors the\n * legacy logic at\n * `wildo-application-lifecycle.service.ts:469-479`:\n *\n * const parsed = new URL(adminConnectUrl);\n * parsed.username = perAppUsername;\n * parsed.password = perAppPassword;\n * parsed.pathname = '/' + databaseName;\n *\n * URL-encoding handled automatically by the WHATWG URL class.\n */\nfunction buildPerAppConnectUrl(\n adminConnectUrl: string,\n databaseName: string,\n username: string,\n password: string,\n): string {\n const parsed = new URL(adminConnectUrl);\n parsed.username = encodeURIComponent(username);\n parsed.password = encodeURIComponent(password);\n parsed.pathname = '/' + databaseName;\n return parsed.toString();\n}\n\n/**\n * Per-principal Mongo username — the SAME derivation the revoke path must use.\n *\n * The application backend keeps `<namespace>_user` VERBATIM so an existing\n * deployment's user is never orphaned. Third time this shape appears (Redis,\n * RabbitMQ, now Mongo): a revoke deriving it differently deletes the BACKEND's\n * database user while believing it is removing a minion's.\n *\n * Exported because BOTH MongoDB modules name principals through it — the Atlas\n * module too (#1694) — so a principal's user has one name whichever provider\n * holds the store, and neither module can drift into the backend's name.\n */\nexport function buildMongodbPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildMongodbUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');\n return `${applicationNamespace}_${principal.kind}_${sanitizedName}_user`;\n}\n\nexport const MongodbSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.MONGODB_SELF,\n supportedCapabilities: [InfraCapability.DOCUMENT_DATABASE],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['mongodb'],\n k8sResources: ['templates/infra/k8s/internal/mongodb/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {},\n // The single MONGO_CONNECT_URL secret carries the admin\n // credentials embedded; legacy field name preserved.\n secretEnvVars: ['MONGO_CONNECT_URL'],\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Same persistence shape as Atlas — per-app username +\n // password go into secrets. The stored host/protocol are read\n // by apps-manager from the connection string this module\n // RETURNS when the application declares its provider (#1876),\n // and from BASE_MONGO_SERVER_URL only when it declares none.\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IMongoSelfAdminClient;\n const password = context.secretInputs[MONGODB_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${MONGODB_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Mongo password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the per-app database name.`,\n );\n }\n const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];\n if (!adminConnectUrl) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the admin Mongo connect URL.`,\n );\n }\n\n const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A named principal gets a COLLECTION-SCOPED user (Step 3 / R1 / D14):\n // one custom role carrying authored business grants plus the mandatory\n // runtime floor, and no inherited database-wide privileges.\n //\n // `resourceGrants` is REQUIRED here and its absence is an error rather\n // than a default. Falling back to the app-wide upsert would hand a minion\n // `readWrite` + `dbAdmin` on the whole database — the exact over-grant\n // this path exists to replace, arriving silently through a code path that\n // looks like it is doing the narrow thing.\n const grants = context.resourceGrants;\n if (!grants) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: `\n + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `\n + `Refusing to fall back to the database-wide application user. The orchestrator must pass `\n + `an explicit empty resource grant when only the mandatory runtime floor is required.`,\n );\n }\n const databaseGrants = deriveMinionDatabaseGrants(grants);\n\n await adminClient.upsertCollectionScopedDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n password,\n roleName: `${username}_role`,\n // Resource types → collection names HERE, at the adapter that speaks\n // Mongo. The grant contract itself stays in the platform vocabulary.\n // Authored declaration + the engine floor, through the SAME derivation\n // the PostgreSQL adapter uses — so the two cannot disagree about what\n // the framework itself requires of every runtime principal.\n privileges: buildMinionMongodbPrivileges({\n databaseName,\n readCollections: resolveGrantStorageNames(databaseGrants.read),\n writeCollections: resolveGrantStorageNames(databaseGrants.write),\n appendCollections: resolveGrantStorageNames(databaseGrants.append),\n maintainCollections: resolveGrantStorageNames(databaseGrants.maintain),\n }),\n });\n } else {\n // createUser-or-updateUser over an admin-scoped mongoose\n // connection — idempotent upsert.\n await adminClient.upsertApplicationDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n password,\n });\n }\n\n // Derive the per-app connection URL (the admin URL's own scheme) by swapping credentials + db path\n // in the authored admin URL. The orchestrator uses this for\n // transient app-init seeding; not persisted.\n const connectionString = buildPerAppConnectUrl(\n adminConnectUrl,\n databaseName,\n username,\n password,\n );\n\n return {\n secretBlobValues: {\n [MONGODB_USERNAME_PATH]: username,\n [MONGODB_PASSWORD_PATH]: password,\n },\n runtimeArtifacts: {\n [CONNECTION_STRING_ARTIFACT_KEY]: connectionString,\n },\n };\n },\n\n /**\n * Revoke a principal's Mongo database user (and a minion's custom role); the database\n * and its documents are kept.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation\n * calls this for a removed minion when its application has a MongoDB store, and\n * application withdrawal (`deregisterApplication`, #1307) for the backend and every\n * minion; `restoreApplication` re-mints them.\n * The named principal and database scope identify the grant being revoked.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];\n if (!adminConnectUrl) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the admin Mongo connect URL.`,\n );\n }\n const adminClient = context.adminClient as IMongoSelfAdminClient;\n // Derived through the SAME function the mint uses. Deriving it independently\n // here — which is what this line did before named principals existed — meant\n // revoking a minion left its credential live AND dropped the APPLICATION\n // BACKEND's database user, because the backend's name is what\n // `buildMongodbUsername` returns for every input.\n const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A scoped principal owns a user AND a custom role. Routing it through the\n // user-only door would leave a privileged role behind that the next mint\n // adopts without anybody declaring it.\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: `\n + `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}' for principal `\n + `'${context.principal.kind}:${context.principal.name}'. The role to drop lives IN that `\n + `database — revoking without it would drop the user and silently orphan its privileges.`,\n );\n }\n await adminClient.dropCollectionScopedDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n roleName: `${username}_role`,\n });\n return;\n }\n\n await adminClient.deleteApplicationDatabaseUser({ adminConnectUrl, username });\n },\n\n // No `mintPlatformServiceCredentials` (D-4).\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-self.infra.module.d.ts","sourceRoot":"","sources":["../../../src/postgres-self/postgres-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAOL,mBAAmB,EAMpB,MAAM,+BAA+B,CAAC;AA6HvC,eAAO,MAAM,+BAA+B,EAAE,mBAgM7C,CAAC"}
1
+ {"version":3,"file":"postgres-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/postgres-self/postgres-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAOL,mBAAmB,EAMpB,MAAM,+BAA+B,CAAC;AA6HvC,eAAO,MAAM,+BAA+B,EAAE,mBAgM7C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-self.infra.module.js","sourceRoot":"","sources":["../../../src/postgres-self/postgres-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `PostgresSelfInfraProviderModule` — third concrete `InfraProviderModule`\n * (Slice 4.B.2 of `infra-provider-planner.md`, generalizing the\n * Slice 2 / 4.B.1 pattern).\n *\n * Implements the `POSTGRES_SELF` `InfraProvider` over the\n * `IPostgresAdminClient` transport (raw `pg` Pool against the\n * Postgres admin connection, exposed by\n * `PostgreSQLConnectionManagerService` after the Slice 4.B.2 5C\n * policy/transport split). Owns ALL per-app naming policy — the\n * per-app database-name + role-name conventions (`wildo_<sanitized>_user`,\n * `wildo_<sanitized>` mirroring the legacy\n * `PlatformInfrastructureManagerService.provisionPostgreSQLDatabase`).\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `appSlug` — used as the source for the sanitized database +\n * user name. Mirrors the legacy convention at\n * `platform-infrastructure-manager.service.ts:300-302`\n * (`sanitizedName = sanitizeForPostgreSQL(applicationName)`,\n * `databaseName = wildo_${sanitizedName}`,\n * `username = wildo_${sanitizedName}_user`).\n * - `secretInputs['database.postgresql.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs?.['databaseName']` — optional override\n * for the derived database name. Mirrors the legacy\n * `provisionPostgreSQLDatabase(appSlug, { databaseName? })` arg.\n * - `adminClient` — narrowed to `IPostgresAdminClient`.\n *\n * **What this module IGNORES from the context** (Slice 4.A.2 / M4\n * forward-compat documentation):\n * - `applicationNamespace` — Postgres per-app DB names use the\n * slug-derived sanitized form (env-suffix isolation comes from\n * the surrounding env-scoped admin connection). Future modules\n * may switch to namespace-scoped naming if cross-env collision\n * risk surfaces.\n * - `wildoApplicationId` — Postgres role names embed the\n * sanitized slug, NOT the application id. This is the legacy\n * convention; preserved for back-compat with apps already\n * provisioned.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:\n * cluster-wide Postgres admin creds. Consumers: `apps-manager`\n * (mints per-app DB+role via `mintAppCredentials`), `companion`\n * (mirrors apps-manager admin role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `plan().appRuntime` +\n * `mintAppCredentials`)**: per-customer-app Postgres database +\n * role. Result threaded into\n * `Wildo_Application_Secrets.database.postgresql.{\n * databaseName, username, connectUrl}` via\n * `_merge_WildoApplicationSecrets`. Note: the password is\n * embedded in `connectUrl` — there is no separate\n * `database.postgresql.password` blob path today (matches the\n * legacy persistence shape at\n * `wildo-application-lifecycle.service.ts:2191-2200`).\n *\n * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:\n * - **Audience-2 (platform-service-scoped)**: no platform service\n * uses Postgres for inter-service messaging today. Method omitted.\n *\n * **`revokeAppCredentials`** is called by apps-manager runtime-principal\n * declaration reconciliation when a minion is removed, and for every principal\n * of an application withdrawn from the platform (#1307). A minion's role is\n * DROPPED (it owns nothing); the application backend's role OWNS the database,\n * so it is disabled (`NOLOGIN`, null password, sessions terminated) and the\n * database is kept. Neither path destroys data — that is #1562.\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IPostgresAdminClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipal,\n ApplicationRuntimePrincipalKind,\n deriveMinionDatabaseGrants,\n resolveGrantStorageNames,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Default Postgres port (matches `INFRA_DEFAULT_PORTS.POSTGRES` in\n * the shared platform constants — duplicated as a literal because\n * the planner output is consumed by the CLI which materializes it\n * into `Wildo_Application_Config` blob paths).\n */\nconst DEFAULT_POSTGRES_PORT = 5432;\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes\n * through `mintAppCredentials`. Declared once and reused as\n * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of\n * `secretBlobValues` in the result, so a refactor that renames a\n * path fails loudly at one place.\n *\n * Note: the password is embedded in `connectUrl` (Postgres\n * convention); there is no separate `database.postgresql.password`\n * blob path today. The orchestrator persists `connectUrl`\n * verbatim into `Wildo_Application_Secrets`.\n */\nconst POSTGRES_DATABASE_NAME_PATH = 'database.postgresql.databaseName';\nconst POSTGRES_USERNAME_PATH = 'database.postgresql.username';\nconst POSTGRES_CONNECT_URL_PATH = 'database.postgresql.connectUrl';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [\n POSTGRES_DATABASE_NAME_PATH,\n POSTGRES_USERNAME_PATH,\n POSTGRES_CONNECT_URL_PATH,\n];\n\n\n/**\n * Key the module looks for in `secretInputs` to read the\n * orchestrator-supplied password.\n */\nconst POSTGRES_PASSWORD_INPUT_KEY = 'database.postgresql.password';\n\n/**\n * Optional key the module looks for in `applicationConfigInputs`\n * for an authored database-name override. When omitted, the module\n * derives the database name from `appSlug` via `sanitizeForPostgreSQL`.\n */\nconst POSTGRES_DATABASE_NAME_INPUT_KEY = 'databaseName';\n\n/**\n * Sanitize an application slug for Postgres identifier rules.\n * Mirrors the legacy `sanitizeForPostgreSQL` at\n * `platform-infrastructure-manager.service.ts:371-378`:\n * - lowercase\n * - `[^a-z0-9_]` → `_`\n * - collapse multiple `_`\n * - strip leading/trailing `_`\n * - cap at 50 chars (Postgres identifier limit is 63; leaves room\n * for the `wildo_` prefix and `_user` suffix).\n *\n * Centralized here so the module owns the convention; if the cap\n * or escape rules change, one site changes.\n */\nfunction sanitizeForPostgres(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9_]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Compute the per-app database name. Mirrors legacy convention at\n * `platform-infrastructure-manager.service.ts:301`:\n * `wildo_<sanitized>`\n */\nfunction buildDatabaseName(appSlug: string): string {\n return `wildo_${sanitizeForPostgres(appSlug)}`;\n}\n\n/**\n * Compute the per-app database role. Mirrors legacy convention at\n * `platform-infrastructure-manager.service.ts:302`:\n * `wildo_<sanitized>_user`\n */\nfunction buildUsername(appSlug: string): string {\n return `wildo_${sanitizeForPostgres(appSlug)}_user`;\n}\n\n/**\n * The schema a per-app database's resource tables live in.\n *\n * `public` is what the schema planner targets today. Named as a constant rather\n * than inlined at the four sites that need it because a grant issued into one\n * schema and revoked from another is a revocation that silently succeeds while\n * narrowing nothing.\n */\nconst POSTGRES_APPLICATION_SCHEMA = 'public';\n\n/**\n * Per-principal Postgres role name — the SAME derivation the revoke path uses.\n *\n * Fourth time this shape appears (Redis, RabbitMQ, Mongo, now Postgres), and the\n * consequence is worse here than anywhere else: a revoke that derived the name\n * differently would miss the minion's role AND, on the pre-existing\n * `dropApplicationDatabase` path, take the application's entire database with\n * it. The application backend keeps `wildo_<slug>_user` VERBATIM so an existing\n * deployment's owner role is never orphaned.\n */\nfunction buildPostgresPrincipalUsername(\n appSlug: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildUsername(appSlug);\n }\n return `wildo_${sanitizeForPostgres(appSlug)}_${sanitizeForPostgres(principal.kind)}_`\n + `${sanitizeForPostgres(principal.name ?? '')}_user`;\n}\n\nexport const PostgresSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.POSTGRES_SELF,\n supportedCapabilities: [InfraCapability.RELATIONAL_DATABASE],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['postgres'],\n k8sResources: ['templates/infra/k8s/internal/postgres/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n PG_HOST: 'postgres',\n PG_PORT: String(DEFAULT_POSTGRES_PORT),\n },\n // Admin user + password to provision per-app DBs/roles. The\n // legacy admin-init code reads these from `PG_ADMIN_USER` /\n // `PG_ADMIN_PASSWORD`; the planner emits the keys, the\n // CLI's `env-vars-builder` writes the values at sync time.\n secretEnvVars: ['PG_ADMIN_USER', 'PG_ADMIN_PASSWORD'],\n // Audience-1 only (DB-admin access). No audience-2\n // platform-service-scoped Postgres use case today (D-4).\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Static topology — same for every app in this env. The\n // per-app values (databaseName, username, connectUrl) flow\n // through `perAppSecretBlobPaths`.\n perAppConfigBlobPaths: {\n 'database.postgresql.host': 'postgres',\n 'database.postgresql.port': DEFAULT_POSTGRES_PORT,\n },\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IPostgresAdminClient;\n const password = context.secretInputs[POSTGRES_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `PostgresSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${POSTGRES_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Postgres password — modules are upsert-only.`,\n );\n }\n if (!context.appSlug) {\n throw new Error(\n `PostgresSelfInfraProviderModule.mintAppCredentials: ` +\n `appSlug is empty. ` +\n `The orchestrator MUST supply a non-empty slug.`,\n );\n }\n\n // Allow operator override of the derived database name. Without\n // an override, derive from `appSlug` via the legacy convention.\n const databaseName =\n context.applicationConfigInputs?.[POSTGRES_DATABASE_NAME_INPUT_KEY]\n || buildDatabaseName(context.appSlug);\n const username = buildPostgresPrincipalUsername(context.appSlug, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A named principal gets a TABLE-SCOPED role (Step 3 / R1 / D14): exactly\n // the authored business verbs plus the mandatory runtime floor, inside\n // a database it does not own.\n //\n // `resourceGrants` is REQUIRED and its absence is an error rather than a\n // default. Falling through to `upsertApplicationDatabase` would make the\n // minion the OWNER of the application's database with `ALL PRIVILEGES` —\n // the exact over-grant this path replaces, arriving silently through a\n // code path that reads as if it were doing the narrow thing.\n const grants = context.resourceGrants;\n if (!grants) {\n throw new Error(\n `PostgresSelfInfraProviderModule.mintAppCredentials: `\n + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `\n + `Refusing to fall back to the database-wide application role. The orchestrator must pass `\n + `an explicit empty resource grant when only the mandatory runtime floor is required.`,\n );\n }\n // The authored declaration plus the engine's own floor — the reads and\n // appends the framework performs for ANY principal, which no\n // `resourceAccess` can express and none should have to. Applied through\n // the shared derivation so this adapter and the Mongo one cannot disagree\n // about what that floor is.\n const databaseGrants = deriveMinionDatabaseGrants(grants);\n\n const { connectionUrl: principalConnectionUrl } = await adminClient.upsertTableScopedDatabaseRole({\n databaseName,\n username,\n password,\n schemaName: POSTGRES_APPLICATION_SCHEMA,\n // Resource types → table names HERE, at the adapter that speaks\n // Postgres. The grant contract itself stays in the platform vocabulary,\n // and passes through the SAME translation Mongo uses, so the two\n // adapters cannot disagree about which rows a resource type addresses.\n // SQL identifier casing is applied by the manager, which is the one site\n // that issues the DDL and runs the existence check those names must match.\n readTables: resolveGrantStorageNames(databaseGrants.read),\n writeTables: resolveGrantStorageNames(databaseGrants.write),\n appendTables: resolveGrantStorageNames(databaseGrants.append),\n maintainTables: resolveGrantStorageNames(databaseGrants.maintain),\n });\n\n return {\n secretBlobValues: {\n [POSTGRES_DATABASE_NAME_PATH]: databaseName,\n [POSTGRES_USERNAME_PATH]: username,\n [POSTGRES_CONNECT_URL_PATH]: principalConnectionUrl,\n },\n };\n }\n\n // CREATE/ALTER USER + CREATE DATABASE — idempotent upsert. The\n // admin-client reapplies privileges every time so the call is\n // safe to repeat.\n const { connectionUrl } = await adminClient.upsertApplicationDatabase({\n databaseName,\n username,\n password,\n });\n\n return {\n secretBlobValues: {\n [POSTGRES_DATABASE_NAME_PATH]: databaseName,\n [POSTGRES_USERNAME_PATH]: username,\n [POSTGRES_CONNECT_URL_PATH]: connectionUrl,\n },\n };\n },\n\n /**\n * Revoke a principal's PostgreSQL credential — and destroy no data (#1307). A minion's\n * table-scoped role is dropped (it owns nothing); the application backend's role OWNS the\n * database, so its login is disabled and the role, its ownership and every row are kept.\n * Idempotent: the admin client treats a missing role as a no-op.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation calls this for\n * removed minions, and application withdrawal (`deregisterApplication`) for the backend and every\n * minion; `restoreApplication` re-mints them. Destroying the database is #1562's separate door.\n *\n * **Defensive validation** (Slice 4.A.2 / L4 pattern): empty\n * `appSlug` would derive an empty database name; we fail-loud\n * instead so a bad caller surfaces immediately.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.appSlug) {\n throw new Error(\n `PostgresSelfInfraProviderModule.revokeAppCredentials: ` +\n `appSlug is empty. ` +\n `The orchestrator MUST supply a non-empty slug.`,\n );\n }\n const adminClient = context.adminClient as IPostgresAdminClient;\n const username = buildPostgresPrincipalUsername(context.appSlug, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n const databaseName =\n context.applicationConfigInputs?.[POSTGRES_DATABASE_NAME_INPUT_KEY]\n || buildDatabaseName(context.appSlug);\n // A named principal owns NOTHING but its own role. Routing it through\n // `dropApplicationDatabase` — which is what this method did before named\n // principals existed — would DROP THE APPLICATION'S DATABASE while\n // believing it was retiring one minion's access.\n await adminClient.dropTableScopedDatabaseRole({\n databaseName,\n username,\n schemaName: POSTGRES_APPLICATION_SCHEMA,\n });\n return;\n }\n\n /*\n * The APPLICATION BACKEND's role OWNS the application database, so revoking it disables the LOGIN\n * and keeps the role, its ownership and every row (#1307).\n *\n * This branch used to call `dropApplicationDatabase` — the database AND the role. Nothing called it\n * for the backend until deregistration did, and deregistration is a REVERSIBLE withdrawal: `restore`\n * re-mints the recorded credential and the application boots on the data it left. A revocation that\n * dropped the database would have turned every withdrawal into the irreversible destruction #1562\n * parks behind its own door. Revocation removes authentication; it never removes data.\n */\n await adminClient.revokeApplicationDatabaseLogin({ username });\n },\n\n // No `mintPlatformServiceCredentials` (D-4 of Slice 4.B —\n // audience-2 not applicable to Postgres_Self today).\n};\n"]}
1
+ {"version":3,"file":"postgres-self.infra.module.js","sourceRoot":"","sources":["../../../../src/postgres-self/postgres-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `PostgresSelfInfraProviderModule` — third concrete `InfraProviderModule`\n * (Slice 4.B.2 of `infra-provider-planner.md`, generalizing the\n * Slice 2 / 4.B.1 pattern).\n *\n * Implements the `POSTGRES_SELF` `InfraProvider` over the\n * `IPostgresAdminClient` transport (raw `pg` Pool against the\n * Postgres admin connection, exposed by\n * `PostgreSQLConnectionManagerService` after the Slice 4.B.2 5C\n * policy/transport split). Owns ALL per-app naming policy — the\n * per-app database-name + role-name conventions (`wildo_<sanitized>_user`,\n * `wildo_<sanitized>` mirroring the legacy\n * `PlatformInfrastructureManagerService.provisionPostgreSQLDatabase`).\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `appSlug` — used as the source for the sanitized database +\n * user name. Mirrors the legacy convention at\n * `platform-infrastructure-manager.service.ts:300-302`\n * (`sanitizedName = sanitizeForPostgreSQL(applicationName)`,\n * `databaseName = wildo_${sanitizedName}`,\n * `username = wildo_${sanitizedName}_user`).\n * - `secretInputs['database.postgresql.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs?.['databaseName']` — optional override\n * for the derived database name. Mirrors the legacy\n * `provisionPostgreSQLDatabase(appSlug, { databaseName? })` arg.\n * - `adminClient` — narrowed to `IPostgresAdminClient`.\n *\n * **What this module IGNORES from the context** (Slice 4.A.2 / M4\n * forward-compat documentation):\n * - `applicationNamespace` — Postgres per-app DB names use the\n * slug-derived sanitized form (env-suffix isolation comes from\n * the surrounding env-scoped admin connection). Future modules\n * may switch to namespace-scoped naming if cross-env collision\n * risk surfaces.\n * - `wildoApplicationId` — Postgres role names embed the\n * sanitized slug, NOT the application id. This is the legacy\n * convention; preserved for back-compat with apps already\n * provisioned.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:\n * cluster-wide Postgres admin creds. Consumers: `apps-manager`\n * (mints per-app DB+role via `mintAppCredentials`), `companion`\n * (mirrors apps-manager admin role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `plan().appRuntime` +\n * `mintAppCredentials`)**: per-customer-app Postgres database +\n * role. Result threaded into\n * `Wildo_Application_Secrets.database.postgresql.{\n * databaseName, username, connectUrl}` via\n * `_merge_WildoApplicationSecrets`. Note: the password is\n * embedded in `connectUrl` — there is no separate\n * `database.postgresql.password` blob path today (matches the\n * legacy persistence shape at\n * `wildo-application-lifecycle.service.ts:2191-2200`).\n *\n * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:\n * - **Audience-2 (platform-service-scoped)**: no platform service\n * uses Postgres for inter-service messaging today. Method omitted.\n *\n * **`revokeAppCredentials`** is called by apps-manager runtime-principal\n * declaration reconciliation when a minion is removed, and for every principal\n * of an application withdrawn from the platform (#1307). A minion's role is\n * DROPPED (it owns nothing); the application backend's role OWNS the database,\n * so it is disabled (`NOLOGIN`, null password, sessions terminated) and the\n * database is kept. Neither path destroys data — that is #1562.\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IPostgresAdminClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipal,\n ApplicationRuntimePrincipalKind,\n deriveMinionDatabaseGrants,\n resolveGrantStorageNames,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Default Postgres port (matches `INFRA_DEFAULT_PORTS.POSTGRES` in\n * the shared platform constants — duplicated as a literal because\n * the planner output is consumed by the CLI which materializes it\n * into `Wildo_Application_Config` blob paths).\n */\nconst DEFAULT_POSTGRES_PORT = 5432;\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes\n * through `mintAppCredentials`. Declared once and reused as\n * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of\n * `secretBlobValues` in the result, so a refactor that renames a\n * path fails loudly at one place.\n *\n * Note: the password is embedded in `connectUrl` (Postgres\n * convention); there is no separate `database.postgresql.password`\n * blob path today. The orchestrator persists `connectUrl`\n * verbatim into `Wildo_Application_Secrets`.\n */\nconst POSTGRES_DATABASE_NAME_PATH = 'database.postgresql.databaseName';\nconst POSTGRES_USERNAME_PATH = 'database.postgresql.username';\nconst POSTGRES_CONNECT_URL_PATH = 'database.postgresql.connectUrl';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [\n POSTGRES_DATABASE_NAME_PATH,\n POSTGRES_USERNAME_PATH,\n POSTGRES_CONNECT_URL_PATH,\n];\n\n\n/**\n * Key the module looks for in `secretInputs` to read the\n * orchestrator-supplied password.\n */\nconst POSTGRES_PASSWORD_INPUT_KEY = 'database.postgresql.password';\n\n/**\n * Optional key the module looks for in `applicationConfigInputs`\n * for an authored database-name override. When omitted, the module\n * derives the database name from `appSlug` via `sanitizeForPostgreSQL`.\n */\nconst POSTGRES_DATABASE_NAME_INPUT_KEY = 'databaseName';\n\n/**\n * Sanitize an application slug for Postgres identifier rules.\n * Mirrors the legacy `sanitizeForPostgreSQL` at\n * `platform-infrastructure-manager.service.ts:371-378`:\n * - lowercase\n * - `[^a-z0-9_]` → `_`\n * - collapse multiple `_`\n * - strip leading/trailing `_`\n * - cap at 50 chars (Postgres identifier limit is 63; leaves room\n * for the `wildo_` prefix and `_user` suffix).\n *\n * Centralized here so the module owns the convention; if the cap\n * or escape rules change, one site changes.\n */\nfunction sanitizeForPostgres(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9_]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Compute the per-app database name. Mirrors legacy convention at\n * `platform-infrastructure-manager.service.ts:301`:\n * `wildo_<sanitized>`\n */\nfunction buildDatabaseName(appSlug: string): string {\n return `wildo_${sanitizeForPostgres(appSlug)}`;\n}\n\n/**\n * Compute the per-app database role. Mirrors legacy convention at\n * `platform-infrastructure-manager.service.ts:302`:\n * `wildo_<sanitized>_user`\n */\nfunction buildUsername(appSlug: string): string {\n return `wildo_${sanitizeForPostgres(appSlug)}_user`;\n}\n\n/**\n * The schema a per-app database's resource tables live in.\n *\n * `public` is what the schema planner targets today. Named as a constant rather\n * than inlined at the four sites that need it because a grant issued into one\n * schema and revoked from another is a revocation that silently succeeds while\n * narrowing nothing.\n */\nconst POSTGRES_APPLICATION_SCHEMA = 'public';\n\n/**\n * Per-principal Postgres role name — the SAME derivation the revoke path uses.\n *\n * Fourth time this shape appears (Redis, RabbitMQ, Mongo, now Postgres), and the\n * consequence is worse here than anywhere else: a revoke that derived the name\n * differently would miss the minion's role AND, on the pre-existing\n * `dropApplicationDatabase` path, take the application's entire database with\n * it. The application backend keeps `wildo_<slug>_user` VERBATIM so an existing\n * deployment's owner role is never orphaned.\n */\nfunction buildPostgresPrincipalUsername(\n appSlug: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildUsername(appSlug);\n }\n return `wildo_${sanitizeForPostgres(appSlug)}_${sanitizeForPostgres(principal.kind)}_`\n + `${sanitizeForPostgres(principal.name ?? '')}_user`;\n}\n\nexport const PostgresSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.POSTGRES_SELF,\n supportedCapabilities: [InfraCapability.RELATIONAL_DATABASE],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['postgres'],\n k8sResources: ['templates/infra/k8s/internal/postgres/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n PG_HOST: 'postgres',\n PG_PORT: String(DEFAULT_POSTGRES_PORT),\n },\n // Admin user + password to provision per-app DBs/roles. The\n // legacy admin-init code reads these from `PG_ADMIN_USER` /\n // `PG_ADMIN_PASSWORD`; the planner emits the keys, the\n // CLI's `env-vars-builder` writes the values at sync time.\n secretEnvVars: ['PG_ADMIN_USER', 'PG_ADMIN_PASSWORD'],\n // Audience-1 only (DB-admin access). No audience-2\n // platform-service-scoped Postgres use case today (D-4).\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Static topology — same for every app in this env. The\n // per-app values (databaseName, username, connectUrl) flow\n // through `perAppSecretBlobPaths`.\n perAppConfigBlobPaths: {\n 'database.postgresql.host': 'postgres',\n 'database.postgresql.port': DEFAULT_POSTGRES_PORT,\n },\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IPostgresAdminClient;\n const password = context.secretInputs[POSTGRES_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `PostgresSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${POSTGRES_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Postgres password — modules are upsert-only.`,\n );\n }\n if (!context.appSlug) {\n throw new Error(\n `PostgresSelfInfraProviderModule.mintAppCredentials: ` +\n `appSlug is empty. ` +\n `The orchestrator MUST supply a non-empty slug.`,\n );\n }\n\n // Allow operator override of the derived database name. Without\n // an override, derive from `appSlug` via the legacy convention.\n const databaseName =\n context.applicationConfigInputs?.[POSTGRES_DATABASE_NAME_INPUT_KEY]\n || buildDatabaseName(context.appSlug);\n const username = buildPostgresPrincipalUsername(context.appSlug, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A named principal gets a TABLE-SCOPED role (Step 3 / R1 / D14): exactly\n // the authored business verbs plus the mandatory runtime floor, inside\n // a database it does not own.\n //\n // `resourceGrants` is REQUIRED and its absence is an error rather than a\n // default. Falling through to `upsertApplicationDatabase` would make the\n // minion the OWNER of the application's database with `ALL PRIVILEGES` —\n // the exact over-grant this path replaces, arriving silently through a\n // code path that reads as if it were doing the narrow thing.\n const grants = context.resourceGrants;\n if (!grants) {\n throw new Error(\n `PostgresSelfInfraProviderModule.mintAppCredentials: `\n + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `\n + `Refusing to fall back to the database-wide application role. The orchestrator must pass `\n + `an explicit empty resource grant when only the mandatory runtime floor is required.`,\n );\n }\n // The authored declaration plus the engine's own floor — the reads and\n // appends the framework performs for ANY principal, which no\n // `resourceAccess` can express and none should have to. Applied through\n // the shared derivation so this adapter and the Mongo one cannot disagree\n // about what that floor is.\n const databaseGrants = deriveMinionDatabaseGrants(grants);\n\n const { connectionUrl: principalConnectionUrl } = await adminClient.upsertTableScopedDatabaseRole({\n databaseName,\n username,\n password,\n schemaName: POSTGRES_APPLICATION_SCHEMA,\n // Resource types → table names HERE, at the adapter that speaks\n // Postgres. The grant contract itself stays in the platform vocabulary,\n // and passes through the SAME translation Mongo uses, so the two\n // adapters cannot disagree about which rows a resource type addresses.\n // SQL identifier casing is applied by the manager, which is the one site\n // that issues the DDL and runs the existence check those names must match.\n readTables: resolveGrantStorageNames(databaseGrants.read),\n writeTables: resolveGrantStorageNames(databaseGrants.write),\n appendTables: resolveGrantStorageNames(databaseGrants.append),\n maintainTables: resolveGrantStorageNames(databaseGrants.maintain),\n });\n\n return {\n secretBlobValues: {\n [POSTGRES_DATABASE_NAME_PATH]: databaseName,\n [POSTGRES_USERNAME_PATH]: username,\n [POSTGRES_CONNECT_URL_PATH]: principalConnectionUrl,\n },\n };\n }\n\n // CREATE/ALTER USER + CREATE DATABASE — idempotent upsert. The\n // admin-client reapplies privileges every time so the call is\n // safe to repeat.\n const { connectionUrl } = await adminClient.upsertApplicationDatabase({\n databaseName,\n username,\n password,\n });\n\n return {\n secretBlobValues: {\n [POSTGRES_DATABASE_NAME_PATH]: databaseName,\n [POSTGRES_USERNAME_PATH]: username,\n [POSTGRES_CONNECT_URL_PATH]: connectionUrl,\n },\n };\n },\n\n /**\n * Revoke a principal's PostgreSQL credential — and destroy no data (#1307). A minion's\n * table-scoped role is dropped (it owns nothing); the application backend's role OWNS the\n * database, so its login is disabled and the role, its ownership and every row are kept.\n * Idempotent: the admin client treats a missing role as a no-op.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation calls this for\n * removed minions, and application withdrawal (`deregisterApplication`) for the backend and every\n * minion; `restoreApplication` re-mints them. Destroying the database is #1562's separate door.\n *\n * **Defensive validation** (Slice 4.A.2 / L4 pattern): empty\n * `appSlug` would derive an empty database name; we fail-loud\n * instead so a bad caller surfaces immediately.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.appSlug) {\n throw new Error(\n `PostgresSelfInfraProviderModule.revokeAppCredentials: ` +\n `appSlug is empty. ` +\n `The orchestrator MUST supply a non-empty slug.`,\n );\n }\n const adminClient = context.adminClient as IPostgresAdminClient;\n const username = buildPostgresPrincipalUsername(context.appSlug, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n const databaseName =\n context.applicationConfigInputs?.[POSTGRES_DATABASE_NAME_INPUT_KEY]\n || buildDatabaseName(context.appSlug);\n // A named principal owns NOTHING but its own role. Routing it through\n // `dropApplicationDatabase` — which is what this method did before named\n // principals existed — would DROP THE APPLICATION'S DATABASE while\n // believing it was retiring one minion's access.\n await adminClient.dropTableScopedDatabaseRole({\n databaseName,\n username,\n schemaName: POSTGRES_APPLICATION_SCHEMA,\n });\n return;\n }\n\n /*\n * The APPLICATION BACKEND's role OWNS the application database, so revoking it disables the LOGIN\n * and keeps the role, its ownership and every row (#1307).\n *\n * This branch used to call `dropApplicationDatabase` — the database AND the role. Nothing called it\n * for the backend until deregistration did, and deregistration is a REVERSIBLE withdrawal: `restore`\n * re-mints the recorded credential and the application boots on the data it left. A revocation that\n * dropped the database would have turned every withdrawal into the irreversible destruction #1562\n * parks behind its own door. Revocation removes authentication; it never removes data.\n */\n await adminClient.revokeApplicationDatabaseLogin({ username });\n },\n\n // No `mintPlatformServiceCredentials` (D-4 of Slice 4.B —\n // audience-2 not applicable to Postgres_Self today).\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"rabbitmq-self.infra.module.d.ts","sourceRoot":"","sources":["../../../src/rabbitmq-self/rabbitmq-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAEH,OAAO,EAOL,mBAAmB,EAOpB,MAAM,+BAA+B,CAAC;AAwMvC,eAAO,MAAM,+BAA+B,EAAE,mBAqO7C,CAAC"}
1
+ {"version":3,"file":"rabbitmq-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/rabbitmq-self/rabbitmq-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAEH,OAAO,EAOL,mBAAmB,EAOpB,MAAM,+BAA+B,CAAC;AAwMvC,eAAO,MAAM,+BAA+B,EAAE,mBAqO7C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"rabbitmq-self.infra.module.js","sourceRoot":"","sources":["../../../src/rabbitmq-self/rabbitmq-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RabbitmqSelfInfraProviderModule` — first concrete `InfraProviderModule`\n * (Slices 1–2.5 of `infra-provider-planner.md`).\n *\n * Implements the `RABBITMQ_SELF` `InfraProvider` over the\n * `IRabbitMQManagementClient` transport (raw HTTP `PUT /api/users` etc.,\n * exposed by `RabbitMQConnectionManagerService` after the 5C policy/\n * transport split). Owns ALL per-app and per-platform-service\n * permission policy — the regex shapes, the broader-permission\n * special case for `crontabs-batches-manager`, the per-app username\n * convention, the optional vhost activation.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `applicationNamespace` — used as the rabbit username\n * (`<appSlug>-<envId>`, the legacy convention from\n * `wildo-application-lifecycle.service.ts`).\n * - `wildoApplicationId` — used as the permission-regex disambiguator\n * (matches `^wildo\\.(queue|reply)\\.<id>\\..*`).\n * - `secretInputs['database.rabbitmq.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs?.['vhost']` — optional per-app vhost\n * for true vhost-level isolation (Slice 2.5c-3).\n * - `adminClient` — narrowed to `IRabbitMQManagementClient`.\n *\n * **What this module IGNORES from the context** (Slice 4.A.2 / M4):\n * - `appSlug` — RabbitMQ resources are namespace-scoped, not slug-\n * scoped. Future modules (MongoDB DB name, etc.) WILL use `appSlug`.\n * This is documented forward-compat for module authors: not every\n * module reads every context field; each declares which it consumes.\n *\n * **Three audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:\n * cluster-wide management-API creds (`RABBITMQ_ADMIN_USERNAME` /\n * `RABBITMQ_ADMIN_PASSWORD`). Consumers: `apps-manager`,\n * `companion`. Delivered through process.env (the CLI's\n * `env-vars-builder` writes them per-env). No method.\n *\n * 2. **Audience-2 (platform-service-scoped, via\n * `mintPlatformServiceCredentials`)**: per-platform-service broker\n * users for inter-service messaging (`apps-manager`,\n * `crontabs-batches-manager`, `file-generator`). Username =\n * `serviceName`; permission regex is service-namespaced\n * (`^wildo\\.platform\\.<slug>\\..*`); crontabs gets the broader\n * `^wildo\\.(queue|reply|platform)\\..*` per the legacy override.\n *\n * **Slice 2.5 behavior-parity note**: the legacy code paths\n * `provisionServiceCredentials` and `ensureAppsManagerCredentials`\n * produced slightly different topic-permission regexes for the\n * same apps-manager user (`wildo\\.platform\\.apps_manager\\..*` vs\n * `wildo.platform.apps_manager.*`). User permissions were therefore\n * order-dependent. This module unifies on the dot-escaped variant.\n * Functionally equivalent for real queue names; tighter spec.\n *\n * **Slice 4.A.2 / M2 design note**: the legacy\n * `getDefaultRabbitMQPermissions.read` field declared a BROADER\n * regex than `configure`/`write` (included\n * `wildo.platform.broadcast.*`) — but that widening NEVER reached\n * the broker (the legacy `upsertTenantUserPermissions` ignored\n * the `read` arg and used `resourceNameRegex` for both\n * `configure` and `read`). This module preserves that behavior:\n * `read === configure`. If platform services need a broadcast\n * namespace in the future, extend `mintPlatformServiceCredentials`\n * with an explicit broadcast-read regex.\n *\n * 3. **Audience-3 (app-scoped, via `plan().appRuntime` +\n * `mintAppCredentials`)**: per-customer-app broker user. Username\n * = `applicationNamespace`; permission regex disambiguates by\n * `wildoApplicationId` (Mongo ObjectId — globally unique). Result\n * threaded into `Wildo_Application_Secrets.database.rabbitmq.{\n * username, password}` via `_merge_WildoApplicationSecrets`.\n *\n * **Vhost activation (Slice 2.5c-3, opt-in)**: when the operator\n * authors `backingServices.rabbitmq.vhost` in\n * `wildo.infra.<envName>.config.ts`, the CLI sends it with that\n * application's registration (`backingServiceAddressing`, #1107); the lifecycle\n * service threads it via `applicationConfigInputs.vhost`; this module\n * `ensureVhost`-then-permissions-on-it. Without the field, the\n * module falls back to `/` (legacy permission-regex isolation).\n *\n * **`revokeAppCredentials`** is called by apps-manager runtime-principal\n * declaration reconciliation when a minion is removed, and for every principal\n * (the application backend included) of an application withdrawn from the\n * platform (#1307). The lifecycle service passes the application namespace and\n * the principal; its broker user is deleted; the vhost and its queues stay (#1562 owns destruction).\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IRabbitMQManagementClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n PlatformServiceCredentialMintingContext,\n PlatformServiceCredentialMintingResult,\n ApplicationRuntimePrincipalKind,\n buildRuntimePrincipalQueueSegment,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Default vhost — apps share `/` with regex-based per-user permission\n * isolation. Per-app vhost is intentionally NOT modeled (engine has no\n * `RABBITMQ_VHOST` consumer; isolation comes from the permission regex\n * on the per-app user, not from a per-app vhost).\n *\n * **No `ensureVhost(DEFAULT_VHOST)` call** is needed: vhost `/` is\n * created by RabbitMQ at server boot. Any future module variant that\n * uses a non-default vhost MUST call `adminClient.ensureVhost(...)`\n * before user/permissions setup.\n */\nconst DEFAULT_VHOST = '/';\n\n/**\n * Topic exchange used by every Wildo runtime publisher / consumer.\n * Matches the value already in use at\n * `wildo-application-lifecycle.service.ts:568` and\n * `platform-infrastructure-manager.service.ts:485`.\n */\nconst TOPIC_EXCHANGE = 'amq.topic';\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes through\n * `mintAppCredentials`. Declared once and reused as\n * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of\n * `secretBlobValues` in the result, so a refactor that renames a path\n * fails loudly at one place instead of drifting silently.\n */\nconst RABBITMQ_USERNAME_PATH = 'database.rabbitmq.username';\nconst RABBITMQ_PASSWORD_PATH = 'database.rabbitmq.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [RABBITMQ_USERNAME_PATH, RABBITMQ_PASSWORD_PATH];\n\n\n/**\n * Key the module looks for in\n * `AppCredentialMintingContext.applicationConfigInputs` to read the\n * optional per-app virtual host. Documented here as the public\n * contract: the orchestrator supplies the vhost via this key, the\n * module reads it via this key. Slice 2.5c-3 of\n * `infra-provider-planner.md`.\n */\nconst VHOST_INPUT_KEY = 'vhost';\n\n/**\n * Key the module looks for in\n * `PlatformServiceCredentialMintingContext.secretInputs` to read the\n * orchestrator-supplied platform-service password. Slice 4.A.2 / M5\n * of `infra-provider-planner.md` — extracted as a constant for\n * symmetry with `RABBITMQ_PASSWORD_PATH` (used by `mintAppCredentials`),\n * so a future rename touches one site instead of drifting silently.\n */\nconst PLATFORM_SERVICE_PASSWORD_INPUT_KEY = 'password';\n\n/**\n * Compute the per-app rabbit username. Mirrors the existing convention\n * at `wildo-application-lifecycle.service.ts:434, 440` — username is\n * the application namespace (`<appSlug>-<environmentId>` per\n * `buildApplicationNamespace`), NOT the raw slug. Centralized here so\n * future changes touch one site.\n */\nfunction buildRabbitUsername(applicationNamespace: string): string {\n return applicationNamespace;\n}\n\n/**\n * Identifier of the platform-crontabs-batches-manager service. The\n * crontabs special case (broader permissions covering all per-app\n * queue/reply names so it can fan out scheduled jobs) lives here so\n * the legacy hard-coded check at\n * `platform-infrastructure-manager.service.ts:466-475` becomes a\n * module-internal concern.\n *\n * Hardcoded as a string literal rather than imported from\n * `PlatformApplicationType` because that enum lives in saas-models\n * and importing it here would couple the module to that enum's\n * runtime shape — the value `'platform_crontabs_batches_manager'` is\n * stable and small.\n */\nconst PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME = 'platform_crontabs_batches_manager';\n\n/**\n * Compute the per-platform-service permission regex.\n *\n * Default policy (apps-manager, file-generator, …): the service can\n * configure / write / read on `^wildo\\.platform\\.<serviceSlug>\\..*`\n * where `serviceSlug = serviceName.replace('platform_', '')`. Mirrors\n * the legacy `getDefaultRabbitMQPermissions` at\n * `credentials-provisioner.service.ts:267-275`.\n *\n * Crontabs-batches-manager override: broader regex\n * `^wildo\\.(queue|reply|platform)\\..*` so the crontab can publish\n * scheduled jobs onto every per-app queue and read replies. Mirrors\n * the legacy inline override at\n * `platform-infrastructure-manager.service.ts:466-475`.\n */\nfunction buildPlatformServiceRegex(serviceName: string): string {\n if (serviceName === PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME) {\n return '^wildo\\\\.(queue|reply|platform)\\\\..*';\n }\n const serviceSlug = serviceName.replace('platform_', '');\n return `^wildo\\\\.platform\\\\.${serviceSlug}\\\\..*`;\n}\n\n/**\n * Compute the per-app permission regex. Matches the convention at\n * `wildo-application-lifecycle.service.ts:552`:\n * `^wildo\\.(queue|reply)\\.${wildoApplicationId}\\..*`\n * The disambiguator is the application ID, NOT the slug — slugs could\n * theoretically be reassigned; the id is forever-stable. The `^` and\n * `\\.` escaping is required by the RabbitMQ regex evaluator.\n */\nfunction buildPermissionRegex(wildoApplicationId: string): string {\n // Escape regex metacharacters that may appear in application ids\n // (today they're Mongo ObjectId hex strings — no specials — but the\n // contract only commits to \"stable opaque string\", so guard).\n const escapedId = wildoApplicationId.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return `^wildo\\\\.(queue|reply)\\\\.${escapedId}\\\\..*`;\n}\n\n/**\n * The queues a MINION may CONSUME from — its own tick queue only.\n *\n * ## Why only `read` narrows, measured rather than assumed\n *\n * A minion runs the full backend pipeline headless, so the three RabbitMQ\n * permission axes are not interchangeable here:\n *\n * - **`read`** governs CONSUMING. Narrowing it is the containment that matters:\n * without it a minion can drain the application backend's job queue, consume\n * another minion's ticks, or read RPC replies addressed to the backend.\n * - **`configure`** governs DECLARE. The tick consumer declares its own queue\n * (`channel.assertQueue`) — but the delayed-publish path can declare a delay\n * queue too, so narrowing this risks breaking a legitimate publish.\n * - **`write`** governs PUBLISHING. A minion legitimately enqueues jobs and\n * sends RPC replies into the application's namespace; `publish` calls\n * `sendToQueue` WITHOUT asserting (verified in\n * `rabbitmq-adapter.backend.service.ts`), so it needs `write` on the app\n * namespace and nothing more.\n *\n * **What this therefore does NOT prevent, stated plainly:** a minion can still\n * PUBLISH anywhere in its application's queue namespace — it could enqueue a job\n * the backend will run. Constraining that is a different control (who may\n * request what work), not a broker permission, and pretending otherwise would\n * overstate the boundary.\n *\n * The trailing `(\\..*)?$` admits the tick queue itself and any suffixed\n * variant, while the anchored `$` stops it matching a SIBLING whose name merely\n * starts with this one's.\n */\n/**\n * Per-principal broker username — the SAME derivation `revokeAppCredentials`\n * must use.\n *\n * The application backend keeps `applicationNamespace` VERBATIM so an existing\n * deployment's user is never orphaned. A revoke that derived this differently\n * would delete the BACKEND's broker user while believing it was removing a\n * minion's — the identical trap already caught on the Redis side.\n */\nfunction buildRabbitPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildRabbitUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n return `${applicationNamespace}-${principal.kind}-${sanitizedName}`;\n}\n\nfunction buildMinionConsumeRegex(wildoApplicationId: string, minionName: string): string {\n const escape = (value: string): string => value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const appId = escape(wildoApplicationId);\n const name = escape(minionName);\n // Two alternatives, and nothing else:\n // its tick queue — wildo.queue.<appId>.minion-tick.<name>\n // its OWN reply queue — wildo.reply.<appId>.<segment>.<podId>\n // The reply segment is IMPORTED from the same builder the minion's own AMQP\n // adapter uses, not reproduced here. It used to be hand-written as\n // `minion-<name>` with a comment saying it \"mirrors\" what the adapter emits —\n // which agreed only while every minion name survived sanitisation unchanged.\n // The application backend gets no segment at all (the builder returns\n // undefined for it), keeping the historical layout, so this cannot match its\n // replies.\n const replySegment = escape(\n buildRuntimePrincipalQueueSegment(ApplicationRuntimePrincipalKind.MINION, minionName) ?? '',\n );\n return `^(wildo\\\\.queue\\\\.${appId}\\\\.minion-tick\\\\.${name}(\\\\..*)?`\n + `|wildo\\\\.reply\\\\.${appId}\\\\.${replySegment}\\\\..*)$`;\n}\n\nexport const RabbitmqSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.RABBITMQ_SELF,\n supportedCapabilities: [InfraCapability.QUEUING_SYSTEM],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['rabbitmq'],\n k8sResources: ['templates/infra/k8s/internal/rabbitmq/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n RABBITMQ_HOST: 'rabbitmq',\n RABBITMQ_PORT: '5672',\n RABBITMQ_MANAGEMENT_PORT: '15672',\n },\n secretEnvVars: ['RABBITMQ_ADMIN_USERNAME', 'RABBITMQ_ADMIN_PASSWORD'],\n // Audience-1 only (management-API access). audience-2 platform-\n // service-scoped consumers (apps-manager / crontabs / file-\n // generator publish-consume creds via `provisionAllPlatformServices`)\n // are LD-8 territory, deferred to Slice 2.5 — they get their\n // own `mintPlatformServiceCredentials` method then.\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Declared for forward-compat with Slice 2.5's\n // `_merge_WildoApplicationConfig` integration. Not consumed by\n // any planner path in Slice 2 — the existing hard-coded values\n // at `wildo-application-lifecycle.service.ts:1796-1799` stay in\n // effect (host: 'localhost', port: 5672) until the planner\n // wires this slice's contribution.\n perAppConfigBlobPaths: {\n 'database.rabbitmq.host': 'rabbitmq',\n 'database.rabbitmq.port': 5672,\n },\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IRabbitMQManagementClient;\n const password = context.secretInputs[RABBITMQ_PASSWORD_PATH];\n if (!password) {\n throw new Error(\n `RabbitmqSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${RABBITMQ_PASSWORD_PATH}'. ` +\n `The orchestrator MUST supply the rabbit password — modules are upsert-only.`,\n );\n }\n\n const username = buildRabbitPrincipalUsername(context.applicationNamespace, context.principal);\n const permissionRegex = buildPermissionRegex(context.wildoApplicationId);\n // A MINION consumes exactly two things: its own tick queue, and its own RPC\n // REPLY queue. Both are admitted; everything else in the application's\n // namespace is refused.\n //\n // The reply half is why an earlier attempt at this failed. Reply queues used\n // to be `wildo.reply.<appId>.<podId>`, and `podId` is a per-PROCESS token\n // carrying no principal — so no regex could admit a minion's replies while\n // refusing the backend's, and narrowing `read` crash-looped the minion on\n // its own replies. `buildReplyQueueName` now embeds the owning runtime,\n // which is what makes this expressible at all.\n const readRegex = context.principal?.kind === ApplicationRuntimePrincipalKind.MINION && context.principal.name\n ? buildMinionConsumeRegex(context.wildoApplicationId, context.principal.name)\n : permissionRegex;\n\n // Slice 2.5c-3: optional per-app vhost. When the orchestrator\n // supplies a non-empty `applicationConfigInputs.vhost`, the module\n // ensures the vhost exists and sets permissions ON THAT vhost\n // (provides true vhost-level isolation). When omitted, the module\n // falls back to the default vhost `/` (legacy Slice 2 behavior:\n // permission-regex isolation only). This is the opt-in seam — apps\n // without an authored `backingServices.rabbitmq.vhost` continue to\n // share `/`.\n const vhost = context.applicationConfigInputs?.[VHOST_INPUT_KEY] || DEFAULT_VHOST;\n if (vhost !== DEFAULT_VHOST) {\n await adminClient.ensureVhost(vhost);\n }\n\n // PUT /api/users — idempotent upsert (replaces password + tags on conflict).\n await adminClient.putUser(username, password, []);\n\n // PUT /api/permissions — `configure` and `read` use the per-app\n // queue/reply regex; `write` widens to allow `amq.default` (the\n // default exchange RabbitMQ uses for direct routing) and the empty\n // string (broker-internal). Mirrors the legacy\n // `upsertTenantUserPermissions` write-regex shape so the Wonder\n // Todos behavior is unchanged.\n const writeRegex = `(^$|amq\\\\.default|${permissionRegex})`;\n await adminClient.setPermissions(vhost, username, {\n configure: permissionRegex,\n write: writeRegex,\n read: readRegex,\n });\n\n // PUT /api/topic-permissions — same regex on the topic exchange.\n await adminClient.setTopicPermissions(vhost, username, {\n exchange: TOPIC_EXCHANGE,\n write: permissionRegex,\n read: readRegex,\n });\n\n // Per-app-vhost cron delivery (`.claude/plans/crontab-publisher-vhost-gap.md`).\n //\n // A per-app vhost is a HARD broker isolation boundary: the platform\n // cron/minion publisher (`platform_crontabs_batches_manager`) — a single\n // global user that fans scheduled jobs into every app — cannot reach THIS\n // app's queues until granted access on THIS vhost. On the default vhost `/`\n // it already holds a broad publish grant (see `mintPlatformServiceCredentials`),\n // so the grant is only needed for a non-default per-app vhost.\n //\n // Scope it to this app's own queue namespace (`^wildo\\.(queue|reply)\\.<appId>\\..*`)\n // — the publisher gets exactly the reach it needs and no more, TIGHTER than\n // its broad default-vhost grant. It is PUBLISH-ONLY: `configure` for the\n // passive `checkQueue` existence probe the publisher runs before sending +\n // any declare, `write` (`amq.default`) for `basic.publish`; NO `read` (never\n // consumes) and NO topic permissions (uses the default exchange, not\n // `amq.topic`). The crontabs user is provisioned before any app\n // (apps-manager INIT Phase 4 < Phase 5), so it reliably exists here, and\n // `setPermissions` is an idempotent PUT — a re-init safely re-applies it.\n if (vhost !== DEFAULT_VHOST) {\n await adminClient.setPermissions(vhost, PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME, {\n configure: permissionRegex,\n write: writeRegex,\n read: '',\n });\n }\n\n return {\n secretBlobValues: {\n [RABBITMQ_USERNAME_PATH]: username,\n // Echo the orchestrator-supplied password — proves the module\n // is upsert-only, not generator. The orchestrator persists\n // this exact value into `Wildo_Application_Secrets`.\n [RABBITMQ_PASSWORD_PATH]: password,\n },\n };\n },\n\n /**\n * Tear down the per-app rabbit user. RabbitMQ users are global, so\n * `deleteUser` removes the user (and its permissions on every vhost\n * it had access to) atomically.\n *\n * **Vhost retention** (Slice 4.A.2 / M6): when `mintAppCredentials`\n * provisioned a per-app vhost (Slice 2.5c-3), `revokeAppCredentials`\n * does NOT delete the vhost. Reasoning: vhost deletion drops every\n * queue and exchange inside it, which would destroy in-flight\n * messages mid-flight if a redeploy or restart is happening\n * concurrently. The user-deletion alone is sufficient to revoke\n * access; the empty vhost lingers as a paper trail. A future\n * \"app-deletion lifecycle\" slice can layer vhost cleanup with\n * proper drain semantics.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation\n * calls this for removed minions, and application withdrawal\n * (`deregisterApplication`, #1307) for the backend and every minion. The vhost\n * and its queues are kept; `restoreApplication` re-mints the user.\n *\n * **Defensive validation** (Slice 4.A.2 / L4): empty\n * `applicationNamespace` would call `DELETE /api/users/` and\n * silently 404; we fail-loud instead so a bad caller surfaces\n * immediately.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `RabbitmqSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminClient = context.adminClient as IRabbitMQManagementClient;\n const username = buildRabbitPrincipalUsername(context.applicationNamespace, context.principal);\n await adminClient.deleteUser(username);\n },\n\n async mintPlatformServiceCredentials(\n context: PlatformServiceCredentialMintingContext,\n ): Promise<PlatformServiceCredentialMintingResult> {\n const adminClient = context.adminClient as IRabbitMQManagementClient;\n const password = context.secretInputs[PLATFORM_SERVICE_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `RabbitmqSelfInfraProviderModule.mintPlatformServiceCredentials: ` +\n `secretInputs is missing '${PLATFORM_SERVICE_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the platform-service password — modules are upsert-only.`,\n );\n }\n\n // Username is the platform service identifier verbatim — preserves\n // the legacy convention from\n // `PlatformInfrastructureManagerService.provisionServiceCredentials`\n // (line 478: `username: serviceName`).\n const username = context.serviceName;\n const permissionRegex = buildPlatformServiceRegex(context.serviceName);\n\n // PUT /api/users — idempotent upsert.\n await adminClient.putUser(username, password, []);\n\n // Permissions: configure/read use the per-service regex; write\n // widens to allow `amq.default` + empty string (broker-internal),\n // matching the `upsertTenantUserPermissions` write-regex shape so\n // post-Slice-2.5 broker permissions match pre-Slice-2.5.\n const writeRegex = `(^$|amq\\\\.default|${permissionRegex})`;\n await adminClient.setPermissions(DEFAULT_VHOST, username, {\n configure: permissionRegex,\n write: writeRegex,\n read: permissionRegex,\n });\n\n // Topic permissions: same dot-escaped regex on amq.topic. The\n // legacy two-caller divergence (one path stripped the `^` and\n // produced `wildo\\.platform\\.<slug>\\..*`; the other produced\n // `wildo.platform.<slug>.*` with literal dots) is unified here on\n // the dot-escaped variant — see module JSDoc behavior-parity note.\n await adminClient.setTopicPermissions(DEFAULT_VHOST, username, {\n exchange: TOPIC_EXCHANGE,\n write: permissionRegex,\n read: permissionRegex,\n });\n\n return { username };\n },\n};\n"]}
1
+ {"version":3,"file":"rabbitmq-self.infra.module.js","sourceRoot":"","sources":["../../../../src/rabbitmq-self/rabbitmq-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RabbitmqSelfInfraProviderModule` — first concrete `InfraProviderModule`\n * (Slices 1–2.5 of `infra-provider-planner.md`).\n *\n * Implements the `RABBITMQ_SELF` `InfraProvider` over the\n * `IRabbitMQManagementClient` transport (raw HTTP `PUT /api/users` etc.,\n * exposed by `RabbitMQConnectionManagerService` after the 5C policy/\n * transport split). Owns ALL per-app and per-platform-service\n * permission policy — the regex shapes, the broader-permission\n * special case for `crontabs-batches-manager`, the per-app username\n * convention, the optional vhost activation.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `applicationNamespace` — used as the rabbit username\n * (`<appSlug>-<envId>`, the legacy convention from\n * `wildo-application-lifecycle.service.ts`).\n * - `wildoApplicationId` — used as the permission-regex disambiguator\n * (matches `^wildo\\.(queue|reply)\\.<id>\\..*`).\n * - `secretInputs['database.rabbitmq.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs?.['vhost']` — optional per-app vhost\n * for true vhost-level isolation (Slice 2.5c-3).\n * - `adminClient` — narrowed to `IRabbitMQManagementClient`.\n *\n * **What this module IGNORES from the context** (Slice 4.A.2 / M4):\n * - `appSlug` — RabbitMQ resources are namespace-scoped, not slug-\n * scoped. Future modules (MongoDB DB name, etc.) WILL use `appSlug`.\n * This is documented forward-compat for module authors: not every\n * module reads every context field; each declares which it consumes.\n *\n * **Three audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:\n * cluster-wide management-API creds (`RABBITMQ_ADMIN_USERNAME` /\n * `RABBITMQ_ADMIN_PASSWORD`). Consumers: `apps-manager`,\n * `companion`. Delivered through process.env (the CLI's\n * `env-vars-builder` writes them per-env). No method.\n *\n * 2. **Audience-2 (platform-service-scoped, via\n * `mintPlatformServiceCredentials`)**: per-platform-service broker\n * users for inter-service messaging (`apps-manager`,\n * `crontabs-batches-manager`, `file-generator`). Username =\n * `serviceName`; permission regex is service-namespaced\n * (`^wildo\\.platform\\.<slug>\\..*`); crontabs gets the broader\n * `^wildo\\.(queue|reply|platform)\\..*` per the legacy override.\n *\n * **Slice 2.5 behavior-parity note**: the legacy code paths\n * `provisionServiceCredentials` and `ensureAppsManagerCredentials`\n * produced slightly different topic-permission regexes for the\n * same apps-manager user (`wildo\\.platform\\.apps_manager\\..*` vs\n * `wildo.platform.apps_manager.*`). User permissions were therefore\n * order-dependent. This module unifies on the dot-escaped variant.\n * Functionally equivalent for real queue names; tighter spec.\n *\n * **Slice 4.A.2 / M2 design note**: the legacy\n * `getDefaultRabbitMQPermissions.read` field declared a BROADER\n * regex than `configure`/`write` (included\n * `wildo.platform.broadcast.*`) — but that widening NEVER reached\n * the broker (the legacy `upsertTenantUserPermissions` ignored\n * the `read` arg and used `resourceNameRegex` for both\n * `configure` and `read`). This module preserves that behavior:\n * `read === configure`. If platform services need a broadcast\n * namespace in the future, extend `mintPlatformServiceCredentials`\n * with an explicit broadcast-read regex.\n *\n * 3. **Audience-3 (app-scoped, via `plan().appRuntime` +\n * `mintAppCredentials`)**: per-customer-app broker user. Username\n * = `applicationNamespace`; permission regex disambiguates by\n * `wildoApplicationId` (Mongo ObjectId — globally unique). Result\n * threaded into `Wildo_Application_Secrets.database.rabbitmq.{\n * username, password}` via `_merge_WildoApplicationSecrets`.\n *\n * **Vhost activation (Slice 2.5c-3, opt-in)**: when the operator\n * authors `backingServices.rabbitmq.vhost` in\n * `wildo.infra.<envName>.config.ts`, the CLI sends it with that\n * application's registration (`backingServiceAddressing`, #1107); the lifecycle\n * service threads it via `applicationConfigInputs.vhost`; this module\n * `ensureVhost`-then-permissions-on-it. Without the field, the\n * module falls back to `/` (legacy permission-regex isolation).\n *\n * **`revokeAppCredentials`** is called by apps-manager runtime-principal\n * declaration reconciliation when a minion is removed, and for every principal\n * (the application backend included) of an application withdrawn from the\n * platform (#1307). The lifecycle service passes the application namespace and\n * the principal; its broker user is deleted; the vhost and its queues stay (#1562 owns destruction).\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IRabbitMQManagementClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n PlatformServiceCredentialMintingContext,\n PlatformServiceCredentialMintingResult,\n ApplicationRuntimePrincipalKind,\n buildRuntimePrincipalQueueSegment,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Default vhost — apps share `/` with regex-based per-user permission\n * isolation. Per-app vhost is intentionally NOT modeled (engine has no\n * `RABBITMQ_VHOST` consumer; isolation comes from the permission regex\n * on the per-app user, not from a per-app vhost).\n *\n * **No `ensureVhost(DEFAULT_VHOST)` call** is needed: vhost `/` is\n * created by RabbitMQ at server boot. Any future module variant that\n * uses a non-default vhost MUST call `adminClient.ensureVhost(...)`\n * before user/permissions setup.\n */\nconst DEFAULT_VHOST = '/';\n\n/**\n * Topic exchange used by every Wildo runtime publisher / consumer.\n * Matches the value already in use at\n * `wildo-application-lifecycle.service.ts:568` and\n * `platform-infrastructure-manager.service.ts:485`.\n */\nconst TOPIC_EXCHANGE = 'amq.topic';\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes through\n * `mintAppCredentials`. Declared once and reused as\n * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of\n * `secretBlobValues` in the result, so a refactor that renames a path\n * fails loudly at one place instead of drifting silently.\n */\nconst RABBITMQ_USERNAME_PATH = 'database.rabbitmq.username';\nconst RABBITMQ_PASSWORD_PATH = 'database.rabbitmq.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [RABBITMQ_USERNAME_PATH, RABBITMQ_PASSWORD_PATH];\n\n\n/**\n * Key the module looks for in\n * `AppCredentialMintingContext.applicationConfigInputs` to read the\n * optional per-app virtual host. Documented here as the public\n * contract: the orchestrator supplies the vhost via this key, the\n * module reads it via this key. Slice 2.5c-3 of\n * `infra-provider-planner.md`.\n */\nconst VHOST_INPUT_KEY = 'vhost';\n\n/**\n * Key the module looks for in\n * `PlatformServiceCredentialMintingContext.secretInputs` to read the\n * orchestrator-supplied platform-service password. Slice 4.A.2 / M5\n * of `infra-provider-planner.md` — extracted as a constant for\n * symmetry with `RABBITMQ_PASSWORD_PATH` (used by `mintAppCredentials`),\n * so a future rename touches one site instead of drifting silently.\n */\nconst PLATFORM_SERVICE_PASSWORD_INPUT_KEY = 'password';\n\n/**\n * Compute the per-app rabbit username. Mirrors the existing convention\n * at `wildo-application-lifecycle.service.ts:434, 440` — username is\n * the application namespace (`<appSlug>-<environmentId>` per\n * `buildApplicationNamespace`), NOT the raw slug. Centralized here so\n * future changes touch one site.\n */\nfunction buildRabbitUsername(applicationNamespace: string): string {\n return applicationNamespace;\n}\n\n/**\n * Identifier of the platform-crontabs-batches-manager service. The\n * crontabs special case (broader permissions covering all per-app\n * queue/reply names so it can fan out scheduled jobs) lives here so\n * the legacy hard-coded check at\n * `platform-infrastructure-manager.service.ts:466-475` becomes a\n * module-internal concern.\n *\n * Hardcoded as a string literal rather than imported from\n * `PlatformApplicationType` because that enum lives in saas-models\n * and importing it here would couple the module to that enum's\n * runtime shape — the value `'platform_crontabs_batches_manager'` is\n * stable and small.\n */\nconst PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME = 'platform_crontabs_batches_manager';\n\n/**\n * Compute the per-platform-service permission regex.\n *\n * Default policy (apps-manager, file-generator, …): the service can\n * configure / write / read on `^wildo\\.platform\\.<serviceSlug>\\..*`\n * where `serviceSlug = serviceName.replace('platform_', '')`. Mirrors\n * the legacy `getDefaultRabbitMQPermissions` at\n * `credentials-provisioner.service.ts:267-275`.\n *\n * Crontabs-batches-manager override: broader regex\n * `^wildo\\.(queue|reply|platform)\\..*` so the crontab can publish\n * scheduled jobs onto every per-app queue and read replies. Mirrors\n * the legacy inline override at\n * `platform-infrastructure-manager.service.ts:466-475`.\n */\nfunction buildPlatformServiceRegex(serviceName: string): string {\n if (serviceName === PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME) {\n return '^wildo\\\\.(queue|reply|platform)\\\\..*';\n }\n const serviceSlug = serviceName.replace('platform_', '');\n return `^wildo\\\\.platform\\\\.${serviceSlug}\\\\..*`;\n}\n\n/**\n * Compute the per-app permission regex. Matches the convention at\n * `wildo-application-lifecycle.service.ts:552`:\n * `^wildo\\.(queue|reply)\\.${wildoApplicationId}\\..*`\n * The disambiguator is the application ID, NOT the slug — slugs could\n * theoretically be reassigned; the id is forever-stable. The `^` and\n * `\\.` escaping is required by the RabbitMQ regex evaluator.\n */\nfunction buildPermissionRegex(wildoApplicationId: string): string {\n // Escape regex metacharacters that may appear in application ids\n // (today they're Mongo ObjectId hex strings — no specials — but the\n // contract only commits to \"stable opaque string\", so guard).\n const escapedId = wildoApplicationId.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return `^wildo\\\\.(queue|reply)\\\\.${escapedId}\\\\..*`;\n}\n\n/**\n * The queues a MINION may CONSUME from — its own tick queue only.\n *\n * ## Why only `read` narrows, measured rather than assumed\n *\n * A minion runs the full backend pipeline headless, so the three RabbitMQ\n * permission axes are not interchangeable here:\n *\n * - **`read`** governs CONSUMING. Narrowing it is the containment that matters:\n * without it a minion can drain the application backend's job queue, consume\n * another minion's ticks, or read RPC replies addressed to the backend.\n * - **`configure`** governs DECLARE. The tick consumer declares its own queue\n * (`channel.assertQueue`) — but the delayed-publish path can declare a delay\n * queue too, so narrowing this risks breaking a legitimate publish.\n * - **`write`** governs PUBLISHING. A minion legitimately enqueues jobs and\n * sends RPC replies into the application's namespace; `publish` calls\n * `sendToQueue` WITHOUT asserting (verified in\n * `rabbitmq-adapter.backend.service.ts`), so it needs `write` on the app\n * namespace and nothing more.\n *\n * **What this therefore does NOT prevent, stated plainly:** a minion can still\n * PUBLISH anywhere in its application's queue namespace — it could enqueue a job\n * the backend will run. Constraining that is a different control (who may\n * request what work), not a broker permission, and pretending otherwise would\n * overstate the boundary.\n *\n * The trailing `(\\..*)?$` admits the tick queue itself and any suffixed\n * variant, while the anchored `$` stops it matching a SIBLING whose name merely\n * starts with this one's.\n */\n/**\n * Per-principal broker username — the SAME derivation `revokeAppCredentials`\n * must use.\n *\n * The application backend keeps `applicationNamespace` VERBATIM so an existing\n * deployment's user is never orphaned. A revoke that derived this differently\n * would delete the BACKEND's broker user while believing it was removing a\n * minion's — the identical trap already caught on the Redis side.\n */\nfunction buildRabbitPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildRabbitUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n return `${applicationNamespace}-${principal.kind}-${sanitizedName}`;\n}\n\nfunction buildMinionConsumeRegex(wildoApplicationId: string, minionName: string): string {\n const escape = (value: string): string => value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const appId = escape(wildoApplicationId);\n const name = escape(minionName);\n // Two alternatives, and nothing else:\n // its tick queue — wildo.queue.<appId>.minion-tick.<name>\n // its OWN reply queue — wildo.reply.<appId>.<segment>.<podId>\n // The reply segment is IMPORTED from the same builder the minion's own AMQP\n // adapter uses, not reproduced here. It used to be hand-written as\n // `minion-<name>` with a comment saying it \"mirrors\" what the adapter emits —\n // which agreed only while every minion name survived sanitisation unchanged.\n // The application backend gets no segment at all (the builder returns\n // undefined for it), keeping the historical layout, so this cannot match its\n // replies.\n const replySegment = escape(\n buildRuntimePrincipalQueueSegment(ApplicationRuntimePrincipalKind.MINION, minionName) ?? '',\n );\n return `^(wildo\\\\.queue\\\\.${appId}\\\\.minion-tick\\\\.${name}(\\\\..*)?`\n + `|wildo\\\\.reply\\\\.${appId}\\\\.${replySegment}\\\\..*)$`;\n}\n\nexport const RabbitmqSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.RABBITMQ_SELF,\n supportedCapabilities: [InfraCapability.QUEUING_SYSTEM],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['rabbitmq'],\n k8sResources: ['templates/infra/k8s/internal/rabbitmq/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n RABBITMQ_HOST: 'rabbitmq',\n RABBITMQ_PORT: '5672',\n RABBITMQ_MANAGEMENT_PORT: '15672',\n },\n secretEnvVars: ['RABBITMQ_ADMIN_USERNAME', 'RABBITMQ_ADMIN_PASSWORD'],\n // Audience-1 only (management-API access). audience-2 platform-\n // service-scoped consumers (apps-manager / crontabs / file-\n // generator publish-consume creds via `provisionAllPlatformServices`)\n // are LD-8 territory, deferred to Slice 2.5 — they get their\n // own `mintPlatformServiceCredentials` method then.\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Declared for forward-compat with Slice 2.5's\n // `_merge_WildoApplicationConfig` integration. Not consumed by\n // any planner path in Slice 2 — the existing hard-coded values\n // at `wildo-application-lifecycle.service.ts:1796-1799` stay in\n // effect (host: 'localhost', port: 5672) until the planner\n // wires this slice's contribution.\n perAppConfigBlobPaths: {\n 'database.rabbitmq.host': 'rabbitmq',\n 'database.rabbitmq.port': 5672,\n },\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IRabbitMQManagementClient;\n const password = context.secretInputs[RABBITMQ_PASSWORD_PATH];\n if (!password) {\n throw new Error(\n `RabbitmqSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${RABBITMQ_PASSWORD_PATH}'. ` +\n `The orchestrator MUST supply the rabbit password — modules are upsert-only.`,\n );\n }\n\n const username = buildRabbitPrincipalUsername(context.applicationNamespace, context.principal);\n const permissionRegex = buildPermissionRegex(context.wildoApplicationId);\n // A MINION consumes exactly two things: its own tick queue, and its own RPC\n // REPLY queue. Both are admitted; everything else in the application's\n // namespace is refused.\n //\n // The reply half is why an earlier attempt at this failed. Reply queues used\n // to be `wildo.reply.<appId>.<podId>`, and `podId` is a per-PROCESS token\n // carrying no principal — so no regex could admit a minion's replies while\n // refusing the backend's, and narrowing `read` crash-looped the minion on\n // its own replies. `buildReplyQueueName` now embeds the owning runtime,\n // which is what makes this expressible at all.\n const readRegex = context.principal?.kind === ApplicationRuntimePrincipalKind.MINION && context.principal.name\n ? buildMinionConsumeRegex(context.wildoApplicationId, context.principal.name)\n : permissionRegex;\n\n // Slice 2.5c-3: optional per-app vhost. When the orchestrator\n // supplies a non-empty `applicationConfigInputs.vhost`, the module\n // ensures the vhost exists and sets permissions ON THAT vhost\n // (provides true vhost-level isolation). When omitted, the module\n // falls back to the default vhost `/` (legacy Slice 2 behavior:\n // permission-regex isolation only). This is the opt-in seam — apps\n // without an authored `backingServices.rabbitmq.vhost` continue to\n // share `/`.\n const vhost = context.applicationConfigInputs?.[VHOST_INPUT_KEY] || DEFAULT_VHOST;\n if (vhost !== DEFAULT_VHOST) {\n await adminClient.ensureVhost(vhost);\n }\n\n // PUT /api/users — idempotent upsert (replaces password + tags on conflict).\n await adminClient.putUser(username, password, []);\n\n // PUT /api/permissions — `configure` and `read` use the per-app\n // queue/reply regex; `write` widens to allow `amq.default` (the\n // default exchange RabbitMQ uses for direct routing) and the empty\n // string (broker-internal). Mirrors the legacy\n // `upsertTenantUserPermissions` write-regex shape so the Wonder\n // Todos behavior is unchanged.\n const writeRegex = `(^$|amq\\\\.default|${permissionRegex})`;\n await adminClient.setPermissions(vhost, username, {\n configure: permissionRegex,\n write: writeRegex,\n read: readRegex,\n });\n\n // PUT /api/topic-permissions — same regex on the topic exchange.\n await adminClient.setTopicPermissions(vhost, username, {\n exchange: TOPIC_EXCHANGE,\n write: permissionRegex,\n read: readRegex,\n });\n\n // Per-app-vhost cron delivery (`.claude/plans/crontab-publisher-vhost-gap.md`).\n //\n // A per-app vhost is a HARD broker isolation boundary: the platform\n // cron/minion publisher (`platform_crontabs_batches_manager`) — a single\n // global user that fans scheduled jobs into every app — cannot reach THIS\n // app's queues until granted access on THIS vhost. On the default vhost `/`\n // it already holds a broad publish grant (see `mintPlatformServiceCredentials`),\n // so the grant is only needed for a non-default per-app vhost.\n //\n // Scope it to this app's own queue namespace (`^wildo\\.(queue|reply)\\.<appId>\\..*`)\n // — the publisher gets exactly the reach it needs and no more, TIGHTER than\n // its broad default-vhost grant. It is PUBLISH-ONLY: `configure` for the\n // passive `checkQueue` existence probe the publisher runs before sending +\n // any declare, `write` (`amq.default`) for `basic.publish`; NO `read` (never\n // consumes) and NO topic permissions (uses the default exchange, not\n // `amq.topic`). The crontabs user is provisioned before any app\n // (apps-manager INIT Phase 4 < Phase 5), so it reliably exists here, and\n // `setPermissions` is an idempotent PUT — a re-init safely re-applies it.\n if (vhost !== DEFAULT_VHOST) {\n await adminClient.setPermissions(vhost, PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME, {\n configure: permissionRegex,\n write: writeRegex,\n read: '',\n });\n }\n\n return {\n secretBlobValues: {\n [RABBITMQ_USERNAME_PATH]: username,\n // Echo the orchestrator-supplied password — proves the module\n // is upsert-only, not generator. The orchestrator persists\n // this exact value into `Wildo_Application_Secrets`.\n [RABBITMQ_PASSWORD_PATH]: password,\n },\n };\n },\n\n /**\n * Tear down the per-app rabbit user. RabbitMQ users are global, so\n * `deleteUser` removes the user (and its permissions on every vhost\n * it had access to) atomically.\n *\n * **Vhost retention** (Slice 4.A.2 / M6): when `mintAppCredentials`\n * provisioned a per-app vhost (Slice 2.5c-3), `revokeAppCredentials`\n * does NOT delete the vhost. Reasoning: vhost deletion drops every\n * queue and exchange inside it, which would destroy in-flight\n * messages mid-flight if a redeploy or restart is happening\n * concurrently. The user-deletion alone is sufficient to revoke\n * access; the empty vhost lingers as a paper trail. A future\n * \"app-deletion lifecycle\" slice can layer vhost cleanup with\n * proper drain semantics.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation\n * calls this for removed minions, and application withdrawal\n * (`deregisterApplication`, #1307) for the backend and every minion. The vhost\n * and its queues are kept; `restoreApplication` re-mints the user.\n *\n * **Defensive validation** (Slice 4.A.2 / L4): empty\n * `applicationNamespace` would call `DELETE /api/users/` and\n * silently 404; we fail-loud instead so a bad caller surfaces\n * immediately.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `RabbitmqSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminClient = context.adminClient as IRabbitMQManagementClient;\n const username = buildRabbitPrincipalUsername(context.applicationNamespace, context.principal);\n await adminClient.deleteUser(username);\n },\n\n async mintPlatformServiceCredentials(\n context: PlatformServiceCredentialMintingContext,\n ): Promise<PlatformServiceCredentialMintingResult> {\n const adminClient = context.adminClient as IRabbitMQManagementClient;\n const password = context.secretInputs[PLATFORM_SERVICE_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `RabbitmqSelfInfraProviderModule.mintPlatformServiceCredentials: ` +\n `secretInputs is missing '${PLATFORM_SERVICE_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the platform-service password — modules are upsert-only.`,\n );\n }\n\n // Username is the platform service identifier verbatim — preserves\n // the legacy convention from\n // `PlatformInfrastructureManagerService.provisionServiceCredentials`\n // (line 478: `username: serviceName`).\n const username = context.serviceName;\n const permissionRegex = buildPlatformServiceRegex(context.serviceName);\n\n // PUT /api/users — idempotent upsert.\n await adminClient.putUser(username, password, []);\n\n // Permissions: configure/read use the per-service regex; write\n // widens to allow `amq.default` + empty string (broker-internal),\n // matching the `upsertTenantUserPermissions` write-regex shape so\n // post-Slice-2.5 broker permissions match pre-Slice-2.5.\n const writeRegex = `(^$|amq\\\\.default|${permissionRegex})`;\n await adminClient.setPermissions(DEFAULT_VHOST, username, {\n configure: permissionRegex,\n write: writeRegex,\n read: permissionRegex,\n });\n\n // Topic permissions: same dot-escaped regex on amq.topic. The\n // legacy two-caller divergence (one path stripped the `^` and\n // produced `wildo\\.platform\\.<slug>\\..*`; the other produced\n // `wildo.platform.<slug>.*` with literal dots) is unified here on\n // the dot-escaped variant — see module JSDoc behavior-parity note.\n await adminClient.setTopicPermissions(DEFAULT_VHOST, username, {\n exchange: TOPIC_EXCHANGE,\n write: permissionRegex,\n read: permissionRegex,\n });\n\n return { username };\n },\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"redis-self.infra.module.d.ts","sourceRoot":"","sources":["../../../src/redis-self/redis-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AAEH,OAAO,EAOL,mBAAmB,EAKpB,MAAM,+BAA+B,CAAC;AA+GvC,eAAO,MAAM,4BAA4B,EAAE,mBA+J1C,CAAC"}
1
+ {"version":3,"file":"redis-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/redis-self/redis-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AAEH,OAAO,EAOL,mBAAmB,EAKpB,MAAM,+BAA+B,CAAC;AA+GvC,eAAO,MAAM,4BAA4B,EAAE,mBA+J1C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"redis-self.infra.module.js","sourceRoot":"","sources":["../../../src/redis-self/redis-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RedisSelfInfraProviderModule` — second concrete `InfraProviderModule`\n * (Slice 4.B.1 of `infra-provider-planner.md`, generalizing the\n * Slice 2 `RabbitmqSelfInfraProviderModule` pattern).\n *\n * Implements the `REDIS_SELF` `InfraProvider` over the\n * `IRedisAclClient` transport (Valkey/Redis ACL commands, exposed by\n * `RedisConnectionManagerService` after the Slice 4.B.1 5C policy/\n * transport split). Owns ALL per-app ACL policy — the per-app\n * keyPattern, the channel-pattern fan-out family, the per-app\n * username convention.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `appSlug` — used as the **key-pattern** prefix\n * (`<appSlug>:*`). This MUST match the backend's effective\n * `dataServices.redis.keyPrefix`, which is the slug-derived key\n * namespace every backend write is rooted under (see\n * `engine/saas-backend-lib/src/databases/services/redis.service.ts`\n * `appPrefix` field — every `org:`/`global:` scope is composed with\n * that prefix). Slot owner: the SaaS app's authored config.\n * ACL invariant: `keyPrefix` MUST start with `<appSlug>:` so the\n * `~<appSlug>:*` grant covers every key the backend writes.\n * - `applicationNamespace` — used as the Redis ACL **username** AND\n * as the channel-pattern prefix (`<ns>#*`, `<ns>-request#*`,\n * `<ns>-response#*`). Mirrors the legacy username convention from\n * `wildo-application-lifecycle.service.ts:441` (`redisUsername =\n * ns`). Channels stay namespace-scoped because the Socket.IO Redis\n * adapter (`engine/saas-backend-lib/src/requests/service/websocket\n * .service.ts:1501`) uses the Redis username as its `key:` — the\n * ACL channel pattern MUST match that.\n * - `secretInputs['database.redis.password']` — orchestrator-supplied\n * password (module is upsert-only, never generator).\n * - `adminClient` — narrowed to `IRedisAclClient`.\n *\n * **Key vs channel pattern split** (2026-05-26 framework-correct fix):\n * Pre-fix, this module derived BOTH key and channel patterns from\n * `applicationNamespace` (`<ns>:*` for keys, `<ns>#*` for channels).\n * That was correct pre-Slice-3.6, when the backend used the username\n * to namespace keys too. Slice 3.6 moved key namespacing to\n * `dataServices.redis.keyPrefix` (default + recommended value\n * `<appSlug>:`) but the ACL provisioner was not updated — every per-app\n * Redis read/write returned `NOPERM No permissions to access a key`\n * because the ACL only granted `<ns>:*` while the backend wrote\n * `<appSlug>:*`. Fix: key pattern now derives from `appSlug`, channel\n * patterns continue to derive from the username/namespace (Socket.IO\n * adapter still uses the username as its channel key, so channels\n * stay correct unchanged).\n *\n * **What this module IGNORES from the context** (Slice 4.A.2 / M4\n * forward-compat documentation):\n * - `wildoApplicationId` — the keyPattern uses appSlug + the channel\n * patterns use namespace; the application id is redundant for both\n * (no globally-shared key namespace risk because slug/namespace\n * already disambiguate by tenant and environment).\n * - `applicationConfigInputs` — Redis_Self has no per-app authored\n * topology (no equivalent of RabbitMQ's vhost). Future modules may\n * read it for per-app db-index overrides etc.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:\n * cluster-wide ACL admin creds (`REDIS_HOST`, `REDIS_PORT`,\n * `REDIS_DATABASE` env + `REDIS_ADMIN_PASSWORD` secret env).\n * Consumers: `apps-manager` (mints per-app ACL users via\n * `mintAppCredentials`), `companion` (mirrors apps-manager admin\n * role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `plan().appRuntime` +\n * `mintAppCredentials`)**: per-customer-app Redis ACL user.\n * Username = `applicationNamespace`; keyPattern disambiguates by\n * the same namespace. Result threaded into\n * `Wildo_Application_Secrets.database.redis.{username, password}`\n * via `_merge_WildoApplicationSecrets`.\n *\n * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:\n * - **Audience-2 (platform-service-scoped)**: Redis is currently NOT\n * used for inter-platform-service messaging (RabbitMQ is the bus).\n * `mintPlatformServiceCredentials` is therefore not implemented.\n * When/if a platform service surfaces a Redis-scoped per-service\n * credential need, this module gains the method.\n *\n * **`revokeAppCredentials`** is called by apps-manager runtime-principal\n * declaration reconciliation when a minion is removed, and for every principal\n * (the application backend included) of an application withdrawn from the\n * platform (#1307). The lifecycle service passes the application namespace and\n * the principal; its ACL user is deleted; the keys it wrote stay (#1562 owns destruction).\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IRedisAclClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipalKind,\n buildMinionRedisKeyPrefix,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Default Valkey port (matches `INFRA_DEFAULT_PORTS.VALKEY` in the\n * shared platform constants — duplicated here as a literal because\n * the planner output is consumed by the CLI which materializes it\n * into `Wildo_Application_Config` blob paths; the value is stable\n * and small).\n */\nconst DEFAULT_VALKEY_PORT = 6379;\n\n/**\n * Default DB index for app workloads. Wildo applications use db `0`\n * across the board; per-app DB-index isolation is intentionally NOT\n * modeled — keyPattern + ACL is the isolation seam.\n */\nconst DEFAULT_DB_INDEX = 0;\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes\n * through `mintAppCredentials`. Declared once and reused as\n * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of\n * `secretBlobValues` in the result, so a refactor that renames a\n * path fails loudly at one place instead of drifting silently.\n */\nconst REDIS_USERNAME_PATH = 'database.redis.username';\nconst REDIS_PASSWORD_PATH = 'database.redis.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [REDIS_USERNAME_PATH, REDIS_PASSWORD_PATH];\n\n\n/**\n * Compute the per-app Redis ACL username. Mirrors the existing\n * convention at `wildo-application-lifecycle.service.ts:441`\n * (`redisUsername = ns`) — username is the application namespace\n * (`<appSlug>-<environmentId>`), NOT the raw slug. Centralized here\n * so future changes touch one site.\n */\nfunction buildRedisUsername(applicationNamespace: string): string {\n return applicationNamespace;\n}\n\n/**\n * Per-principal ACL username (Step 2 / D17).\n *\n * The application backend keeps `applicationNamespace` VERBATIM — its username\n * must not move, or an existing deployment's ACL user is orphaned and a running\n * backend loses Redis on the next sync.\n *\n * A named principal appends its kind and name. The separator is `-`: an ACL\n * username is whitespace-delimited in `ACL SETUSER`, and the principal key's own\n * `:` is already meaningful in Redis KEY space, so reusing it in a username\n * blurs the two namespaces at a glance. Non-alphanumeric characters in a bucket\n * key are collapsed — the key is a TypeScript object key and may legally contain\n * `$` or `_`, while a username reaching the wire should stay conservative.\n *\n * Used by BOTH `mintAppCredentials` and `revokeAppCredentials`. That is the\n * point of it being a function: a revoke that derived the username differently\n * would delete the APPLICATION BACKEND's ACL user while believing it was\n * removing a minion's.\n */\nfunction buildRedisPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildRedisUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n return `${applicationNamespace}-${principal.kind}-${sanitizedName}`;\n}\n\n/**\n * Compute the per-app Redis ACL key pattern.\n *\n * The pattern derives from `appSlug` (NOT `applicationNamespace`)\n * because the backend's effective key namespace is sourced from\n * `dataServices.redis.keyPrefix` (Slice 3.6 — see\n * `engine/saas-backend-lib/src/databases/services/redis.service.ts`\n * `appPrefix` field JSDoc) whose default + recommended value is\n * `<appSlug>:`. The ACL `~<appSlug>:*` rule must match the key shape\n * the backend writes; ANY namespace-derived ACL (the pre-2026-05-26\n * shape) would only grant `<ns>:*` and `NOPERM` every backend access.\n *\n * The channel patterns are computed separately from the username\n * (== applicationNamespace) inside `IRedisAclClient.upsertUser`,\n * because the Socket.IO Redis adapter uses the username as its\n * channel key (`engine/saas-backend-lib/src/requests/service/\n * websocket.service.ts:1501`). The two namespaces are split:\n * - key namespace = `<appSlug>:` (app-authored\n * `dataServices.redis.keyPrefix`)\n * - channel namespace = `<applicationNamespace>#` (Socket.IO adapter\n * key, == redisUsername)\n *\n * App-author contract (validated by `validateKeyPrefixContract`\n * downstream — see `databases.backend.schemas.ts`): if the SaaS app\n * overrides the default `keyPrefix`, the override MUST still start\n * with `<appSlug>:` so the `~<appSlug>:*` grant remains a superset\n * of the keys the backend writes.\n */\nfunction buildRedisKeyPattern(appSlug: string): string {\n return `${appSlug}:*`;\n}\n\nexport const RedisSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.REDIS_SELF,\n supportedCapabilities: [InfraCapability.CACHE_MANAGER],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['valkey'],\n k8sResources: ['templates/infra/k8s/internal/valkey/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n REDIS_HOST: 'valkey',\n REDIS_PORT: String(DEFAULT_VALKEY_PORT),\n REDIS_DATABASE: String(DEFAULT_DB_INDEX),\n },\n // ACL admin password is the only secret env. The legacy\n // `RedisConnectionManagerService.initializeAclAdmin` reads it\n // from `REDIS_ADMIN_PASSWORD`; the planner emits the key, the\n // CLI's `env-vars-builder` writes the value at sync time.\n secretEnvVars: ['REDIS_ADMIN_PASSWORD'],\n // Audience-1 only (ACL-admin access). No audience-2\n // platform-service-scoped Redis use case today (D-4).\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Static topology — same for every app in this env.\n // Per-app values (username/password) flow through\n // `perAppSecretBlobPaths` below.\n perAppConfigBlobPaths: {\n 'database.redis.host': 'valkey',\n 'database.redis.port': DEFAULT_VALKEY_PORT,\n 'database.redis.db': DEFAULT_DB_INDEX,\n },\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IRedisAclClient;\n const password = context.secretInputs[REDIS_PASSWORD_PATH];\n if (!password) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${REDIS_PASSWORD_PATH}'. ` +\n `The orchestrator MUST supply the Redis password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n if (!context.appSlug) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `appSlug is empty. ` +\n `The orchestrator MUST supply a non-empty appSlug — the ACL key ` +\n `pattern derives from it (must match the backend's effective ` +\n `\\`dataServices.redis.keyPrefix\\`, default \\`<appSlug>:\\`).`,\n );\n }\n\n const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);\n // Key pattern derives from appSlug (matches backend `keyPrefix`),\n // channel patterns derive from username/namespace inside upsertUser\n // (matches Socket.IO adapter's `key:` config). See module JSDoc for\n // the full key-vs-channel-namespace split rationale.\n // A MINION is confined to its own namespace (D20); the application backend\n // keeps the app-wide pattern.\n //\n // This and the minion's `REDIS_KEY_PREFIX` are ONE COUPLED PAIR — a\n // mismatch DENIES every operation the minion performs, and the error names\n // an ACL rather than the mismatch. Both sides derive from\n // `buildMinionRedisKeyPrefix`, so they cannot drift.\n const keyPattern = context.principal?.kind === ApplicationRuntimePrincipalKind.MINION && context.principal.name\n ? `${buildMinionRedisKeyPrefix(context.appSlug, context.principal.name)}*`\n : buildRedisKeyPattern(context.appSlug);\n\n // ACL SETUSER — idempotent upsert. Replaces password + key\n // patterns + channel patterns + category allow/deny on conflict.\n //\n // **Atomicity guard** (Slice 6 audit / F-2, 2026-05-10): the\n // sequential `await upsertUser(); await persistAcl();` shape\n // intentionally relies on JavaScript exception propagation —\n // when `upsertUser` throws, the thrown error bubbles up the\n // async stack BEFORE `persistAcl` is reached, leaving the ACL\n // list on disk unchanged. The\n // `redis-self.infra.module.test.ts` \"atomicity guard\" case pins\n // this behavior. **DO NOT wrap this pair in a try/catch that\n // swallows the upsert error** — that would silently persist a\n // partially-applied state. Any future refactor that introduces\n // a try-block here MUST re-throw on the upsert error AND must\n // gate `persistAcl` on the upsert's success.\n await adminClient.upsertUser({\n username,\n password,\n keyPattern,\n });\n\n // Persist ACL list to disk so Valkey restarts don't lose tenant\n // users. Mirrors the legacy\n // `wildo-application-lifecycle.service.ts:540` pairing.\n await adminClient.persistAcl();\n\n return {\n secretBlobValues: {\n [REDIS_USERNAME_PATH]: username,\n // Echo the orchestrator-supplied password — proves the module\n // is upsert-only, not generator. The orchestrator persists\n // this exact value into `Wildo_Application_Secrets`.\n [REDIS_PASSWORD_PATH]: password,\n },\n };\n },\n\n /**\n * Tear down the per-app Redis ACL user. Valkey ACL users are\n * global (no vhost equivalent), so `deleteUser` removes the user\n * (and its key/channel access) atomically.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation\n * calls this for removed minions, and application withdrawal\n * (`deregisterApplication`, #1307) for the backend and every minion. The keys the\n * user wrote are kept; `restoreApplication` re-mints the user.\n *\n * **Defensive validation** (Slice 4.A.2 / L4 pattern): empty\n * `applicationNamespace` would call `ACL DELUSER` with an empty\n * username (Valkey would either error or silently no-op,\n * depending on version); we fail-loud instead so a bad caller\n * surfaces immediately.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `RedisSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminClient = context.adminClient as IRedisAclClient;\n const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);\n await adminClient.deleteUser(username);\n // Persist after deletion so the ACL list on disk stays in sync\n // with the in-memory state. Mirrors mintAppCredentials' pairing.\n await adminClient.persistAcl();\n },\n\n // No `mintPlatformServiceCredentials` (D-4 of Slice 4.B —\n // audience-2 not applicable to Redis_Self today). When a real\n // need surfaces, add the method here following the\n // `RabbitmqSelfInfraProviderModule` shape.\n};\n"]}
1
+ {"version":3,"file":"redis-self.infra.module.js","sourceRoot":"","sources":["../../../../src/redis-self/redis-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RedisSelfInfraProviderModule` — second concrete `InfraProviderModule`\n * (Slice 4.B.1 of `infra-provider-planner.md`, generalizing the\n * Slice 2 `RabbitmqSelfInfraProviderModule` pattern).\n *\n * Implements the `REDIS_SELF` `InfraProvider` over the\n * `IRedisAclClient` transport (Valkey/Redis ACL commands, exposed by\n * `RedisConnectionManagerService` after the Slice 4.B.1 5C policy/\n * transport split). Owns ALL per-app ACL policy — the per-app\n * keyPattern, the channel-pattern fan-out family, the per-app\n * username convention.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `appSlug` — used as the **key-pattern** prefix\n * (`<appSlug>:*`). This MUST match the backend's effective\n * `dataServices.redis.keyPrefix`, which is the slug-derived key\n * namespace every backend write is rooted under (see\n * `engine/saas-backend-lib/src/databases/services/redis.service.ts`\n * `appPrefix` field — every `org:`/`global:` scope is composed with\n * that prefix). Slot owner: the SaaS app's authored config.\n * ACL invariant: `keyPrefix` MUST start with `<appSlug>:` so the\n * `~<appSlug>:*` grant covers every key the backend writes.\n * - `applicationNamespace` — used as the Redis ACL **username** AND\n * as the channel-pattern prefix (`<ns>#*`, `<ns>-request#*`,\n * `<ns>-response#*`). Mirrors the legacy username convention from\n * `wildo-application-lifecycle.service.ts:441` (`redisUsername =\n * ns`). Channels stay namespace-scoped because the Socket.IO Redis\n * adapter (`engine/saas-backend-lib/src/requests/service/websocket\n * .service.ts:1501`) uses the Redis username as its `key:` — the\n * ACL channel pattern MUST match that.\n * - `secretInputs['database.redis.password']` — orchestrator-supplied\n * password (module is upsert-only, never generator).\n * - `adminClient` — narrowed to `IRedisAclClient`.\n *\n * **Key vs channel pattern split** (2026-05-26 framework-correct fix):\n * Pre-fix, this module derived BOTH key and channel patterns from\n * `applicationNamespace` (`<ns>:*` for keys, `<ns>#*` for channels).\n * That was correct pre-Slice-3.6, when the backend used the username\n * to namespace keys too. Slice 3.6 moved key namespacing to\n * `dataServices.redis.keyPrefix` (default + recommended value\n * `<appSlug>:`) but the ACL provisioner was not updated — every per-app\n * Redis read/write returned `NOPERM No permissions to access a key`\n * because the ACL only granted `<ns>:*` while the backend wrote\n * `<appSlug>:*`. Fix: key pattern now derives from `appSlug`, channel\n * patterns continue to derive from the username/namespace (Socket.IO\n * adapter still uses the username as its channel key, so channels\n * stay correct unchanged).\n *\n * **What this module IGNORES from the context** (Slice 4.A.2 / M4\n * forward-compat documentation):\n * - `wildoApplicationId` — the keyPattern uses appSlug + the channel\n * patterns use namespace; the application id is redundant for both\n * (no globally-shared key namespace risk because slug/namespace\n * already disambiguate by tenant and environment).\n * - `applicationConfigInputs` — Redis_Self has no per-app authored\n * topology (no equivalent of RabbitMQ's vhost). Future modules may\n * read it for per-app db-index overrides etc.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:\n * cluster-wide ACL admin creds (`REDIS_HOST`, `REDIS_PORT`,\n * `REDIS_DATABASE` env + `REDIS_ADMIN_PASSWORD` secret env).\n * Consumers: `apps-manager` (mints per-app ACL users via\n * `mintAppCredentials`), `companion` (mirrors apps-manager admin\n * role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `plan().appRuntime` +\n * `mintAppCredentials`)**: per-customer-app Redis ACL user.\n * Username = `applicationNamespace`; keyPattern disambiguates by\n * the same namespace. Result threaded into\n * `Wildo_Application_Secrets.database.redis.{username, password}`\n * via `_merge_WildoApplicationSecrets`.\n *\n * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:\n * - **Audience-2 (platform-service-scoped)**: Redis is currently NOT\n * used for inter-platform-service messaging (RabbitMQ is the bus).\n * `mintPlatformServiceCredentials` is therefore not implemented.\n * When/if a platform service surfaces a Redis-scoped per-service\n * credential need, this module gains the method.\n *\n * **`revokeAppCredentials`** is called by apps-manager runtime-principal\n * declaration reconciliation when a minion is removed, and for every principal\n * (the application backend included) of an application withdrawn from the\n * platform (#1307). The lifecycle service passes the application namespace and\n * the principal; its ACL user is deleted; the keys it wrote stay (#1562 owns destruction).\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IRedisAclClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipalKind,\n buildMinionRedisKeyPrefix,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Default Valkey port (matches `INFRA_DEFAULT_PORTS.VALKEY` in the\n * shared platform constants — duplicated here as a literal because\n * the planner output is consumed by the CLI which materializes it\n * into `Wildo_Application_Config` blob paths; the value is stable\n * and small).\n */\nconst DEFAULT_VALKEY_PORT = 6379;\n\n/**\n * Default DB index for app workloads. Wildo applications use db `0`\n * across the board; per-app DB-index isolation is intentionally NOT\n * modeled — keyPattern + ACL is the isolation seam.\n */\nconst DEFAULT_DB_INDEX = 0;\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes\n * through `mintAppCredentials`. Declared once and reused as\n * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of\n * `secretBlobValues` in the result, so a refactor that renames a\n * path fails loudly at one place instead of drifting silently.\n */\nconst REDIS_USERNAME_PATH = 'database.redis.username';\nconst REDIS_PASSWORD_PATH = 'database.redis.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [REDIS_USERNAME_PATH, REDIS_PASSWORD_PATH];\n\n\n/**\n * Compute the per-app Redis ACL username. Mirrors the existing\n * convention at `wildo-application-lifecycle.service.ts:441`\n * (`redisUsername = ns`) — username is the application namespace\n * (`<appSlug>-<environmentId>`), NOT the raw slug. Centralized here\n * so future changes touch one site.\n */\nfunction buildRedisUsername(applicationNamespace: string): string {\n return applicationNamespace;\n}\n\n/**\n * Per-principal ACL username (Step 2 / D17).\n *\n * The application backend keeps `applicationNamespace` VERBATIM — its username\n * must not move, or an existing deployment's ACL user is orphaned and a running\n * backend loses Redis on the next sync.\n *\n * A named principal appends its kind and name. The separator is `-`: an ACL\n * username is whitespace-delimited in `ACL SETUSER`, and the principal key's own\n * `:` is already meaningful in Redis KEY space, so reusing it in a username\n * blurs the two namespaces at a glance. Non-alphanumeric characters in a bucket\n * key are collapsed — the key is a TypeScript object key and may legally contain\n * `$` or `_`, while a username reaching the wire should stay conservative.\n *\n * Used by BOTH `mintAppCredentials` and `revokeAppCredentials`. That is the\n * point of it being a function: a revoke that derived the username differently\n * would delete the APPLICATION BACKEND's ACL user while believing it was\n * removing a minion's.\n */\nfunction buildRedisPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildRedisUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n return `${applicationNamespace}-${principal.kind}-${sanitizedName}`;\n}\n\n/**\n * Compute the per-app Redis ACL key pattern.\n *\n * The pattern derives from `appSlug` (NOT `applicationNamespace`)\n * because the backend's effective key namespace is sourced from\n * `dataServices.redis.keyPrefix` (Slice 3.6 — see\n * `engine/saas-backend-lib/src/databases/services/redis.service.ts`\n * `appPrefix` field JSDoc) whose default + recommended value is\n * `<appSlug>:`. The ACL `~<appSlug>:*` rule must match the key shape\n * the backend writes; ANY namespace-derived ACL (the pre-2026-05-26\n * shape) would only grant `<ns>:*` and `NOPERM` every backend access.\n *\n * The channel patterns are computed separately from the username\n * (== applicationNamespace) inside `IRedisAclClient.upsertUser`,\n * because the Socket.IO Redis adapter uses the username as its\n * channel key (`engine/saas-backend-lib/src/requests/service/\n * websocket.service.ts:1501`). The two namespaces are split:\n * - key namespace = `<appSlug>:` (app-authored\n * `dataServices.redis.keyPrefix`)\n * - channel namespace = `<applicationNamespace>#` (Socket.IO adapter\n * key, == redisUsername)\n *\n * App-author contract (validated by `validateKeyPrefixContract`\n * downstream — see `databases.backend.schemas.ts`): if the SaaS app\n * overrides the default `keyPrefix`, the override MUST still start\n * with `<appSlug>:` so the `~<appSlug>:*` grant remains a superset\n * of the keys the backend writes.\n */\nfunction buildRedisKeyPattern(appSlug: string): string {\n return `${appSlug}:*`;\n}\n\nexport const RedisSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.REDIS_SELF,\n supportedCapabilities: [InfraCapability.CACHE_MANAGER],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['valkey'],\n k8sResources: ['templates/infra/k8s/internal/valkey/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n REDIS_HOST: 'valkey',\n REDIS_PORT: String(DEFAULT_VALKEY_PORT),\n REDIS_DATABASE: String(DEFAULT_DB_INDEX),\n },\n // ACL admin password is the only secret env. The legacy\n // `RedisConnectionManagerService.initializeAclAdmin` reads it\n // from `REDIS_ADMIN_PASSWORD`; the planner emits the key, the\n // CLI's `env-vars-builder` writes the value at sync time.\n secretEnvVars: ['REDIS_ADMIN_PASSWORD'],\n // Audience-1 only (ACL-admin access). No audience-2\n // platform-service-scoped Redis use case today (D-4).\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Static topology — same for every app in this env.\n // Per-app values (username/password) flow through\n // `perAppSecretBlobPaths` below.\n perAppConfigBlobPaths: {\n 'database.redis.host': 'valkey',\n 'database.redis.port': DEFAULT_VALKEY_PORT,\n 'database.redis.db': DEFAULT_DB_INDEX,\n },\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IRedisAclClient;\n const password = context.secretInputs[REDIS_PASSWORD_PATH];\n if (!password) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${REDIS_PASSWORD_PATH}'. ` +\n `The orchestrator MUST supply the Redis password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n if (!context.appSlug) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `appSlug is empty. ` +\n `The orchestrator MUST supply a non-empty appSlug — the ACL key ` +\n `pattern derives from it (must match the backend's effective ` +\n `\\`dataServices.redis.keyPrefix\\`, default \\`<appSlug>:\\`).`,\n );\n }\n\n const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);\n // Key pattern derives from appSlug (matches backend `keyPrefix`),\n // channel patterns derive from username/namespace inside upsertUser\n // (matches Socket.IO adapter's `key:` config). See module JSDoc for\n // the full key-vs-channel-namespace split rationale.\n // A MINION is confined to its own namespace (D20); the application backend\n // keeps the app-wide pattern.\n //\n // This and the minion's `REDIS_KEY_PREFIX` are ONE COUPLED PAIR — a\n // mismatch DENIES every operation the minion performs, and the error names\n // an ACL rather than the mismatch. Both sides derive from\n // `buildMinionRedisKeyPrefix`, so they cannot drift.\n const keyPattern = context.principal?.kind === ApplicationRuntimePrincipalKind.MINION && context.principal.name\n ? `${buildMinionRedisKeyPrefix(context.appSlug, context.principal.name)}*`\n : buildRedisKeyPattern(context.appSlug);\n\n // ACL SETUSER — idempotent upsert. Replaces password + key\n // patterns + channel patterns + category allow/deny on conflict.\n //\n // **Atomicity guard** (Slice 6 audit / F-2, 2026-05-10): the\n // sequential `await upsertUser(); await persistAcl();` shape\n // intentionally relies on JavaScript exception propagation —\n // when `upsertUser` throws, the thrown error bubbles up the\n // async stack BEFORE `persistAcl` is reached, leaving the ACL\n // list on disk unchanged. The\n // `redis-self.infra.module.test.ts` \"atomicity guard\" case pins\n // this behavior. **DO NOT wrap this pair in a try/catch that\n // swallows the upsert error** — that would silently persist a\n // partially-applied state. Any future refactor that introduces\n // a try-block here MUST re-throw on the upsert error AND must\n // gate `persistAcl` on the upsert's success.\n await adminClient.upsertUser({\n username,\n password,\n keyPattern,\n });\n\n // Persist ACL list to disk so Valkey restarts don't lose tenant\n // users. Mirrors the legacy\n // `wildo-application-lifecycle.service.ts:540` pairing.\n await adminClient.persistAcl();\n\n return {\n secretBlobValues: {\n [REDIS_USERNAME_PATH]: username,\n // Echo the orchestrator-supplied password — proves the module\n // is upsert-only, not generator. The orchestrator persists\n // this exact value into `Wildo_Application_Secrets`.\n [REDIS_PASSWORD_PATH]: password,\n },\n };\n },\n\n /**\n * Tear down the per-app Redis ACL user. Valkey ACL users are\n * global (no vhost equivalent), so `deleteUser` removes the user\n * (and its key/channel access) atomically.\n *\n * **Caller status**: apps-manager runtime-principal declaration reconciliation\n * calls this for removed minions, and application withdrawal\n * (`deregisterApplication`, #1307) for the backend and every minion. The keys the\n * user wrote are kept; `restoreApplication` re-mints the user.\n *\n * **Defensive validation** (Slice 4.A.2 / L4 pattern): empty\n * `applicationNamespace` would call `ACL DELUSER` with an empty\n * username (Valkey would either error or silently no-op,\n * depending on version); we fail-loud instead so a bad caller\n * surfaces immediately.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `RedisSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminClient = context.adminClient as IRedisAclClient;\n const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);\n await adminClient.deleteUser(username);\n // Persist after deletion so the ACL list on disk stays in sync\n // with the in-memory state. Mirrors mintAppCredentials' pairing.\n await adminClient.persistAcl();\n },\n\n // No `mintPlatformServiceCredentials` (D-4 of Slice 4.B —\n // audience-2 not applicable to Redis_Self today). When a real\n // need surfaces, add the method here following the\n // `RabbitmqSelfInfraProviderModule` shape.\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildo-ai/platform-infra-providers",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Concrete InfraProviderModule catalog (RabbitMQ_Self, Redis_Self, Postgres_Self, MongoDB_Atlas, MongoDB_Self) — tier-portable, importable by both platform-apps-manager (orchestrator) and wildo-cli (lockfile materializer). Slice 2.5c-1 of infra-provider-planner.md.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Wildo AI",
@@ -30,7 +30,7 @@
30
30
  "test:watch": "vitest"
31
31
  },
32
32
  "dependencies": {
33
- "@wildo-ai/platform-config-lib": "1.1.7"
33
+ "@wildo-ai/platform-config-lib": "1.1.8"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "zod": "^4.3.6"