@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,341 @@
1
+ /**
2
+ * Tests for `RedisSelfInfraProviderModule` (Slice 4.B.1 of
3
+ * `infra-provider-planner.md`).
4
+ *
5
+ * Goals:
6
+ * - Pin the `plan()` shape so a future regression that drops e.g.
7
+ * `REDIS_ADMIN_PASSWORD` from `secretEnvVars` fails loud.
8
+ * - Pin the orchestrator-pattern contract: `mintAppCredentials`
9
+ * accepts the password via `secretInputs` and echoes it back —
10
+ * proves the module is upsert-only, not generator.
11
+ * - Pin the username + keyPattern conventions against the existing
12
+ * `wildo-application-lifecycle.service.ts:441, 536` conventions so
13
+ * a future code change here surfaces as a diff in this test, not
14
+ * as a silent ACL drift.
15
+ * - Pin the `revokeAppCredentials` upsert-with-persist pairing.
16
+ * - Pin defensive validation of empty `applicationNamespace` and
17
+ * missing `secretInputs` keys.
18
+ */
19
+
20
+ import { describe, expect, it, vi } from 'vitest';
21
+ import {
22
+ AppCredentialMintingContext,
23
+ IRedisAclClient,
24
+ InfraCapability,
25
+ InfraPlatformAdminConsumer,
26
+ InfraProvider,
27
+ ApplicationRuntimePrincipalKind,
28
+ buildMinionRedisKeyPrefix,
29
+ } from '@wildo-ai/platform-config-lib';
30
+ import { RedisSelfInfraProviderModule } from '../redis-self.infra.module';
31
+
32
+ function makeAdminClientStub(): IRedisAclClient & {
33
+ upsertUserSpy: ReturnType<typeof vi.fn>;
34
+ deleteUserSpy: ReturnType<typeof vi.fn>;
35
+ persistAclSpy: ReturnType<typeof vi.fn>;
36
+ } {
37
+ const upsertUserSpy = vi.fn().mockResolvedValue(undefined);
38
+ const deleteUserSpy = vi.fn().mockResolvedValue(undefined);
39
+ const persistAclSpy = vi.fn().mockResolvedValue(undefined);
40
+ return {
41
+ upsertUser: upsertUserSpy,
42
+ deleteUser: deleteUserSpy,
43
+ persistAcl: persistAclSpy,
44
+ upsertUserSpy,
45
+ deleteUserSpy,
46
+ persistAclSpy,
47
+ };
48
+ }
49
+
50
+ function makeMintContext(
51
+ adminClient: IRedisAclClient,
52
+ overrides: Partial<AppCredentialMintingContext> = {},
53
+ ): AppCredentialMintingContext {
54
+ return {
55
+ appSlug: 'wonder-todos',
56
+ applicationNamespace: 'wonder-todos-local',
57
+ wildoApplicationId: '60d21bdf1c4ae5cda1b2c3d4',
58
+ adminClient,
59
+ secretInputs: {
60
+ 'database.redis.password': 'orchestrator-supplied-password',
61
+ },
62
+ ...overrides,
63
+ };
64
+ }
65
+
66
+ describe('RedisSelfInfraProviderModule.plan()', () => {
67
+ const contribution = RedisSelfInfraProviderModule.plan({
68
+ environment: 'local',
69
+ runtime: 'docker-compose',
70
+ backingServiceKey: 'redis',
71
+ config: {},
72
+ });
73
+
74
+ it('declares the right provider + capability triple', () => {
75
+ expect(RedisSelfInfraProviderModule.provider).toBe(InfraProvider.REDIS_SELF);
76
+ expect(RedisSelfInfraProviderModule.supportedCapabilities).toEqual([
77
+ InfraCapability.CACHE_MANAGER,
78
+ ]);
79
+ });
80
+
81
+ it('contributes the valkey compose service + K8s manifest', () => {
82
+ expect(contribution.dockerComposeServices).toEqual(['valkey']);
83
+ expect(contribution.k8sResources).toEqual([
84
+ 'templates/infra/k8s/internal/valkey/deployment.yaml.hbs',
85
+ ]);
86
+ });
87
+
88
+ it('declares platformAdmin (audience-1) for apps-manager + companion only', () => {
89
+ expect(contribution.platformAdmin?.envVars).toEqual({
90
+ REDIS_HOST: 'valkey',
91
+ REDIS_PORT: '6379',
92
+ REDIS_DATABASE: '0',
93
+ });
94
+ expect(contribution.platformAdmin?.secretEnvVars).toEqual([
95
+ 'REDIS_ADMIN_PASSWORD',
96
+ ]);
97
+ // Audience-1 only — Redis has no audience-2 platform-service-scoped
98
+ // use case today (D-4 of Slice 4.B locked decisions).
99
+ expect(contribution.platformAdmin?.consumers).toEqual([
100
+ InfraPlatformAdminConsumer.APPS_MANAGER,
101
+ InfraPlatformAdminConsumer.COMPANION,
102
+ ]);
103
+ });
104
+
105
+ it('declares appRuntime blob paths for the customer-app pod', () => {
106
+ expect(contribution.appRuntime?.perAppConfigBlobPaths).toEqual({
107
+ 'database.redis.host': 'valkey',
108
+ 'database.redis.port': 6379,
109
+ 'database.redis.db': 0,
110
+ });
111
+ expect(contribution.appRuntime?.perAppSecretBlobPaths).toEqual([
112
+ 'database.redis.username',
113
+ 'database.redis.password',
114
+ ]);
115
+ });
116
+ });
117
+
118
+ describe('RedisSelfInfraProviderModule.mintAppCredentials', () => {
119
+ it('upserts an ACL user with namespace-derived username and slug-derived keyPattern, then persists the ACL list', async () => {
120
+ // ACL key/channel namespace split (2026-05-26 fix):
121
+ // username → applicationNamespace ('wonder-todos-local')
122
+ // keyPattern → appSlug + ':*' ('wonder-todos:*')
123
+ // channel pattern → derived inside upsertUser from the username
124
+ // The keyPattern uses appSlug because the backend's effective
125
+ // `dataServices.redis.keyPrefix` defaults to `<appSlug>:` and any
126
+ // authored override must start with that prefix (validated in
127
+ // `RedisService.connect()`). Pre-fix, both used namespace, which
128
+ // produced `NOPERM` for every per-app key access.
129
+ const adminClient = makeAdminClientStub();
130
+ const result = await RedisSelfInfraProviderModule.mintAppCredentials!(
131
+ makeMintContext(adminClient),
132
+ );
133
+
134
+ expect(adminClient.upsertUserSpy).toHaveBeenCalledTimes(1);
135
+ expect(adminClient.upsertUserSpy).toHaveBeenCalledWith({
136
+ username: 'wonder-todos-local',
137
+ password: 'orchestrator-supplied-password',
138
+ keyPattern: 'wonder-todos:*',
139
+ });
140
+ expect(adminClient.persistAclSpy).toHaveBeenCalledTimes(1);
141
+ // upsert MUST come before persist (legacy pairing parity).
142
+ expect(adminClient.upsertUserSpy.mock.invocationCallOrder[0])
143
+ .toBeLessThan(adminClient.persistAclSpy.mock.invocationCallOrder[0]);
144
+
145
+ expect(result).toEqual({
146
+ secretBlobValues: {
147
+ 'database.redis.username': 'wonder-todos-local',
148
+ // Echo of the input password — proves upsert-only, not generator.
149
+ 'database.redis.password': 'orchestrator-supplied-password',
150
+ },
151
+ });
152
+ });
153
+
154
+ it('fails loud when secretInputs is missing the password', async () => {
155
+ const adminClient = makeAdminClientStub();
156
+ await expect(
157
+ RedisSelfInfraProviderModule.mintAppCredentials!(
158
+ makeMintContext(adminClient, { secretInputs: {} }),
159
+ ),
160
+ ).rejects.toThrow(/missing 'database\.redis\.password'/);
161
+ expect(adminClient.upsertUserSpy).not.toHaveBeenCalled();
162
+ expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it('fails loud when applicationNamespace is empty', async () => {
166
+ const adminClient = makeAdminClientStub();
167
+ await expect(
168
+ RedisSelfInfraProviderModule.mintAppCredentials!(
169
+ makeMintContext(adminClient, { applicationNamespace: '' }),
170
+ ),
171
+ ).rejects.toThrow(/applicationNamespace is empty/);
172
+ expect(adminClient.upsertUserSpy).not.toHaveBeenCalled();
173
+ expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it('fails loud when appSlug is empty (the ACL key pattern derives from it)', async () => {
177
+ // ACL key namespace must be slug-rooted; an empty slug would mint
178
+ // `~:*` which is a near-global grant — guard against it explicitly
179
+ // instead of letting Valkey accept the malformed pattern.
180
+ const adminClient = makeAdminClientStub();
181
+ await expect(
182
+ RedisSelfInfraProviderModule.mintAppCredentials!(
183
+ makeMintContext(adminClient, { appSlug: '' }),
184
+ ),
185
+ ).rejects.toThrow(/appSlug is empty/);
186
+ expect(adminClient.upsertUserSpy).not.toHaveBeenCalled();
187
+ expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it('keyPattern derives from appSlug independently of applicationNamespace (key/channel split)', async () => {
191
+ // Confirms the key vs channel namespace split is honored: a slug
192
+ // that differs from the namespace stem produces the slug-rooted
193
+ // key pattern, NOT the namespace-rooted one. This is the contract
194
+ // that the backend's `<appSlug>:` keyPrefix relies on.
195
+ const adminClient = makeAdminClientStub();
196
+ await RedisSelfInfraProviderModule.mintAppCredentials!(
197
+ makeMintContext(adminClient, {
198
+ appSlug: 'acme-suite',
199
+ applicationNamespace: 'acme-suite-staging',
200
+ }),
201
+ );
202
+ expect(adminClient.upsertUserSpy).toHaveBeenCalledWith({
203
+ username: 'acme-suite-staging',
204
+ password: 'orchestrator-supplied-password',
205
+ keyPattern: 'acme-suite:*',
206
+ });
207
+ });
208
+
209
+ it('does NOT call persistAcl when upsertUser throws (atomicity guard)', async () => {
210
+ const adminClient = makeAdminClientStub();
211
+ adminClient.upsertUserSpy.mockRejectedValue(new Error('SETUSER refused'));
212
+ await expect(
213
+ RedisSelfInfraProviderModule.mintAppCredentials!(makeMintContext(adminClient)),
214
+ ).rejects.toThrow(/SETUSER refused/);
215
+ expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
216
+ });
217
+ });
218
+
219
+ describe('RedisSelfInfraProviderModule.revokeAppCredentials', () => {
220
+ it('deletes the per-app ACL user and persists the list', async () => {
221
+ const adminClient = makeAdminClientStub();
222
+ await RedisSelfInfraProviderModule.revokeAppCredentials!(
223
+ makeMintContext(adminClient),
224
+ );
225
+
226
+ expect(adminClient.deleteUserSpy).toHaveBeenCalledTimes(1);
227
+ expect(adminClient.deleteUserSpy).toHaveBeenCalledWith('wonder-todos-local');
228
+ expect(adminClient.persistAclSpy).toHaveBeenCalledTimes(1);
229
+ // delete MUST come before persist.
230
+ expect(adminClient.deleteUserSpy.mock.invocationCallOrder[0])
231
+ .toBeLessThan(adminClient.persistAclSpy.mock.invocationCallOrder[0]);
232
+ });
233
+
234
+ it('fails loud when applicationNamespace is empty (does not call DELUSER on empty)', async () => {
235
+ const adminClient = makeAdminClientStub();
236
+ await expect(
237
+ RedisSelfInfraProviderModule.revokeAppCredentials!(
238
+ makeMintContext(adminClient, { applicationNamespace: '' }),
239
+ ),
240
+ ).rejects.toThrow(/applicationNamespace is empty/);
241
+ expect(adminClient.deleteUserSpy).not.toHaveBeenCalled();
242
+ expect(adminClient.persistAclSpy).not.toHaveBeenCalled();
243
+ });
244
+ });
245
+
246
+ describe('RedisSelfInfraProviderModule per-principal ACL users (Step 2 / D17)', () => {
247
+ const MINION = { kind: ApplicationRuntimePrincipalKind.MINION, name: 'marketingScrapper' } as const;
248
+
249
+ it('leaves the APPLICATION BACKEND username exactly where it was', async () => {
250
+ // Load-bearing: if the backend's ACL username moved, an existing deployment's
251
+ // user is orphaned on the next sync and a running backend loses Redis. Both
252
+ // the absent-principal case (every pre-existing call site) and the explicit
253
+ // backend principal must render the bare namespace.
254
+ for (const principal of [undefined, { kind: ApplicationRuntimePrincipalKind.APPLICATION_BACKEND }]) {
255
+ const adminClient = makeAdminClientStub();
256
+ await RedisSelfInfraProviderModule.mintAppCredentials!(
257
+ makeMintContext(adminClient, principal ? { principal } : {}),
258
+ );
259
+ expect(adminClient.upsertUserSpy).toHaveBeenCalledWith(
260
+ expect.objectContaining({ username: 'wonder-todos-local' }),
261
+ );
262
+ }
263
+ });
264
+
265
+ it('gives a named principal its OWN ACL user', async () => {
266
+ const adminClient = makeAdminClientStub();
267
+ await RedisSelfInfraProviderModule.mintAppCredentials!(
268
+ makeMintContext(adminClient, { principal: MINION }),
269
+ );
270
+ expect(adminClient.upsertUserSpy).toHaveBeenCalledWith(
271
+ expect.objectContaining({ username: 'wonder-todos-local-minion-marketingScrapper' }),
272
+ );
273
+ });
274
+
275
+ it('confines a MINION to its own key namespace (D20) while the backend keeps the app-wide pattern', async () => {
276
+ // The ACL pattern and the minion's `REDIS_KEY_PREFIX` are ONE COUPLED PAIR —
277
+ // a mismatch DENIES every operation the minion performs and the error names
278
+ // an ACL, not the mismatch. Both sides derive from
279
+ // `buildMinionRedisKeyPrefix`, so this asserts the CONFINEMENT and the
280
+ // shared derivation together.
281
+ const minionClient = makeAdminClientStub();
282
+ await RedisSelfInfraProviderModule.mintAppCredentials!(makeMintContext(minionClient, { principal: MINION }));
283
+ // Asserted BOTH ways on purpose: the literal catches an accidental change to
284
+ // the namespace shape, and the derived form pins the INVARIANT that the ACL
285
+ // pattern is exactly the runtime's prefix plus `*`. The CLI writes that same
286
+ // prefix into the minion's `REDIS_KEY_PREFIX`, so this is the seam where the
287
+ // two halves of the coupled pair are checked against each other.
288
+ expect(minionClient.upsertUserSpy).toHaveBeenCalledWith(
289
+ expect.objectContaining({ keyPattern: 'wonder-todos:minions:marketingScrapper:*' }),
290
+ );
291
+ expect(minionClient.upsertUserSpy).toHaveBeenCalledWith(
292
+ expect.objectContaining({ keyPattern: `${buildMinionRedisKeyPrefix('wonder-todos', 'marketingScrapper')}*` }),
293
+ );
294
+
295
+ // The minion's namespace is a strict EXTENSION of the application's, which
296
+ // is what keeps `RedisService`'s "authored prefix must start with
297
+ // `<appSlug>:`" rule true without changing it.
298
+ expect('wonder-todos:minions:marketingScrapper:*'.startsWith('wonder-todos:')).toBe(true);
299
+
300
+ const backendClient = makeAdminClientStub();
301
+ await RedisSelfInfraProviderModule.mintAppCredentials!(makeMintContext(backendClient));
302
+ expect(backendClient.upsertUserSpy).toHaveBeenCalledWith(
303
+ expect.objectContaining({ keyPattern: 'wonder-todos:*' }),
304
+ );
305
+ });
306
+
307
+ it('REVOKES the principal\'s own user, never the application backend\'s', async () => {
308
+ // The site that makes a shared derivation mandatory: a revoke that computed
309
+ // the username differently would DELUSER the backend's ACL user while
310
+ // believing it was removing a minion's — an outage caused by a cleanup.
311
+ const adminClient = makeAdminClientStub();
312
+ await RedisSelfInfraProviderModule.revokeAppCredentials!(
313
+ makeMintContext(adminClient, { principal: MINION }),
314
+ );
315
+ expect(adminClient.deleteUserSpy).toHaveBeenCalledWith('wonder-todos-local-minion-marketingScrapper');
316
+ expect(adminClient.deleteUserSpy).not.toHaveBeenCalledWith('wonder-todos-local');
317
+ });
318
+
319
+ it('sanitises a bucket key that is not ACL-safe', async () => {
320
+ // A `wildo.saas.config.ts` bucket key is a TypeScript object key and may
321
+ // legally contain characters an ACL username should not carry to the wire.
322
+ const adminClient = makeAdminClientStub();
323
+ await RedisSelfInfraProviderModule.mintAppCredentials!(
324
+ makeMintContext(adminClient, {
325
+ principal: { kind: ApplicationRuntimePrincipalKind.WORKER, name: 'my_odd$worker' },
326
+ }),
327
+ );
328
+ expect(adminClient.upsertUserSpy).toHaveBeenCalledWith(
329
+ expect.objectContaining({ username: 'wonder-todos-local-worker-my-odd-worker' }),
330
+ );
331
+ });
332
+ });
333
+
334
+ describe('RedisSelfInfraProviderModule audience-2 absence (D-4)', () => {
335
+ it('does NOT declare mintPlatformServiceCredentials', () => {
336
+ // Redis has no audience-2 platform-service-scoped use case today
337
+ // (D-4 of Slice 4.B locked decisions). When/if a real need
338
+ // surfaces, add the method to the module.
339
+ expect(RedisSelfInfraProviderModule.mintPlatformServiceCredentials).toBeUndefined();
340
+ });
341
+ });