@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,81 @@
1
+ /**
2
+ * `MongodbSelfInfraProviderModule` — fifth concrete `InfraProviderModule`
3
+ * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the
4
+ * pattern to self-managed MongoDB).
5
+ *
6
+ * Implements the `MONGODB_SELF` `InfraProvider` over the
7
+ * `IMongoSelfAdminClient` transport (admin-side mongoose connection,
8
+ * exposed by `MongoSelfManagerService` after the Slice 4.B.3 5C
9
+ * policy/transport split). Owns ALL per-app naming policy +
10
+ * URL-derivation policy — the per-app username convention
11
+ * (`<applicationNamespace>_user`, mirroring the Atlas variant) and
12
+ * the per-app SRV URL derivation by swapping admin credentials
13
+ * with per-app credentials in the authored
14
+ * `BASE_MONGO_SERVER_URL`-derived admin connect URL.
15
+ *
16
+ * **What this module reads from `AppCredentialMintingContext`**:
17
+ * - `applicationNamespace` — used as the source for the per-app
18
+ * username (legacy convention `${ns}_user`).
19
+ * - `secretInputs['database.mongodb.password']` — orchestrator-
20
+ * supplied password (module is upsert-only, never generator).
21
+ * - `applicationConfigInputs['databaseName']` — required.
22
+ * - `applicationConfigInputs['adminConnectUrl']` — required, the
23
+ * admin-scoped MongoDB connect URL the orchestrator reads from
24
+ * `MONGO_CONNECT_URL` env at app-init time.
25
+ * - `adminClient` — narrowed to `IMongoSelfAdminClient`.
26
+ *
27
+ * **Two audiences this module serves:**
28
+ *
29
+ * 1. **Audience-1 (platform-admin)**: cluster-wide Mongo admin
30
+ * connect URL (`MONGO_CONNECT_URL`). Consumers: `apps-manager`
31
+ * (mints per-app users via `mintAppCredentials`), `companion`
32
+ * (mirrors apps-manager admin role for local dev).
33
+ *
34
+ * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:
35
+ * per-customer-app Mongo database user. Result threaded into
36
+ * `Wildo_Application_Secrets.database.mongodb.{username,password}`
37
+ * via `_merge_WildoApplicationSecrets`; the per-app connect URL
38
+ * returned via `runtimeArtifacts.connectionString` for
39
+ * transient app-init operations.
40
+ *
41
+ * **NOT served (D-4)**: audience-2 not applicable.
42
+ */
43
+ import { InfraProviderModule } from '@wildo-ai/platform-config-lib';
44
+ /** One `createRole`/`updateRole` privilege entry. */
45
+ export interface MongodbCollectionPrivilege {
46
+ readonly resource: {
47
+ readonly db: string;
48
+ readonly collection: string;
49
+ };
50
+ readonly actions: readonly string[];
51
+ }
52
+ /**
53
+ * Turn a minion's grants into collection-scoped MongoDB privileges (D14, R1).
54
+ *
55
+ * ## What this deliberately never produces
56
+ *
57
+ * No `dbAdmin` and no `readWrite` — the two roles the application backend
58
+ * currently holds on the whole database. `dbAdmin` is banned outright by D14;
59
+ * `readWrite` is database-WIDE, so granting it to a minion would make every
60
+ * collection reachable regardless of what `resourceAccess` declared, which is
61
+ * the containment this exists to create.
62
+ *
63
+ * No privilege on the database RESOURCE either (`{ db, collection: '' }`), which
64
+ * is Mongo's way of saying "every collection in this database". Every entry
65
+ * names one collection explicitly.
66
+ *
67
+ * ## Read and write are separate entries on purpose
68
+ *
69
+ * A collection granted both axes yields TWO privilege entries rather than one
70
+ * merged action list. Mongo unions them identically, but the emitted role then
71
+ * reads back as the declaration that produced it — which is what makes an
72
+ * operator inspecting `db.getRole()` able to check the grant against the config
73
+ * without re-deriving it.
74
+ */
75
+ export declare function buildMinionMongodbPrivileges(input: {
76
+ databaseName: string;
77
+ readCollections: readonly string[];
78
+ writeCollections: readonly string[];
79
+ }): MongodbCollectionPrivilege[];
80
+ export declare const MongodbSelfInfraProviderModule: InfraProviderModule;
81
+ //# sourceMappingURL=mongodb-self.infra.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mongodb-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/mongodb-self/mongodb-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAOL,mBAAmB,EAKpB,MAAM,+BAA+B,CAAC;AAmDvC,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,GAAG,0BAA0B,EAAE,CAS/B;AA+CD,eAAO,MAAM,8BAA8B,EAAE,mBA4L5C,CAAC"}
@@ -0,0 +1,311 @@
1
+ /**
2
+ * `MongodbSelfInfraProviderModule` — fifth concrete `InfraProviderModule`
3
+ * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the
4
+ * pattern to self-managed MongoDB).
5
+ *
6
+ * Implements the `MONGODB_SELF` `InfraProvider` over the
7
+ * `IMongoSelfAdminClient` transport (admin-side mongoose connection,
8
+ * exposed by `MongoSelfManagerService` after the Slice 4.B.3 5C
9
+ * policy/transport split). Owns ALL per-app naming policy +
10
+ * URL-derivation policy — the per-app username convention
11
+ * (`<applicationNamespace>_user`, mirroring the Atlas variant) and
12
+ * the per-app SRV URL derivation by swapping admin credentials
13
+ * with per-app credentials in the authored
14
+ * `BASE_MONGO_SERVER_URL`-derived admin connect URL.
15
+ *
16
+ * **What this module reads from `AppCredentialMintingContext`**:
17
+ * - `applicationNamespace` — used as the source for the per-app
18
+ * username (legacy convention `${ns}_user`).
19
+ * - `secretInputs['database.mongodb.password']` — orchestrator-
20
+ * supplied password (module is upsert-only, never generator).
21
+ * - `applicationConfigInputs['databaseName']` — required.
22
+ * - `applicationConfigInputs['adminConnectUrl']` — required, the
23
+ * admin-scoped MongoDB connect URL the orchestrator reads from
24
+ * `MONGO_CONNECT_URL` env at app-init time.
25
+ * - `adminClient` — narrowed to `IMongoSelfAdminClient`.
26
+ *
27
+ * **Two audiences this module serves:**
28
+ *
29
+ * 1. **Audience-1 (platform-admin)**: cluster-wide Mongo admin
30
+ * connect URL (`MONGO_CONNECT_URL`). Consumers: `apps-manager`
31
+ * (mints per-app users via `mintAppCredentials`), `companion`
32
+ * (mirrors apps-manager admin role for local dev).
33
+ *
34
+ * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:
35
+ * per-customer-app Mongo database user. Result threaded into
36
+ * `Wildo_Application_Secrets.database.mongodb.{username,password}`
37
+ * via `_merge_WildoApplicationSecrets`; the per-app connect URL
38
+ * returned via `runtimeArtifacts.connectionString` for
39
+ * transient app-init operations.
40
+ *
41
+ * **NOT served (D-4)**: audience-2 not applicable.
42
+ */
43
+ import { InfraCapability, InfraPlatformAdminConsumer, InfraProvider, ApplicationRuntimePrincipalKind, resolveGrantStorageNames, } from '@wildo-ai/platform-config-lib';
44
+ const MONGODB_USERNAME_PATH = 'database.mongodb.username';
45
+ const MONGODB_PASSWORD_PATH = 'database.mongodb.password';
46
+ /**
47
+ * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.
48
+ *
49
+ * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip
50
+ * these from the application blob before delivering a principal's own material) AND spread into
51
+ * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery
52
+ * path would silently stop covering a path this module still mints.
53
+ */
54
+ const PER_PRINCIPAL_SECRET_BLOB_PATHS = [MONGODB_USERNAME_PATH, MONGODB_PASSWORD_PATH];
55
+ const MONGODB_PASSWORD_INPUT_KEY = 'database.mongodb.password';
56
+ const DATABASE_NAME_INPUT_KEY = 'databaseName';
57
+ const ADMIN_CONNECT_URL_INPUT_KEY = 'adminConnectUrl';
58
+ const CONNECTION_STRING_ARTIFACT_KEY = 'connectionString';
59
+ /**
60
+ * Compute the per-app username. Mirrors the Atlas module's
61
+ * convention (`${ns}_user`) so the persistence shape is identical
62
+ * regardless of provider.
63
+ */
64
+ function buildMongodbUsername(applicationNamespace) {
65
+ return `${applicationNamespace}_user`;
66
+ }
67
+ /**
68
+ * MongoDB privilege ACTIONS, per grant axis.
69
+ *
70
+ * Named here rather than inlined because these ARE the security semantics: what
71
+ * "read" and "write" mean once they reach the database. Mongo's action
72
+ * vocabulary is an external one, so it lives at this adapter — the module that
73
+ * speaks Mongo — and never leaks into the platform's grant model.
74
+ *
75
+ * `find` alone for read: no `listCollections`, no `collStats`, nothing that
76
+ * enumerates. A minion granted read on one resource should not be able to
77
+ * discover what else exists.
78
+ *
79
+ * `insert`/`update`/`remove` for write, and deliberately NOT `find`. The
80
+ * platform-level derivation refuses to widen write into read, and re-adding it
81
+ * here would defeat that decision at the last hop — see
82
+ * `deriveMinionResourceGrants`.
83
+ */
84
+ const MONGODB_READ_ACTIONS = ['find'];
85
+ const MONGODB_WRITE_ACTIONS = ['insert', 'update', 'remove'];
86
+ /**
87
+ * Turn a minion's grants into collection-scoped MongoDB privileges (D14, R1).
88
+ *
89
+ * ## What this deliberately never produces
90
+ *
91
+ * No `dbAdmin` and no `readWrite` — the two roles the application backend
92
+ * currently holds on the whole database. `dbAdmin` is banned outright by D14;
93
+ * `readWrite` is database-WIDE, so granting it to a minion would make every
94
+ * collection reachable regardless of what `resourceAccess` declared, which is
95
+ * the containment this exists to create.
96
+ *
97
+ * No privilege on the database RESOURCE either (`{ db, collection: '' }`), which
98
+ * is Mongo's way of saying "every collection in this database". Every entry
99
+ * names one collection explicitly.
100
+ *
101
+ * ## Read and write are separate entries on purpose
102
+ *
103
+ * A collection granted both axes yields TWO privilege entries rather than one
104
+ * merged action list. Mongo unions them identically, but the emitted role then
105
+ * reads back as the declaration that produced it — which is what makes an
106
+ * operator inspecting `db.getRole()` able to check the grant against the config
107
+ * without re-deriving it.
108
+ */
109
+ export function buildMinionMongodbPrivileges(input) {
110
+ const privileges = [];
111
+ for (const collection of input.readCollections) {
112
+ privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_READ_ACTIONS] });
113
+ }
114
+ for (const collection of input.writeCollections) {
115
+ privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_WRITE_ACTIONS] });
116
+ }
117
+ return privileges;
118
+ }
119
+ /**
120
+ * Derive the per-app connect URL by swapping admin credentials and
121
+ * the database path in the authored `adminConnectUrl`. Mirrors the
122
+ * legacy logic at
123
+ * `wildo-application-lifecycle.service.ts:469-479`:
124
+ *
125
+ * const parsed = new URL(adminConnectUrl);
126
+ * parsed.username = perAppUsername;
127
+ * parsed.password = perAppPassword;
128
+ * parsed.pathname = '/' + databaseName;
129
+ *
130
+ * URL-encoding handled automatically by the WHATWG URL class.
131
+ */
132
+ function buildPerAppConnectUrl(adminConnectUrl, databaseName, username, password) {
133
+ const parsed = new URL(adminConnectUrl);
134
+ parsed.username = encodeURIComponent(username);
135
+ parsed.password = encodeURIComponent(password);
136
+ parsed.pathname = '/' + databaseName;
137
+ return parsed.toString();
138
+ }
139
+ /**
140
+ * Per-principal Mongo username — the SAME derivation the revoke path must use.
141
+ *
142
+ * The application backend keeps `<namespace>_user` VERBATIM so an existing
143
+ * deployment's user is never orphaned. Third time this shape appears (Redis,
144
+ * RabbitMQ, now Mongo): a revoke deriving it differently deletes the BACKEND's
145
+ * database user while believing it is removing a minion's.
146
+ */
147
+ function buildMongodbPrincipalUsername(applicationNamespace, principal) {
148
+ if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {
149
+ return buildMongodbUsername(applicationNamespace);
150
+ }
151
+ const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
152
+ return `${applicationNamespace}_${principal.kind}_${sanitizedName}_user`;
153
+ }
154
+ export const MongodbSelfInfraProviderModule = {
155
+ provider: InfraProvider.MONGODB_SELF,
156
+ supportedCapabilities: [InfraCapability.DOCUMENT_DATABASE],
157
+ perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,
158
+ plan() {
159
+ return {
160
+ dockerComposeServices: ['mongodb'],
161
+ k8sResources: ['templates/infra/k8s/internal/mongodb/deployment.yaml.hbs'],
162
+ platformAdmin: {
163
+ envVars: {},
164
+ // The single MONGO_CONNECT_URL secret carries the admin
165
+ // credentials embedded; legacy field name preserved.
166
+ secretEnvVars: ['MONGO_CONNECT_URL'],
167
+ consumers: [
168
+ InfraPlatformAdminConsumer.APPS_MANAGER,
169
+ InfraPlatformAdminConsumer.COMPANION,
170
+ ],
171
+ },
172
+ appRuntime: {
173
+ // Same persistence shape as Atlas — per-app username +
174
+ // password go into secrets; the host/protocol/options come
175
+ // from `_merge_WildoApplicationConfig` which derives them
176
+ // from env-config (BASE_MONGO_SERVER_URL).
177
+ perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],
178
+ },
179
+ };
180
+ },
181
+ async mintAppCredentials(context) {
182
+ const adminClient = context.adminClient;
183
+ const password = context.secretInputs[MONGODB_PASSWORD_INPUT_KEY];
184
+ if (!password) {
185
+ throw new Error(`MongodbSelfInfraProviderModule.mintAppCredentials: ` +
186
+ `secretInputs is missing '${MONGODB_PASSWORD_INPUT_KEY}'. ` +
187
+ `The orchestrator MUST supply the Mongo password — modules are upsert-only.`);
188
+ }
189
+ if (!context.applicationNamespace) {
190
+ throw new Error(`MongodbSelfInfraProviderModule.mintAppCredentials: ` +
191
+ `applicationNamespace is empty. ` +
192
+ `The orchestrator MUST supply a non-empty namespace.`);
193
+ }
194
+ const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];
195
+ if (!databaseName) {
196
+ throw new Error(`MongodbSelfInfraProviderModule.mintAppCredentials: ` +
197
+ `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}'. ` +
198
+ `The orchestrator MUST supply the per-app database name.`);
199
+ }
200
+ const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];
201
+ if (!adminConnectUrl) {
202
+ throw new Error(`MongodbSelfInfraProviderModule.mintAppCredentials: ` +
203
+ `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +
204
+ `The orchestrator MUST supply the admin Mongo connect URL.`);
205
+ }
206
+ const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);
207
+ if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {
208
+ // A named principal gets a COLLECTION-SCOPED user (Step 3 / R1 / D14):
209
+ // one custom role carrying exactly the privileges its `resourceAccess`
210
+ // derived, and nothing inherited.
211
+ //
212
+ // `resourceGrants` is REQUIRED here and its absence is an error rather
213
+ // than a default. Falling back to the app-wide upsert would hand a minion
214
+ // `readWrite` + `dbAdmin` on the whole database — the exact over-grant
215
+ // this path exists to replace, arriving silently through a code path that
216
+ // looks like it is doing the narrow thing.
217
+ const grants = context.resourceGrants;
218
+ if (!grants) {
219
+ throw new Error(`MongodbSelfInfraProviderModule.mintAppCredentials: `
220
+ + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `
221
+ + `Refusing to fall back to the database-wide application user — a minion with no declared `
222
+ + `access must get NO database user at all (D11/R3), which is the orchestrator's decision to make.`);
223
+ }
224
+ await adminClient.upsertCollectionScopedDatabaseUser({
225
+ adminConnectUrl,
226
+ databaseName,
227
+ username,
228
+ password,
229
+ roleName: `${username}_role`,
230
+ // Resource types → collection names HERE, at the adapter that speaks
231
+ // Mongo. The grant contract itself stays in the platform vocabulary.
232
+ privileges: buildMinionMongodbPrivileges({
233
+ databaseName,
234
+ readCollections: resolveGrantStorageNames(grants.read),
235
+ writeCollections: resolveGrantStorageNames(grants.write),
236
+ }),
237
+ });
238
+ }
239
+ else {
240
+ // createUser-or-updateUser over an admin-scoped mongoose
241
+ // connection — idempotent upsert.
242
+ await adminClient.upsertApplicationDatabaseUser({
243
+ adminConnectUrl,
244
+ databaseName,
245
+ username,
246
+ password,
247
+ });
248
+ }
249
+ // Derive the per-app SRV URL by swapping credentials + db path
250
+ // in the authored admin URL. The orchestrator uses this for
251
+ // transient app-init seeding; not persisted.
252
+ const connectionString = buildPerAppConnectUrl(adminConnectUrl, databaseName, username, password);
253
+ return {
254
+ secretBlobValues: {
255
+ [MONGODB_USERNAME_PATH]: username,
256
+ [MONGODB_PASSWORD_PATH]: password,
257
+ },
258
+ runtimeArtifacts: {
259
+ [CONNECTION_STRING_ARTIFACT_KEY]: connectionString,
260
+ },
261
+ };
262
+ },
263
+ /**
264
+ * Tear down the per-app Mongo database user.
265
+ *
266
+ * **Caller status**: no caller wires this in Slices 1–4.B.3.
267
+ * Forward-compat seam.
268
+ */
269
+ async revokeAppCredentials(context) {
270
+ if (!context.applicationNamespace) {
271
+ throw new Error(`MongodbSelfInfraProviderModule.revokeAppCredentials: ` +
272
+ `applicationNamespace is empty. ` +
273
+ `The orchestrator MUST supply a non-empty namespace.`);
274
+ }
275
+ const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];
276
+ if (!adminConnectUrl) {
277
+ throw new Error(`MongodbSelfInfraProviderModule.revokeAppCredentials: ` +
278
+ `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +
279
+ `The orchestrator MUST supply the admin Mongo connect URL.`);
280
+ }
281
+ const adminClient = context.adminClient;
282
+ // Derived through the SAME function the mint uses. Deriving it independently
283
+ // here — which is what this line did before named principals existed — meant
284
+ // revoking a minion left its credential live AND dropped the APPLICATION
285
+ // BACKEND's database user, because the backend's name is what
286
+ // `buildMongodbUsername` returns for every input.
287
+ const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);
288
+ if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {
289
+ // A scoped principal owns a user AND a custom role. Routing it through the
290
+ // user-only door would leave a privileged role behind that the next mint
291
+ // adopts without anybody declaring it.
292
+ const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];
293
+ if (!databaseName) {
294
+ throw new Error(`MongodbSelfInfraProviderModule.revokeAppCredentials: `
295
+ + `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}' for principal `
296
+ + `'${context.principal.kind}:${context.principal.name}'. The role to drop lives IN that `
297
+ + `database — revoking without it would drop the user and silently orphan its privileges.`);
298
+ }
299
+ await adminClient.dropCollectionScopedDatabaseUser({
300
+ adminConnectUrl,
301
+ databaseName,
302
+ username,
303
+ roleName: `${username}_role`,
304
+ });
305
+ return;
306
+ }
307
+ await adminClient.deleteApplicationDatabaseUser({ adminConnectUrl, username });
308
+ },
309
+ // No `mintPlatformServiceCredentials` (D-4).
310
+ };
311
+ //# sourceMappingURL=mongodb-self.infra.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mongodb-self.infra.module.js","sourceRoot":"","sources":["../../../../src/mongodb-self/mongodb-self.infra.module.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MongodbSelfInfraProviderModule` — fifth concrete `InfraProviderModule`\n * (Slice 4.B.3 of `infra-provider-planner.md`, generalizing the\n * pattern to self-managed MongoDB).\n *\n * Implements the `MONGODB_SELF` `InfraProvider` over the\n * `IMongoSelfAdminClient` transport (admin-side mongoose connection,\n * exposed by `MongoSelfManagerService` after the Slice 4.B.3 5C\n * policy/transport split). Owns ALL per-app naming policy +\n * URL-derivation policy — the per-app username convention\n * (`<applicationNamespace>_user`, mirroring the Atlas variant) and\n * the per-app SRV URL derivation by swapping admin credentials\n * with per-app credentials in the authored\n * `BASE_MONGO_SERVER_URL`-derived admin connect URL.\n *\n * **What this module reads from `AppCredentialMintingContext`**:\n * - `applicationNamespace` — used as the source for the per-app\n * username (legacy convention `${ns}_user`).\n * - `secretInputs['database.mongodb.password']` — orchestrator-\n * supplied password (module is upsert-only, never generator).\n * - `applicationConfigInputs['databaseName']` — required.\n * - `applicationConfigInputs['adminConnectUrl']` — required, the\n * admin-scoped MongoDB connect URL the orchestrator reads from\n * `MONGO_CONNECT_URL` env at app-init time.\n * - `adminClient` — narrowed to `IMongoSelfAdminClient`.\n *\n * **Two audiences this module serves:**\n *\n * 1. **Audience-1 (platform-admin)**: cluster-wide Mongo admin\n * connect URL (`MONGO_CONNECT_URL`). Consumers: `apps-manager`\n * (mints per-app users via `mintAppCredentials`), `companion`\n * (mirrors apps-manager admin role for local dev).\n *\n * 2. **Audience-3 (app-scoped, via `mintAppCredentials`)**:\n * per-customer-app Mongo database user. Result threaded into\n * `Wildo_Application_Secrets.database.mongodb.{username,password}`\n * via `_merge_WildoApplicationSecrets`; the per-app connect URL\n * returned via `runtimeArtifacts.connectionString` for\n * transient app-init operations.\n *\n * **NOT served (D-4)**: audience-2 not applicable.\n */\n\nimport {\n AppCredentialMintingContext,\n AppCredentialMintingResult,\n IMongoSelfAdminClient,\n InfraCapability,\n InfraPlatformAdminConsumer,\n InfraProvider,\n InfraProviderModule,\n InfraProviderPlanContribution,\n ApplicationRuntimePrincipalKind,\n resolveGrantStorageNames,\n type ApplicationRuntimePrincipal,\n} from '@wildo-ai/platform-config-lib';\n\nconst MONGODB_USERNAME_PATH = 'database.mongodb.username';\nconst MONGODB_PASSWORD_PATH = 'database.mongodb.password';\n/**\n * The paths this module mints PER RUNTIME PRINCIPAL — the single source for both consumers.\n *\n * Read by `InfraProviderModule.perPrincipalSecretBlobPaths` (statically, so apps-manager can strip\n * these from the application blob before delivering a principal's own material) AND spread into\n * `appRuntime.perAppSecretBlobPaths` inside `plan()`. Declaring the list twice is how the delivery\n * path would silently stop covering a path this module still mints.\n */\nconst PER_PRINCIPAL_SECRET_BLOB_PATHS: ReadonlyArray<string> = [MONGODB_USERNAME_PATH, MONGODB_PASSWORD_PATH];\n\n\nconst MONGODB_PASSWORD_INPUT_KEY = 'database.mongodb.password';\n\nconst DATABASE_NAME_INPUT_KEY = 'databaseName';\nconst ADMIN_CONNECT_URL_INPUT_KEY = 'adminConnectUrl';\n\nconst CONNECTION_STRING_ARTIFACT_KEY = 'connectionString';\n\n/**\n * Compute the per-app username. Mirrors the Atlas module's\n * convention (`${ns}_user`) so the persistence shape is identical\n * regardless of provider.\n */\nfunction buildMongodbUsername(applicationNamespace: string): string {\n return `${applicationNamespace}_user`;\n}\n\n/**\n * MongoDB privilege ACTIONS, per grant axis.\n *\n * Named here rather than inlined because these ARE the security semantics: what\n * \"read\" and \"write\" mean once they reach the database. Mongo's action\n * vocabulary is an external one, so it lives at this adapter — the module that\n * speaks Mongo — and never leaks into the platform's grant model.\n *\n * `find` alone for read: no `listCollections`, no `collStats`, nothing that\n * enumerates. A minion granted read on one resource should not be able to\n * discover what else exists.\n *\n * `insert`/`update`/`remove` for write, and deliberately NOT `find`. The\n * platform-level derivation refuses to widen write into read, and re-adding it\n * here would defeat that decision at the last hop — see\n * `deriveMinionResourceGrants`.\n */\nconst MONGODB_READ_ACTIONS: readonly string[] = ['find'];\nconst MONGODB_WRITE_ACTIONS: readonly string[] = ['insert', 'update', 'remove'];\n\n/** One `createRole`/`updateRole` privilege entry. */\nexport interface MongodbCollectionPrivilege {\n readonly resource: { readonly db: string; readonly collection: string };\n readonly actions: readonly string[];\n}\n\n/**\n * Turn a minion's grants into collection-scoped MongoDB privileges (D14, R1).\n *\n * ## What this deliberately never produces\n *\n * No `dbAdmin` and no `readWrite` — the two roles the application backend\n * currently holds on the whole database. `dbAdmin` is banned outright by D14;\n * `readWrite` is database-WIDE, so granting it to a minion would make every\n * collection reachable regardless of what `resourceAccess` declared, which is\n * the containment this exists to create.\n *\n * No privilege on the database RESOURCE either (`{ db, collection: '' }`), which\n * is Mongo's way of saying \"every collection in this database\". Every entry\n * names one collection explicitly.\n *\n * ## Read and write are separate entries on purpose\n *\n * A collection granted both axes yields TWO privilege entries rather than one\n * merged action list. Mongo unions them identically, but the emitted role then\n * reads back as the declaration that produced it — which is what makes an\n * operator inspecting `db.getRole()` able to check the grant against the config\n * without re-deriving it.\n */\nexport function buildMinionMongodbPrivileges(input: {\n databaseName: string;\n readCollections: readonly string[];\n writeCollections: readonly string[];\n}): MongodbCollectionPrivilege[] {\n const privileges: MongodbCollectionPrivilege[] = [];\n for (const collection of input.readCollections) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_READ_ACTIONS] });\n }\n for (const collection of input.writeCollections) {\n privileges.push({ resource: { db: input.databaseName, collection }, actions: [...MONGODB_WRITE_ACTIONS] });\n }\n return privileges;\n}\n\n/**\n * Derive the per-app connect URL by swapping admin credentials and\n * the database path in the authored `adminConnectUrl`. Mirrors the\n * legacy logic at\n * `wildo-application-lifecycle.service.ts:469-479`:\n *\n * const parsed = new URL(adminConnectUrl);\n * parsed.username = perAppUsername;\n * parsed.password = perAppPassword;\n * parsed.pathname = '/' + databaseName;\n *\n * URL-encoding handled automatically by the WHATWG URL class.\n */\nfunction buildPerAppConnectUrl(\n adminConnectUrl: string,\n databaseName: string,\n username: string,\n password: string,\n): string {\n const parsed = new URL(adminConnectUrl);\n parsed.username = encodeURIComponent(username);\n parsed.password = encodeURIComponent(password);\n parsed.pathname = '/' + databaseName;\n return parsed.toString();\n}\n\n/**\n * Per-principal Mongo username — the SAME derivation the revoke path must use.\n *\n * The application backend keeps `<namespace>_user` VERBATIM so an existing\n * deployment's user is never orphaned. Third time this shape appears (Redis,\n * RabbitMQ, now Mongo): a revoke deriving it differently deletes the BACKEND's\n * database user while believing it is removing a minion's.\n */\nfunction buildMongodbPrincipalUsername(\n applicationNamespace: string,\n principal: ApplicationRuntimePrincipal | undefined,\n): string {\n if (!principal || principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n return buildMongodbUsername(applicationNamespace);\n }\n const sanitizedName = (principal.name ?? '').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');\n return `${applicationNamespace}_${principal.kind}_${sanitizedName}_user`;\n}\n\nexport const MongodbSelfInfraProviderModule: InfraProviderModule = {\n provider: InfraProvider.MONGODB_SELF,\n supportedCapabilities: [InfraCapability.DOCUMENT_DATABASE],\n perPrincipalSecretBlobPaths: PER_PRINCIPAL_SECRET_BLOB_PATHS,\n\n plan(): InfraProviderPlanContribution {\n return {\n dockerComposeServices: ['mongodb'],\n k8sResources: ['templates/infra/k8s/internal/mongodb/deployment.yaml.hbs'],\n platformAdmin: {\n envVars: {},\n // The single MONGO_CONNECT_URL secret carries the admin\n // credentials embedded; legacy field name preserved.\n secretEnvVars: ['MONGO_CONNECT_URL'],\n consumers: [\n InfraPlatformAdminConsumer.APPS_MANAGER,\n InfraPlatformAdminConsumer.COMPANION,\n ],\n },\n appRuntime: {\n // Same persistence shape as Atlas — per-app username +\n // password go into secrets; the host/protocol/options come\n // from `_merge_WildoApplicationConfig` which derives them\n // from env-config (BASE_MONGO_SERVER_URL).\n perAppSecretBlobPaths: [...PER_PRINCIPAL_SECRET_BLOB_PATHS],\n },\n };\n },\n\n async mintAppCredentials(\n context: AppCredentialMintingContext,\n ): Promise<AppCredentialMintingResult> {\n const adminClient = context.adminClient as IMongoSelfAdminClient;\n const password = context.secretInputs[MONGODB_PASSWORD_INPUT_KEY];\n if (!password) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `secretInputs is missing '${MONGODB_PASSWORD_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the Mongo password — modules are upsert-only.`,\n );\n }\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the per-app database name.`,\n );\n }\n const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];\n if (!adminConnectUrl) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: ` +\n `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the admin Mongo connect URL.`,\n );\n }\n\n const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A named principal gets a COLLECTION-SCOPED user (Step 3 / R1 / D14):\n // one custom role carrying exactly the privileges its `resourceAccess`\n // derived, and nothing inherited.\n //\n // `resourceGrants` is REQUIRED here and its absence is an error rather\n // than a default. Falling back to the app-wide upsert would hand a minion\n // `readWrite` + `dbAdmin` on the whole database — the exact over-grant\n // this path exists to replace, arriving silently through a code path that\n // looks like it is doing the narrow thing.\n const grants = context.resourceGrants;\n if (!grants) {\n throw new Error(\n `MongodbSelfInfraProviderModule.mintAppCredentials: `\n + `principal '${context.principal.kind}:${context.principal.name}' has no resourceGrants. `\n + `Refusing to fall back to the database-wide application user — a minion with no declared `\n + `access must get NO database user at all (D11/R3), which is the orchestrator's decision to make.`,\n );\n }\n await adminClient.upsertCollectionScopedDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n password,\n roleName: `${username}_role`,\n // Resource types → collection names HERE, at the adapter that speaks\n // Mongo. The grant contract itself stays in the platform vocabulary.\n privileges: buildMinionMongodbPrivileges({\n databaseName,\n readCollections: resolveGrantStorageNames(grants.read),\n writeCollections: resolveGrantStorageNames(grants.write),\n }),\n });\n } else {\n // createUser-or-updateUser over an admin-scoped mongoose\n // connection — idempotent upsert.\n await adminClient.upsertApplicationDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n password,\n });\n }\n\n // Derive the per-app SRV URL by swapping credentials + db path\n // in the authored admin URL. The orchestrator uses this for\n // transient app-init seeding; not persisted.\n const connectionString = buildPerAppConnectUrl(\n adminConnectUrl,\n databaseName,\n username,\n password,\n );\n\n return {\n secretBlobValues: {\n [MONGODB_USERNAME_PATH]: username,\n [MONGODB_PASSWORD_PATH]: password,\n },\n runtimeArtifacts: {\n [CONNECTION_STRING_ARTIFACT_KEY]: connectionString,\n },\n };\n },\n\n /**\n * Tear down the per-app Mongo database user.\n *\n * **Caller status**: no caller wires this in Slices 1–4.B.3.\n * Forward-compat seam.\n */\n async revokeAppCredentials(context: AppCredentialMintingContext): Promise<void> {\n if (!context.applicationNamespace) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationNamespace is empty. ` +\n `The orchestrator MUST supply a non-empty namespace.`,\n );\n }\n const adminConnectUrl = context.applicationConfigInputs?.[ADMIN_CONNECT_URL_INPUT_KEY];\n if (!adminConnectUrl) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: ` +\n `applicationConfigInputs is missing '${ADMIN_CONNECT_URL_INPUT_KEY}'. ` +\n `The orchestrator MUST supply the admin Mongo connect URL.`,\n );\n }\n const adminClient = context.adminClient as IMongoSelfAdminClient;\n // Derived through the SAME function the mint uses. Deriving it independently\n // here — which is what this line did before named principals existed — meant\n // revoking a minion left its credential live AND dropped the APPLICATION\n // BACKEND's database user, because the backend's name is what\n // `buildMongodbUsername` returns for every input.\n const username = buildMongodbPrincipalUsername(context.applicationNamespace, context.principal);\n\n if (context.principal && context.principal.kind !== ApplicationRuntimePrincipalKind.APPLICATION_BACKEND) {\n // A scoped principal owns a user AND a custom role. Routing it through the\n // user-only door would leave a privileged role behind that the next mint\n // adopts without anybody declaring it.\n const databaseName = context.applicationConfigInputs?.[DATABASE_NAME_INPUT_KEY];\n if (!databaseName) {\n throw new Error(\n `MongodbSelfInfraProviderModule.revokeAppCredentials: `\n + `applicationConfigInputs is missing '${DATABASE_NAME_INPUT_KEY}' for principal `\n + `'${context.principal.kind}:${context.principal.name}'. The role to drop lives IN that `\n + `database — revoking without it would drop the user and silently orphan its privileges.`,\n );\n }\n await adminClient.dropCollectionScopedDatabaseUser({\n adminConnectUrl,\n databaseName,\n username,\n roleName: `${username}_role`,\n });\n return;\n }\n\n await adminClient.deleteApplicationDatabaseUser({ adminConnectUrl, username });\n },\n\n // No `mintPlatformServiceCredentials` (D-4).\n};\n"]}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `PostgresSelfInfraProviderModule` — third concrete `InfraProviderModule`
3
+ * (Slice 4.B.2 of `infra-provider-planner.md`, generalizing the
4
+ * Slice 2 / 4.B.1 pattern).
5
+ *
6
+ * Implements the `POSTGRES_SELF` `InfraProvider` over the
7
+ * `IPostgresAdminClient` transport (raw `pg` Pool against the
8
+ * Postgres admin connection, exposed by
9
+ * `PostgreSQLConnectionManagerService` after the Slice 4.B.2 5C
10
+ * policy/transport split). Owns ALL per-app naming policy — the
11
+ * per-app database-name + role-name conventions (`wildo_<sanitized>_user`,
12
+ * `wildo_<sanitized>` mirroring the legacy
13
+ * `PlatformInfrastructureManagerService.provisionPostgreSQLDatabase`).
14
+ *
15
+ * **What this module reads from `AppCredentialMintingContext`**:
16
+ * - `appSlug` — used as the source for the sanitized database +
17
+ * user name. Mirrors the legacy convention at
18
+ * `platform-infrastructure-manager.service.ts:300-302`
19
+ * (`sanitizedName = sanitizeForPostgreSQL(applicationName)`,
20
+ * `databaseName = wildo_${sanitizedName}`,
21
+ * `username = wildo_${sanitizedName}_user`).
22
+ * - `secretInputs['database.postgresql.password']` — orchestrator-
23
+ * supplied password (module is upsert-only, never generator).
24
+ * - `applicationConfigInputs?.['databaseName']` — optional override
25
+ * for the derived database name. Mirrors the legacy
26
+ * `provisionPostgreSQLDatabase(appSlug, { databaseName? })` arg.
27
+ * - `adminClient` — narrowed to `IPostgresAdminClient`.
28
+ *
29
+ * **What this module IGNORES from the context** (Slice 4.A.2 / M4
30
+ * forward-compat documentation):
31
+ * - `applicationNamespace` — Postgres per-app DB names use the
32
+ * slug-derived sanitized form (env-suffix isolation comes from
33
+ * the surrounding env-scoped admin connection). Future modules
34
+ * may switch to namespace-scoped naming if cross-env collision
35
+ * risk surfaces.
36
+ * - `wildoApplicationId` — Postgres role names embed the
37
+ * sanitized slug, NOT the application id. This is the legacy
38
+ * convention; preserved for back-compat with apps already
39
+ * provisioned.
40
+ *
41
+ * **Two audiences this module serves:**
42
+ *
43
+ * 1. **Audience-1 (platform-admin, declared via `plan().platformAdmin`)**:
44
+ * cluster-wide Postgres admin creds. Consumers: `apps-manager`
45
+ * (mints per-app DB+role via `mintAppCredentials`), `companion`
46
+ * (mirrors apps-manager admin role for local dev).
47
+ *
48
+ * 2. **Audience-3 (app-scoped, via `plan().appRuntime` +
49
+ * `mintAppCredentials`)**: per-customer-app Postgres database +
50
+ * role. Result threaded into
51
+ * `Wildo_Application_Secrets.database.postgresql.{
52
+ * databaseName, username, connectUrl}` via
53
+ * `_merge_WildoApplicationSecrets`. Note: the password is
54
+ * embedded in `connectUrl` — there is no separate
55
+ * `database.postgresql.password` blob path today (matches the
56
+ * legacy persistence shape at
57
+ * `wildo-application-lifecycle.service.ts:2191-2200`).
58
+ *
59
+ * **NOT served (D-4 of Slice 4.B locked decisions, 2026-05-10)**:
60
+ * - **Audience-2 (platform-service-scoped)**: no platform service
61
+ * uses Postgres for inter-service messaging today. Method omitted.
62
+ *
63
+ * **`revokeAppCredentials`**: declared and tested, but the integration
64
+ * call site does NOT exist in `wildo-application-lifecycle.service.ts`
65
+ * (no app-deletion ceremony today — F6 of plan review 2026-05-06).
66
+ * Method ships as a forward-compat seam.
67
+ */
68
+ import { InfraProviderModule } from '@wildo-ai/platform-config-lib';
69
+ export declare const PostgresSelfInfraProviderModule: InfraProviderModule;
70
+ //# sourceMappingURL=postgres-self.infra.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-self.infra.module.d.ts","sourceRoot":"","sources":["../../../../src/postgres-self/postgres-self.infra.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,EAOL,mBAAmB,EAKpB,MAAM,+BAA+B,CAAC;AA6HvC,eAAO,MAAM,+BAA+B,EAAE,mBA2K7C,CAAC"}