@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,116 @@
1
+ /**
2
+ * `MinioSelfInfraProviderModule` — sixth concrete `InfraProviderModule`
3
+ * (Slice 5.1 of `infra-provider-planner.md`, generalizing the
4
+ * Slice 2 / 4.B pattern to object storage).
5
+ *
6
+ * Implements the `MINIO_SELF` `InfraProvider`. One shared MinIO bucket
7
+ * serves every application; the object-key tree is namespaced
8
+ * `${prefix}/applications/${applicationId}/files/${fileId}`.
9
+ *
10
+ * `mintAppCredentials` / `revokeAppCredentials` are absent from THIS
11
+ * module, and that remains accurate — but it no longer means what it
12
+ * used to. Per-application credential isolation exists; it is simply
13
+ * not implemented here. See below.
14
+ *
15
+ * ## ⚠️ Corrected 2026-08-10 — this block described a posture the
16
+ * system no longer has
17
+ *
18
+ * It previously stated that every app shares "one access-key/secret-key
19
+ * pair", that per-app isolation "comes from `STORAGE_BUCKET_PREFIX`",
20
+ * and that app-scoped credentials were "NONE — apps consume the shared
21
+ * `STORAGE_*` env". All three were true when written and are now false:
22
+ *
23
+ * - Applications no longer receive any long-lived storage credential.
24
+ * `STORAGE_ACCESS_KEY` / `STORAGE_SECRET_KEY` are no longer emitted
25
+ * into application-tier env at all — they were the object store's
26
+ * ROOT pair, and handing them to every app, minion and worker is the
27
+ * exposure this was corrected to remove.
28
+ * - Isolation is enforced by a **credential**, not by a prefix
29
+ * convention. `apps-manager`'s `ObjectStorageStsBrokerService` issues
30
+ * short-lived STS sessions whose inline policy confines each
31
+ * application to `applications/<applicationId>/`, keyed on the
32
+ * platform-minted id — never on `STORAGE_BUCKET_PREFIX`, which an
33
+ * application authors for itself and therefore cannot be a security
34
+ * boundary. The prefix is now cosmetic namespacing.
35
+ *
36
+ * The three prerequisites this block listed for per-app credentials
37
+ * were addressed rather than deferred: the adapter threads per-principal
38
+ * credentials through a credential provider (1); the session policy
39
+ * replaces per-app bucket-policy authoring (2); and rotation safety (3)
40
+ * dissolved — STS credentials expire by construction, so there is no
41
+ * long-lived key whose rotation must be coordinated across pods.
42
+ *
43
+ * **Why the mint methods still do not live here.** MinIO's JS client
44
+ * ships no user-management API, so minting would need the `mc` binary or
45
+ * hand-written admin-API crypto. STS needs neither, and belongs with the
46
+ * service that holds the admin credential — apps-manager — rather than
47
+ * with this planning module, which contributes deployment topology.
48
+ *
49
+ * **Audience-1 (platform-admin)**: cluster-wide MinIO admin creds
50
+ * delivered via env. Consumers: `apps-manager` (which brokers STS from
51
+ * them) and `companion`. Whether these consumers should hold *root*
52
+ * specifically, rather than a narrower admin credential, is OQ-2 in the
53
+ * plan below.
54
+ *
55
+ * **Audience-3 (app-scoped)**: no per-app MinIO STATE is minted here —
56
+ * but applications DO receive a per-application storage credential at
57
+ * runtime, from apps-manager. "No app-scoped audience in this module" is
58
+ * not "no app-scoped credential in the system".
59
+ *
60
+ * See `.claude/plans/object-storage-root-credential-distribution.md`.
61
+ */
62
+
63
+ import {
64
+ InfraCapability,
65
+ InfraPlatformAdminConsumer,
66
+ InfraProvider,
67
+ InfraProviderModule,
68
+ InfraProviderPlanContribution,
69
+ } from '@wildo-ai/platform-config-lib';
70
+
71
+ const DEFAULT_MINIO_PORT = 9000;
72
+
73
+ export const MinioSelfInfraProviderModule: InfraProviderModule = {
74
+ provider: InfraProvider.MINIO_SELF,
75
+ supportedCapabilities: [InfraCapability.OBJECT_STORAGE],
76
+
77
+ /**
78
+ * Shared bucket, shared root key — same as AWS S3: object-storage confinement is an STS session
79
+ * policy minted per request, never a per-principal secret in the application blob.
80
+ */
81
+ perPrincipalSecretBlobPaths: [],
82
+
83
+ plan(): InfraProviderPlanContribution {
84
+ return {
85
+ dockerComposeServices: ['minio'],
86
+ k8sResources: ['templates/infra/k8s/internal/minio/deployment.yaml.hbs'],
87
+ platformAdmin: {
88
+ envVars: {
89
+ STORAGE_PROVIDER: 'minio',
90
+ STORAGE_ENDPOINT: 'minio',
91
+ STORAGE_PORT: String(DEFAULT_MINIO_PORT),
92
+ STORAGE_USE_SSL: 'false',
93
+ },
94
+ // Cluster-wide bucket access creds. Every app uses the same
95
+ // pair. Per-app isolation is via `STORAGE_BUCKET_PREFIX`
96
+ // (see module JSDoc).
97
+ secretEnvVars: ['STORAGE_ACCESS_KEY', 'STORAGE_SECRET_KEY'],
98
+ consumers: [
99
+ InfraPlatformAdminConsumer.APPS_MANAGER,
100
+ InfraPlatformAdminConsumer.COMPANION,
101
+ ],
102
+ },
103
+ // Apps receive `STORAGE_*` via the bootstrap env contract
104
+ // (engine-side `application-backend-env.contracts.ts`). No
105
+ // per-app blob-path additions because the values are static
106
+ // across all apps in the same env. `appRuntime` intentionally
107
+ // empty so the contribution stays an honest "shared bucket,
108
+ // no per-app state".
109
+ };
110
+ },
111
+
112
+ // No `mintAppCredentials` / `revokeAppCredentials` —
113
+ // shared-bucket model (see module JSDoc).
114
+ // No `mintPlatformServiceCredentials` — platform services use the
115
+ // same shared bucket as customer apps.
116
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Tests for `MongodbAtlasInfraProviderModule` (Slice 4.B.3 of
3
+ * `infra-provider-planner.md`).
4
+ */
5
+
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import {
8
+ AppCredentialMintingContext,
9
+ IMongoAtlasClient,
10
+ InfraCapability,
11
+ InfraPlatformAdminConsumer,
12
+ InfraProvider,
13
+ } from '@wildo-ai/platform-config-lib';
14
+ import { MongodbAtlasInfraProviderModule } from '../mongodb-atlas.infra.module';
15
+
16
+ function makeAdminClientStub(
17
+ connectionString: string = 'mongodb+srv://wonder-todos-local_user:secret@cluster0.mongodb.net/wonder_todos?retryWrites=true',
18
+ ): IMongoAtlasClient & {
19
+ upsertApplicationDatabaseSpy: ReturnType<typeof vi.fn>;
20
+ deleteDatabaseUserSpy: ReturnType<typeof vi.fn>;
21
+ } {
22
+ const upsertApplicationDatabaseSpy = vi.fn().mockResolvedValue({ connectionString });
23
+ const deleteDatabaseUserSpy = vi.fn().mockResolvedValue(undefined);
24
+ return {
25
+ upsertApplicationDatabase: upsertApplicationDatabaseSpy,
26
+ deleteDatabaseUser: deleteDatabaseUserSpy,
27
+ upsertApplicationDatabaseSpy,
28
+ deleteDatabaseUserSpy,
29
+ };
30
+ }
31
+
32
+ function makeMintContext(
33
+ adminClient: IMongoAtlasClient,
34
+ overrides: Partial<AppCredentialMintingContext> = {},
35
+ ): AppCredentialMintingContext {
36
+ return {
37
+ appSlug: 'wonder-todos',
38
+ applicationNamespace: 'wonder-todos-local',
39
+ wildoApplicationId: '60d21bdf1c4ae5cda1b2c3d4',
40
+ adminClient,
41
+ secretInputs: {
42
+ 'database.mongodb.password': 'orchestrator-supplied-password',
43
+ },
44
+ applicationConfigInputs: {
45
+ databaseName: 'wonder_todos',
46
+ clusterName: 'Cluster0',
47
+ },
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ describe('MongodbAtlasInfraProviderModule.plan()', () => {
53
+ const contribution = MongodbAtlasInfraProviderModule.plan({
54
+ environment: 'staging',
55
+ runtime: 'kubernetes',
56
+ backingServiceKey: 'mongodb',
57
+ config: {},
58
+ });
59
+
60
+ it('declares the right provider + capability triple', () => {
61
+ expect(MongodbAtlasInfraProviderModule.provider).toBe(InfraProvider.MONGODB_ATLAS);
62
+ expect(MongodbAtlasInfraProviderModule.supportedCapabilities).toEqual([
63
+ InfraCapability.DOCUMENT_DATABASE,
64
+ ]);
65
+ });
66
+
67
+ it('does NOT contribute compose / K8s resources (Atlas is remote)', () => {
68
+ expect(contribution.dockerComposeServices).toBeUndefined();
69
+ expect(contribution.k8sResources).toBeUndefined();
70
+ });
71
+
72
+ it('declares platformAdmin (audience-1) for apps-manager only', () => {
73
+ expect(contribution.platformAdmin?.envVars).toEqual({
74
+ ATLAS_API_BASE_URL: 'https://cloud.mongodb.com/api/atlas/v1.0',
75
+ ATLAS_PROJECT_ID: '',
76
+ ATLAS_CLUSTER_NAME: 'Cluster0',
77
+ });
78
+ expect(contribution.platformAdmin?.secretEnvVars).toEqual([
79
+ 'ATLAS_PUBLIC_KEY',
80
+ 'ATLAS_PRIVATE_KEY',
81
+ ]);
82
+ // Atlas is admin-API only for apps-manager. Companion runs against
83
+ // self-managed Mongo locally — it is NOT a consumer here.
84
+ expect(contribution.platformAdmin?.consumers).toEqual([
85
+ InfraPlatformAdminConsumer.APPS_MANAGER,
86
+ ]);
87
+ });
88
+
89
+ it('declares appRuntime per-app secret blob paths (username + password only)', () => {
90
+ expect(contribution.appRuntime?.perAppSecretBlobPaths).toEqual([
91
+ 'database.mongodb.username',
92
+ 'database.mongodb.password',
93
+ ]);
94
+ // Host/protocol/options are derived from env-config at runtime,
95
+ // NOT from this contribution.
96
+ expect(contribution.appRuntime?.perAppConfigBlobPaths).toBeUndefined();
97
+ });
98
+ });
99
+
100
+ describe('MongodbAtlasInfraProviderModule.mintAppCredentials', () => {
101
+ it('upserts an Atlas DB user keyed by namespace and returns the SRV connection in runtimeArtifacts', async () => {
102
+ const adminClient = makeAdminClientStub();
103
+ const result = await MongodbAtlasInfraProviderModule.mintAppCredentials!(
104
+ makeMintContext(adminClient),
105
+ );
106
+
107
+ expect(adminClient.upsertApplicationDatabaseSpy).toHaveBeenCalledTimes(1);
108
+ expect(adminClient.upsertApplicationDatabaseSpy).toHaveBeenCalledWith({
109
+ clusterName: 'Cluster0',
110
+ databaseName: 'wonder_todos',
111
+ username: 'wonder-todos-local_user',
112
+ password: 'orchestrator-supplied-password',
113
+ });
114
+
115
+ expect(result.secretBlobValues).toEqual({
116
+ 'database.mongodb.username': 'wonder-todos-local_user',
117
+ 'database.mongodb.password': 'orchestrator-supplied-password',
118
+ });
119
+ // SRV URL with credentials is in runtimeArtifacts (NOT secretBlobValues),
120
+ // because the persistence shape is split (config: host/protocol/options;
121
+ // secrets: username/password).
122
+ expect(result.runtimeArtifacts).toEqual({
123
+ connectionString: 'mongodb+srv://wonder-todos-local_user:secret@cluster0.mongodb.net/wonder_todos?retryWrites=true',
124
+ });
125
+ });
126
+
127
+ it('fails loud when secretInputs is missing the password', async () => {
128
+ const adminClient = makeAdminClientStub();
129
+ await expect(
130
+ MongodbAtlasInfraProviderModule.mintAppCredentials!(
131
+ makeMintContext(adminClient, { secretInputs: {} }),
132
+ ),
133
+ ).rejects.toThrow(/missing 'database\.mongodb\.password'/);
134
+ expect(adminClient.upsertApplicationDatabaseSpy).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('fails loud when applicationNamespace is empty', async () => {
138
+ const adminClient = makeAdminClientStub();
139
+ await expect(
140
+ MongodbAtlasInfraProviderModule.mintAppCredentials!(
141
+ makeMintContext(adminClient, { applicationNamespace: '' }),
142
+ ),
143
+ ).rejects.toThrow(/applicationNamespace is empty/);
144
+ expect(adminClient.upsertApplicationDatabaseSpy).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it('fails loud when applicationConfigInputs.databaseName is missing', async () => {
148
+ const adminClient = makeAdminClientStub();
149
+ await expect(
150
+ MongodbAtlasInfraProviderModule.mintAppCredentials!(
151
+ makeMintContext(adminClient, {
152
+ applicationConfigInputs: { clusterName: 'Cluster0' },
153
+ }),
154
+ ),
155
+ ).rejects.toThrow(/missing 'databaseName'/);
156
+ expect(adminClient.upsertApplicationDatabaseSpy).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it('fails loud when applicationConfigInputs.clusterName is missing', async () => {
160
+ const adminClient = makeAdminClientStub();
161
+ await expect(
162
+ MongodbAtlasInfraProviderModule.mintAppCredentials!(
163
+ makeMintContext(adminClient, {
164
+ applicationConfigInputs: { databaseName: 'wonder_todos' },
165
+ }),
166
+ ),
167
+ ).rejects.toThrow(/missing 'clusterName'/);
168
+ });
169
+ });
170
+
171
+ describe('MongodbAtlasInfraProviderModule.revokeAppCredentials', () => {
172
+ it('deletes the per-app Atlas DB user', async () => {
173
+ const adminClient = makeAdminClientStub();
174
+ await MongodbAtlasInfraProviderModule.revokeAppCredentials!(
175
+ makeMintContext(adminClient),
176
+ );
177
+ expect(adminClient.deleteDatabaseUserSpy).toHaveBeenCalledTimes(1);
178
+ expect(adminClient.deleteDatabaseUserSpy).toHaveBeenCalledWith(
179
+ 'wonder-todos-local_user',
180
+ );
181
+ });
182
+
183
+ it('fails loud when applicationNamespace is empty', async () => {
184
+ const adminClient = makeAdminClientStub();
185
+ await expect(
186
+ MongodbAtlasInfraProviderModule.revokeAppCredentials!(
187
+ makeMintContext(adminClient, { applicationNamespace: '' }),
188
+ ),
189
+ ).rejects.toThrow(/applicationNamespace is empty/);
190
+ expect(adminClient.deleteDatabaseUserSpy).not.toHaveBeenCalled();
191
+ });
192
+ });
193
+
194
+ describe('MongodbAtlasInfraProviderModule audience-2 absence (D-4)', () => {
195
+ it('does NOT declare mintPlatformServiceCredentials', () => {
196
+ expect(MongodbAtlasInfraProviderModule.mintPlatformServiceCredentials).toBeUndefined();
197
+ });
198
+ });
@@ -0,0 +1,236 @@
1
+ /**
2
+ * `MongodbAtlasInfraProviderModule` — fourth concrete `InfraProviderModule`
3
+ * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the Slice
4
+ * 2 / 4.B.1 / 4.B.2 pattern to MongoDB Atlas).
5
+ *
6
+ * Implements the `MONGODB_ATLAS` `InfraProvider` over the
7
+ * `IMongoAtlasClient` transport (Atlas REST API + digest auth, exposed
8
+ * by `MongoAtlasManagerService` after the Slice 4.B.3 5C policy/
9
+ * transport split). Owns ALL per-app naming policy — the per-app
10
+ * username convention (`<applicationNamespace>_user`, mirroring the
11
+ * legacy `getOrGenerateCredentials.generateNewCredentials`).
12
+ *
13
+ * **What this module reads from `AppCredentialMintingContext`**:
14
+ * - `applicationNamespace` — used as the source for the per-app
15
+ * username (legacy convention `${ns}_user`).
16
+ * - `secretInputs['database.mongodb.password']` — orchestrator-
17
+ * supplied password (module is upsert-only, never generator).
18
+ * - `applicationConfigInputs['databaseName']` — required, the
19
+ * per-app database name (orchestrator derives via
20
+ * `deriveDefaultMongoDatabaseName`).
21
+ * - `applicationConfigInputs['clusterName']` — required, the
22
+ * Atlas cluster name (orchestrator passes `appInfo.clusterName ||
23
+ * 'Cluster0'`).
24
+ * - `adminClient` — narrowed to `IMongoAtlasClient`.
25
+ *
26
+ * **Two audiences this module serves:**
27
+ *
28
+ * 1. **Audience-1 (platform-admin)**: Atlas API public/private key
29
+ * pair + project id + base URL. Consumers: `apps-manager` only
30
+ * (companion uses self-managed locally; Atlas is remote-only).
31
+ *
32
+ * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:
33
+ * per-customer-app Atlas database user. Result threaded into
34
+ * `Wildo_Application_Secrets.database.mongodb.{username,password}`
35
+ * via `_merge_WildoApplicationSecrets`; the SRV connection
36
+ * string returned via `runtimeArtifacts.connectionString` for
37
+ * transient app-init operations (NOT persisted — the persistence
38
+ * shape is split between config (host/protocol/options/database)
39
+ * and secrets (username/password)).
40
+ *
41
+ * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:
42
+ * - **Audience-2 (platform-service-scoped)**: no platform service
43
+ * uses Atlas for inter-service messaging today. Method omitted.
44
+ *
45
+ * **`revokeAppCredentials`**: declared and tested, but the integration
46
+ * call site does NOT exist in `wildo-application-lifecycle.service.ts`
47
+ * (no app-deletion ceremony today — F6 of plan review 2026-05-06).
48
+ * Method ships as a forward-compat seam.
49
+ */
50
+
51
+ import {
52
+ AppCredentialMintingContext,
53
+ AppCredentialMintingResult,
54
+ IMongoAtlasClient,
55
+ InfraCapability,
56
+ InfraPlatformAdminConsumer,
57
+ InfraProvider,
58
+ InfraProviderModule,
59
+ InfraProviderPlanContribution,
60
+ } from '@wildo-ai/platform-config-lib';
61
+
62
+ /**
63
+ * The `Wildo_Application_Secrets` JSON paths this module writes
64
+ * through `mintAppCredentials.secretBlobValues`. Declared once and
65
+ * reused as `appRuntime.perAppSecretBlobPaths` in `plan()` AND as
66
+ * the keys of the result, so a refactor that renames a path fails
67
+ * loudly at one place.
68
+ */
69
+ const MONGODB_USERNAME_PATH = 'database.mongodb.username';
70
+ const MONGODB_PASSWORD_PATH = 'database.mongodb.password';
71
+ /**
72
+ * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.
73
+ *
74
+ * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip
75
+ * these from the application blob before delivering a principal's own material) AND spread into
76
+ * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery
77
+ * path would silently stop covering a path this module still mints.
78
+ */
79
+ const PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [MONGODB_USERNAME_PATH, MONGODB_PASSWORD_PATH];
80
+
81
+
82
+ /**
83
+ * Key the module looks for in `secretInputs` for the orchestrator-
84
+ * supplied password.
85
+ */
86
+ const MONGODB_PASSWORD_INPUT_KEY = 'database.mongodb.password';
87
+
88
+ /**
89
+ * Required keys in `applicationConfigInputs`. The orchestrator
90
+ * derives both before calling `mintAppCredentials`.
91
+ */
92
+ const DATABASE_NAME_INPUT_KEY = 'databaseName';
93
+ const CLUSTER_NAME_INPUT_KEY = 'clusterName';
94
+
95
+ /**
96
+ * Key in `runtimeArtifacts` carrying the SRV connection string with
97
+ * credentials embedded. Exposed for orchestrator transient use
98
+ * (app-init seeding); deliberately NOT persisted.
99
+ */
100
+ const CONNECTION_STRING_ARTIFACT_KEY = 'connectionString';
101
+
102
+ /**
103
+ * Compute the per-app Atlas database username. Mirrors the legacy
104
+ * convention at
105
+ * `wildo-application-lifecycle.service.ts:2607` /
106
+ * `generateNewCredentials` — `mongoUsername = ${ns}_user`.
107
+ * Centralized here so future changes touch one site.
108
+ */
109
+ function buildMongodbUsername(applicationNamespace: string): string {
110
+ return `${applicationNamespace}_user`;
111
+ }
112
+
113
+ export const MongodbAtlasInfraProviderModule: InfraProviderModule = {
114
+ provider: InfraProvider.MONGODB_ATLAS,
115
+ supportedCapabilities: [InfraCapability.DOCUMENT_DATABASE],
116
+ perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,
117
+
118
+ plan(): InfraProviderPlanContribution {
119
+ return {
120
+ // Atlas is remote-only — no compose service / no K8s manifest.
121
+ // The plan's `externalEndpoints` (in the wider envelope) carry
122
+ // the Atlas host topology; this module's `plan()` covers only
123
+ // admin-API access + per-app secret paths.
124
+ platformAdmin: {
125
+ envVars: {
126
+ // ATLAS_API_BASE_URL is non-secret because it identifies
127
+ // the Atlas API surface (default https://cloud.mongodb.com/api/atlas/v1.0).
128
+ // ATLAS_PROJECT_ID + ATLAS_CLUSTER_NAME are non-secret
129
+ // identifiers — public information about the customer's
130
+ // Atlas project.
131
+ ATLAS_API_BASE_URL: 'https://cloud.mongodb.com/api/atlas/v1.0',
132
+ // PROJECT_ID + CLUSTER_NAME default to authored values; the
133
+ // CLI's env-vars-builder writes them per-env from the
134
+ // operator's `wildo.infra.<env>.config.ts > backingServices.mongodb`.
135
+ ATLAS_PROJECT_ID: '',
136
+ ATLAS_CLUSTER_NAME: 'Cluster0',
137
+ },
138
+ secretEnvVars: ['ATLAS_PUBLIC_KEY', 'ATLAS_PRIVATE_KEY'],
139
+ // Atlas is admin-API only for apps-manager. Companion runs
140
+ // locally against self-managed Mongo, NOT Atlas (no
141
+ // companion consumer for the Atlas variant).
142
+ consumers: [InfraPlatformAdminConsumer.APPS_MANAGER],
143
+ },
144
+ appRuntime: {
145
+ // Static topology for the app pod. The actual host /
146
+ // protocol / options for Atlas connections are derived
147
+ // server-side at runtime from cluster info; the persistence
148
+ // shape mirrors the legacy `_merge_WildoApplicationConfig`
149
+ // mongodb section.
150
+ perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],
151
+ },
152
+ };
153
+ },
154
+
155
+ async mintAppCredentials(
156
+ context: AppCredentialMintingContext,
157
+ ): Promise<AppCredentialMintingResult> {
158
+ const adminClient = context.adminClient as IMongoAtlasClient;
159
+ const password = context.secretInputs[MONGODB_PASSWORD_INPUT_KEY];
160
+ if (!password) {
161
+ throw new Error(
162
+ `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +
163
+ `secretInputs is missing '${MONGODB_PASSWORD_INPUT_KEY}'. ` +
164
+ `The orchestrator MUST supply the Mongo password — modules are upsert-only.`,
165
+ );
166
+ }
167
+ if (!context.applicationNamespace) {
168
+ throw new Error(
169
+ `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +
170
+ `applicationNamespace is empty. ` +
171
+ `The orchestrator MUST supply a non-empty namespace.`,
172
+ );
173
+ }
174
+ const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];
175
+ if (!databaseName) {
176
+ throw new Error(
177
+ `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +
178
+ `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}'. ` +
179
+ `The orchestrator MUST supply the per-app database name.`,
180
+ );
181
+ }
182
+ const clusterName = context.applicationConfigInputs?.[CLUSTER_NAME_INPUT_KEY];
183
+ if (!clusterName) {
184
+ throw new Error(
185
+ `MongodbAtlasInfraProviderModule.mintAppCredentials: ` +
186
+ `applicationConfigInputs is missing '${CLUSTER_NAME_INPUT_KEY}'. ` +
187
+ `The orchestrator MUST supply the Atlas cluster name.`,
188
+ );
189
+ }
190
+
191
+ const username = buildMongodbUsername(context.applicationNamespace);
192
+
193
+ // POST /databaseUsers (with PATCH-fallback) — idempotent upsert.
194
+ // Returns the SRV connection string with credentials embedded.
195
+ const { connectionString } = await adminClient.upsertApplicationDatabase({
196
+ clusterName,
197
+ databaseName,
198
+ username,
199
+ password,
200
+ });
201
+
202
+ return {
203
+ secretBlobValues: {
204
+ [MONGODB_USERNAME_PATH]: username,
205
+ // Echo of input password — proves upsert-only.
206
+ [MONGODB_PASSWORD_PATH]: password,
207
+ },
208
+ runtimeArtifacts: {
209
+ // Transient only — orchestrator uses for app-init seeding,
210
+ // does NOT persist.
211
+ [CONNECTION_STRING_ARTIFACT_KEY]: connectionString,
212
+ },
213
+ };
214
+ },
215
+
216
+ /**
217
+ * Tear down the per-app Atlas database user. Idempotent.
218
+ *
219
+ * **Caller status**: no caller wires this in Slices 1–4.B.3.
220
+ * Forward-compat seam.
221
+ */
222
+ async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {
223
+ if (!context.applicationNamespace) {
224
+ throw new Error(
225
+ `MongodbAtlasInfraProviderModule.revokeAppCredentials: ` +
226
+ `applicationNamespace is empty. ` +
227
+ `The orchestrator MUST supply a non-empty namespace.`,
228
+ );
229
+ }
230
+ const adminClient = context.adminClient as IMongoAtlasClient;
231
+ const username = buildMongodbUsername(context.applicationNamespace);
232
+ await adminClient.deleteDatabaseUser(username);
233
+ },
234
+
235
+ // No `mintPlatformServiceCredentials` (D-4).
236
+ };