@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.
- package/LICENSE +34 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.d.ts +63 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.d.ts.map +1 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.js +110 -0
- package/dist/esm/aws-s3/aws-s3.infra.module.js.map +1 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.d.ts +30 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.d.ts.map +1 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.js +61 -0
- package/dist/esm/clamav-self/clamav-self.infra.module.js.map +1 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts +26 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.d.ts.map +1 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js +54 -0
- package/dist/esm/gotenberg-self/gotenberg-self.infra.module.js.map +1 -0
- package/dist/esm/index.d.ts +88 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +125 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/minio-self/minio-self.infra.module.d.ts +64 -0
- package/dist/esm/minio-self/minio-self.infra.module.d.ts.map +1 -0
- package/dist/esm/minio-self/minio-self.infra.module.js +105 -0
- package/dist/esm/minio-self/minio-self.infra.module.js.map +1 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts +52 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.d.ts.map +1 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js +201 -0
- package/dist/esm/mongodb-atlas/mongodb-atlas.infra.module.js.map +1 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts +81 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.d.ts.map +1 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.js +311 -0
- package/dist/esm/mongodb-self/mongodb-self.infra.module.js.map +1 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.d.ts +70 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.d.ts.map +1 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.js +328 -0
- package/dist/esm/postgres-self/postgres-self.infra.module.js.map +1 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts +88 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.d.ts.map +1 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js +473 -0
- package/dist/esm/rabbitmq-self/rabbitmq-self.infra.module.js.map +1 -0
- package/dist/esm/redis-self/redis-self.infra.module.d.ts +89 -0
- package/dist/esm/redis-self/redis-self.infra.module.d.ts.map +1 -0
- package/dist/esm/redis-self/redis-self.infra.module.js +331 -0
- package/dist/esm/redis-self/redis-self.infra.module.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +52 -0
- package/src/aws-s3/__tests__/aws-s3.infra.module.test.ts +67 -0
- package/src/aws-s3/aws-s3.infra.module.ts +120 -0
- package/src/clamav-self/__tests__/clamav-self.infra.module.test.ts +65 -0
- package/src/clamav-self/clamav-self.infra.module.ts +72 -0
- package/src/gotenberg-self/__tests__/gotenberg-self.infra.module.test.ts +65 -0
- package/src/gotenberg-self/gotenberg-self.infra.module.ts +65 -0
- package/src/index.ts +133 -0
- package/src/minio-self/__tests__/minio-self.infra.module.test.ts +70 -0
- package/src/minio-self/minio-self.infra.module.ts +116 -0
- package/src/mongodb-atlas/__tests__/mongodb-atlas.infra.module.test.ts +198 -0
- package/src/mongodb-atlas/mongodb-atlas.infra.module.ts +236 -0
- package/src/mongodb-self/__tests__/mongodb-self.infra.module.test.ts +419 -0
- package/src/mongodb-self/mongodb-self.infra.module.ts +384 -0
- package/src/postgres-self/__tests__/postgres-self.infra.module.test.ts +344 -0
- package/src/postgres-self/postgres-self.infra.module.ts +377 -0
- package/src/rabbitmq-self/__tests__/rabbitmq-self.infra.module.test.ts +499 -0
- package/src/rabbitmq-self/rabbitmq-self.infra.module.ts +531 -0
- package/src/redis-self/__tests__/redis-self.infra.module.test.ts +341 -0
- 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
|
+
};
|