@wildo-ai/platform-infra-providers 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/aws-s3/aws-s3.infra.module.d.ts +63 -0
  3. package/dist/esm/aws-s3/aws-s3.infra.module.d.ts.map +1 -0
  4. package/dist/esm/aws-s3/aws-s3.infra.module.js +110 -0
  5. package/dist/esm/aws-s3/aws-s3.infra.module.js.map +1 -0
  6. package/dist/esm/clamav-self/clamav-self.infra.module.d.ts +30 -0
  7. package/dist/esm/clamav-self/clamav-self.infra.module.d.ts.map +1 -0
  8. package/dist/esm/clamav-self/clamav-self.infra.module.js +61 -0
  9. package/dist/esm/clamav-self/clamav-self.infra.module.js.map +1 -0
  10. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts +26 -0
  11. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts.map +1 -0
  12. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js +54 -0
  13. package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js.map +1 -0
  14. package/dist/esm/index.d.ts +88 -0
  15. package/dist/esm/index.d.ts.map +1 -0
  16. package/dist/esm/index.js +125 -0
  17. package/dist/esm/index.js.map +1 -0
  18. package/dist/esm/minio-self/minio-self.infra.module.d.ts +64 -0
  19. package/dist/esm/minio-self/minio-self.infra.module.d.ts.map +1 -0
  20. package/dist/esm/minio-self/minio-self.infra.module.js +105 -0
  21. package/dist/esm/minio-self/minio-self.infra.module.js.map +1 -0
  22. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts +52 -0
  23. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts.map +1 -0
  24. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js +201 -0
  25. package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js.map +1 -0
  26. package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts +81 -0
  27. package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts.map +1 -0
  28. package/dist/esm/mongodb-self/mongodb-self.infra.module.js +311 -0
  29. package/dist/esm/mongodb-self/mongodb-self.infra.module.js.map +1 -0
  30. package/dist/esm/postgres-self/postgres-self.infra.module.d.ts +70 -0
  31. package/dist/esm/postgres-self/postgres-self.infra.module.d.ts.map +1 -0
  32. package/dist/esm/postgres-self/postgres-self.infra.module.js +328 -0
  33. package/dist/esm/postgres-self/postgres-self.infra.module.js.map +1 -0
  34. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts +88 -0
  35. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts.map +1 -0
  36. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js +473 -0
  37. package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js.map +1 -0
  38. package/dist/esm/redis-self/redis-self.infra.module.d.ts +89 -0
  39. package/dist/esm/redis-self/redis-self.infra.module.d.ts.map +1 -0
  40. package/dist/esm/redis-self/redis-self.infra.module.js +331 -0
  41. package/dist/esm/redis-self/redis-self.infra.module.js.map +1 -0
  42. package/dist/tsconfig.build.tsbuildinfo +1 -0
  43. package/package.json +52 -0
  44. package/src/aws-s3/__tests__/aws-s3.infra.module.test.ts +67 -0
  45. package/src/aws-s3/aws-s3.infra.module.ts +120 -0
  46. package/src/clamav-self/__tests__/clamav-self.infra.module.test.ts +65 -0
  47. package/src/clamav-self/clamav-self.infra.module.ts +72 -0
  48. package/src/gotenberg-self/__tests__/gotenberg-self.infra.module.test.ts +65 -0
  49. package/src/gotenberg-self/gotenberg-self.infra.module.ts +65 -0
  50. package/src/index.ts +133 -0
  51. package/src/minio-self/__tests__/minio-self.infra.module.test.ts +70 -0
  52. package/src/minio-self/minio-self.infra.module.ts +116 -0
  53. package/src/mongodb-atlas/__tests__/mongodb-atlas.infra.module.test.ts +198 -0
  54. package/src/mongodb-atlas/mongodb-atlas.infra.module.ts +236 -0
  55. package/src/mongodb-self/__tests__/mongodb-self.infra.module.test.ts +419 -0
  56. package/src/mongodb-self/mongodb-self.infra.module.ts +384 -0
  57. package/src/postgres-self/__tests__/postgres-self.infra.module.test.ts +344 -0
  58. package/src/postgres-self/postgres-self.infra.module.ts +377 -0
  59. package/src/rabbitmq-self/__tests__/rabbitmq-self.infra.module.test.ts +499 -0
  60. package/src/rabbitmq-self/rabbitmq-self.infra.module.ts +531 -0
  61. package/src/redis-self/__tests__/redis-self.infra.module.test.ts +341 -0
  62. package/src/redis-self/redis-self.infra.module.ts +372 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * `RabbitmqSelfInfraProviderModule` — first concrete `InfraProviderModule`
