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

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.
@@ -85,5 +85,51 @@
85
85
  * the principal; its broker user is deleted; the vhost and its queues stay (#1562 owns destruction).
86
86
  */
87
87
  import { InfraProviderModule } from '@wildo-ai/platform-config-lib';
88
+ /**
89
+ * The bound on an application's DEAD-LETTER queues (#2214), declared as a broker POLICY.
90
+ *
91
+ * ## Why it exists
92
+ *
93
+ * Every application dead-letter queue (`<category>.deadletter`, and `domain.<name>.deadletter`) is
94
+ * terminal and consumer-less BY DECISION (#469, 2026-09-08): a parked message is inspected and
95
+ * replayed with the broker's own tooling, never through a Wildo read surface, because a job payload
96
+ * may carry personal data. Measured on 2026-10-01, that left a sink with no reader AND no bound —
97
+ * `scheduled.deadletter` held 359 messages for one local application after one day of restarts,
98
+ * and every restart added to it permanently. A queue an operator is expected to read must stay small
99
+ * enough to read, and must not grow without limit in a deployment that lives for months.
100
+ *
101
+ * ## Why a policy and not a queue argument
102
+ *
103
+ * RabbitMQ refuses to change the arguments of an existing queue (406 PRECONDITION_FAILED), so a
104
+ * `x-max-length` argument would only reach a queue declared after the change and would break the
105
+ * declaration of every queue that already exists. A policy reaches existing and future queues alike
106
+ * and can be changed later. RabbitMQ's documentation recommends a policy for exactly this setting.
107
+ * spec-cite: https://www.rabbitmq.com/docs/maxlength — max length "using a policy (this option is highly recommended)"; `overflow` defaults to `drop-head`.
108
+ * spec-cite: https://www.rabbitmq.com/docs/policies — "At most one policy matches a queue"; policies "can change at any time" and are re-applied.
109
+ * spec-cite: https://www.rabbitmq.com/docs/ttl — `message-ttl` in milliseconds; with no dead-letter exchange an expired message is removed.
110
+ *
111
+ * ## The values
112
+ *
113
+ * - `message-ttl` 14 days: a parked message is kept long enough for an operator to notice and act on
114
+ * it, then removed. The dead-letter queues have no dead-letter exchange of their own, so an expired
115
+ * message is discarded, not moved.
116
+ * - `max-length` 10 000 with `overflow: drop-head`: a burst cannot grow the queue past what a person
117
+ * can inspect; the OLDEST message goes first, so the most recent failures — the ones being
118
+ * investigated — survive.
119
+ *
120
+ * Scoped to ONE application's own dead-letter queues, so the policies of applications sharing the
121
+ * default vhost never overlap: at most one policy applies to a queue, and two overlapping policies at
122
+ * the same priority would be applied non-deterministically.
123
+ */
124
+ export declare const RABBITMQ_DEAD_LETTER_QUEUE_BOUND: {
125
+ /** 14 days, in milliseconds. */
126
+ readonly messageTtlMs: number;
127
+ readonly maxLength: 10000;
128
+ readonly priority: 0;
129
+ };
130
+ /** The policy name, per application — unique on a shared vhost. */
131
+ export declare function buildDeadLetterBoundPolicyName(wildoApplicationId: string): string;
132
+ /** Every dead-letter queue of ONE application: `wildo.queue.<appId>.<anything>.deadletter`. */
133
+ export declare function buildDeadLetterBoundPolicyPattern(wildoApplicationId: string): string;
88
134
  export declare const RabbitmqSelfInfraProviderModule: InfraProviderModule;
89
135
  //# sourceMappingURL=rabbitmq-self.infra.module.d.ts.map
@@ -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;AAiIvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,gCAAgC;IAC3C,gCAAgC;;;;CAIxB,CAAC;AAEX,mEAAmE;AACnE,wBAAgB,8BAA8B,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,+FAA+F;AAC/F,wBAAgB,iCAAiC,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAGpF;AAyED,eAAO,MAAM,+BAA+B,EAAE,mBA0P7C,CAAC"}
@@ -202,6 +202,57 @@ function buildPermissionRegex(wildoApplicationId) {
202
202
  const escapedId = wildoApplicationId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
203
203
  return `^wildo\\.(queue|reply)\\.${escapedId}\\..*`;
204
204
  }
205
+ /**
206
+ * The bound on an application's DEAD-LETTER queues (#2214), declared as a broker POLICY.
207
+ *
208
+ * ## Why it exists
209
+ *
210
+ * Every application dead-letter queue (`<category>.deadletter`, and `domain.<name>.deadletter`) is
211
+ * terminal and consumer-less BY DECISION (#469, 2026-09-08): a parked message is inspected and
212
+ * replayed with the broker's own tooling, never through a Wildo read surface, because a job payload
213
+ * may carry personal data. Measured on 2026-10-01, that left a sink with no reader AND no bound —
214
+ * `scheduled.deadletter` held 359 messages for one local application after one day of restarts,
215
+ * and every restart added to it permanently. A queue an operator is expected to read must stay small
216
+ * enough to read, and must not grow without limit in a deployment that lives for months.
217
+ *
218
+ * ## Why a policy and not a queue argument
219
+ *
220
+ * RabbitMQ refuses to change the arguments of an existing queue (406 PRECONDITION_FAILED), so a
221
+ * `x-max-length` argument would only reach a queue declared after the change and would break the
222
+ * declaration of every queue that already exists. A policy reaches existing and future queues alike
223
+ * and can be changed later. RabbitMQ's documentation recommends a policy for exactly this setting.
224
+ * spec-cite: https://www.rabbitmq.com/docs/maxlength — max length "using a policy (this option is highly recommended)"; `overflow` defaults to `drop-head`.
225
+ * spec-cite: https://www.rabbitmq.com/docs/policies — "At most one policy matches a queue"; policies "can change at any time" and are re-applied.
226
+ * spec-cite: https://www.rabbitmq.com/docs/ttl — `message-ttl` in milliseconds; with no dead-letter exchange an expired message is removed.
227
+ *
228
+ * ## The values
229
+ *
230
+ * - `message-ttl` 14 days: a parked message is kept long enough for an operator to notice and act on
231
+ * it, then removed. The dead-letter queues have no dead-letter exchange of their own, so an expired
232
+ * message is discarded, not moved.
233
+ * - `max-length` 10 000 with `overflow: drop-head`: a burst cannot grow the queue past what a person
234
+ * can inspect; the OLDEST message goes first, so the most recent failures — the ones being
235
+ * investigated — survive.
236
+ *
237
+ * Scoped to ONE application's own dead-letter queues, so the policies of applications sharing the
238
+ * default vhost never overlap: at most one policy applies to a queue, and two overlapping policies at
239
+ * the same priority would be applied non-deterministically.
240
+ */
241
+ export const RABBITMQ_DEAD_LETTER_QUEUE_BOUND = {
242
+ /** 14 days, in milliseconds. */
243
+ messageTtlMs: 14 * 24 * 60 * 60 * 1000,
244
+ maxLength: 10_000,
245
+ priority: 0,
246
+ };
247
+ /** The policy name, per application — unique on a shared vhost. */
248
+ export function buildDeadLetterBoundPolicyName(wildoApplicationId) {
249
+ return `wildo-deadletter-bound.${wildoApplicationId}`;
250
+ }
251
+ /** Every dead-letter queue of ONE application: `wildo.queue.<appId>.<anything>.deadletter`. */
252
+ export function buildDeadLetterBoundPolicyPattern(wildoApplicationId) {
253
+ const escapedId = wildoApplicationId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
254
+ return `^wildo\\.queue\\.${escapedId}\\..+\\.deadletter$`;
255
+ }
205
256
  /**
206
257
  * The queues a MINION may CONSUME from — its own tick queue only.
207
258
  *
@@ -386,6 +437,26 @@ export const RabbitmqSelfInfraProviderModule = {
386
437
  read: '',
387
438
  });
388
439
  }
440
+ // Bound this application's dead-letter queues (#2214) — on the default vhost too, where the
441
+ // per-application pattern keeps it from touching any other application's queues. Re-applied on
442
+ // every mint (backend and each minion) because it is an idempotent PUT; it reaches queues that
443
+ // already exist. Revocation leaves it in place, like the vhost and the queues it governs (on the
444
+ // shared default vhost it then matches nothing, which is harmless).
445
+ //
446
+ // FAIL-CLOSED, deliberately, and last: the credential is already upserted above, so a failure
447
+ // here aborts the mint after the access is in place rather than before. A management API that
448
+ // accepts users and permissions but refuses a policy is a broker this module cannot reason
449
+ // about, and a mint that reported success there would leave the bound silently missing.
450
+ await adminClient.ensureQueuePolicy(vhost, buildDeadLetterBoundPolicyName(context.wildoApplicationId), {
451
+ pattern: buildDeadLetterBoundPolicyPattern(context.wildoApplicationId),
452
+ definition: {
453
+ 'message-ttl': RABBITMQ_DEAD_LETTER_QUEUE_BOUND.messageTtlMs,
454
+ 'max-length': RABBITMQ_DEAD_LETTER_QUEUE_BOUND.maxLength,
455
+ // semantic-literal-boundary: RabbitMQ's own overflow vocabulary.
456
+ overflow: 'drop-head',
457
+ },
458
+ priority: RABBITMQ_DEAD_LETTER_QUEUE_BOUND.priority,
459
+ });
389
460
  return {
390
461
  secretBlobValues: {
391
462
  [RABBITMQ_USERNAME_PATH]: username,
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAEH,OAAO,EAIL,eAAe,EACf,0BAA0B,EAC1B,aAAa,EAKb,+BAA+B,EAC/B,iCAAiC,GAElC,MAAM,+BAA+B,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,cAAc,GAAG,WAAW,CAAC;AAEnC;;;;;;GAMG;AACH,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;AAC5D,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;AAC5D;;;;;;;GAOG;AACH,MAAM,+BAA+B,GAA0B,CAAC,sBAAsB,EAAE,sBAAsB,CAAC,CAAC;AAGhH;;;;;;;GAOG;AACH,MAAM,eAAe,GAAG,OAAO,CAAC;AAEhC;;;;;;;GAOG;AACH,MAAM,mCAAmC,GAAG,UAAU,CAAC;AAEvD;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,oBAA4B;IACvD,OAAO,oBAAoB,CAAC;AAC9B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,8CAA8C,GAAG,mCAAmC,CAAC;AAE3F;;;;;;;;;;;;;;GAcG;AACH,SAAS,yBAAyB,CAAC,WAAmB;IACpD,IAAI,WAAW,KAAK,8CAA8C,EAAE,CAAC;QACnE,OAAO,sCAAsC,CAAC;IAChD,CAAC;IACD,MAAM,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO,uBAAuB,WAAW,OAAO,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAAC,kBAA0B;IACtD,iEAAiE;IACjE,oEAAoE;IACpE,8DAA8D;IAC9D,MAAM,SAAS,GAAG,kBAAkB,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IAC5E,OAAO,4BAA4B,SAAS,OAAO,CAAC;AACtD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG;IAC9C,gCAAgC;IAChC,YAAY,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IACtC,SAAS,EAAE,MAAM;IACjB,QAAQ,EAAE,CAAC;CACH,CAAC;AAEX,mEAAmE;AACnE,MAAM,UAAU,8BAA8B,CAAC,kBAA0B;IACvE,OAAO,0BAA0B,kBAAkB,EAAE,CAAC;AACxD,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,iCAAiC,CAAC,kBAA0B;IAC1E,MAAM,SAAS,GAAG,kBAAkB,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IAC5E,OAAO,oBAAoB,SAAS,qBAAqB,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;;;;;;;;GAQG;AACH,SAAS,4BAA4B,CACnC,oBAA4B,EAC5B,SAAkD;IAElD,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,IAAI,KAAK,+BAA+B,CAAC,mBAAmB,EAAE,CAAC;QACzF,OAAO,mBAAmB,CAAC,oBAAoB,CAAC,CAAC;IACnD,CAAC;IACD,MAAM,aAAa,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACpG,OAAO,GAAG,oBAAoB,IAAI,SAAS,CAAC,IAAI,IAAI,aAAa,EAAE,CAAC;AACtE,CAAC;AAED,SAAS,uBAAuB,CAAC,kBAA0B,EAAE,UAAkB;IAC7E,MAAM,MAAM,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IACvF,MAAM,KAAK,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IAChC,sCAAsC;IACtC,iEAAiE;IACjE,gEAAgE;IAChE,4EAA4E;IAC5E,mEAAmE;IACnE,8EAA8E;IAC9E,6EAA6E;IAC7E,sEAAsE;IACtE,6EAA6E;IAC7E,WAAW;IACX,MAAM,YAAY,GAAG,MAAM,CACzB,iCAAiC,CAAC,+BAA+B,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,EAAE,CAC5F,CAAC;IACF,OAAO,qBAAqB,KAAK,oBAAoB,IAAI,UAAU;UAC/D,oBAAoB,KAAK,MAAM,YAAY,SAAS,CAAC;AAC3D,CAAC;AAED,MAAM,CAAC,MAAM,+BAA+B,GAAwB;IAClE,QAAQ,EAAE,aAAa,CAAC,aAAa;IACrC,qBAAqB,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC;IACvD,2BAA2B,EAAE,+BAA+B;IAE5D,IAAI;QACF,OAAO;YACL,qBAAqB,EAAE,CAAC,UAAU,CAAC;YACnC,YAAY,EAAE,CAAC,2DAA2D,CAAC;YAC3E,aAAa,EAAE;gBACb,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU;oBACzB,aAAa,EAAE,MAAM;oBACrB,wBAAwB,EAAE,OAAO;iBAClC;gBACD,aAAa,EAAE,CAAC,yBAAyB,EAAE,yBAAyB,CAAC;gBACrE,gEAAgE;gBAChE,4DAA4D;gBAC5D,sEAAsE;gBACtE,6DAA6D;gBAC7D,oDAAoD;gBACpD,SAAS,EAAE;oBACT,0BAA0B,CAAC,YAAY;oBACvC,0BAA0B,CAAC,SAAS;iBACrC;aACF;YACD,UAAU,EAAE;gBACV,+CAA+C;gBAC/C,+DAA+D;gBAC/D,+DAA+D;gBAC/D,gEAAgE;gBAChE,2DAA2D;gBAC3D,mCAAmC;gBACnC,qBAAqB,EAAE;oBACrB,wBAAwB,EAAE,UAAU;oBACpC,wBAAwB,EAAE,IAAI;iBAC/B;gBACD,qBAAqB,EAAE,CAAC,GAAG,+BAA+B,CAAC;aAC5D;SACF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,kBAAkB,CACtB,OAAoC;QAEpC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAwC,CAAC;QACrE,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC;QAC9D,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,sDAAsD;gBACtD,4BAA4B,sBAAsB,KAAK;gBACvD,6EAA6E,CAC9E,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,4BAA4B,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/F,MAAM,eAAe,GAAG,oBAAoB,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACzE,4EAA4E;QAC5E,uEAAuE;QACvE,wBAAwB;QACxB,EAAE;QACF,6EAA6E;QAC7E,0EAA0E;QAC1E,2EAA2E;QAC3E,0EAA0E;QAC1E,wEAAwE;QACxE,+CAA+C;QAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,KAAK,+BAA+B,CAAC,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI;YAC5G,CAAC,CAAC,uBAAuB,CAAC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC;YAC7E,CAAC,CAAC,eAAe,CAAC;QAEpB,8DAA8D;QAC9D,mEAAmE;QACnE,8DAA8D;QAC9D,kEAAkE;QAClE,gEAAgE;QAChE,mEAAmE;QACnE,mEAAmE;QACnE,aAAa;QACb,MAAM,KAAK,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,eAAe,CAAC,IAAI,aAAa,CAAC;QAClF,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;YAC5B,MAAM,WAAW,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;QAED,6EAA6E;QAC7E,MAAM,WAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QAElD,gEAAgE;QAChE,gEAAgE;QAChE,mEAAmE;QACnE,+CAA+C;QAC/C,gEAAgE;QAChE,+BAA+B;QAC/B,MAAM,UAAU,GAAG,qBAAqB,eAAe,GAAG,CAAC;QAC3D,MAAM,WAAW,CAAC,cAAc,CAAC,KAAK,EAAE,QAAQ,EAAE;YAChD,SAAS,EAAE,eAAe;YAC1B,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,SAAS;SAChB,CAAC,CAAC;QAEH,iEAAiE;QACjE,MAAM,WAAW,CAAC,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE;YACrD,QAAQ,EAAE,cAAc;YACxB,KAAK,EAAE,eAAe;YACtB,IAAI,EAAE,SAAS;SAChB,CAAC,CAAC;QAEH,gFAAgF;QAChF,EAAE;QACF,oEAAoE;QACpE,yEAAyE;QACzE,0EAA0E;QAC1E,4EAA4E;QAC5E,iFAAiF;QACjF,+DAA+D;QAC/D,EAAE;QACF,oFAAoF;QACpF,4EAA4E;QAC5E,yEAAyE;QACzE,2EAA2E;QAC3E,6EAA6E;QAC7E,qEAAqE;QACrE,gEAAgE;QAChE,yEAAyE;QACzE,0EAA0E;QAC1E,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;YAC5B,MAAM,WAAW,CAAC,cAAc,CAAC,KAAK,EAAE,8CAA8C,EAAE;gBACtF,SAAS,EAAE,eAAe;gBAC1B,KAAK,EAAE,UAAU;gBACjB,IAAI,EAAE,EAAE;aACT,CAAC,CAAC;QACL,CAAC;QAED,4FAA4F;QAC5F,+FAA+F;QAC/F,+FAA+F;QAC/F,iGAAiG;QACjG,oEAAoE;QACpE,EAAE;QACF,8FAA8F;QAC9F,8FAA8F;QAC9F,2FAA2F;QAC3F,wFAAwF;QACxF,MAAM,WAAW,CAAC,iBAAiB,CAAC,KAAK,EAAE,8BAA8B,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE;YACrG,OAAO,EAAE,iCAAiC,CAAC,OAAO,CAAC,kBAAkB,CAAC;YACtE,UAAU,EAAE;gBACV,aAAa,EAAE,gCAAgC,CAAC,YAAY;gBAC5D,YAAY,EAAE,gCAAgC,CAAC,SAAS;gBACxD,iEAAiE;gBACjE,QAAQ,EAAE,WAAW;aACtB;YACD,QAAQ,EAAE,gCAAgC,CAAC,QAAQ;SACpD,CAAC,CAAC;QAEH,OAAO;YACL,gBAAgB,EAAE;gBAChB,CAAC,sBAAsB,CAAC,EAAE,QAAQ;gBAClC,8DAA8D;gBAC9D,2DAA2D;gBAC3D,qDAAqD;gBACrD,CAAC,sBAAsB,CAAC,EAAE,QAAQ;aACnC;SACF,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,KAAK,CAAC,oBAAoB,CAAC,OAAoC;QAC7D,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,wDAAwD;gBACxD,iCAAiC;gBACjC,qDAAqD,CACtD,CAAC;QACJ,CAAC;QACD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAwC,CAAC;QACrE,MAAM,QAAQ,GAAG,4BAA4B,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/F,MAAM,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,8BAA8B,CAClC,OAAgD;QAEhD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAwC,CAAC;QACrE,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,mCAAmC,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,kEAAkE;gBAClE,4BAA4B,mCAAmC,KAAK;gBACpE,uFAAuF,CACxF,CAAC;QACJ,CAAC;QAED,mEAAmE;QACnE,6BAA6B;QAC7B,qEAAqE;QACrE,uCAAuC;QACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;QACrC,MAAM,eAAe,GAAG,yBAAyB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAEvE,sCAAsC;QACtC,MAAM,WAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QAElD,+DAA+D;QAC/D,kEAAkE;QAClE,kEAAkE;QAClE,yDAAyD;QACzD,MAAM,UAAU,GAAG,qBAAqB,eAAe,GAAG,CAAC;QAC3D,MAAM,WAAW,CAAC,cAAc,CAAC,aAAa,EAAE,QAAQ,EAAE;YACxD,SAAS,EAAE,eAAe;YAC1B,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,eAAe;SACtB,CAAC,CAAC;QAEH,8DAA8D;QAC9D,8DAA8D;QAC9D,6DAA6D;QAC7D,kEAAkE;QAClE,mEAAmE;QACnE,MAAM,WAAW,CAAC,mBAAmB,CAAC,aAAa,EAAE,QAAQ,EAAE;YAC7D,QAAQ,EAAE,cAAc;YACxB,KAAK,EAAE,eAAe;YACtB,IAAI,EAAE,eAAe;SACtB,CAAC,CAAC;QAEH,OAAO,EAAE,QAAQ,EAAE,CAAC;IACtB,CAAC;CACF,CAAC","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 bound on an application's DEAD-LETTER queues (#2214), declared as a broker POLICY.\n *\n * ## Why it exists\n *\n * Every application dead-letter queue (`<category>.deadletter`, and `domain.<name>.deadletter`) is\n * terminal and consumer-less BY DECISION (#469, 2026-09-08): a parked message is inspected and\n * replayed with the broker's own tooling, never through a Wildo read surface, because a job payload\n * may carry personal data. Measured on 2026-10-01, that left a sink with no reader AND no bound —\n * `scheduled.deadletter` held 359 messages for one local application after one day of restarts,\n * and every restart added to it permanently. A queue an operator is expected to read must stay small\n * enough to read, and must not grow without limit in a deployment that lives for months.\n *\n * ## Why a policy and not a queue argument\n *\n * RabbitMQ refuses to change the arguments of an existing queue (406 PRECONDITION_FAILED), so a\n * `x-max-length` argument would only reach a queue declared after the change and would break the\n * declaration of every queue that already exists. A policy reaches existing and future queues alike\n * and can be changed later. RabbitMQ's documentation recommends a policy for exactly this setting.\n * spec-cite: https://www.rabbitmq.com/docs/maxlength — max length \"using a policy (this option is highly recommended)\"; `overflow` defaults to `drop-head`.\n * spec-cite: https://www.rabbitmq.com/docs/policies — \"At most one policy matches a queue\"; policies \"can change at any time\" and are re-applied.\n * spec-cite: https://www.rabbitmq.com/docs/ttl — `message-ttl` in milliseconds; with no dead-letter exchange an expired message is removed.\n *\n * ## The values\n *\n * - `message-ttl` 14 days: a parked message is kept long enough for an operator to notice and act on\n * it, then removed. The dead-letter queues have no dead-letter exchange of their own, so an expired\n * message is discarded, not moved.\n * - `max-length` 10 000 with `overflow: drop-head`: a burst cannot grow the queue past what a person\n * can inspect; the OLDEST message goes first, so the most recent failures — the ones being\n * investigated — survive.\n *\n * Scoped to ONE application's own dead-letter queues, so the policies of applications sharing the\n * default vhost never overlap: at most one policy applies to a queue, and two overlapping policies at\n * the same priority would be applied non-deterministically.\n */\nexport const RABBITMQ_DEAD_LETTER_QUEUE_BOUND = {\n /** 14 days, in milliseconds. */\n messageTtlMs: 14 * 24 * 60 * 60 * 1000,\n maxLength: 10_000,\n priority: 0,\n} as const;\n\n/** The policy name, per application — unique on a shared vhost. */\nexport function buildDeadLetterBoundPolicyName(wildoApplicationId: string): string {\n return `wildo-deadletter-bound.${wildoApplicationId}`;\n}\n\n/** Every dead-letter queue of ONE application: `wildo.queue.<appId>.<anything>.deadletter`. */\nexport function buildDeadLetterBoundPolicyPattern(wildoApplicationId: string): string {\n const escapedId = wildoApplicationId.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return `^wildo\\\\.queue\\\\.${escapedId}\\\\..+\\\\.deadletter$`;\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 // Bound this application's dead-letter queues (#2214) — on the default vhost too, where the\n // per-application pattern keeps it from touching any other application's queues. Re-applied on\n // every mint (backend and each minion) because it is an idempotent PUT; it reaches queues that\n // already exist. Revocation leaves it in place, like the vhost and the queues it governs (on the\n // shared default vhost it then matches nothing, which is harmless).\n //\n // FAIL-CLOSED, deliberately, and last: the credential is already upserted above, so a failure\n // here aborts the mint after the access is in place rather than before. A management API that\n // accepts users and permissions but refuses a policy is a broker this module cannot reason\n // about, and a mint that reported success there would leave the bound silently missing.\n await adminClient.ensureQueuePolicy(vhost, buildDeadLetterBoundPolicyName(context.wildoApplicationId), {\n pattern: buildDeadLetterBoundPolicyPattern(context.wildoApplicationId),\n definition: {\n 'message-ttl': RABBITMQ_DEAD_LETTER_QUEUE_BOUND.messageTtlMs,\n 'max-length': RABBITMQ_DEAD_LETTER_QUEUE_BOUND.maxLength,\n // semantic-literal-boundary: RabbitMQ's own overflow vocabulary.\n overflow: 'drop-head',\n },\n priority: RABBITMQ_DEAD_LETTER_QUEUE_BOUND.priority,\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"]}