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