3
+ * (Slices 1–2.5 of `infra-provider-planner.md`).
4
+ *
5
+ * Implements the `RABBITMQ_SELF` `InfraProvider` over the
6
+ * `IRabbitMQManagementClient` transport (raw HTTP `PUT /api/users` etc.,
7
+ * exposed by `RabbitMQConnectionManagerService` after the 5C policy/
8
+ * transport split). Owns ALL per-app and per-platform-service
9
+ * permission policy — the regex shapes, the broader-permission
10
+ * special case for `crontabs-batches-manager`, the per-app username
11
+ * convention, the optional vhost activation.
12
+ *
13
+ * **What this module reads from `AppCredentialMintingContext`**:
14
+ * - `applicationNamespace` — used as the rabbit username
15
+ * (`<appSlug>-<envId>`, the legacy convention from
16
+ * `wildo-application-lifecycle.service.ts`).
17
+ * - `wildoApplicationId` — used as the permission-regex disambiguator
18
+ * (matches `^wildo\.(queue|reply)\.<id>\..*`).
19
+ * - `secretInputs['database.rabbitmq.password']` — orchestrator-
20
+ * supplied password (module is upsert-only, never generator).
21
+ * - `applicationConfigInputs?.['vhost']` — optional per-app vhost
22
+ * for true vhost-level isolation (Slice 2.5c-3).
23
+ * - `adminClient` — narrowed to `IRabbitMQManagementClient`.
24
+ *
25
+ * **What this module IGNORES from the context** (Slice 4.A.2 / M4):
26
+ * - `appSlug` — RabbitMQ resources are namespace-scoped, not slug-
27
+ * scoped. Future modules (MongoDB DB name, etc.) WILL use `appSlug`.
28
+ * This is documented forward-compat for module authors: not every
29
+ * module reads every context field; each declares which it consumes.
30
+ *
31
+ * **Three audiences this module serves:**
32
+ *
33
+ * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:
34
+ * cluster-wide management-API creds (`RABBITMQ_ADMIN_USERNAME` /
35
+ * `RABBITMQ_ADMIN_PASSWORD`). Consumers: `apps-manager`,
36
+ * `companion`. Delivered through process.env (the CLI's
37
+ * `env-vars-builder` writes them per-env). No method.
38
+ *
39
+ * 2. **Audience-2 (platform-service-scoped, via
40
+ * `mintPlatformServiceCredentials`)**: per-platform-service broker
41
+ * users for inter-service messaging (`apps-manager`,
42
+ * `crontabs-batches-manager`, `file-generator`). Username =
43
+ * `serviceName`; permission regex is service-namespaced
44
+ * (`^wildo\.platform\.<slug>\..*`); crontabs gets the broader
45
+ * `^wildo\.(queue|reply|platform)\..*` per the legacy override.
46
+ *
47
+ * **Slice 2.5 behavior-parity note**: the legacy code paths
48
+ * `provisionServiceCredentials` and `ensureAppsManagerCredentials`
49
+ * produced slightly different topic-permission regexes for the
50
+ * same apps-manager user (`wildo\.platform\.apps_manager\..*` vs
51
+ * `wildo.platform.apps_manager.*`). User permissions were therefore
52
+ * order-dependent. This module unifies on the dot-escaped variant.
53
+ * Functionally equivalent for real queue names; tighter spec.
54
+ *
55
+ * **Slice 4.A.2 / M2 design note**: the legacy
56
+ * `getDefaultRabbitMQPermissions.read` field declared a BROADER
57
+ * regex than `configure`/`write` (included
58
+ * `wildo.platform.broadcast.*`) — but that widening NEVER reached
59
+ * the broker (the legacy `upsertTenantUserPermissions` ignored
60
+ * the `read` arg and used `resourceNameRegex` for both
61
+ * `configure` and `read`). This module preserves that behavior:
62
+ * `read === configure`. If platform services need a broadcast
63
+ * namespace in the future, extend `mintPlatformServiceCredentials`
64
+ * with an explicit broadcast-read regex.
65
+ *
66
+ * 3. **Audience-3 (app-scoped, via `plan().appRuntime` +
67
+ * `mintAppCredentials`)**: per-customer-app broker user. Username
68
+ * = `applicationNamespace`; permission regex disambiguates by
69
+ * `wildoApplicationId` (Mongo ObjectId — globally unique). Result
70
+ * threaded into `Wildo_Application_Secrets.database.rabbitmq.{
71
+ * username, password}` via `_merge_WildoApplicationSecrets`.
72
+ *
73
+ * **Vhost activation (Slice 2.5c-3, opt-in)**: when the operator
74
+ * authors `backingServices.rabbitmq.vhost` in
75
+ * `wildo.infra.<envName>.config.ts`, the CLI emits `RABBITMQ_VHOST`
76
+ * into apps-manager's process.env; the lifecycle service threads it
77
+ * via `applicationConfigInputs.vhost`; this module
78
+ * `ensureVhost`-then-permissions-on-it. Without the field, the
79
+ * module falls back to `/` (legacy permission-regex isolation).
80
+ *
81
+ * **`revokeAppCredentials`**: declared and tested, but the integration
82
+ * call site does NOT exist in `wildo-application-lifecycle.service.ts`
83
+ * (no app-deletion ceremony today — F6 of plan review 2026-05-06).
84
+ * Method ships as a forward-compat seam.
85
+ */
86
+ import { InfraCapability, InfraPlatformAdminConsumer, InfraProvider, ApplicationRuntimePrincipalKind, buildRuntimePrincipalQueueSegment, } from '@wildo-ai/platform-config-lib';
87
+ /**
88
+ * Default vhost — apps share `/` with regex-based per-user permission
89
+ * isolation. Per-app vhost is intentionally NOT modeled (engine has no
90
+ * `RABBITMQ_VHOST` consumer; isolation comes from the permission regex
91
+ * on the per-app user, not from a per-app vhost).
92
+ *
93
+ * **No `ensureVhost(DEFAULT_VHOST)` call** is needed: vhost `/` is
94
+ * created by RabbitMQ at server boot. Any future module variant that
95
+ * uses a non-default vhost MUST call `adminClient.ensureVhost(...)`
96
+ * before user/permissions setup.
97
+ */
98
+ const DEFAULT_VHOST = '/';
99
+ /**
100
+ * Topic exchange used by every Wildo runtime publisher / consumer.
101
+ * Matches the value already in use at
102
+ * `wildo-application-lifecycle.service.ts:568` and
103
+ * `platform-infrastructure-manager.service.ts:485`.
104
+ */
105
+ const TOPIC_EXCHANGE = 'amq.topic';
106
+ /**
107
+ * The `Wildo_Application_Secrets` JSON paths this module writes through
108
+ * `mintAppCredentials`. Declared once and reused as
109
+ * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of
110
+ * `secretBlobValues` in the result, so a refactor that renames a path
111
+ * fails loudly at one place instead of drifting silently.
112
+ */
113
+ const RABBITMQ_USERNAME_PATH = 'database.rabbitmq.username';
114
+ const RABBITMQ_PASSWORD_PATH = 'database.rabbitmq.password';
115
+ /**
116
+ * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.
117
+ *
118
+ * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip
119
+ * these from the application blob before delivering a principal's own material) AND spread into
120
+ * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery
121
+ * path would silently stop covering a path this module still mints.
122
+ */
123
+ const PER_PRINCIPAL_SECRET_BLOB_PATHS = [RABBITMQ_USERNAME_PATH, RABBITMQ_PASSWORD_PATH];
124
+ /**
125
+ * Key the module looks for in
126
+ * `AppCredentialMintingContext.applicationConfigInputs` to read the
127
+ * optional per-app virtual host. Documented here as the public
128
+ * contract: the orchestrator supplies the vhost via this key, the
129
+ * module reads it via this key. Slice 2.5c-3 of
130
+ * `infra-provider-planner.md`.
131
+ */
132
+ const VHOST_INPUT_KEY = 'vhost';
133
+ /**
134
+ * Key the module looks for in
135
+ * `PlatformServiceCredentialMintingContext.secretInputs` to read the
136
+ * orchestrator-supplied platform-service password. Slice 4.A.2 / M5
137
+ * of `infra-provider-planner.md` — extracted as a constant for
138
+ * symmetry with `RABBITMQ_PASSWORD_PATH` (used by `mintAppCredentials`),
139
+ * so a future rename touches one site instead of drifting silently.
140
+ */
141
+ const PLATFORM_SERVICE_PASSWORD_INPUT_KEY = 'password';
142
+ /**
143
+ * Compute the per-app rabbit username. Mirrors the existing convention
144
+ * at `wildo-application-lifecycle.service.ts:434, 440` — username is
145
+ * the application namespace (`<appSlug>-<environmentId>` per
146
+ * `buildApplicationNamespace`), NOT the raw slug. Centralized here so
147
+ * future changes touch one site.
148
+ */
149
+ function buildRabbitUsername(applicationNamespace) {
150
+ return applicationNamespace;
151
+ }
152
+ /**
153
+ * Identifier of the platform-crontabs-batches-manager service. The
154
+ * crontabs special case (broader permissions covering all per-app
155
+ * queue/reply names so it can fan out scheduled jobs) lives here so
156
+ * the legacy hard-coded check at
157
+ * `platform-infrastructure-manager.service.ts:466-475` becomes a
158
+ * module-internal concern.
159
+ *
160
+ * Hardcoded as a string literal rather than imported from
161
+ * `PlatformApplicationType` because that enum lives in saas-models
162
+ * and importing it here would couple the module to that enum's
163
+ * runtime shape — the value `'platform_crontabs_batches_manager'` is
164
+ * stable and small.
165
+ */
166
+ const PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME = 'platform_crontabs_batches_manager';
167
+ /**
168
+ * Compute the per-platform-service permission regex.
169
+ *
170
+ * Default policy (apps-manager, file-generator, …): the service can
171
+ * configure / write / read on `^wildo\.platform\.<serviceSlug>\..*`
172
+ * where `serviceSlug = serviceName.replace('platform_', '')`. Mirrors
173
+ * the legacy `getDefaultRabbitMQPermissions` at
174
+ * `credentials-provisioner.service.ts:267-275`.
175
+ *
176
+ * Crontabs-batches-manager override: broader regex
177
+ * `^wildo\.(queue|reply|platform)\..*` so the crontab can publish
178
+ * scheduled jobs onto every per-app queue and read replies. Mirrors
179
+ * the legacy inline override at
180
+ * `platform-infrastructure-manager.service.ts:466-475`.
181
+ */
182
+ function buildPlatformServiceRegex(serviceName) {
183
+ if (serviceName === PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME) {
184
+ return '^wildo\\.(queue|reply|platform)\\..*';
185
+ }
186
+ const serviceSlug = serviceName.replace('platform_', '');
187
+ return `^wildo\\.platform\\.${serviceSlug}\\..*`;
188
+ }
189
+ /**
190
+ * Compute the per-app permission regex. Matches the convention at
191
+ * `wildo-application-lifecycle.service.ts:552`:
192
+ * `^wildo\.(queue|reply)\.${wildoApplicationId}\..*`
193
+ * The disambiguator is the application ID, NOT the slug — slugs could
194
+ * theoretically be reassigned; the id is forever-stable. The `^` and
195
+ * `\.` escaping is required by the RabbitMQ regex evaluator.
196
+ */
197
+ function buildPermissionRegex(wildoApplicationId) {
198
+ // Escape regex metacharacters that may appear in application ids
199
+ // (today they're Mongo ObjectId hex strings — no specials — but the
200
+ // contract only commits to "stable opaque string", so guard).
201
+ const escapedId = wildoApplicationId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
202
+ return `^wildo\\.(queue|reply)\\.${escapedId}\\..*`;
203
+ }
204
+ /**
205
+ * The queues a MINION may CONSUME from — its own tick queue only.
206
+ *
207
+ * ## Why only `read` narrows, measured rather than assumed
208
+ *
209
+ * A minion runs the full backend pipeline headless, so the three RabbitMQ
210
+ * permission axes are not interchangeable here:
211
+ *
212
+ * - **`read`** governs CONSUMING. Narrowing it is the containment that matters:
213
+ * without it a minion can drain the application backend's job queue, consume
214
+ * another minion's ticks, or read RPC replies addressed to the backend.
215
+ * - **`configure`** governs DECLARE. The tick consumer declares its own queue
216
+ * (`channel.assertQueue`) — but the delayed-publish path can declare a delay
217
+ * queue too, so narrowing this risks breaking a legitimate publish.
218
+ * - **`write`** governs PUBLISHING. A minion legitimately enqueues jobs and
219
+ * sends RPC replies into the application's namespace; `publish` calls
220
+ * `sendToQueue` WITHOUT asserting (verified in
221
+ * `rabbitmq-adapter.backend.service.ts`), so it needs `write` on the app
222
+ * namespace and nothing more.
223
+ *
224
+ * **What this therefore does NOT prevent, stated plainly:** a minion can still
225
+ * PUBLISH anywhere in its application's queue namespace — it could enqueue a job
226
+ * the backend will run. Constraining that is a different control (who may
227
+ * request what work), not a broker permission, and pretending otherwise would
228
+ * overstate the boundary.
229
+ *
230
+ * The trailing `(\..*)?$` admits the tick queue itself and any suffixed
231
+ * variant, while the anchored `$` stops it matching a SIBLING whose name merely
232
+ * starts with this one's.
233
+ */
234
+ /**
235
+ * Per-principal broker username — the SAME derivation `revokeAppCredentials`
236
+ * must use.
237
+ *
238
+ * The application backend keeps `applicationNamespace` VERBATIM so an existing
239
+ * deployment's user is never orphaned. A revoke that derived this differently
240
+ * would delete the BACKEND's broker user while believing it was removing a
241
+ * minion's — the identical trap already caught on the Redis side.
242
+ */
243
+ function buildRabbitPrincipalUsername(applicationNamespace, principal) {
244
+ if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {
245
+ return buildRabbitUsername(applicationNamespace);
246
+ }
247
+ const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');
248
+ return `${applicationNamespace}-${principal.kind}-${sanitizedName}`;
249
+ }
250
+ function buildMinionConsumeRegex(wildoApplicationId, minionName) {
251
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
252
+ const appId = escape(wildoApplicationId);
253
+ const name = escape(minionName);
254
+ // Two alternatives, and nothing else:
255
+ // its tick queue — wildo.queue.<appId>.minion-tick.<name>
256
+ // its OWN reply queue — wildo.reply.<appId>.<segment>.<podId>
257
+ // The reply segment is IMPORTED from the same builder the minion's own AMQP
258
+ // adapter uses, not reproduced here. It used to be hand-written as
259
+ // `minion-<name>` with a comment saying it "mirrors" what the adapter emits —
260
+ // which agreed only while every minion name survived sanitisation unchanged.
261
+ // The application backend gets no segment at all (the builder returns
262
+ // undefined for it), keeping the historical layout, so this cannot match its
263
+ // replies.
264
+ const replySegment = escape(buildRuntimePrincipalQueueSegment(ApplicationRuntimePrincipalKind.MINION, minionName) ?? '');
265
+ return `^(wildo\\.queue\\.${appId}\\.minion-tick\\.${name}(\\..*)?`
266
+ + `|wildo\\.reply\\.${appId}\\.${replySegment}\\..*)$`;
267
+ }
268
+ export const RabbitmqSelfInfraProviderModule = {
269
+ provider: InfraProvider.RABBITMQ_SELF,
270
+ supportedCapabilities: [InfraCapability.QUEUING_SYSTEM],
271
+ perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,
272
+ plan() {
273
+ return {
274
+ dockerComposeServices: ['rabbitmq'],
275
+ k8sResources: ['templates/infra/k8s/internal/rabbitmq/deployment.yaml.hbs'],
276
+ platformAdmin: {
277
+ envVars: {
278
+ RABBITMQ_HOST: 'rabbitmq',
279
+ RABBITMQ_PORT: '5672',
280
+ RABBITMQ_MANAGEMENT_PORT: '15672',
281
+ },
282
+ secretEnvVars: ['RABBITMQ_ADMIN_USERNAME', 'RABBITMQ_ADMIN_PASSWORD'],
283
+ // Audience-1 only (management-API access). audience-2 platform-
284
+ // service-scoped consumers (apps-manager / crontabs / file-
285
+ // generator publish-consume creds via `provisionAllPlatformServices`)
286
+ // are LD-8 territory, deferred to Slice 2.5 — they get their
287
+ // own `mintPlatformServiceCredentials` method then.
288
+ consumers: [
289
+ InfraPlatformAdminConsumer.APPS_MANAGER,
290
+ InfraPlatformAdminConsumer.COMPANION,
291
+ ],
292
+ },
293
+ appRuntime: {
294
+ // Declared for forward-compat with Slice 2.5's
295
+ // `_merge_WildoApplicationConfig` integration. Not consumed by
296
+ // any planner path in Slice 2 — the existing hard-coded values
297
+ // at `wildo-application-lifecycle.service.ts:1796-1799` stay in
298
+ // effect (host: 'localhost', port: 5672) until the planner
299
+ // wires this slice's contribution.
300
+ perAppConfigBlobPaths: {
301
+ 'database.rabbitmq.host': 'rabbitmq',
302
+ 'database.rabbitmq.port': 5672,
303
+ },
304
+ perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],
305
+ },
306
+ };
307
+ },
308
+ async mintAppCredentials(context) {
309
+ const adminClient = context.adminClient;
310
+ const password = context.secretInputs[RABBITMQ_PASSWORD_PATH];
311
+ if (!password) {
312
+ throw new Error(`RabbitmqSelfInfraProviderModule.mintAppCredentials: ` +
313
+ `secretInputs is missing '${RABBITMQ_PASSWORD_PATH}'. ` +
314
+ `The orchestrator MUST supply the rabbit password — modules are upsert-only.`);
315
+ }
316
+ const username = buildRabbitPrincipalUsername(context.applicationNamespace, context.principal);
317
+ const permissionRegex = buildPermissionRegex(context.wildoApplicationId);
318
+ // A MINION consumes exactly two things: its own tick queue, and its own RPC
319
+ // REPLY queue. Both are admitted; everything else in the application's
320
+ // namespace is refused.
321
+ //
322
+ // The reply half is why an earlier attempt at this failed. Reply queues used
323
+ // to be `wildo.reply.<appId>.<podId>`, and `podId` is a per-PROCESS token
324
+ // carrying no principal — so no regex could admit a minion's replies while
325
+ // refusing the backend's, and narrowing `read` crash-looped the minion on
326
+ // its own replies. `buildReplyQueueName` now embeds the owning runtime,
327
+ // which is what makes this expressible at all.
328
+ const readRegex = context.principal?.kind === ApplicationRuntimePrincipalKind.MINION && context.principal.name
329
+ ? buildMinionConsumeRegex(context.wildoApplicationId, context.principal.name)
330
+ : permissionRegex;
331
+ // Slice 2.5c-3: optional per-app vhost. When the orchestrator
332
+ // supplies a non-empty `applicationConfigInputs.vhost`, the module
333
+ // ensures the vhost exists and sets permissions ON THAT vhost
334
+ // (provides true vhost-level isolation). When omitted, the module
335
+ // falls back to the default vhost `/` (legacy Slice 2 behavior:
336
+ // permission-regex isolation only). This is the opt-in seam — apps
337
+ // without an authored `backingServices.rabbitmq.vhost` continue to
338
+ // share `/`.
339
+ const vhost = context.applicationConfigInputs?.[VHOST_INPUT_KEY] || DEFAULT_VHOST;
340
+ if (vhost !== DEFAULT_VHOST) {
341
+ await adminClient.ensureVhost(vhost);
342
+ }
343
+ // PUT /api/users — idempotent upsert (replaces password + tags on conflict).
344
+ await adminClient.putUser(username, password, []);
345
+ // PUT /api/permissions — `configure` and `read` use the per-app
346
+ // queue/reply regex; `write` widens to allow `amq.default` (the
347
+ // default exchange RabbitMQ uses for direct routing) and the empty
348
+ // string (broker-internal). Mirrors the legacy
349
+ // `upsertTenantUserPermissions` write-regex shape so the Wonder
350
+ // Todos behavior is unchanged.
351
+ const writeRegex = `(^$|amq\\.default|${permissionRegex})`;
352
+ await adminClient.setPermissions(vhost, username, {
353
+ configure: permissionRegex,
354
+ write: writeRegex,
355
+ read: readRegex,
356
+ });
357
+ // PUT /api/topic-permissions — same regex on the topic exchange.
358
+ await adminClient.setTopicPermissions(vhost, username, {
359
+ exchange: TOPIC_EXCHANGE,
360
+ write: permissionRegex,
361
+ read: readRegex,
362
+ });
363
+ // Per-app-vhost cron delivery (`.claude/plans/crontab-publisher-vhost-gap.md`).
364
+ //
365
+ // A per-app vhost is a HARD broker isolation boundary: the platform
366
+ // cron/minion publisher (`platform_crontabs_batches_manager`) — a single
367
+ // global user that fans scheduled jobs into every app — cannot reach THIS
368
+ // app's queues until granted access on THIS vhost. On the default vhost `/`
369
+ // it already holds a broad publish grant (see `mintPlatformServiceCredentials`),
370
+ // so the grant is only needed for a non-default per-app vhost.
371
+ //
372
+ // Scope it to this app's own queue namespace (`^wildo\.(queue|reply)\.<appId>\..*`)
373
+ // — the publisher gets exactly the reach it needs and no more, TIGHTER than
374
+ // its broad default-vhost grant. It is PUBLISH-ONLY: `configure` for the
375
+ // passive `checkQueue` existence probe the publisher runs before sending +
376
+ // any declare, `write` (`amq.default`) for `basic.publish`; NO `read` (never
377
+ // consumes) and NO topic permissions (uses the default exchange, not
378
+ // `amq.topic`). The crontabs user is provisioned before any app
379
+ // (apps-manager INIT Phase 4 < Phase 5), so it reliably exists here, and
380
+ // `setPermissions` is an idempotent PUT — a re-init safely re-applies it.
381
+ if (vhost !== DEFAULT_VHOST) {
382
+ await adminClient.setPermissions(vhost, PLATFORM_CRONTABS_BATCHES_MANAGER_SERVICE_NAME, {
383
+ configure: permissionRegex,
384
+ write: writeRegex,
385
+ read: '',
386
+ });
387
+ }
388
+ return {
389
+ secretBlobValues: {
390
+ [RABBITMQ_USERNAME_PATH]: username,
391
+ // Echo the orchestrator-supplied password — proves the module
392
+ // is upsert-only, not generator. The orchestrator persists
393
+ // this exact value into `Wildo_Application_Secrets`.
394
+ [RABBITMQ_PASSWORD_PATH]: password,
395
+ },
396
+ };
397
+ },
398
+ /**
399
+ * Tear down the per-app rabbit user. RabbitMQ users are global, so
400
+ * `deleteUser` removes the user (and its permissions on every vhost
401
+ * it had access to) atomically.
402
+ *
403
+ * **Vhost retention** (Slice 4.A.2 / M6): when `mintAppCredentials`
404
+ * provisioned a per-app vhost (Slice 2.5c-3), `revokeAppCredentials`
405
+ * does NOT delete the vhost. Reasoning: vhost deletion drops every
406
+ * queue and exchange inside it, which would destroy in-flight
407
+ * messages mid-flight if a redeploy or restart is happening
408
+ * concurrently. The user-deletion alone is sufficient to revoke
409
+ * access; the empty vhost lingers as a paper trail. A future
410
+ * "app-deletion lifecycle" slice can layer vhost cleanup with
411
+ * proper drain semantics.
412
+ *
413
+ * **Caller status** (F6 of 2026-05-06 plan review): no caller wires
414
+ * this in Slices 1–2.5. `wildo-application-lifecycle.service.ts`
415
+ * has no app-deletion ceremony to invoke it from. Method ships as a
416
+ * forward-compat seam for whichever future slice designs the
417
+ * app-deletion lifecycle.
418
+ *
419
+ * **Defensive validation** (Slice 4.A.2 / L4): empty
420
+ * `applicationNamespace` would call `DELETE /api/users/` and
421
+ * silently 404; we fail-loud instead so a bad caller surfaces
422
+ * immediately.
423
+ */
424
+ async revokeAppCredentials(context) {
425
+ if (!context.applicationNamespace) {
426
+ throw new Error(`RabbitmqSelfInfraProviderModule.revokeAppCredentials: ` +
427
+ `applicationNamespace is empty. ` +
428
+ `The orchestrator MUST supply a non-empty namespace.`);
429
+ }
430
+ const adminClient = context.adminClient;
431
+ const username = buildRabbitPrincipalUsername(context.applicationNamespace, context.principal);
432
+ await adminClient.deleteUser(username);
433
+ },
434
+ async mintPlatformServiceCredentials(context) {
435
+ const adminClient = context.adminClient;
436
+ const password = context.secretInputs[PLATFORM_SERVICE_PASSWORD_INPUT_KEY];
437
+ if (!password) {
438
+ throw new Error(`RabbitmqSelfInfraProviderModule.mintPlatformServiceCredentials: ` +
439
+ `secretInputs is missing '${PLATFORM_SERVICE_PASSWORD_INPUT_KEY}'. ` +
440
+ `The orchestrator MUST supply the platform-service password — modules are upsert-only.`);
441
+ }
442
+ // Username is the platform service identifier verbatim — preserves
443
+ // the legacy convention from
444
+ // `PlatformInfrastructureManagerService.provisionServiceCredentials`
445
+ // (line 478: `username: serviceName`).
446
+ const username = context.serviceName;
447
+ const permissionRegex = buildPlatformServiceRegex(context.serviceName);
448
+ // PUT /api/users — idempotent upsert.
449
+ await adminClient.putUser(username, password, []);
450
+ // Permissions: configure/read use the per-service regex; write
451
+ // widens to allow `amq.default` + empty string (broker-internal),
452
+ // matching the `upsertTenantUserPermissions` write-regex shape so
453
+ // post-Slice-2.5 broker permissions match pre-Slice-2.5.
454
+ const writeRegex = `(^$|amq\\.default|${permissionRegex})`;
455
+ await adminClient.setPermissions(DEFAULT_VHOST, username, {
456
+ configure: permissionRegex,
457
+ write: writeRegex,
458
+ read: permissionRegex,
459
+ });
460
+ // Topic permissions: same dot-escaped regex on amq.topic. The
461
+ // legacy two-caller divergence (one path stripped the `^` and
462
+ // produced `wildo\.platform\.<slug>\..*`; the other produced
463
+ // `wildo.platform.<slug>.*` with literal dots) is unified here on
464
+ // the dot-escaped variant — see module JSDoc behavior-parity note.
465
+ await adminClient.setTopicPermissions(DEFAULT_VHOST, username, {
466
+ exchange: TOPIC_EXCHANGE,
467
+ write: permissionRegex,
468
+ read: permissionRegex,
469
+ });
470
+ return { username };
471
+ },
472
+ };
473
+ //# sourceMappingURL=rabbitmq-self.infra.module.js.map
@@ -0,0 +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 emits `RABBITMQ_VHOST`\n * into apps-manager's process.env; the lifecycle service threads it\n * 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`**: declared and tested, but the integration\n * call site does NOT exist in `wildo-application-lifecycle.service.ts`\n * (no app-deletion ceremony today — F6 of plan review 2026-05-06).\n * Method ships as a forward-compat seam.\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** (F6 of 2026-05-06 plan review): no caller wires\n * this in Slices 1–2.5. `wildo-application-lifecycle.service.ts`\n * has no app-deletion ceremony to invoke it from. Method ships as a\n * forward-compat seam for whichever future slice designs the\n * app-deletion lifecycle.\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"]}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `RedisSelfInfraProviderModule` — second concrete `InfraProviderModule`
3
+ * (Slice 4.B.1 of `infra-provider-planner.md`, generalizing the
4
+ * Slice 2 `RabbitmqSelfInfraProviderModule` pattern).
5
+ *
6
+ * Implements the `REDIS_SELF` `InfraProvider` over the
7
+ * `IRedisAclClient` transport (Valkey/Redis ACL commands, exposed by
8
+ * `RedisConnectionManagerService` after the Slice 4.B.1 5C policy/
9
+ * transport split). Owns ALL per-app ACL policy — the per-app
10
+ * keyPattern, the channel-pattern fan-out family, the per-app
11
+ * username convention.
12
+ *
13
+ * **What this module reads from `AppCredentialMintingContext`**:
14
+ * - `appSlug` — used as the **key-pattern** prefix
15
+ * (`<appSlug>:*`). This MUST match the backend's effective
16
+ * `dataServices.redis.keyPrefix`, which is the slug-derived key
17
+ * namespace every backend write is rooted under (see
18
+ * `engine/saas-backend-lib/src/databases/services/redis.service.ts`
19
+ * `appPrefix` field — every `org:`/`global:` scope is composed with
20
+ * that prefix). Slot owner: the SaaS app's authored config.
21
+ * ACL invariant: `keyPrefix` MUST start with `<appSlug>:` so the
22
+ * `~<appSlug>:*` grant covers every key the backend writes.
23
+ * - `applicationNamespace` — used as the Redis ACL **username** AND
24
+ * as the channel-pattern prefix (`<ns>#*`, `<ns>-request#*`,
25
+ * `<ns>-response#*`). Mirrors the legacy username convention from
26
+ * `wildo-application-lifecycle.service.ts:441` (`redisUsername =
27
+ * ns`). Channels stay namespace-scoped because the Socket.IO Redis
28
+ * adapter (`engine/saas-backend-lib/src/requests/service/websocket
29
+ * .service.ts:1501`) uses the Redis username as its `key:` — the
30
+ * ACL channel pattern MUST match that.
31
+ * - `secretInputs['database.redis.password']` — orchestrator-supplied
32
+ * password (module is upsert-only, never generator).
33
+ * - `adminClient` — narrowed to `IRedisAclClient`.
34
+ *
35
+ * **Key vs channel pattern split** (2026-05-26 framework-correct fix):
36
+ * Pre-fix, this module derived BOTH key and channel patterns from
37
+ * `applicationNamespace` (`<ns>:*` for keys, `<ns>#*` for channels).
38
+ * That was correct pre-Slice-3.6, when the backend used the username
39
+ * to namespace keys too. Slice 3.6 moved key namespacing to
40
+ * `dataServices.redis.keyPrefix` (default + recommended value
41
+ * `<appSlug>:`) but the ACL provisioner was not updated — every per-app
42
+ * Redis read/write returned `NOPERM No permissions to access a key`
43
+ * because the ACL only granted `<ns>:*` while the backend wrote
44
+ * `<appSlug>:*`. Fix: key pattern now derives from `appSlug`, channel
45
+ * patterns continue to derive from the username/namespace (Socket.IO
46
+ * adapter still uses the username as its channel key, so channels
47
+ * stay correct unchanged).
48
+ *
49
+ * **What this module IGNORES from the context** (Slice 4.A.2 / M4
50
+ * forward-compat documentation):
51
+ * - `wildoApplicationId` — the keyPattern uses appSlug + the channel
52
+ * patterns use namespace; the application id is redundant for both
53
+ * (no globally-shared key namespace risk because slug/namespace
54
+ * already disambiguate by tenant and environment).
55
+ * - `applicationConfigInputs` — Redis_Self has no per-app authored
56
+ * topology (no equivalent of RabbitMQ's vhost). Future modules may
57
+ * read it for per-app db-index overrides etc.
58
+ *
59
+ * **Two audiences this module serves:**
60
+ *
61
+ * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:
62
+ * cluster-wide ACL admin creds (`REDIS_HOST`, `REDIS_PORT`,
63
+ * `REDIS_DATABASE` env + `REDIS_ADMIN_PASSWORD` secret env).
64
+ * Consumers: `apps-manager` (mints per-app ACL users via
65
+ * `mintAppCredentials`), `companion` (mirrors apps-manager admin
66
+ * role for local dev).
67
+ *
68
+ * 2. **Audience-3 (app-scoped, via `plan().appRuntime` +
69
+ * `mintAppCredentials`)**: per-customer-app Redis ACL user.
70
+ * Username = `applicationNamespace`; keyPattern disambiguates by
71
+ * the same namespace. Result threaded into
72
+ * `Wildo_Application_Secrets.database.redis.{username, password}`
73
+ * via `_merge_WildoApplicationSecrets`.
74
+ *
75
+ * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:
76
+ * - **Audience-2 (platform-service-scoped)**: Redis is currently NOT
77
+ * used for inter-platform-service messaging (RabbitMQ is the bus).
78
+ * `mintPlatformServiceCredentials` is therefore not implemented.
79
+ * When/if a platform service surfaces a Redis-scoped per-service
80
+ * credential need, this module gains the method.
81
+ *
82
+ * **`revokeAppCredentials`**: declared and tested, but the integration
83
+ * call site does NOT exist in `wildo-application-lifecycle.service.ts`
84
+ * (no app-deletion ceremony today — F6 of plan review 2026-05-06).
85
+ * Method ships as a forward-compat seam.
86
+ */
87
+ import { InfraProviderModule } from '@wildo-ai/platform-config-lib';
88
+ export declare const RedisSelfInfraProviderModule: InfraProviderModule;
89
+ //# sourceMappingURL=redis-self.infra.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/redis-self/redis-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAEH,OAAO,EAOL,mBAAmB,EAKpB,MAAM,+BAA+B,CAAC;AA+GvC,eAAO,MAAM,4BAA4B,EAAE,mBAiK1C,CAAC"}