@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,331 @@
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 { InfraCapability, InfraPlatformAdminConsumer, InfraProvider, ApplicationRuntimePrincipalKind, buildMinionRedisKeyPrefix, } from '@wildo-ai/platform-config-lib';
88
+ /**
89
+ * Default Valkey port (matches `INFRA_DEFAULT_PORTS.VALKEY` in the
90
+ * shared platform constants — duplicated here as a literal because
91
+ * the planner output is consumed by the CLI which materializes it
92
+ * into `Wildo_Application_Config` blob paths; the value is stable
93
+ * and small).
94
+ */
95
+ const DEFAULT_VALKEY_PORT = 6379;
96
+ /**
97
+ * Default DB index for app workloads. Wildo applications use db `0`
98
+ * across the board; per-app DB-index isolation is intentionally NOT
99
+ * modeled — keyPattern + ACL is the isolation seam.
100
+ */
101
+ const DEFAULT_DB_INDEX = 0;
102
+ /**
103
+ * The `Wildo_Application_Secrets` JSON paths this module writes
104
+ * through `mintAppCredentials`. Declared once and reused as
105
+ * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of
106
+ * `secretBlobValues` in the result, so a refactor that renames a
107
+ * path fails loudly at one place instead of drifting silently.
108
+ */
109
+ const REDIS_USERNAME_PATH = 'database.redis.username';
110
+ const REDIS_PASSWORD_PATH = 'database.redis.password';
111
+ /**
112
+ * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.
113
+ *
114
+ * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip
115
+ * these from the application blob before delivering a principal's own material) AND spread into
116
+ * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery
117
+ * path would silently stop covering a path this module still mints.
118
+ */
119
+ const PER_PRINCIPAL_SECRET_BLOB_PATHS = [REDIS_USERNAME_PATH, REDIS_PASSWORD_PATH];
120
+ /**
121
+ * Compute the per-app Redis ACL username. Mirrors the existing
122
+ * convention at `wildo-application-lifecycle.service.ts:441`
123
+ * (`redisUsername = ns`) — username is the application namespace
124
+ * (`<appSlug>-<environmentId>`), NOT the raw slug. Centralized here
125
+ * so future changes touch one site.
126
+ */
127
+ function buildRedisUsername(applicationNamespace) {
128
+ return applicationNamespace;
129
+ }
130
+ /**
131
+ * Per-principal ACL username (Step 2 / D17).
132
+ *
133
+ * The application backend keeps `applicationNamespace` VERBATIM — its username
134
+ * must not move, or an existing deployment's ACL user is orphaned and a running
135
+ * backend loses Redis on the next sync.
136
+ *
137
+ * A named principal appends its kind and name. The separator is `-`: an ACL
138
+ * username is whitespace-delimited in `ACL SETUSER`, and the principal key's own
139
+ * `:` is already meaningful in Redis KEY space, so reusing it in a username
140
+ * blurs the two namespaces at a glance. Non-alphanumeric characters in a bucket
141
+ * key are collapsed — the key is a TypeScript object key and may legally contain
142
+ * `$` or `_`, while a username reaching the wire should stay conservative.
143
+ *
144
+ * Used by BOTH `mintAppCredentials` and `revokeAppCredentials`. That is the
145
+ * point of it being a function: a revoke that derived the username differently
146
+ * would delete the APPLICATION BACKEND's ACL user while believing it was
147
+ * removing a minion's.
148
+ */
149
+ function buildRedisPrincipalUsername(applicationNamespace, principal) {
150
+ if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {
151
+ return buildRedisUsername(applicationNamespace);
152
+ }
153
+ const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');
154
+ return `${applicationNamespace}-${principal.kind}-${sanitizedName}`;
155
+ }
156
+ /**
157
+ * Compute the per-app Redis ACL key pattern.
158
+ *
159
+ * The pattern derives from `appSlug` (NOT `applicationNamespace`)
160
+ * because the backend's effective key namespace is sourced from
161
+ * `dataServices.redis.keyPrefix` (Slice 3.6 — see
162
+ * `engine/saas-backend-lib/src/databases/services/redis.service.ts`
163
+ * `appPrefix` field JSDoc) whose default + recommended value is
164
+ * `<appSlug>:`. The ACL `~<appSlug>:*` rule must match the key shape
165
+ * the backend writes; ANY namespace-derived ACL (the pre-2026-05-26
166
+ * shape) would only grant `<ns>:*` and `NOPERM` every backend access.
167
+ *
168
+ * The channel patterns are computed separately from the username
169
+ * (== applicationNamespace) inside `IRedisAclClient.upsertUser`,
170
+ * because the Socket.IO Redis adapter uses the username as its
171
+ * channel key (`engine/saas-backend-lib/src/requests/service/
172
+ * websocket.service.ts:1501`). The two namespaces are split:
173
+ * - key namespace = `<appSlug>:` (app-authored
174
+ * `dataServices.redis.keyPrefix`)
175
+ * - channel namespace = `<applicationNamespace>#` (Socket.IO adapter
176
+ * key, == redisUsername)
177
+ *
178
+ * App-author contract (validated by `validateKeyPrefixContract`
179
+ * downstream — see `databases.backend.schemas.ts`): if the SaaS app
180
+ * overrides the default `keyPrefix`, the override MUST still start
181
+ * with `<appSlug>:` so the `~<appSlug>:*` grant remains a superset
182
+ * of the keys the backend writes.
183
+ */
184
+ function buildRedisKeyPattern(appSlug) {
185
+ return `${appSlug}:*`;
186
+ }
187
+ export const RedisSelfInfraProviderModule = {
188
+ provider: InfraProvider.REDIS_SELF,
189
+ supportedCapabilities: [InfraCapability.CACHE_MANAGER],
190
+ perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,
191
+ plan() {
192
+ return {
193
+ dockerComposeServices: ['valkey'],
194
+ k8sResources: ['templates/infra/k8s/internal/valkey/deployment.yaml.hbs'],
195
+ platformAdmin: {
196
+ envVars: {
197
+ REDIS_HOST: 'valkey',
198
+ REDIS_PORT: String(DEFAULT_VALKEY_PORT),
199
+ REDIS_DATABASE: String(DEFAULT_DB_INDEX),
200
+ },
201
+ // ACL admin password is the only secret env. The legacy
202
+ // `RedisConnectionManagerService.initializeAclAdmin` reads it
203
+ // from `REDIS_ADMIN_PASSWORD`; the planner emits the key, the
204
+ // CLI's `env-vars-builder` writes the value at sync time.
205
+ secretEnvVars: ['REDIS_ADMIN_PASSWORD'],
206
+ // Audience-1 only (ACL-admin access). No audience-2
207
+ // platform-service-scoped Redis use case today (D-4).
208
+ consumers: [
209
+ InfraPlatformAdminConsumer.APPS_MANAGER,
210
+ InfraPlatformAdminConsumer.COMPANION,
211
+ ],
212
+ },
213
+ appRuntime: {
214
+ // Static topology — same for every app in this env.
215
+ // Per-app values (username/password) flow through
216
+ // `perAppSecretBlobPaths` below.
217
+ perAppConfigBlobPaths: {
218
+ 'database.redis.host': 'valkey',
219
+ 'database.redis.port': DEFAULT_VALKEY_PORT,
220
+ 'database.redis.db': DEFAULT_DB_INDEX,
221
+ },
222
+ perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],
223
+ },
224
+ };
225
+ },
226
+ async mintAppCredentials(context) {
227
+ const adminClient = context.adminClient;
228
+ const password = context.secretInputs[REDIS_PASSWORD_PATH];
229
+ if (!password) {
230
+ throw new Error(`RedisSelfInfraProviderModule.mintAppCredentials: ` +
231
+ `secretInputs is missing '${REDIS_PASSWORD_PATH}'. ` +
232
+ `The orchestrator MUST supply the Redis password — modules are upsert-only.`);
233
+ }
234
+ if (!context.applicationNamespace) {
235
+ throw new Error(`RedisSelfInfraProviderModule.mintAppCredentials: ` +
236
+ `applicationNamespace is empty. ` +
237
+ `The orchestrator MUST supply a non-empty namespace.`);
238
+ }
239
+ if (!context.appSlug) {
240
+ throw new Error(`RedisSelfInfraProviderModule.mintAppCredentials: ` +
241
+ `appSlug is empty. ` +
242
+ `The orchestrator MUST supply a non-empty appSlug — the ACL key ` +
243
+ `pattern derives from it (must match the backend's effective ` +
244
+ `\`dataServices.redis.keyPrefix\`, default \`<appSlug>:\`).`);
245
+ }
246
+ const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);
247
+ // Key pattern derives from appSlug (matches backend `keyPrefix`),
248
+ // channel patterns derive from username/namespace inside upsertUser
249
+ // (matches Socket.IO adapter's `key:` config). See module JSDoc for
250
+ // the full key-vs-channel-namespace split rationale.
251
+ // A MINION is confined to its own namespace (D20); the application backend
252
+ // keeps the app-wide pattern.
253
+ //
254
+ // This and the minion's `REDIS_KEY_PREFIX` are ONE COUPLED PAIR — a
255
+ // mismatch DENIES every operation the minion performs, and the error names
256
+ // an ACL rather than the mismatch. Both sides derive from
257
+ // `buildMinionRedisKeyPrefix`, so they cannot drift.
258
+ const keyPattern = context.principal?.kind === ApplicationRuntimePrincipalKind.MINION && context.principal.name
259
+ ? `${buildMinionRedisKeyPrefix(context.appSlug, context.principal.name)}*`
260
+ : buildRedisKeyPattern(context.appSlug);
261
+ // ACL SETUSER — idempotent upsert. Replaces password + key
262
+ // patterns + channel patterns + category allow/deny on conflict.
263
+ //
264
+ // **Atomicity guard** (Slice 6 audit / F-2, 2026-05-10): the
265
+ // sequential `await upsertUser(); await persistAcl();` shape
266
+ // intentionally relies on JavaScript exception propagation —
267
+ // when `upsertUser` throws, the thrown error bubbles up the
268
+ // async stack BEFORE `persistAcl` is reached, leaving the ACL
269
+ // list on disk unchanged. The
270
+ // `redis-self.infra.module.test.ts` "atomicity guard" case pins
271
+ // this behavior. **DO NOT wrap this pair in a try/catch that
272
+ // swallows the upsert error** — that would silently persist a
273
+ // partially-applied state. Any future refactor that introduces
274
+ // a try-block here MUST re-throw on the upsert error AND must
275
+ // gate `persistAcl` on the upsert's success.
276
+ await adminClient.upsertUser({
277
+ username,
278
+ password,
279
+ keyPattern,
280
+ });
281
+ // Persist ACL list to disk so Valkey restarts don't lose tenant
282
+ // users. Mirrors the legacy
283
+ // `wildo-application-lifecycle.service.ts:540` pairing.
284
+ await adminClient.persistAcl();
285
+ return {
286
+ secretBlobValues: {
287
+ [REDIS_USERNAME_PATH]: username,
288
+ // Echo the orchestrator-supplied password — proves the module
289
+ // is upsert-only, not generator. The orchestrator persists
290
+ // this exact value into `Wildo_Application_Secrets`.
291
+ [REDIS_PASSWORD_PATH]: password,
292
+ },
293
+ };
294
+ },
295
+ /**
296
+ * Tear down the per-app Redis ACL user. Valkey ACL users are
297
+ * global (no vhost equivalent), so `deleteUser` removes the user
298
+ * (and its key/channel access) atomically.
299
+ *
300
+ * **Caller status** (F6 of 2026-05-06 plan review): no caller
301
+ * wires this in Slices 1–4.B.1.
302
+ * `wildo-application-lifecycle.service.ts` has no app-deletion
303
+ * ceremony to invoke it from. Method ships as a forward-compat
304
+ * seam for whichever future slice designs the app-deletion
305
+ * lifecycle.
306
+ *
307
+ * **Defensive validation** (Slice 4.A.2 / L4 pattern): empty
308
+ * `applicationNamespace` would call `ACL DELUSER` with an empty
309
+ * username (Valkey would either error or silently no-op,
310
+ * depending on version); we fail-loud instead so a bad caller
311
+ * surfaces immediately.
312
+ */
313
+ async revokeAppCredentials(context) {
314
+ if (!context.applicationNamespace) {
315
+ throw new Error(`RedisSelfInfraProviderModule.revokeAppCredentials: ` +
316
+ `applicationNamespace is empty. ` +
317
+ `The orchestrator MUST supply a non-empty namespace.`);
318
+ }
319
+ const adminClient = context.adminClient;
320
+ const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);
321
+ await adminClient.deleteUser(username);
322
+ // Persist after deletion so the ACL list on disk stays in sync
323
+ // with the in-memory state. Mirrors mintAppCredentials' pairing.
324
+ await adminClient.persistAcl();
325
+ },
326
+ // No `mintPlatformServiceCredentials` (D-4 of Slice 4.B —
327
+ // audience-2 not applicable to Redis_Self today). When a real
328
+ // need surfaces, add the method here following the
329
+ // `RabbitmqSelfInfraProviderModule` shape.
330
+ };
331
+ //# sourceMappingURL=redis-self.infra.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-self.infra.module.js","sourceRoot":"","sources":["../../../../src/redis-self/redis-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `RedisSelfInfraProviderModule` — second concrete `InfraProviderModule`\n * (Slice 4.B.1 of `infra-provider-planner.md`, generalizing the\n * Slice 2 `RabbitmqSelfInfraProviderModule` pattern).\n *\n * Implements the `REDIS_SELF` `InfraProvider` over the\n * `IRedisAclClient` transport (Valkey/Redis ACL commands, exposed by\n * `RedisConnectionManagerService` after the Slice 4.B.1 5C policy/\n * transport split). Owns ALL per-app ACL policy — the per-app\n * keyPattern, the channel-pattern fan-out family, the per-app\n * username convention.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `appSlug` — used as the **key-pattern** prefix\n * (`<appSlug>:*`). This MUST match the backend's effective\n * `dataServices.redis.keyPrefix`, which is the slug-derived key\n * namespace every backend write is rooted under (see\n * `engine/saas-backend-lib/src/databases/services/redis.service.ts`\n * `appPrefix` field — every `org:`/`global:` scope is composed with\n * that prefix). Slot owner: the SaaS app's authored config.\n * ACL invariant: `keyPrefix` MUST start with `<appSlug>:` so the\n * `~<appSlug>:*` grant covers every key the backend writes.\n * - `applicationNamespace` — used as the Redis ACL **username** AND\n * as the channel-pattern prefix (`<ns>#*`, `<ns>-request#*`,\n * `<ns>-response#*`). Mirrors the legacy username convention from\n * `wildo-application-lifecycle.service.ts:441` (`redisUsername =\n * ns`). Channels stay namespace-scoped because the Socket.IO Redis\n * adapter (`engine/saas-backend-lib/src/requests/service/websocket\n * .service.ts:1501`) uses the Redis username as its `key:` — the\n * ACL channel pattern MUST match that.\n * - `secretInputs['database.redis.password']` — orchestrator-supplied\n * password (module is upsert-only, never generator).\n * - `adminClient` — narrowed to `IRedisAclClient`.\n *\n * **Key vs channel pattern split** (2026-05-26 framework-correct fix):\n * Pre-fix, this module derived BOTH key and channel patterns from\n * `applicationNamespace` (`<ns>:*` for keys, `<ns>#*` for channels).\n * That was correct pre-Slice-3.6, when the backend used the username\n * to namespace keys too. Slice 3.6 moved key namespacing to\n * `dataServices.redis.keyPrefix` (default + recommended value\n * `<appSlug>:`) but the ACL provisioner was not updated — every per-app\n * Redis read/write returned `NOPERM No permissions to access a key`\n * because the ACL only granted `<ns>:*` while the backend wrote\n * `<appSlug>:*`. Fix: key pattern now derives from `appSlug`, channel\n * patterns continue to derive from the username/namespace (Socket.IO\n * adapter still uses the username as its channel key, so channels\n * stay correct unchanged).\n *\n * **What this module IGNORES from the context** (Slice 4.A.2 / M4\n * forward-compat documentation):\n * - `wildoApplicationId` — the keyPattern uses appSlug + the channel\n * patterns use namespace; the application id is redundant for both\n * (no globally-shared key namespace risk because slug/namespace\n * already disambiguate by tenant and environment).\n * - `applicationConfigInputs` — Redis_Self has no per-app authored\n * topology (no equivalent of RabbitMQ's vhost). Future modules may\n * read it for per-app db-index overrides etc.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:\n * cluster-wide ACL admin creds (`REDIS_HOST`, `REDIS_PORT`,\n * `REDIS_DATABASE` env + `REDIS_ADMIN_PASSWORD` secret env).\n * Consumers: `apps-manager` (mints per-app ACL users via\n * `mintAppCredentials`), `companion` (mirrors apps-manager admin\n * role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `plan().appRuntime` +\n * `mintAppCredentials`)**: per-customer-app Redis ACL user.\n * Username = `applicationNamespace`; keyPattern disambiguates by\n * the same namespace. Result threaded into\n * `Wildo_Application_Secrets.database.redis.{username, password}`\n * via `_merge_WildoApplicationSecrets`.\n *\n * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:\n * - **Audience-2 (platform-service-scoped)**: Redis is currently NOT\n * used for inter-platform-service messaging (RabbitMQ is the bus).\n * `mintPlatformServiceCredentials` is therefore not implemented.\n * When/if a platform service surfaces a Redis-scoped per-service\n * credential need, this module gains the method.\n *\n * **`revokeAppCredentials`**: 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 IRedisAclClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipalKind,\n buildMinionRedisKeyPrefix,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Default Valkey port (matches `INFRA_DEFAULT_PORTS.VALKEY` in the\n * shared platform constants — duplicated here as a literal because\n * the planner output is consumed by the CLI which materializes it\n * into `Wildo_Application_Config` blob paths; the value is stable\n * and small).\n */\nconst DEFAULT_VALKEY_PORT = 6379;\n\n/**\n * Default DB index for app workloads. Wildo applications use db `0`\n * across the board; per-app DB-index isolation is intentionally NOT\n * modeled — keyPattern + ACL is the isolation seam.\n */\nconst DEFAULT_DB_INDEX = 0;\n\n/**\n * The `Wildo_Application_Secrets` JSON paths this module writes\n * through `mintAppCredentials`. Declared once and reused as\n * `appRuntime.perAppSecretBlobPaths` in `plan()` AND as the keys of\n * `secretBlobValues` in the result, so a refactor that renames a\n * path fails loudly at one place instead of drifting silently.\n */\nconst REDIS_USERNAME_PATH = 'database.redis.username';\nconst REDIS_PASSWORD_PATH = 'database.redis.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [REDIS_USERNAME_PATH, REDIS_PASSWORD_PATH];\n\n\n/**\n * Compute the per-app Redis ACL username. Mirrors the existing\n * convention at `wildo-application-lifecycle.service.ts:441`\n * (`redisUsername = ns`) — username is the application namespace\n * (`<appSlug>-<environmentId>`), NOT the raw slug. Centralized here\n * so future changes touch one site.\n */\nfunction buildRedisUsername(applicationNamespace: string): string {\n return applicationNamespace;\n}\n\n/**\n * Per-principal ACL username (Step 2 / D17).\n *\n * The application backend keeps `applicationNamespace` VERBATIM — its username\n * must not move, or an existing deployment's ACL user is orphaned and a running\n * backend loses Redis on the next sync.\n *\n * A named principal appends its kind and name. The separator is `-`: an ACL\n * username is whitespace-delimited in `ACL SETUSER`, and the principal key's own\n * `:` is already meaningful in Redis KEY space, so reusing it in a username\n * blurs the two namespaces at a glance. Non-alphanumeric characters in a bucket\n * key are collapsed — the key is a TypeScript object key and may legally contain\n * `$` or `_`, while a username reaching the wire should stay conservative.\n *\n * Used by BOTH `mintAppCredentials` and `revokeAppCredentials`. That is the\n * point of it being a function: a revoke that derived the username differently\n * would delete the APPLICATION BACKEND's ACL user while believing it was\n * removing a minion's.\n */\nfunction buildRedisPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildRedisUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n return `${applicationNamespace}-${principal.kind}-${sanitizedName}`;\n}\n\n/**\n * Compute the per-app Redis ACL key pattern.\n *\n * The pattern derives from `appSlug` (NOT `applicationNamespace`)\n * because the backend's effective key namespace is sourced from\n * `dataServices.redis.keyPrefix` (Slice 3.6 — see\n * `engine/saas-backend-lib/src/databases/services/redis.service.ts`\n * `appPrefix` field JSDoc) whose default + recommended value is\n * `<appSlug>:`. The ACL `~<appSlug>:*` rule must match the key shape\n * the backend writes; ANY namespace-derived ACL (the pre-2026-05-26\n * shape) would only grant `<ns>:*` and `NOPERM` every backend access.\n *\n * The channel patterns are computed separately from the username\n * (== applicationNamespace) inside `IRedisAclClient.upsertUser`,\n * because the Socket.IO Redis adapter uses the username as its\n * channel key (`engine/saas-backend-lib/src/requests/service/\n * websocket.service.ts:1501`). The two namespaces are split:\n * - key namespace = `<appSlug>:` (app-authored\n * `dataServices.redis.keyPrefix`)\n * - channel namespace = `<applicationNamespace>#` (Socket.IO adapter\n * key, == redisUsername)\n *\n * App-author contract (validated by `validateKeyPrefixContract`\n * downstream — see `databases.backend.schemas.ts`): if the SaaS app\n * overrides the default `keyPrefix`, the override MUST still start\n * with `<appSlug>:` so the `~<appSlug>:*` grant remains a superset\n * of the keys the backend writes.\n */\nfunction buildRedisKeyPattern(appSlug: string): string {\n return `${appSlug}:*`;\n}\n\nexport const RedisSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.REDIS_SELF,\n supportedCapabilities: [InfraCapability.CACHE_MANAGER],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['valkey'],\n k8sResources: ['templates/infra/k8s/internal/valkey/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {\n REDIS_HOST: 'valkey',\n REDIS_PORT: String(DEFAULT_VALKEY_PORT),\n REDIS_DATABASE: String(DEFAULT_DB_INDEX),\n },\n // ACL admin password is the only secret env. The legacy\n // `RedisConnectionManagerService.initializeAclAdmin` reads it\n // from `REDIS_ADMIN_PASSWORD`; the planner emits the key, the\n // CLI's `env-vars-builder` writes the value at sync time.\n secretEnvVars: ['REDIS_ADMIN_PASSWORD'],\n // Audience-1 only (ACL-admin access). No audience-2\n // platform-service-scoped Redis use case today (D-4).\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Static topology — same for every app in this env.\n // Per-app values (username/password) flow through\n // `perAppSecretBlobPaths` below.\n perAppConfigBlobPaths: {\n 'database.redis.host': 'valkey',\n 'database.redis.port': DEFAULT_VALKEY_PORT,\n 'database.redis.db': DEFAULT_DB_INDEX,\n },\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IRedisAclClient;\n const password = context.secretInputs[REDIS_PASSWORD_PATH];\n if (!password) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${REDIS_PASSWORD_PATH}'. ` +\n `The orchestrator MUST supply the Redis password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n if (!context.appSlug) {\n throw new Error(\n `RedisSelfInfraProviderModule.mintAppCredentials: ` +\n `appSlug is empty. ` +\n `The orchestrator MUST supply a non-empty appSlug — the ACL key ` +\n `pattern derives from it (must match the backend's effective ` +\n `\\`dataServices.redis.keyPrefix\\`, default \\`<appSlug>:\\`).`,\n );\n }\n\n const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);\n // Key pattern derives from appSlug (matches backend `keyPrefix`),\n // channel patterns derive from username/namespace inside upsertUser\n // (matches Socket.IO adapter's `key:` config). See module JSDoc for\n // the full key-vs-channel-namespace split rationale.\n // A MINION is confined to its own namespace (D20); the application backend\n // keeps the app-wide pattern.\n //\n // This and the minion's `REDIS_KEY_PREFIX` are ONE COUPLED PAIR — a\n // mismatch DENIES every operation the minion performs, and the error names\n // an ACL rather than the mismatch. Both sides derive from\n // `buildMinionRedisKeyPrefix`, so they cannot drift.\n const keyPattern = context.principal?.kind === ApplicationRuntimePrincipalKind.MINION && context.principal.name\n ? `${buildMinionRedisKeyPrefix(context.appSlug, context.principal.name)}*`\n : buildRedisKeyPattern(context.appSlug);\n\n // ACL SETUSER — idempotent upsert. Replaces password + key\n // patterns + channel patterns + category allow/deny on conflict.\n //\n // **Atomicity guard** (Slice 6 audit / F-2, 2026-05-10): the\n // sequential `await upsertUser(); await persistAcl();` shape\n // intentionally relies on JavaScript exception propagation —\n // when `upsertUser` throws, the thrown error bubbles up the\n // async stack BEFORE `persistAcl` is reached, leaving the ACL\n // list on disk unchanged. The\n // `redis-self.infra.module.test.ts` \"atomicity guard\" case pins\n // this behavior. **DO NOT wrap this pair in a try/catch that\n // swallows the upsert error** — that would silently persist a\n // partially-applied state. Any future refactor that introduces\n // a try-block here MUST re-throw on the upsert error AND must\n // gate `persistAcl` on the upsert's success.\n await adminClient.upsertUser({\n username,\n password,\n keyPattern,\n });\n\n // Persist ACL list to disk so Valkey restarts don't lose tenant\n // users. Mirrors the legacy\n // `wildo-application-lifecycle.service.ts:540` pairing.\n await adminClient.persistAcl();\n\n return {\n secretBlobValues: {\n [REDIS_USERNAME_PATH]: username,\n // Echo the orchestrator-supplied password — proves the module\n // is upsert-only, not generator. The orchestrator persists\n // this exact value into `Wildo_Application_Secrets`.\n [REDIS_PASSWORD_PATH]: password,\n },\n };\n },\n\n /**\n * Tear down the per-app Redis ACL user. Valkey ACL users are\n * global (no vhost equivalent), so `deleteUser` removes the user\n * (and its key/channel access) atomically.\n *\n * **Caller status** (F6 of 2026-05-06 plan review): no caller\n * wires this in Slices 1–4.B.1.\n * `wildo-application-lifecycle.service.ts` has no app-deletion\n * ceremony to invoke it from. Method ships as a forward-compat\n * seam for whichever future slice designs the app-deletion\n * lifecycle.\n *\n * **Defensive validation** (Slice 4.A.2 / L4 pattern): empty\n * `applicationNamespace` would call `ACL DELUSER` with an empty\n * username (Valkey would either error or silently no-op,\n * depending on version); we fail-loud instead so a bad caller\n * surfaces immediately.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `RedisSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminClient = context.adminClient as IRedisAclClient;\n const username = buildRedisPrincipalUsername(context.applicationNamespace, context.principal);\n await adminClient.deleteUser(username);\n // Persist after deletion so the ACL list on disk stays in sync\n // with the in-memory state. Mirrors mintAppCredentials' pairing.\n await adminClient.persistAcl();\n },\n\n // No `mintPlatformServiceCredentials` (D-4 of Slice 4.B —\n // audience-2 not applicable to Redis_Self today). When a real\n // need surfaces, add the method here following the\n // `RabbitmqSelfInfraProviderModule` shape.\n};\n"]}