@prisma/composer-prisma-cloud 0.1.0 → 0.2.0

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 (47) hide show
  1. package/dist/control.d.mts +52 -38
  2. package/dist/control.mjs +1050 -326
  3. package/dist/control.mjs.map +1 -1
  4. package/dist/cron/index.d.mts +9 -6
  5. package/dist/cron/index.mjs +114 -5
  6. package/dist/cron/index.mjs.map +1 -1
  7. package/dist/cron/scheduler-entrypoint.mjs +281 -111
  8. package/dist/cron/scheduler-entrypoint.mjs.map +1 -1
  9. package/dist/cron/scheduler-service.mjs +114 -5
  10. package/dist/cron/scheduler-service.mjs.map +1 -1
  11. package/dist/index.d.mts +118 -23
  12. package/dist/index.mjs +44 -2
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/{prisma-next-COrwlg3N.mjs → prisma-next-DlU01kXJ-2sP5mQb6.mjs} +3 -3
  15. package/dist/prisma-next-DlU01kXJ-2sP5mQb6.mjs.map +1 -0
  16. package/dist/prisma-next.d.mts +1 -2
  17. package/dist/prisma-next.mjs +1 -1
  18. package/dist/provisioned-edges-DIQAR4q4-Bn9op-JG.mjs +151 -0
  19. package/dist/provisioned-edges-DIQAR4q4-Bn9op-JG.mjs.map +1 -0
  20. package/dist/{serializer-C2CsA7xm-29Eg2Tjl.mjs → serializer-CX4VYdf_-KKGoAxfx.mjs} +61 -7
  21. package/dist/serializer-CX4VYdf_-KKGoAxfx.mjs.map +1 -0
  22. package/dist/serializer-Cx5slrV4-xJfH6EWS.d.mts +36 -0
  23. package/dist/storage/index.d.mts +5 -16
  24. package/dist/storage/index.mjs +114 -3
  25. package/dist/storage/index.mjs.map +1 -1
  26. package/dist/storage/storage-entrypoint.mjs +7242 -12
  27. package/dist/storage/storage-entrypoint.mjs.map +1 -1
  28. package/dist/storage/storage-service.mjs +114 -3
  29. package/dist/storage/storage-service.mjs.map +1 -1
  30. package/dist/storage/testing.d.mts +7 -4
  31. package/dist/storage/testing.mjs.map +1 -1
  32. package/dist/streams/index.d.mts +250 -0
  33. package/dist/streams/index.mjs +4058 -0
  34. package/dist/streams/index.mjs.map +1 -0
  35. package/dist/streams/streams-entrypoint.mjs +48095 -0
  36. package/dist/streams/streams-entrypoint.mjs.map +1 -0
  37. package/dist/streams/streams-service.mjs +811 -0
  38. package/dist/streams/streams-service.mjs.map +1 -0
  39. package/dist/streams/testing.d.mts +33 -0
  40. package/dist/streams/testing.mjs +31335 -0
  41. package/dist/streams/testing.mjs.map +1 -0
  42. package/dist/testing.d.mts +1 -2
  43. package/dist/testing.mjs +1 -1
  44. package/dist/testing.mjs.map +1 -1
  45. package/package.json +27 -22
  46. package/dist/prisma-next-COrwlg3N.mjs.map +0 -1
  47. package/dist/serializer-C2CsA7xm-29Eg2Tjl.mjs.map +0 -1
package/dist/control.mjs CHANGED
@@ -1,12 +1,14 @@
1
- import { c as secretPointerRows, i as encode, o as paramEntries, s as secretName, t as configKey } from "./serializer-C2CsA7xm-29Eg2Tjl.mjs";
2
- import { a as normalizeSslMode, n as isPnPostgresResourceNode, o as withConnectionRetry } from "./prisma-next-COrwlg3N.mjs";
3
- import { provisionManifest } from "@prisma/composer";
1
+ import { a as encodeParamPointer, c as secretName, i as encode, l as secretPointerRows, s as paramEntries, t as configKey } from "./serializer-CX4VYdf_-KKGoAxfx.mjs";
2
+ import { a as isEnvParamSource, c as provisionedEdges, n as STREAMS_API_KEY, o as paramBindingFor, s as paramName, t as RESERVED_PROVIDER_PARAMS } from "./provisioned-edges-DIQAR4q4-Bn9op-JG.mjs";
3
+ import { a as normalizeSslMode, n as isPnPostgresResourceNode, o as withConnectionRetry } from "./prisma-next-DlU01kXJ-2sP5mQb6.mjs";
4
+ import { isParamSource, paramManifest, provisionManifest } from "@prisma/composer";
4
5
  import { blindCast } from "@prisma/composer/casts";
6
+ import { RPC_PEER_KEY } from "@prisma/composer/service-rpc";
5
7
  import pg from "pg";
6
- import * as Layer from "effect/Layer";
7
8
  import { createManagementApiClient } from "@prisma/management-api-sdk";
8
9
  import * as Context from "effect/Context";
9
10
  import * as Effect from "effect/Effect";
11
+ import * as Layer from "effect/Layer";
10
12
  import * as Redacted from "effect/Redacted";
11
13
  import * as Config$1 from "effect/Config";
12
14
  import * as Data from "effect/Data";
@@ -18,15 +20,15 @@ import * as crypto$1 from "node:crypto";
18
20
  import * as os from "node:os";
19
21
  import * as path from "node:path";
20
22
  import * as zlib from "node:zlib";
21
- import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveStateRecursive } from "alchemy/State";
22
23
  import postgres from "postgres";
24
+ import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveStateRecursive } from "alchemy/State";
23
25
  import * as Output from "alchemy/Output";
24
- import { loadConfig } from "@prisma-next/cli/config-loader";
26
+ import { loadConfig } from "@prisma-next/config-loader";
25
27
  import { resolve } from "pathe";
26
28
  import { readRef } from "@prisma-next/migration-tools/refs";
27
29
  import { APP_SPACE_ID, readContractSpaceHeadRef, spaceMigrationDirectory, spaceRefsDirectory } from "@prisma-next/migration-tools/spaces";
28
30
  import { createPostgresControlClient } from "@prisma-next/postgres/control";
29
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CxGdfSAP.mjs
31
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CJM0E6rj.mjs
30
32
  /**
31
33
  * The Prisma service token used to authenticate Management API calls. Kept
32
34
  * as a Redacted value so it never lands in logs or error output.
@@ -66,9 +68,87 @@ const callOptional = (f) => attempt$1(f).pipe(Effect.flatMap((r) => r.response.s
66
68
  /** Fire-and-forget a call, tolerating a 404 (already deleted). */
67
69
  const callVoid = (f) => attempt$1(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
68
70
  //#endregion
69
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/compute/index.mjs
71
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/buckets.mjs
72
+ /** A Prisma **Object Store bucket** inside a project. */
73
+ const Bucket = Resource("Prisma.Bucket");
74
+ const BucketProvider = () => Provider.effect(Bucket, Effect.gen(function* () {
75
+ const client = yield* ManagementClient;
76
+ return {
77
+ stables: ["id"],
78
+ list: () => Effect.succeed([]),
79
+ reconcile: Effect.fn(function* ({ news, output }) {
80
+ const observed = output?.id ? yield* callOptional(() => client.GET("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } })) : void 0;
81
+ if (observed) return {
82
+ id: observed.data.id,
83
+ name: observed.data.name
84
+ };
85
+ const created = yield* call(() => client.POST("/v1/buckets", { body: {
86
+ projectId: news.projectId,
87
+ name: news.name,
88
+ ...news.branchId !== void 0 ? { branchId: news.branchId } : {}
89
+ } }));
90
+ return {
91
+ id: created.data.id,
92
+ name: created.data.name
93
+ };
94
+ }),
95
+ delete: Effect.fn(function* ({ output }) {
96
+ yield* callVoid(() => client.DELETE("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } }));
97
+ }),
98
+ read: Effect.fn(function* ({ output }) {
99
+ if (!output?.id) return void 0;
100
+ const b = yield* callOptional(() => client.GET("/v1/buckets/{bucketId}", { params: { path: { bucketId: output.id } } }));
101
+ return b ? {
102
+ id: b.data.id,
103
+ name: b.data.name
104
+ } : void 0;
105
+ })
106
+ };
107
+ }));
108
+ /** A **bucket access key** for a Prisma Object Store bucket — yields the S3 credentials. */
109
+ const BucketKey = Resource("Prisma.BucketKey");
110
+ const BucketKeyProvider = () => Provider.effect(BucketKey, Effect.gen(function* () {
111
+ const client = yield* ManagementClient;
112
+ return {
113
+ stables: [
114
+ "id",
115
+ "bucketId",
116
+ "secretAccessKey",
117
+ "accessKeyId",
118
+ "endpoint",
119
+ "bucketName"
120
+ ],
121
+ list: () => Effect.succeed([]),
122
+ reconcile: Effect.fn(function* ({ news, output }) {
123
+ if (output?.id) return output;
124
+ const created = yield* call(() => client.POST("/v1/buckets/{bucketId}/keys", {
125
+ params: { path: { bucketId: news.bucketId } },
126
+ body: {
127
+ name: news.name,
128
+ role: news.role
129
+ }
130
+ }));
131
+ return {
132
+ id: created.data.id,
133
+ bucketId: news.bucketId,
134
+ accessKeyId: created.data.accessKeyId,
135
+ secretAccessKey: Redacted.make(created.data.secretAccessKey),
136
+ endpoint: created.data.endpoint,
137
+ bucketName: created.data.bucketName
138
+ };
139
+ }),
140
+ delete: Effect.fn(function* ({ output }) {
141
+ yield* callVoid(() => client.DELETE("/v1/buckets/{bucketId}/keys/{keyId}", { params: { path: {
142
+ bucketId: output.bucketId,
143
+ keyId: output.id
144
+ } } }));
145
+ })
146
+ };
147
+ }));
148
+ //#endregion
149
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/compute.mjs
70
150
  /**
71
- * Stopping a deployment before the compute service that owns it can be
151
+ * Stopping a deployment before the app that owns it can be
72
152
  * deleted is asynchronous on the platform's side: DELETE can 409 with this
73
153
  * message while the deployment is still winding down. Retrying blindly on
74
154
  * every API error would mask real failures (bad auth, a genuinely conflicting
@@ -99,64 +179,62 @@ const ComputeServiceProvider = () => Provider.effect(ComputeService, Effect.gen(
99
179
  stables: ["id"],
100
180
  list: () => Effect.succeed([]),
101
181
  reconcile: Effect.fn(function* ({ news, output }) {
102
- const observed = output?.id ? yield* callOptional(() => client.GET("/v1/compute-services/{computeServiceId}", { params: { path: { computeServiceId: output.id } } })) : void 0;
182
+ const observed = output?.id ? yield* callOptional(() => client.GET("/v1/apps/{appId}", { params: { path: { appId: output.id } } })) : void 0;
103
183
  if (observed) return {
104
184
  id: observed.data.id,
105
185
  name: observed.data.name,
106
- endpointDomain: observed.data.serviceEndpointDomain
186
+ endpointDomain: observed.data.appEndpointDomain
107
187
  };
108
- const created = yield* call(() => client.POST("/v1/projects/{projectId}/compute-services", {
109
- params: { path: { projectId: news.projectId } },
110
- body: {
111
- displayName: news.name,
112
- ...news.region && { regionId: news.region },
113
- ...news.branchId !== void 0 && { branchId: news.branchId }
114
- }
115
- }));
188
+ const created = yield* call(() => client.POST("/v1/apps", { body: {
189
+ displayName: news.name,
190
+ projectId: news.projectId,
191
+ ...news.region && { regionId: news.region },
192
+ ...news.branchId !== void 0 && { branchId: news.branchId }
193
+ } }));
116
194
  return {
117
195
  id: created.data.id,
118
196
  name: created.data.name,
119
- endpointDomain: created.data.serviceEndpointDomain
197
+ endpointDomain: created.data.appEndpointDomain
120
198
  };
121
199
  }),
122
200
  delete: Effect.fn(function* ({ output }) {
123
- yield* callVoid(() => client.DELETE("/v1/compute-services/{computeServiceId}", { params: { path: { computeServiceId: output.id } } })).pipe(Effect.retry({
201
+ yield* callVoid(() => client.DELETE("/v1/apps/{appId}", { params: { path: { appId: output.id } } })).pipe(Effect.retry({
124
202
  schedule: deleteSafeRetrySchedule,
125
203
  while: isDeleteNotSafeYet
126
204
  }));
127
205
  }),
128
206
  read: Effect.fn(function* ({ output }) {
129
207
  if (!output?.id) return void 0;
130
- const s = yield* callOptional(() => client.GET("/v1/compute-services/{computeServiceId}", { params: { path: { computeServiceId: output.id } } }));
208
+ const s = yield* callOptional(() => client.GET("/v1/apps/{appId}", { params: { path: { appId: output.id } } }));
131
209
  return s ? {
132
210
  id: s.data.id,
133
211
  name: s.data.name,
134
- endpointDomain: s.data.serviceEndpointDomain
212
+ endpointDomain: s.data.appEndpointDomain
135
213
  } : void 0;
136
214
  })
137
215
  };
138
216
  }));
139
217
  /**
140
- * A **deployment** of a Prisma Compute service — creates a version, uploads
218
+ * A **deployment** of a Prisma app — creates a deployment, uploads
141
219
  * its artifact, starts the VM, waits for it to run, then promotes it to the
142
- * service's stable endpoint.
220
+ * app's stable endpoint.
143
221
  */
144
222
  const Deployment = Resource("Prisma.Deployment");
145
223
  const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
146
224
  const client = yield* ManagementClient;
147
- const waitForRunning = (versionId) => call(() => client.GET("/v1/compute-services/versions/{versionId}", { params: { path: { versionId } } })).pipe(Effect.flatMap((v) => v.data.status === "running" ? Effect.void : Effect.fail(new PrismaApiError({
225
+ const waitForRunning = (deploymentId) => call(() => client.GET("/v1/deployments/{deploymentId}", { params: { path: { deploymentId } } })).pipe(Effect.flatMap((v) => v.data.status === "running" ? Effect.void : Effect.fail(new PrismaApiError({
148
226
  status: 409,
149
- message: `compute version ${versionId} is ${v.data.status}, not running`
227
+ message: `deployment ${deploymentId} is ${v.data.status}, not running`
150
228
  }))), Effect.retry(Schedule.both(Schedule.spaced("2 seconds"), Schedule.during("2 minutes"))));
151
229
  return {
152
230
  stables: [],
153
231
  list: () => Effect.succeed([]),
154
232
  reconcile: Effect.fn(function* ({ news }) {
155
- const created = yield* call(() => client.POST("/v1/compute-services/{computeServiceId}/versions", {
156
- params: { path: { computeServiceId: news.computeServiceId } },
233
+ const created = yield* call(() => client.POST("/v1/apps/{appId}/deployments", {
234
+ params: { path: { appId: news.computeServiceId } },
157
235
  body: news.port !== void 0 ? { portMapping: { http: news.port } } : {}
158
236
  }));
159
- const versionId = created.data.id;
237
+ const deploymentId = created.data.id;
160
238
  if (created.data.uploadUrl) {
161
239
  const uploadUrl = created.data.uploadUrl;
162
240
  const artifact = yield* Effect.try({
@@ -183,24 +261,23 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
183
261
  })
184
262
  });
185
263
  }
186
- yield* call(() => client.POST("/v1/compute-services/versions/{versionId}/start", { params: { path: { versionId } } }));
187
- yield* waitForRunning(versionId);
188
- yield* call(() => client.POST("/v1/compute-services/{computeServiceId}/promote", {
189
- params: { path: { computeServiceId: news.computeServiceId } },
190
- body: { versionId }
191
- }));
192
- const deployedUrl = (yield* call(() => client.GET("/v1/compute-services/{computeServiceId}", { params: { path: { computeServiceId: news.computeServiceId } } }))).data.serviceEndpointDomain;
264
+ yield* call(() => client.POST("/v1/deployments/{deploymentId}/start", { params: { path: { deploymentId } } }));
265
+ yield* waitForRunning(deploymentId);
266
+ const deployedUrl = (yield* call(() => client.POST("/v1/apps/{appId}/promote", {
267
+ params: { path: { appId: news.computeServiceId } },
268
+ body: { deploymentId }
269
+ }))).data.appEndpointDomain;
193
270
  return {
194
- versionId,
271
+ deploymentId,
195
272
  ...deployedUrl !== void 0 && { deployedUrl }
196
273
  };
197
274
  }),
198
275
  delete: Effect.fn(function* () {}),
199
276
  read: Effect.fn(function* ({ output }) {
200
- if (!output?.versionId) return void 0;
201
- const v = yield* callOptional(() => client.GET("/v1/compute-services/versions/{versionId}", { params: { path: { versionId: output.versionId } } }));
277
+ if (!output?.deploymentId) return void 0;
278
+ const v = yield* callOptional(() => client.GET("/v1/deployments/{deploymentId}", { params: { path: { deploymentId: output.deploymentId } } }));
202
279
  return v ? {
203
- versionId: v.data.id,
280
+ deploymentId: v.data.id,
204
281
  ...v.data.previewDomain && { deployedUrl: v.data.previewDomain }
205
282
  } : void 0;
206
283
  })
@@ -231,7 +308,7 @@ const EnvironmentVariableProvider = () => Provider.effect(EnvironmentVariable, E
231
308
  key: news.key
232
309
  } } }))).data?.[0]?.id;
233
310
  if (matchId !== void 0) {
234
- if (!(news.key === "DATABASE_URL" || news.key === "DATABASE_URL_POOLED")) throw new Error(`EnvironmentVariable "${news.key}" (project "${news.projectId}", class "${cls}") exists but is untracked in this deploy state — refusing to overwrite a reserved COMPOSE_ key. Restore this deploy's hosted state, or remove the variable to let this deploy recreate it.`);
311
+ if (!(news.key === "DATABASE_URL" || news.key === "DATABASE_URL_POOLED")) throw new Error(`EnvironmentVariable "${news.key}" (project "${news.projectId}", class "${cls}") exists but is untracked in this deploy state — refusing to overwrite a reserved COMPOSER_ key. Restore this deploy's hosted state, or remove the variable to let this deploy recreate it.`);
235
312
  id = matchId;
236
313
  }
237
314
  }
@@ -400,8 +477,39 @@ function packageComputeArtifact(opts) {
400
477
  sha256
401
478
  };
402
479
  }
480
+ /**
481
+ * The `ServiceKey` Alchemy resource (ADR-0030) — mints a random 256-bit key
482
+ * ONCE at create and keeps it STABLE across deploys, so an unchanged edge
483
+ * no-ops on redeploy. Same mint-once-stable lifecycle as `S3Credentials`
484
+ * (`packages/1-prisma-cloud/1-extensions/target/src/s3-credentials-resource.ts`):
485
+ * Web Crypto only (`crypto.getRandomValues` — no `node:` import), persisted in
486
+ * Alchemy state; on every later apply the provider returns the persisted
487
+ * attributes (`reconcile`'s `output`) unchanged. One resource per RPC edge;
488
+ * rotation is destroy/recreate.
489
+ */
490
+ /** The `ServiceKey` resource constructor — `yield* Prisma.ServiceKey(id, {})` in the lowering. */
491
+ const ServiceKey = Resource("PrismaCloud.ServiceKey");
492
+ /** A fresh 256-bit key as 64 lowercase hex chars (Web Crypto — no node import). */
493
+ function mintServiceKey() {
494
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
495
+ return { value: Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("") };
496
+ }
497
+ /**
498
+ * The `ServiceKey` provider service. `reconcile` runs for create and update;
499
+ * it returns the persisted `output` when present (a redeploy reuses the
500
+ * stored key — the no-op property) and mints a fresh key only on first
501
+ * create. Nothing to enumerate (`list` → `[]`) or tear down (`delete` →
502
+ * no-op; the key lives only in state).
503
+ */
504
+ const serviceKeyProviderService = {
505
+ list: () => Effect.succeed([]),
506
+ reconcile: ({ output }) => Effect.sync(() => output ?? mintServiceKey()),
507
+ delete: () => Effect.void
508
+ };
509
+ /** The `ServiceKey` provider layer — merged into the Prisma Cloud extension's `providers()`. */
510
+ const ServiceKeyProvider = () => Provider.effect(ServiceKey, Effect.succeed(serviceKeyProviderService));
403
511
  //#endregion
404
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/postgres/index.mjs
512
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/postgres.mjs
405
513
  /** A **connection** to a Prisma Postgres database — yields the connection string. */
406
514
  const Connection = Resource("Prisma.Connection");
407
515
  const ConnectionProvider = () => Provider.effect(Connection, Effect.gen(function* () {
@@ -440,25 +548,23 @@ const DatabaseProvider = () => Provider.effect(Database, Effect.gen(function* ()
440
548
  list: () => Effect.succeed([]),
441
549
  reconcile: Effect.fn(function* ({ news, output }) {
442
550
  const observed = output?.id ? yield* callOptional(() => client.GET("/v1/databases/{databaseId}", { params: { path: { databaseId: output.id } } })) : void 0;
443
- let result;
444
- if (observed) result = {
445
- id: observed.data.id,
446
- name: observed.data.name
447
- };
448
- else {
449
- const created = yield* call(() => client.POST("/v1/projects/{projectId}/databases", {
450
- params: { path: { projectId: news.projectId } },
451
- body: {
452
- name: news.name,
453
- region: news.region,
454
- ...news.isDefault !== void 0 && { isDefault: news.isDefault }
455
- }
456
- }));
457
- result = {
551
+ if (!observed) {
552
+ const created = yield* call(() => client.POST("/v1/databases", { body: {
553
+ projectId: news.projectId,
554
+ name: news.name,
555
+ region: news.region,
556
+ ...news.isDefault !== void 0 && { isDefault: news.isDefault },
557
+ ...news.branchId !== void 0 && { branchId: news.branchId }
558
+ } }));
559
+ return {
458
560
  id: created.data.id,
459
561
  name: created.data.name
460
562
  };
461
563
  }
564
+ const result = {
565
+ id: observed.data.id,
566
+ name: observed.data.name
567
+ };
462
568
  if (news.branchId !== void 0) {
463
569
  const branchId = news.branchId;
464
570
  yield* call(() => client.PATCH("/v1/databases/{databaseId}", {
@@ -516,7 +622,107 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
516
622
  })
517
623
  };
518
624
  }));
519
- Data.TaggedError("ContainerNotFoundError");
625
+ //#endregion
626
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
627
+ /** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
628
+ var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
629
+ const listAllProjects = (client) => Effect.gen(function* () {
630
+ const projects = [];
631
+ let cursor;
632
+ for (;;) {
633
+ const query = cursor === void 0 ? {} : { cursor };
634
+ const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
635
+ projects.push(...page.data);
636
+ if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
637
+ cursor = page.pagination.nextCursor;
638
+ }
639
+ return projects;
640
+ });
641
+ /**
642
+ * Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
643
+ * bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
644
+ * one (the same normalization `state/bootstrap.ts` applies to the same
645
+ * `/v1/projects` listing).
646
+ */
647
+ const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
648
+ /**
649
+ * Finds the app's Project by name in the workspace — PDP allows duplicate
650
+ * project names, so more than one can match; the oldest wins. Creates one
651
+ * if none match, unless `ensure` is `false` (find-only — `destroy`), in
652
+ * which case an absent Project fails with `ContainerNotFoundError`. No
653
+ * ownership marker and no `--project` override (both deferred — see
654
+ * ADR-0019).
655
+ */
656
+ const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
657
+ const oldest = (yield* listAllProjects(client)).filter((p) => bareWorkspaceId(p.workspace.id) === bareWorkspaceId(workspaceId) && p.name === appName).sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
658
+ if (oldest !== void 0) return oldest.id;
659
+ if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
660
+ return (yield* call(() => client.POST("/v1/projects", { body: {
661
+ name: appName,
662
+ workspaceId
663
+ } }))).data.id;
664
+ });
665
+ const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
666
+ path: { projectId },
667
+ query: { gitName }
668
+ } })).pipe(Effect.map((page) => page.data[0]?.id));
669
+ /**
670
+ * Finds the stage's Branch by its exact `gitName`, creating it if absent
671
+ * unless `ensure` is `false` (find-only — `destroy`), in which case an
672
+ * absent Branch fails with `ContainerNotFoundError`. The Management API has
673
+ * no server-side "create-or-return" idempotency (`POST
674
+ * /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
675
+ * field to make that a no-op), so idempotency is client-side: observe
676
+ * first, and on a racing 409 from create, re-observe rather than fail.
677
+ */
678
+ const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
679
+ const existing = yield* findBranchId(client, projectId, gitName);
680
+ if (existing !== void 0) return existing;
681
+ if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
682
+ appName,
683
+ stage: gitName
684
+ }));
685
+ return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
686
+ params: { path: { projectId } },
687
+ body: { gitName }
688
+ })).pipe(Effect.map((r) => r.data.id), Effect.catch((err) => err.status === 409 ? findBranchId(client, projectId, gitName).pipe(Effect.flatMap((id) => id === void 0 ? Effect.fail(err) : Effect.succeed(id))) : Effect.fail(err)));
689
+ });
690
+ /**
691
+ * Resolves the two containers a stage's deploy runs into (ADR-0019): the
692
+ * app's **Project**, found-or-created by name, and — for a named stage
693
+ * only — its **Branch**, found-or-created by `gitName`. The default stage
694
+ * (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
695
+ * false` (`destroy`), nothing is created — an absent Project or Branch
696
+ * fails with `ContainerNotFoundError` instead.
697
+ */
698
+ const resolveContainer = (opts) => Effect.gen(function* () {
699
+ const client = yield* ManagementClient;
700
+ const ensure = opts.ensure ?? true;
701
+ const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
702
+ if (opts.stage === void 0) return { projectId };
703
+ return {
704
+ projectId,
705
+ branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
706
+ };
707
+ });
708
+ /**
709
+ * Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
710
+ * the Branch still has live members or is the production/default Branch —
711
+ * that surfaces as a `PrismaApiError`.
712
+ */
713
+ const deleteBranch = (branchId) => Effect.gen(function* () {
714
+ const client = yield* ManagementClient;
715
+ yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
716
+ });
717
+ /**
718
+ * Deletes a Project. Tolerates a 404 (already gone). The API refuses with a
719
+ * 400 if the Project still has live dependencies (e.g. another stage's
720
+ * Branch/resources) — that surfaces as a `PrismaApiError`.
721
+ */
722
+ const deleteProject = (projectId) => Effect.gen(function* () {
723
+ const client = yield* ManagementClient;
724
+ yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: projectId } } }));
725
+ });
520
726
  /** The collection of Prisma resource providers. */
521
727
  var Providers = class extends Provider.ProviderCollection()("Prisma") {};
522
728
  /**
@@ -530,10 +736,101 @@ const providers = () => Layer.effect(Providers, Provider.collection([
530
736
  Connection,
531
737
  ComputeService,
532
738
  Deployment,
533
- EnvironmentVariable
534
- ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DatabaseProvider(), ConnectionProvider(), ComputeServiceProvider(), DeploymentProvider(), EnvironmentVariableProvider())), Layer.provideMerge(layer()), Layer.provideMerge(fromEnv()), Layer.orDie);
739
+ EnvironmentVariable,
740
+ Bucket,
741
+ BucketKey
742
+ ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DatabaseProvider(), ConnectionProvider(), ComputeServiceProvider(), DeploymentProvider(), EnvironmentVariableProvider(), BucketProvider(), BucketKeyProvider())), Layer.provideMerge(layer()), Layer.provideMerge(fromEnv()), Layer.orDie);
535
743
  //#endregion
536
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/state/index.mjs
744
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/state.mjs
745
+ /** The framework-owned database a stage's deploy state lives in — a child of that stage's Branch (ADR-0034). */
746
+ const STATE_DATABASE_NAME = "prisma-composer-state";
747
+ /** Every connection created against a state database carries this prefix — see `cleanupAgedConnections`. */
748
+ const CONNECTION_NAME_PREFIX = "prisma-composer-state-";
749
+ const listAllBranches = (client, projectId) => Effect.gen(function* () {
750
+ const branches = [];
751
+ let cursor;
752
+ for (;;) {
753
+ const query = cursor === void 0 ? {} : { cursor };
754
+ const page = yield* call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
755
+ path: { projectId },
756
+ query
757
+ } }));
758
+ branches.push(...page.data);
759
+ if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
760
+ cursor = page.pagination.nextCursor;
761
+ }
762
+ return branches;
763
+ });
764
+ /**
765
+ * The project's implicit default Branch — every live Project owns exactly
766
+ * one (a platform invariant). The list endpoint has no `isDefault` filter, so
767
+ * this pages through every Branch and picks it out client-side. Never creates
768
+ * one: its absence means the platform's invariant is broken, which is not
769
+ * something a deploy can repair.
770
+ */
771
+ const resolveDefaultBranchId = (client, projectId) => Effect.gen(function* () {
772
+ const found = (yield* listAllBranches(client, projectId)).find((b) => b.isDefault);
773
+ if (found !== void 0) return found.id;
774
+ return yield* Effect.fail(new PrismaApiError({
775
+ status: 0,
776
+ message: `project ${projectId} has no default Branch — the platform guarantees every live Project owns one; contact support.`
777
+ }));
778
+ });
779
+ /** A named stage carries its `branchId`; production omits it, and its state lives on the Project's default Branch. */
780
+ const resolveBranchId = (client, container) => container.branchId !== void 0 ? Effect.succeed(container.branchId) : resolveDefaultBranchId(client, container.projectId);
781
+ /**
782
+ * Every database on this Branch. Uses the flat `GET /v1/databases`, which
783
+ * accepts `projectId` and `branchId` together — the project-scoped listing
784
+ * has no branch filter at all.
785
+ */
786
+ const listAllDatabasesOnBranch = (client, projectId, branchId) => Effect.gen(function* () {
787
+ const databases = [];
788
+ let cursor;
789
+ for (;;) {
790
+ const query = cursor === void 0 ? {
791
+ projectId,
792
+ branchId
793
+ } : {
794
+ projectId,
795
+ branchId,
796
+ cursor
797
+ };
798
+ const page = yield* call(() => client.GET("/v1/databases", { params: { query } }));
799
+ databases.push(...page.data);
800
+ if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
801
+ cursor = page.pagination.nextCursor;
802
+ }
803
+ return databases;
804
+ });
805
+ /**
806
+ * Databases on this Branch named `prisma-composer-state`, oldest first,
807
+ * excluding the Branch's own default database. The default database is always
808
+ * the user's, never ours to adopt or delete. A name match alone proves
809
+ * nothing — the platform allows duplicate names — so every caller must still
810
+ * verify ownership before acting on a candidate.
811
+ */
812
+ const listStateDatabaseCandidates = (client, projectId, branchId) => listAllDatabasesOnBranch(client, projectId, branchId).pipe(Effect.map((databases) => databases.filter((d) => d.name === "prisma-composer-state" && !d.isDefault).sort((a, b) => a.createdAt.localeCompare(b.createdAt))));
813
+ /**
814
+ * Creates a fresh Postgres connection and reads its connection string. Reads
815
+ * `endpoints.direct.connectionString` only — never `endpoints.pooled`, and
816
+ * never the deprecated top-level `connectionString`/`url` (PRO-212), neither
817
+ * of which the platform guarantees.
818
+ *
819
+ * The connection string is returned only when the connection is created and
820
+ * cannot be read back afterwards, which is why every run creates a fresh
821
+ * connection instead of reusing one.
822
+ */
823
+ const createConnection = (client, databaseId) => call(() => client.POST("/v1/databases/{databaseId}/connections", {
824
+ params: { path: { databaseId } },
825
+ body: { name: `${CONNECTION_NAME_PREFIX}${Date.now()}` }
826
+ })).pipe(Effect.flatMap((r) => {
827
+ const created = r.data;
828
+ const dsn = created.endpoints.direct?.connectionString;
829
+ return dsn === void 0 ? Effect.fail(new PrismaApiError({
830
+ status: 0,
831
+ message: `connection ${created.id} returned no endpoints.direct.connectionString (PRO-212)`
832
+ })) : Effect.succeed(Redacted.make(dsn));
833
+ }));
537
834
  /** Collapses any thrown value (postgres.js failures included) into a {@link StateStoreError}. */
538
835
  const toStateStoreError = (cause) => cause instanceof Error ? new StateStoreError({
539
836
  message: cause.message,
@@ -541,13 +838,13 @@ const toStateStoreError = (cause) => cause instanceof Error ? new StateStoreErro
541
838
  }) : new StateStoreError({ message: String(cause) });
542
839
  /**
543
840
  * An operator-facing failure from the hosted-state bootstrap pipeline
544
- * (project/database discovery, connection mint, schema migration, or lock
841
+ * (branch/database discovery, connection creation, schema migration, or lock
545
842
  * acquisition) — what a deployer actually sees, instead of a raw Effect
546
843
  * defect.
547
844
  */
548
845
  var HostedStateBootstrapError = class extends Data.TaggedError("HostedStateBootstrapError") {
549
846
  get message() {
550
- return `hosted-state bootstrap failed for workspace ${this.workspaceId}: ${this.step} — ${this.reason}`;
847
+ return `hosted-state bootstrap failed in ${this.container}: ${this.step} — ${this.reason}`;
551
848
  }
552
849
  };
553
850
  /**
@@ -557,8 +854,8 @@ var HostedStateBootstrapError = class extends Data.TaggedError("HostedStateBoots
557
854
  * omit the DSN or credentials, so only the extracted message text survives
558
855
  * into the operator-facing error.
559
856
  */
560
- const hostedStateBootstrapError = (workspaceId, step, cause) => new HostedStateBootstrapError({
561
- workspaceId,
857
+ const hostedStateBootstrapError = (container, step, cause) => new HostedStateBootstrapError({
858
+ container,
562
859
  step,
563
860
  reason: cause instanceof Error ? cause.message : String(cause)
564
861
  });
@@ -609,85 +906,30 @@ const migratePrismaState = (sql) => Effect.tryPromise({
609
906
  },
610
907
  catch: toStateStoreError
611
908
  });
612
- /**
613
- * The workspace's dedicated project for hosted deploy state. A project is
614
- * the closest expressible stand-in for "ambient platform infrastructure" —
615
- * PDP has no workspace-level database, and the app's own project is
616
- * circular (it doesn't exist before the first apply, and is itself tracked
617
- * in the state it would have to host).
618
- */
619
- const STATE_PROJECT_NAME = "prisma-composer-state";
620
- /** Every connection this bootstrap mints carries this prefix — see `cleanupAgedConnections`. */
621
- const CONNECTION_NAME_PREFIX = "prisma-composer-state-";
622
909
  const CONNECTION_MAX_AGE_MS = 1440 * 60 * 1e3;
623
- const DEFAULT_DATABASE_POLL_ATTEMPTS = 5;
624
- const DEFAULT_DATABASE_POLL_DELAY = "500 millis";
625
- const listAllProjects = (client) => Effect.gen(function* () {
626
- const projects = [];
627
- let cursor;
628
- for (;;) {
629
- const query = cursor === void 0 ? {} : { cursor };
630
- const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
631
- projects.push(...page.data);
632
- if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
633
- cursor = page.pagination.nextCursor;
634
- }
635
- return projects;
636
- });
637
910
  /**
638
- * Workspace ids circulate in two shapes: the API returns them `wksp_`-prefixed
639
- * (`wksp_abc…`), while tokens/config often carry the bare id (`abc…`) the
640
- * API accepts both on writes. Comparing them raw silently never matches when
641
- * the shapes differ, which made bootstrap re-provision a fresh state project
642
- * on every run in CI. Compare bare-to-bare.
911
+ * Creates the state database on the stage's Branch, using the flat endpoint
912
+ * because it is the only one that accepts a `branchId`. The platform still
913
+ * creates the row on the project's default Branch and attaches it afterwards,
914
+ * so a failed attach can leave a database behind on the default Branch. It
915
+ * checks the Branch exists before creating, and this is one request instead of
916
+ * two, so that window is much narrower than attaching from here — but it is
917
+ * narrowed, not closed.
918
+ *
919
+ * `isDefault` is left at the API's `false` default: the state database must
920
+ * never be the stage's default database, which belongs to the user.
643
921
  */
644
- const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
645
- /**
646
- * All projects named `prisma-composer-state` in the workspace — plural, because PDP
647
- * allows duplicate project names (verified 2026-07-09), so name-based
648
- * discovery can never assume there is at most one. See `resolveStateProject`
649
- * for how candidates get disambiguated.
650
- */
651
- const listStateProjects = (client, workspaceId) => listAllProjects(client).pipe(Effect.map((projects) => projects.filter((p) => bareWorkspaceId(p.workspace.id) === bareWorkspaceId(workspaceId) && p.name === STATE_PROJECT_NAME)));
652
- const createStateProject = (client, workspaceId) => call(() => client.POST("/v1/projects", { body: {
653
- name: STATE_PROJECT_NAME,
654
- workspaceId
655
- } })).pipe(Effect.map((r) => r.data));
656
- const listAllDatabases = (client, projectId) => Effect.gen(function* () {
657
- const databases = [];
658
- let cursor;
659
- for (;;) {
660
- const query = cursor === void 0 ? {} : { cursor };
661
- const page = yield* call(() => client.GET("/v1/projects/{projectId}/databases", { params: {
662
- path: { projectId },
663
- query
664
- } }));
665
- databases.push(...page.data);
666
- if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
667
- cursor = page.pagination.nextCursor;
668
- }
669
- return databases;
670
- });
671
- /**
672
- * The project's default database — auto-provisioned at project creation.
673
- * Never create a database here: a project already has exactly one default,
674
- * and creating another 409s (FT-5220). Whether the default is listable in
675
- * the same tick as the project-create response is not a documented
676
- * contract, so a fresh project polls a few times with a short backoff
677
- * before giving up — the observed live behaviour is synchronous, but this
678
- * does not assume that holds on every run.
679
- */
680
- const findDefaultDatabase = (client, projectId) => Effect.gen(function* () {
681
- for (let attempt = 1; attempt <= DEFAULT_DATABASE_POLL_ATTEMPTS; attempt++) {
682
- const found = (yield* listAllDatabases(client, projectId)).find((d) => d.isDefault);
683
- if (found !== void 0) return found;
684
- if (attempt < DEFAULT_DATABASE_POLL_ATTEMPTS) yield* Effect.sleep(DEFAULT_DATABASE_POLL_DELAY);
685
- }
686
- return yield* Effect.fail(new PrismaApiError({
687
- status: 0,
688
- message: `project ${projectId} (${STATE_PROJECT_NAME}) has no default database after ${DEFAULT_DATABASE_POLL_ATTEMPTS} attempts — it may still be provisioning; re-run the deploy.`
689
- }));
690
- });
922
+ const createStateDatabase = (client, projectId, branchId) => call(() => client.POST("/v1/databases", { body: {
923
+ projectId,
924
+ name: STATE_DATABASE_NAME,
925
+ region: "inherit",
926
+ branchId
927
+ } })).pipe(Effect.map((created) => ({
928
+ id: created.data.id,
929
+ name: created.data.name,
930
+ isDefault: created.data.isDefault,
931
+ createdAt: created.data.createdAt
932
+ })));
691
933
  const listAllConnections = (client, databaseId) => Effect.gen(function* () {
692
934
  const connections = [];
693
935
  let cursor;
@@ -705,43 +947,24 @@ const listAllConnections = (client, databaseId) => Effect.gen(function* () {
705
947
  });
706
948
  const deleteConnection = (client, connectionId) => callVoid(() => client.DELETE("/v1/connections/{id}", { params: { path: { id: connectionId } } }));
707
949
  /**
708
- * Every deploy mints a fresh connection (`mintConnection`) and nothing ever
709
- * closes it, so the store's default database otherwise accumulates one
710
- * connection resource per run without bound. Best-effort, never blocks
711
- * bootstrap: lists this database's connections, deletes the ones matching
712
- * our naming pattern older than the age threshold, and swallows any failure
713
- * (a transient API error here must never fail the deploy it's cleaning up
714
- * after).
950
+ * Every deploy creates a fresh connection (`createConnection`) and nothing ever
951
+ * closes it, so the state database otherwise accumulates one connection
952
+ * resource per run without bound. Best-effort, never blocks bootstrap: lists
953
+ * this database's connections, deletes the ones matching our naming pattern
954
+ * older than the age threshold, and swallows any failure (a transient API
955
+ * error here must never fail the deploy it's cleaning up after).
715
956
  */
716
957
  const cleanupAgedConnections = (client, databaseId) => Effect.gen(function* () {
717
958
  const connections = yield* listAllConnections(client, databaseId);
718
959
  const cutoff = Date.now() - CONNECTION_MAX_AGE_MS;
719
- const aged = connections.filter((c) => c.name.startsWith(CONNECTION_NAME_PREFIX) && Date.parse(c.createdAt) < cutoff);
960
+ const aged = connections.filter((c) => c.name.startsWith("prisma-composer-state-") && Date.parse(c.createdAt) < cutoff);
720
961
  yield* Effect.forEach(aged, (c) => deleteConnection(client, c.id), { discard: true });
721
962
  }).pipe(Effect.ignore);
722
963
  /**
723
- * Mints a fresh Postgres connection and reads the direct endpoint's DSN.
724
- * Never `endpoints.pooled`, the deprecated top-level `connectionString`/`url`
725
- * (PRO-212) — those are not guaranteed by the platform. The DSN is
726
- * write-only on read (a stored connection can't be re-read later), which is
727
- * exactly why a fresh connection is minted every run instead of reusing one.
728
- */
729
- const mintConnection = (client, databaseId) => call(() => client.POST("/v1/databases/{databaseId}/connections", {
730
- params: { path: { databaseId } },
731
- body: { name: `${CONNECTION_NAME_PREFIX}${Date.now()}` }
732
- })).pipe(Effect.flatMap((r) => {
733
- const created = r.data;
734
- const dsn = created.endpoints.direct?.connectionString;
735
- return dsn === void 0 ? Effect.fail(new PrismaApiError({
736
- status: 0,
737
- message: `connection ${created.id} returned no endpoints.direct.connectionString (PRO-212)`
738
- })) : Effect.succeed(Redacted.make(dsn));
739
- }));
740
- /**
741
- * PDP allows duplicate project names, so a project named `prisma-composer-state`
964
+ * PDP allows duplicate database names, so a database named `prisma-composer-state`
742
965
  * found by listing is not proof it's ours — it could be an unrelated
743
- * project that happens to share the name (a squatter, deliberate or not).
744
- * Connects to the candidate's default database and inspects its tables:
966
+ * database that happens to share the name (a squatter, deliberate or not).
967
+ * Connects to the candidate and inspects its tables:
745
968
  *
746
969
  * - our marker table with our marker row present → `ours`, adopt outright.
747
970
  * - our state tables (`alchemy_resource_state`/`alchemy_stack_output`) but no
@@ -749,7 +972,7 @@ const mintConnection = (client, databaseId) => call(() => client.POST("/v1/datab
749
972
  * The real, currently-in-use workspace state is in this shape today, so it
750
973
  * must keep working — adopt it, and `migratePrismaState` (idempotent)
751
974
  * writes the marker on the way in.
752
- * - no tables at all → `empty`, a freshly-created default database — adopt.
975
+ * - no tables at all → `empty`, a freshly-created database — adopt.
753
976
  * - anything else → `squatter`: foreign data occupies the name; refuse it.
754
977
  */
755
978
  const verifyOwnership = (connectionString) => Effect.tryPromise({
@@ -784,76 +1007,124 @@ const verifyOwnership = (connectionString) => Effect.tryPromise({
784
1007
  })
785
1008
  });
786
1009
  /**
787
- * Finds the workspace's `prisma-composer-state` project, verifying ownership rather
788
- * than trusting the name alone (PDP allows duplicate names — see
789
- * `verifyOwnership`). Zero candidates creates one: nothing to verify, since
790
- * only this run could possibly have touched the brand-new database between
791
- * create and here (`migratePrismaState` writes the marker once bootstrap
792
- * hands the connection off). One or more candidates are tried
793
- * oldest-`createdAt` first, deterministically, and the first that verifies
794
- * as ours (or adoptable legacy/empty) wins; a candidate that fails
795
- * verification is skipped, not fatal, unless every candidate fails, in
796
- * which case the failure names every rejected project id so an operator can
797
- * act on it.
798
- */
799
- const resolveStateProject = (client, workspaceId, verify) => Effect.gen(function* () {
800
- const candidates = yield* listStateProjects(client, workspaceId);
1010
+ * Finds the Branch's `prisma-composer-state` database, verifying ownership
1011
+ * rather than trusting the name alone (PDP allows duplicate names — see
1012
+ * `verifyOwnership`). Finding none creates one, with nothing to verify: only
1013
+ * this run can have touched a database it just created.
1014
+ *
1015
+ * Candidates are tried oldest first, so repeated runs pick the same one. The
1016
+ * first that verifies as ours is used. A candidate that fails verification is
1017
+ * skipped. If every candidate fails, bootstrap fails and names each rejected
1018
+ * database id, so an operator knows which to rename or remove.
1019
+ */
1020
+ const resolveStateDatabase = (client, projectId, branchId, verify) => Effect.gen(function* () {
1021
+ const candidates = yield* listStateDatabaseCandidates(client, projectId, branchId);
801
1022
  if (candidates.length === 0) {
802
- const project = yield* createStateProject(client, workspaceId);
803
- const database = yield* findDefaultDatabase(client, project.id);
804
- const connectionString = yield* mintConnection(client, database.id);
805
- console.error(`hosted state: provisioned new state project ${project.id} (db ${database.id}) in workspace ${workspaceId}`);
1023
+ const database = yield* createStateDatabase(client, projectId, branchId);
1024
+ const connectionString = yield* createConnection(client, database.id);
1025
+ console.error(`hosted state: provisioned state database ${database.id} on branch ${branchId} (project ${projectId})`);
806
1026
  return {
807
- project,
808
1027
  database,
809
1028
  connectionString
810
1029
  };
811
1030
  }
812
- const sorted = [...candidates].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
813
1031
  const rejected = [];
814
- for (const candidate of sorted) {
815
- const database = yield* findDefaultDatabase(client, candidate.id);
816
- const connectionString = yield* mintConnection(client, database.id);
1032
+ for (const candidate of candidates) {
1033
+ const connectionString = yield* createConnection(client, candidate.id);
817
1034
  const verdict = yield* verify(connectionString);
818
1035
  if (verdict.kind === "squatter") {
819
- rejected.push(`${candidate.id} (foreign tables: ${verdict.tables.join(", ") || "none named"})`);
1036
+ rejected.push(`${candidate.id} (foreign tables: ${verdict.tables.join(", ")})`);
820
1037
  continue;
821
1038
  }
822
- console.error(`hosted state: using state project ${candidate.id} (db ${database.id}, ${verdict.kind}) — ${sorted.length} candidate(s) named ${STATE_PROJECT_NAME} in workspace ${workspaceId}`);
1039
+ console.error(`hosted state: using state database ${candidate.id} on branch ${branchId} (${verdict.kind}) — ${candidates.length} candidate(s) named ${STATE_DATABASE_NAME}`);
823
1040
  return {
824
- project: candidate,
825
- database,
1041
+ database: candidate,
826
1042
  connectionString
827
1043
  };
828
1044
  }
829
1045
  return yield* Effect.fail(new PrismaApiError({
830
1046
  status: 0,
831
- message: `found ${sorted.length} project(s) named "${STATE_PROJECT_NAME}" in workspace ${workspaceId}, but none verified as Prisma App's state store: ${rejected.join("; ")}. The name is squatted by unrelated data — rename or remove the offending project(s), or see platform-ask.md (reserved/unique state project names).`
1047
+ message: `found ${candidates.length} database(s) named "${STATE_DATABASE_NAME}" on branch ${branchId}, but none verified as Composer's state store: ${rejected.join("; ")}. Rename or remove the offending database(s).`
832
1048
  }));
833
1049
  });
834
1050
  /**
835
- * Find-or-create the workspace's `prisma-composer-state` project, resolve its
836
- * default database, and mint a fresh connection — the automatic bootstrap
837
- * every deploy runs once, needing nothing beyond the service token and
838
- * workspace id a deployer already has.
1051
+ * Resolves the stage's Branch, find-or-creates its `prisma-composer-state`
1052
+ * database, and creates a fresh connection — the automatic bootstrap every
1053
+ * deploy runs once, needing nothing beyond the service token and the
1054
+ * Project/Branch ids the CLI already resolved.
839
1055
  */
840
- const bootstrapStateConnection = (workspaceId) => bootstrapStateConnectionWith(workspaceId, verifyOwnership);
1056
+ const bootstrapStateConnection = (container) => bootstrapStateConnectionWith(container, verifyOwnership);
841
1057
  /**
842
- * Test seam: identical to {@link bootstrapStateConnection} but with the
843
- * ownership verifier injectable, so `bootstrap.test.ts` can stub ownership
844
- * decisions against its fake DSNs without opening a real Postgres
845
- * connection to them.
1058
+ * Identical to {@link bootstrapStateConnection}, except the ownership check is
1059
+ * a parameter so `bootstrap.test.ts` can supply a fake instead of opening a
1060
+ * real Postgres connection.
846
1061
  */
847
- const bootstrapStateConnectionWith = (workspaceId, verify) => Effect.gen(function* () {
1062
+ const bootstrapStateConnectionWith = (container, verify) => Effect.gen(function* () {
848
1063
  const client = yield* ManagementClient;
849
- const { project, database, connectionString } = yield* resolveStateProject(client, workspaceId, verify);
1064
+ const branchId = yield* resolveBranchId(client, container);
1065
+ const { database, connectionString } = yield* resolveStateDatabase(client, container.projectId, branchId, verify);
850
1066
  yield* cleanupAgedConnections(client, database.id);
851
1067
  return {
852
- projectId: project.id,
1068
+ projectId: container.projectId,
853
1069
  databaseId: database.id,
854
1070
  connectionString
855
1071
  };
856
1072
  });
1073
+ const deleteDatabase = (client, databaseId) => callVoid(() => client.DELETE("/v1/databases/{databaseId}", { params: { path: { databaseId } } }));
1074
+ /**
1075
+ * Identical to {@link deleteStateDatabase}, except the ownership check is a
1076
+ * parameter so tests can supply a fake instead of opening a real Postgres
1077
+ * connection.
1078
+ */
1079
+ const deleteStateDatabaseWith = (container, verify) => Effect.gen(function* () {
1080
+ const client = yield* ManagementClient;
1081
+ const branchId = yield* resolveBranchId(client, container);
1082
+ const candidates = yield* listStateDatabaseCandidates(client, container.projectId, branchId);
1083
+ for (const candidate of candidates) {
1084
+ const verdict = yield* verify(yield* createConnection(client, candidate.id));
1085
+ if (verdict.kind === "squatter") {
1086
+ console.warn(`hosted state: left database ${candidate.id} on branch ${branchId} alone — it is named ${STATE_DATABASE_NAME} but holds unrelated data (tables: ${verdict.tables.join(", ")}).`);
1087
+ continue;
1088
+ }
1089
+ yield* deleteDatabase(client, candidate.id);
1090
+ console.error(`hosted state: removed state database ${candidate.id} from branch ${branchId}`);
1091
+ }
1092
+ });
1093
+ /** Codes for "can't reach the host yet" — DNS/refusal, not a mid-session drop. */
1094
+ const ESTABLISHMENT_CODES = /* @__PURE__ */ new Set([
1095
+ "ECONNREFUSED",
1096
+ "ENOTFOUND",
1097
+ "EAI_AGAIN"
1098
+ ]);
1099
+ /** Connection-establishment failure messages (no useful `err.code`). `upstream database` is PPg's edge proxy while a cold/idle upstream warms up. */
1100
+ const ESTABLISHMENT_MESSAGE_FRAGMENTS = ["upstream database", "connection refused"];
1101
+ /**
1102
+ * Whether a failure is a connection-establishment error against a warming
1103
+ * upstream (retry), as opposed to a lost lease, a mid-session drop, or a real
1104
+ * query error (all of which must surface at once). Unwraps a
1105
+ * {@link StateStoreError} to its driver `cause` so a code-only error still
1106
+ * classifies.
1107
+ */
1108
+ const isColdStartConnectError = (error) => {
1109
+ if (typeof error !== "object" || error === null) return false;
1110
+ const code = "code" in error && typeof error.code === "string" ? error.code : void 0;
1111
+ if (code !== void 0 && ESTABLISHMENT_CODES.has(code)) return true;
1112
+ const message = "message" in error && typeof error.message === "string" ? error.message.toLowerCase() : "";
1113
+ if (ESTABLISHMENT_MESSAGE_FRAGMENTS.some((fragment) => message.includes(fragment))) return true;
1114
+ const cause = "cause" in error ? error.cause : void 0;
1115
+ return cause !== void 0 && cause !== error && isColdStartConnectError(cause);
1116
+ };
1117
+ /** The same ~2-minute budget the bootstrap migration uses (`layer.ts`): retry every 5s, up to 2 minutes. */
1118
+ const COLD_START_SCHEDULE = Schedule.both(Schedule.spaced("5 seconds"), Schedule.during("2 minutes"));
1119
+ /**
1120
+ * Retries a state operation past a cold-start connection rejection only; every
1121
+ * other failure surfaces immediately. `schedule` is injectable so tests drive
1122
+ * it without real delay.
1123
+ */
1124
+ const retryColdStart = (operation, schedule = COLD_START_SCHEDULE) => Effect.retry(operation, {
1125
+ while: isColdStartConnectError,
1126
+ schedule
1127
+ });
857
1128
  /** Another deploy already holds the lock for this stack/stage. Never queued — fails immediately. */
858
1129
  var StateLockContentionError = class extends Data.TaggedError("StateLockContentionError") {
859
1130
  get message() {
@@ -894,22 +1165,22 @@ const acquireStateLock = (sql, stack, stage) => Effect.gen(function* () {
894
1165
  }));
895
1166
  }
896
1167
  const lockPid = acquired.pid;
897
- const checkLive = Effect.tryPromise({
1168
+ const checkLive = retryColdStart(Effect.tryPromise({
898
1169
  try: async () => {
899
1170
  return (await sql`
900
- select exists (
901
- select 1 from pg_locks
902
- where locktype = 'advisory'
903
- and pid = ${lockPid}
904
- and objsubid = 1
905
- and granted
906
- and ((classid::bigint << 32) | (objid::bigint & 4294967295))
907
- = hashtextextended(${key}, 0)
908
- ) as live
909
- `)[0]?.live ?? false;
1171
+ select exists (
1172
+ select 1 from pg_locks
1173
+ where locktype = 'advisory'
1174
+ and pid = ${lockPid}
1175
+ and objsubid = 1
1176
+ and granted
1177
+ and ((classid::bigint << 32) | (objid::bigint & 4294967295))
1178
+ = hashtextextended(${key}, 0)
1179
+ ) as live
1180
+ `)[0]?.live ?? false;
910
1181
  },
911
1182
  catch: toStateStoreError
912
- }).pipe(Effect.flatMap((live) => live ? Effect.void : Effect.fail(new StateStoreError({ message: `the state lock for ${stack}/${stage} was lost mid-run; refusing to continue unlocked` }))));
1183
+ }).pipe(Effect.flatMap((live) => live ? Effect.void : Effect.fail(new StateStoreError({ message: `the state lock for ${stack}/${stage} was lost mid-run; refusing to continue unlocked` })))));
913
1184
  const release = async () => {
914
1185
  try {
915
1186
  await reserved`select pg_advisory_unlock(hashtextextended(${key}, 0))`;
@@ -922,10 +1193,10 @@ const acquireStateLock = (sql, stack, stage) => Effect.gen(function* () {
922
1193
  release
923
1194
  };
924
1195
  });
925
- const attempt = (f) => Effect.tryPromise({
1196
+ const attempt = (f) => retryColdStart(Effect.tryPromise({
926
1197
  try: f,
927
1198
  catch: toStateStoreError
928
- });
1199
+ }));
929
1200
  /**
930
1201
  * Wraps an already-`encodeState`d value as a jsonb-typed bind parameter.
931
1202
  * Must go through `sql.json(...)` (not `JSON.stringify(...)::jsonb`) —
@@ -1093,29 +1364,32 @@ const guardStateService = (service, checkLive, now = Date.now) => {
1093
1364
  };
1094
1365
  /**
1095
1366
  * The hosted Alchemy state store. On layer init (scoped, once per stack
1096
- * run): find-or-create the workspace's `prisma-composer-state` project, mint a
1097
- * fresh connection to its default database, migrate the schema, and
1098
- * acquire the (stack, stage) advisory lock — see `bootstrap.ts` and
1099
- * `lock.ts`. The Management API plumbing (`ManagementClient`,
1100
- * `PrismaCredentials`) is provided internally, so the returned layer's
1101
- * only requirements are the ones alchemy itself already provides to every
1102
- * state store (`StackServices`).
1367
+ * run): resolve the stage's Branch, find-or-create its `prisma-composer-state`
1368
+ * database, create a fresh connection, migrate the schema, and acquire the
1369
+ * (stack, stage) advisory lock — see `bootstrap.ts` and `lock.ts`. The
1370
+ * Management API plumbing (`ManagementClient`, `PrismaCredentials`) is
1371
+ * provided internally, so the returned layer's only requirements are the
1372
+ * ones alchemy itself already provides to every state store
1373
+ * (`StackServices`).
1103
1374
  *
1104
1375
  * Any bootstrap/lock/migration failure is wrapped into an operator-facing
1105
- * `HostedStateBootstrapError` (naming the workspace and the step that
1376
+ * `HostedStateBootstrapError` (naming the Project/Branch and the step that
1106
1377
  * failed, never the raw driver/API error — see `errors.ts`) before dying the
1107
1378
  * layer (loud, immediate, unrecoverable) rather than surfacing as a typed
1108
1379
  * error — matching core's `LowerOptions.state: Layer.Layer<State, never,
1109
1380
  * StackServices>` contract and alchemy's own convention (e.g. a missing
1110
1381
  * state store is `Effect.die` in `Stack.make`).
1111
1382
  */
1112
- const prismaState = (opts = {}) => {
1113
- const workspaceId = opts.workspaceId ?? process.env["PRISMA_WORKSPACE_ID"];
1114
- if (workspaceId === void 0 || workspaceId.length === 0) throw new Error("prismaState(): environment variable PRISMA_WORKSPACE_ID is required.");
1383
+ const prismaStateLayer = (ids) => {
1384
+ const { projectId, branchId } = ids;
1115
1385
  return Layer.effect(State, Effect.gen(function* () {
1116
1386
  const stack = yield* Stack;
1117
- const bootstrapError = (step) => (cause) => hostedStateBootstrapError(workspaceId, step, cause);
1118
- const { connectionString } = yield* bootstrapStateConnection(workspaceId).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))), Effect.mapError(bootstrapError("finding/creating the prisma-composer-state project")));
1387
+ const container = branchId === void 0 ? projectId : `${projectId}/${branchId}`;
1388
+ const bootstrapError = (step) => (cause) => hostedStateBootstrapError(container, step, cause);
1389
+ const { connectionString } = yield* bootstrapStateConnection(branchId === void 0 ? { projectId } : {
1390
+ projectId,
1391
+ branchId
1392
+ }).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))), Effect.mapError(bootstrapError("resolving the state database on the stage branch")));
1119
1393
  const sql = postgres(Redacted.value(connectionString), {
1120
1394
  max: 5,
1121
1395
  onnotice: () => {}
@@ -1130,41 +1404,260 @@ const prismaState = (opts = {}) => {
1130
1404
  };
1131
1405
  //#endregion
1132
1406
  //#region ../../1-prisma-cloud/1-extensions/target/dist/control.mjs
1407
+ const PRISMA_CLOUD_EXTENSION_ID = "@prisma/composer-prisma-cloud";
1408
+ var PrismaCloudContainer = class {
1409
+ input;
1410
+ projectId;
1411
+ branchId;
1412
+ constructor(input, projectId, branchId) {
1413
+ this.input = input;
1414
+ this.projectId = projectId;
1415
+ this.branchId = branchId;
1416
+ }
1417
+ serialize() {
1418
+ return JSON.stringify({
1419
+ input: this.input,
1420
+ projectId: this.projectId,
1421
+ ...this.branchId !== void 0 ? { branchId: this.branchId } : {}
1422
+ });
1423
+ }
1424
+ };
1425
+ /** `instanceof` — parent-side instances and child-side deserialized instances are both constructed by this module. */
1426
+ function isPrismaCloudContainer(value) {
1427
+ return value instanceof PrismaCloudContainer;
1428
+ }
1429
+ /** Narrow-or-throw for hook inputs. */
1430
+ function prismaCloudContainerOf(value) {
1431
+ if (!isPrismaCloudContainer(value)) throw new Error("the Prisma Cloud container was not resolved — the extension's container descriptor did not run.");
1432
+ return value;
1433
+ }
1434
+ function isRecord(value) {
1435
+ return typeof value === "object" && value !== null;
1436
+ }
1437
+ function invalidPayloadError(reason) {
1438
+ return /* @__PURE__ */ new Error(`${PRISMA_CLOUD_EXTENSION_ID}: invalid container transport payload — ${reason}.`);
1439
+ }
1440
+ /** Reconstructs a `PrismaCloudContainer` from `serialize()`'s JSON output — real narrowing, no casts. */
1441
+ function deserialize(serialized) {
1442
+ let parsed;
1443
+ try {
1444
+ parsed = JSON.parse(serialized);
1445
+ } catch (error) {
1446
+ throw invalidPayloadError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`);
1447
+ }
1448
+ if (!isRecord(parsed)) throw invalidPayloadError("not an object");
1449
+ const input = parsed["input"];
1450
+ if (!isRecord(input)) throw invalidPayloadError("\"input\" is not an object");
1451
+ const appName = input["appName"];
1452
+ if (typeof appName !== "string") throw invalidPayloadError("\"input.appName\" is not a string");
1453
+ const stage = input["stage"];
1454
+ if (stage !== void 0 && typeof stage !== "string") throw invalidPayloadError("\"input.stage\" is not a string or absent");
1455
+ const projectId = parsed["projectId"];
1456
+ if (typeof projectId !== "string") throw invalidPayloadError("\"projectId\" is not a string");
1457
+ const branchId = parsed["branchId"];
1458
+ if (branchId !== void 0 && typeof branchId !== "string") throw invalidPayloadError("\"branchId\" is not a string or absent");
1459
+ return new PrismaCloudContainer({
1460
+ appName,
1461
+ stage
1462
+ }, projectId, branchId);
1463
+ }
1464
+ const workspaceRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_WORKSPACE_ID is required.");
1465
+ const tokenRequiredError$2 = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required.");
1466
+ function requireWorkspaceId() {
1467
+ const workspaceId = process.env["PRISMA_WORKSPACE_ID"];
1468
+ if (workspaceId === void 0 || workspaceId.length === 0) throw workspaceRequiredError();
1469
+ return workspaceId;
1470
+ }
1471
+ function requireTokenUnlessInjected(deps) {
1472
+ if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError$2();
1473
+ }
1474
+ async function ensureContainer(input, deps) {
1475
+ const workspaceId = requireWorkspaceId();
1476
+ requireTokenUnlessInjected(deps);
1477
+ const program = resolveContainer({
1478
+ workspaceId,
1479
+ appName: input.appName,
1480
+ ...input.stage !== void 0 ? { stage: input.stage } : {},
1481
+ ensure: true
1482
+ }).pipe(Effect.map((c) => ({
1483
+ ok: true,
1484
+ container: c
1485
+ })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
1486
+ ok: false,
1487
+ message: `Prisma Management API error resolving containers: ${e.message}.`
1488
+ })));
1489
+ const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
1490
+ const outcome = await Effect.runPromise(provided);
1491
+ if (!outcome.ok) throw new Error(outcome.message);
1492
+ return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
1493
+ }
1494
+ async function locateContainer(input, deps) {
1495
+ const workspaceId = requireWorkspaceId();
1496
+ requireTokenUnlessInjected(deps);
1497
+ const program = resolveContainer({
1498
+ workspaceId,
1499
+ appName: input.appName,
1500
+ ...input.stage !== void 0 ? { stage: input.stage } : {},
1501
+ ensure: false
1502
+ }).pipe(Effect.map((c) => ({
1503
+ ok: true,
1504
+ container: c
1505
+ })), Effect.catchTag("ContainerNotFoundError", () => Effect.succeed({ ok: false })), Effect.catchTag("PrismaApiError", (e) => Effect.fail(/* @__PURE__ */ new Error(`Prisma Management API error resolving containers: ${e.message}.`))));
1506
+ const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
1507
+ const outcome = await Effect.runPromise(provided);
1508
+ if (!outcome.ok) return void 0;
1509
+ return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
1510
+ }
1511
+ /**
1512
+ * Soft-deletes a named stage's Branch after a successful `alchemy destroy`
1513
+ * has removed its members — the Management API refuses to delete a Branch
1514
+ * that still has live members.
1515
+ */
1516
+ async function removeStageBranch(branchId, deps) {
1517
+ requireTokenUnlessInjected(deps);
1518
+ const program = deleteBranch(branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
1519
+ ok: false,
1520
+ message: `Failed to delete the stage Branch: ${e.message}.`
1521
+ })));
1522
+ const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
1523
+ const outcome = await Effect.runPromise(provided);
1524
+ if (!outcome.ok) throw new Error(outcome.message);
1525
+ }
1526
+ /**
1527
+ * Best-effort cleanup after a successful `--production` destroy: removes
1528
+ * the app's Project so hand-run stacks don't accumulate as empty Projects
1529
+ * (they eventually hit the workspace's plan limit). Unlike `removeStageBranch`,
1530
+ * this never throws: the destroy itself already succeeded, and the API's own
1531
+ * 400 ("still has dependencies") is the only check that matters — failing
1532
+ * the command over a cleanup step would be worse than leaving a Project shell.
1533
+ */
1534
+ async function removeAppProject(projectId, deps) {
1535
+ if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) {
1536
+ console.warn(`Skipped removing the Project (${projectId}): PRISMA_SERVICE_TOKEN is not set.`);
1537
+ return;
1538
+ }
1539
+ const program = deleteProject(projectId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
1540
+ ok: false,
1541
+ error: e
1542
+ })));
1543
+ const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
1544
+ const outcome = await Effect.runPromise(provided);
1545
+ if (outcome.ok) {
1546
+ console.log(`Removed the Project (${projectId}) — nothing was left in it.`);
1547
+ return;
1548
+ }
1549
+ if (outcome.error.status === 400) {
1550
+ console.log(`Kept the Project (${projectId}) — it still has another stage's resources.`);
1551
+ return;
1552
+ }
1553
+ console.warn(`Could not remove the Project (${projectId}) after destroy: ${outcome.error.message}.`);
1554
+ }
1555
+ function containerDescriptor(deps) {
1556
+ return {
1557
+ ensure: (input) => ensureContainer(input, deps),
1558
+ locate: (input) => locateContainer(input, deps),
1559
+ remove: (instance) => instance.input.stage !== void 0 ? removeStageBranch(instance.branchId ?? missingBranchId(instance), deps) : removeAppProject(instance.projectId, deps),
1560
+ deserialize
1561
+ };
1562
+ }
1563
+ /** Defensive: a named-stage container always resolves a Branch together with its stage — `ensure`/`locate`/`deserialize` never produce one without the other. */
1564
+ function missingBranchId(instance) {
1565
+ throw new Error(`${PRISMA_CLOUD_EXTENSION_ID}: a named-stage ("${instance.input.stage}") container instance is missing its branchId — this is a bug in ensure/locate/deserialize.`);
1566
+ }
1133
1567
  const PRISMA_NAME_MIN = 3;
1134
1568
  const PRISMA_NAME_MAX = 65;
1135
1569
  function validateName(value, source) {
1136
1570
  if (value.length < PRISMA_NAME_MIN || value.length > PRISMA_NAME_MAX) throw new Error(`prisma-cloud: ${source} "${value}" (${value.length} characters) is not a valid Prisma resource name — Prisma requires ${PRISMA_NAME_MIN}–${PRISMA_NAME_MAX} characters. Rename the provision id (or the deploy --name) to fit.`);
1137
1571
  }
1138
- /** The application/provisioned hook's `projectId` output — `LoweredNode.outputs` is typed `unknown`, so this is the one asserted read. */
1139
- const projectIdOf = (hook) => blindCast(hook.outputs["projectId"]);
1572
+ function isCloudApplication(value) {
1573
+ return typeof value === "object" && value !== null && "projectId" in value && typeof value.projectId === "string" && "branchId" in value && (value.branchId === void 0 || typeof value.branchId === "string");
1574
+ }
1575
+ /** Narrows `ctx.application`, which core hands over as `unknown`, to this extension's own product; throws naming the hook when it hasn't run. */
1576
+ function cloudApplicationOf(application) {
1577
+ if (!isCloudApplication(application)) throw new Error("prisma-cloud: ctx.application is not this extension's application product — the prismaCloud() application hook must run before any node lowers.");
1578
+ return application;
1579
+ }
1580
+ function projectIdOf(application) {
1581
+ return cloudApplicationOf(application).projectId;
1582
+ }
1583
+ /**
1584
+ * One Bucket per module-provisioned bucket resource — `id` is the module
1585
+ * provision id, so a resource shared by several consumers is created exactly
1586
+ * once. A BucketKey is minted for the bucket: it is the reveal-once credential
1587
+ * carrier, and its attributes (endpoint, bucketName, accessKeyId,
1588
+ * secretAccessKey) become the four S3Config outputs consumers resolve by name.
1589
+ */
1590
+ function bucketDescriptor(_o) {
1591
+ const lowering = ({ id, application }) => Effect.gen(function* () {
1592
+ validateName(id, "resource name (from provision id)");
1593
+ const branchId = cloudApplicationOf(application).branchId;
1594
+ const bkt = yield* Bucket(`${id}-bucket`, {
1595
+ projectId: projectIdOf(application),
1596
+ name: id,
1597
+ ...branchId !== void 0 ? { branchId } : {}
1598
+ });
1599
+ const key = yield* BucketKey(`${id}-key`, {
1600
+ bucketId: bkt.id,
1601
+ name: id,
1602
+ role: "read_write"
1603
+ });
1604
+ const secretAccessKey = Output.map(key.secretAccessKey, (v) => Redacted.value(v));
1605
+ return {
1606
+ outputs: {
1607
+ url: key.endpoint,
1608
+ bucket: key.bucketName,
1609
+ accessKeyId: key.accessKeyId,
1610
+ secretAccessKey
1611
+ },
1612
+ entities: [{
1613
+ kind: "bucket",
1614
+ id: bkt.id
1615
+ }]
1616
+ };
1617
+ });
1618
+ return Object.assign(lowering, { kind: "resource" });
1619
+ }
1620
+ /** The `compute` node kind's descriptor: the four service hooks — provision, serialize, package, deploy. */
1621
+ /**
1622
+ * Returns the PRECISE descriptor type, not the erased `NodeDescriptor`: the
1623
+ * registry in control.ts erases it on assignment anyway (method bivariance),
1624
+ * but s3-store composes over these hooks and needs their P/S to stay visible.
1625
+ * Annotating this `NodeDescriptor` would force s3-store to cast them back.
1626
+ */
1140
1627
  function computeDescriptor(o) {
1141
1628
  return {
1142
1629
  kind: "service",
1143
1630
  provision: ({ id, application }) => Effect.gen(function* () {
1144
1631
  validateName(id, "service name (from provision id)");
1145
- return { outputs: {
1632
+ const projectId = projectIdOf(application);
1633
+ const branchId = cloudApplicationOf(application).branchId;
1634
+ return {
1146
1635
  serviceId: (yield* ComputeService(`${id}-svc`, {
1147
- projectId: projectIdOf(application),
1636
+ projectId,
1148
1637
  name: id,
1149
1638
  region: o.region ?? "us-east-1",
1150
- ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
1639
+ ...branchId !== void 0 ? { branchId } : {}
1151
1640
  })).id,
1152
- projectId: application.outputs["projectId"]
1153
- } };
1641
+ projectId
1642
+ };
1154
1643
  }),
1155
- serialize: ({ address, node, graph }, provisioned, config) => Effect.gen(function* () {
1156
- const cls = o.branchId ? "preview" : "production";
1157
- const branch = o.branchId !== void 0 ? { branchId: o.branchId } : {};
1158
- const projectId = projectIdOf(provisioned);
1644
+ serialize: (ctx, provisioned, config) => Effect.gen(function* () {
1645
+ const { address, node, graph } = ctx;
1646
+ const branchId = cloudApplicationOf(ctx.application).branchId;
1647
+ const cls = branchId ? "preview" : "production";
1648
+ const branch = branchId !== void 0 ? { branchId } : {};
1649
+ const projectId = provisioned.projectId;
1159
1650
  const svc = node;
1160
1651
  const records = [];
1161
1652
  for (const d of paramEntries(svc)) {
1162
1653
  const value = d.owner === "service" ? config.service[d.name] : config.inputs[d.owner.input]?.[d.name];
1654
+ if (value === void 0) continue;
1163
1655
  const key = configKey(address, d);
1656
+ const rowValue = d.owner === "service" && isParamSource(value) ? encodeParamPointer(paramName(paramBindingFor(graph.params, address, d.name))) : encode(d.owner, value);
1164
1657
  records.push(yield* EnvironmentVariable(`${key}-var`, {
1165
1658
  projectId,
1166
1659
  key,
1167
- value: encode(d.owner, value),
1660
+ value: rowValue,
1168
1661
  class: cls,
1169
1662
  ...branch
1170
1663
  }));
@@ -1176,10 +1669,37 @@ function computeDescriptor(o) {
1176
1669
  class: cls,
1177
1670
  ...branch
1178
1671
  }));
1179
- return { outputs: {
1672
+ if (svc.expose !== void 0 && Object.keys(svc.expose).length > 0) {
1673
+ const refsByBrand = /* @__PURE__ */ new Map();
1674
+ for (const edge of provisionedEdges(graph)) {
1675
+ if (edge.providerAddress !== address) continue;
1676
+ const ref = ctx.provisioned.get(edge.edgeId);
1677
+ if (ref === void 0) continue;
1678
+ const refs = refsByBrand.get(edge.brand) ?? [];
1679
+ refs.push(ref);
1680
+ refsByBrand.set(edge.brand, refs);
1681
+ }
1682
+ for (const [brand, entry] of o.providerParams) {
1683
+ const raw = entry.value(refsByBrand.get(brand) ?? []);
1684
+ if (raw === void 0) continue;
1685
+ const key = configKey(address, {
1686
+ owner: "service",
1687
+ name: entry.name
1688
+ });
1689
+ const value = Output.isOutput(raw) ? Output.map(raw, (v) => encode("service", v)) : encode("service", raw);
1690
+ records.push(yield* EnvironmentVariable(`${key}-var`, {
1691
+ projectId,
1692
+ key,
1693
+ value,
1694
+ class: cls,
1695
+ ...branch
1696
+ }));
1697
+ }
1698
+ }
1699
+ return {
1180
1700
  environment: records,
1181
1701
  port: typeof config.service["port"] === "number" ? config.service["port"] : 3e3
1182
- } };
1702
+ };
1183
1703
  }),
1184
1704
  package: ({ id }, { assembled, address }) => Effect.try(() => packageComputeArtifact({
1185
1705
  id,
@@ -1188,16 +1708,24 @@ function computeDescriptor(o) {
1188
1708
  address
1189
1709
  })),
1190
1710
  deploy: ({ id }, provisioned, artifact, serialized) => Effect.gen(function* () {
1191
- return { outputs: {
1192
- url: (yield* Deployment(`${id}-deploy`, {
1193
- computeServiceId: provisioned.outputs["serviceId"],
1194
- artifactPath: artifact.path,
1195
- artifactHash: artifact.sha256,
1196
- environment: serialized.outputs["environment"],
1197
- port: typeof serialized.outputs["port"] === "number" ? serialized.outputs["port"] : 3e3
1198
- })).deployedUrl,
1199
- projectId: provisioned.outputs["projectId"]
1200
- } };
1711
+ const deployment = yield* Deployment(`${id}-deploy`, {
1712
+ computeServiceId: provisioned.serviceId,
1713
+ artifactPath: artifact.path,
1714
+ artifactHash: artifact.sha256,
1715
+ environment: serialized.environment,
1716
+ port: serialized.port
1717
+ });
1718
+ return {
1719
+ outputs: {
1720
+ url: deployment.deployedUrl,
1721
+ projectId: provisioned.projectId
1722
+ },
1723
+ entities: [{
1724
+ kind: "compute-service",
1725
+ id: provisioned.serviceId,
1726
+ url: deployment.deployedUrl
1727
+ }]
1728
+ };
1201
1729
  })
1202
1730
  };
1203
1731
  }
@@ -1257,18 +1785,25 @@ const PgWarmProvider = () => Provider.effect(PgWarm, Effect.succeed(pgWarmProvid
1257
1785
  function postgresDescriptor(o) {
1258
1786
  const lowering = ({ id, application }) => Effect.gen(function* () {
1259
1787
  validateName(id, "resource name (from provision id)");
1788
+ const branchId = cloudApplicationOf(application).branchId;
1260
1789
  const db = yield* Database(`${id}-db`, {
1261
1790
  projectId: projectIdOf(application),
1262
1791
  name: id,
1263
1792
  region: o.region ?? "us-east-1",
1264
- ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
1793
+ ...branchId !== void 0 ? { branchId } : {}
1265
1794
  });
1266
1795
  const conn = yield* Connection(`${id}-conn`, {
1267
1796
  databaseId: db.id,
1268
1797
  name: id
1269
1798
  });
1270
1799
  const url = Output.map(conn.connectionString, (value) => Redacted.value(value));
1271
- return { outputs: { url: (yield* PgWarm(`${id}-warm`, { url })).url } };
1800
+ return {
1801
+ outputs: { url: (yield* PgWarm(`${id}-warm`, { url })).url },
1802
+ entities: [{
1803
+ kind: "postgres-database",
1804
+ id: db.id
1805
+ }]
1806
+ };
1272
1807
  });
1273
1808
  return Object.assign(lowering, { kind: "resource" });
1274
1809
  }
@@ -1519,11 +2054,12 @@ const PnMigrationProvider = () => Provider.effect(PnMigration, Effect.succeed(pn
1519
2054
  function prismaNextDescriptor(o) {
1520
2055
  const lowering = ({ id, node, application }) => Effect.gen(function* () {
1521
2056
  validateName(id, "resource name (from provision id)");
2057
+ const branchId = cloudApplicationOf(application).branchId;
1522
2058
  const db = yield* Database(`${id}-db`, {
1523
2059
  projectId: projectIdOf(application),
1524
2060
  name: id,
1525
2061
  region: o.region ?? "us-east-1",
1526
- ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
2062
+ ...branchId !== void 0 ? { branchId } : {}
1527
2063
  });
1528
2064
  const conn = yield* Connection(`${id}-conn`, {
1529
2065
  databaseId: db.id,
@@ -1543,7 +2079,13 @@ function prismaNextDescriptor(o) {
1543
2079
  invariants: [...ref.invariants].sort(),
1544
2080
  ...node.targetRef !== void 0 ? { refName: node.targetRef } : {}
1545
2081
  });
1546
- return { outputs: { url: warm.url } };
2082
+ return {
2083
+ outputs: { url: warm.url },
2084
+ entities: [{
2085
+ kind: "postgres-database",
2086
+ id: db.id
2087
+ }]
2088
+ };
1547
2089
  });
1548
2090
  return Object.assign(lowering, { kind: "resource" });
1549
2091
  }
@@ -1599,16 +2141,18 @@ const S3CredentialsProvider = () => Provider.effect(S3Credentials, Effect.succee
1599
2141
  function s3CredentialsDescriptor(_o) {
1600
2142
  const lowering = ({ id }) => Effect.gen(function* () {
1601
2143
  const creds = yield* S3Credentials(`${id}-creds`, {});
1602
- return { outputs: {
1603
- accessKeyId: creds.accessKeyId,
1604
- secretAccessKey: creds.secretAccessKey
1605
- } };
2144
+ return {
2145
+ outputs: {
2146
+ accessKeyId: creds.accessKeyId,
2147
+ secretAccessKey: creds.secretAccessKey
2148
+ },
2149
+ entities: []
2150
+ };
1606
2151
  });
1607
2152
  return Object.assign(lowering, { kind: "resource" });
1608
2153
  }
1609
2154
  function s3StoreDescriptor(o) {
1610
2155
  const base = computeDescriptor(o);
1611
- if (base.kind !== "service") throw new Error("computeDescriptor must be a service descriptor");
1612
2156
  return {
1613
2157
  kind: "service",
1614
2158
  provision: base.provision,
@@ -1618,27 +2162,33 @@ function s3StoreDescriptor(o) {
1618
2162
  const credentials = config.inputs["credentials"] ?? {};
1619
2163
  const bucket = config.service["bucket"];
1620
2164
  if (credentials["accessKeyId"] === void 0 || credentials["secretAccessKey"] === void 0 || bucket === void 0) throw new Error("s3-store service must wire a 'credentials' dependency and a 'bucket' param");
1621
- return { outputs: {
1622
- ...serialized.outputs,
2165
+ return {
2166
+ ...serialized,
1623
2167
  bucket,
1624
2168
  accessKeyId: credentials["accessKeyId"],
1625
2169
  secretAccessKey: credentials["secretAccessKey"]
1626
- } };
2170
+ };
1627
2171
  }),
1628
2172
  deploy: (ctx, provisioned, artifact, serialized) => Effect.gen(function* () {
1629
- return { outputs: {
1630
- ...(yield* base.deploy(ctx, provisioned, artifact, serialized)).outputs,
1631
- bucket: serialized.outputs["bucket"],
1632
- accessKeyId: serialized.outputs["accessKeyId"],
1633
- secretAccessKey: serialized.outputs["secretAccessKey"]
1634
- } };
2173
+ const deployed = yield* base.deploy(ctx, provisioned, artifact, serialized);
2174
+ return {
2175
+ ...deployed,
2176
+ outputs: {
2177
+ ...deployed.outputs,
2178
+ bucket: serialized.bucket,
2179
+ accessKeyId: serialized.accessKeyId,
2180
+ secretAccessKey: serialized.secretAccessKey
2181
+ }
2182
+ };
1635
2183
  })
1636
2184
  };
1637
2185
  }
1638
2186
  /**
1639
- * Deploy preflight (ADR-0029): before Alchemy runs, verify every secret binding
1640
- * in the app's provision manifest (each a platform env-var NAME) exists on Prisma
1641
- * Cloud for the target stage.
2187
+ * Deploy preflight (ADR-0029, extended for env-sourced params): before Alchemy
2188
+ * runs, verify every secret binding AND every env-sourced param binding in the
2189
+ * app's manifests (each a platform env-var NAME) exists on Prisma Cloud for
2190
+ * the target stage. A literal-bound param never touches the platform — only
2191
+ * `envParam(...)` sources are checked here.
1642
2192
  * A name absent on the platform but present in the deploy shell is provisioned
1643
2193
  * via a direct Management API POST — NEVER an Alchemy resource, so the value
1644
2194
  * never lands in hosted deploy state. A name absent from both fails the deploy,
@@ -1646,7 +2196,7 @@ function s3StoreDescriptor(o) {
1646
2196
  *
1647
2197
  * Control-plane only (imported by control.ts → prisma-composer.config.ts); runs
1648
2198
  * in the CLI parent, so it builds its own Management API client from env — the
1649
- * same credential path `ensureContainers` uses.
2199
+ * same credential path `container.ts`'s `ensure`/`locate` use.
1650
2200
  */
1651
2201
  /** production for the default stage; preview for a named stage — matching how the pack writes config rows. */
1652
2202
  const classFor = (branchId) => branchId === void 0 ? "production" : "preview";
@@ -1689,59 +2239,188 @@ async function existsOnPlatform(client, projectId, branchId, key) {
1689
2239
  * EnvironmentVariable.ts). A 409 means a concurrent deploy already provisioned
1690
2240
  * it — tolerated. The value is never logged.
1691
2241
  */
1692
- async function fillMissing(client, input, key, value) {
2242
+ async function fillMissing(client, projectId, branchId, key, value) {
1693
2243
  const res = await client.POST("/v1/environment-variables", { body: {
1694
- projectId: input.projectId,
1695
- class: classFor(input.branchId),
2244
+ projectId,
2245
+ class: classFor(branchId),
1696
2246
  key,
1697
2247
  value,
1698
- ...input.branchId !== void 0 ? { branchId: input.branchId } : {}
2248
+ ...branchId !== void 0 ? { branchId } : {}
1699
2249
  } });
1700
2250
  if (res.error !== void 0 && res.response.status !== 409) throw fillFailedError(key, res.error);
1701
2251
  }
1702
- const tokenRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for deploy preflight.");
2252
+ const tokenRequiredError$1 = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for deploy preflight.");
1703
2253
  const listFailedError = (key, error) => /* @__PURE__ */ new Error(`deploy preflight: Prisma Management API error listing "${key}": ${JSON.stringify(error)}.`);
1704
2254
  const fillFailedError = (key, error) => /* @__PURE__ */ new Error(`deploy preflight: failed to provision "${key}" from the deploy shell: ${JSON.stringify(error)}.`);
1705
- function missingError(missing, input) {
1706
- const scope = input.branchId === void 0 ? "the production class (project-level template)" : `the preview class of stage "${input.stage ?? input.branchId}" (branch override or template)`;
2255
+ function missingError(missing, branchId, stage) {
2256
+ const scope = branchId === void 0 ? "the production class (project-level template)" : `the preview class of stage "${stage ?? branchId}" (branch override or template)`;
1707
2257
  const lines = missing.map((m) => ` - ${m.name} (required by service "${m.serviceAddress}")`);
1708
- return /* @__PURE__ */ new Error(`Deploy preflight failed — ${missing.length} secret env var(s) are not provisioned on Prisma Cloud for ${scope}, and are absent from the deploy shell:\n${lines.join("\n")}\n\nSet each in the deploy shell environment (the CLI will provision it on deploy), or create it on the platform (Prisma Console or the Management API) in ${scope}.`);
2258
+ return /* @__PURE__ */ new Error(`Deploy preflight failed — ${missing.length} env var(s) (secret or env-sourced param) are not provisioned on Prisma Cloud for ${scope}, and are absent from the deploy shell:\n${lines.join("\n")}\n\nSet each in the deploy shell environment (the CLI will provision it on deploy), or create it on the platform (Prisma Console or the Management API) in ${scope}.`);
1709
2259
  }
1710
- async function managementClient() {
1711
- if ((process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError();
2260
+ async function managementClient$1() {
2261
+ if ((process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError$1();
1712
2262
  return Effect.runPromise(Effect.gen(function* () {
1713
2263
  return yield* ManagementClient;
1714
2264
  }).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv())))));
1715
2265
  }
1716
2266
  /**
1717
2267
  * The Prisma Cloud extension's `preflight`. Aggregates the target-agnostic
1718
- * manifest (core's `provisionManifest`), checks each pointer secret against the
2268
+ * manifests (core's `provisionManifest` for secrets, `paramManifest` filtered
2269
+ * to env-sourced bindings for params), checks each pointer name against the
1719
2270
  * platform, fills from the shell where possible, and fails loudly on anything
1720
2271
  * absent from both. Accepts an injected client for tests; otherwise builds one
1721
2272
  * from env.
1722
2273
  */
1723
2274
  async function runPreflight(input, deps) {
1724
- const manifest = provisionManifest(input.graph);
1725
- if (manifest.length === 0) return;
2275
+ const { projectId, branchId } = prismaCloudContainerOf(input.container);
1726
2276
  const names = /* @__PURE__ */ new Map();
1727
- for (const binding of manifest) if (!names.has(secretName(binding))) names.set(secretName(binding), {
1728
- name: secretName(binding),
1729
- serviceAddress: binding.serviceAddress
1730
- });
1731
- const client = deps?.client ?? await managementClient();
2277
+ for (const binding of provisionManifest(input.graph)) {
2278
+ const name = secretName(binding);
2279
+ if (!names.has(name)) names.set(name, {
2280
+ name,
2281
+ serviceAddress: binding.serviceAddress
2282
+ });
2283
+ }
2284
+ for (const binding of paramManifest(input.graph)) {
2285
+ if (!isEnvParamSource(binding.binding)) continue;
2286
+ const name = paramName(binding);
2287
+ if (!names.has(name)) names.set(name, {
2288
+ name,
2289
+ serviceAddress: binding.serviceAddress
2290
+ });
2291
+ }
2292
+ if (names.size === 0) return;
2293
+ const client = deps?.client ?? await managementClient$1();
1732
2294
  const missing = [];
1733
2295
  for (const meta of names.values()) {
1734
- if (await existsOnPlatform(client, input.projectId, input.branchId, meta.name)) continue;
2296
+ if (await existsOnPlatform(client, projectId, branchId, meta.name)) continue;
1735
2297
  const shellValue = process.env[meta.name];
1736
2298
  if (shellValue !== void 0 && shellValue.length > 0) {
1737
- await fillMissing(client, input, meta.name, shellValue);
2299
+ await fillMissing(client, projectId, branchId, meta.name, shellValue);
1738
2300
  continue;
1739
2301
  }
1740
2302
  missing.push(meta);
1741
2303
  }
1742
- if (missing.length > 0) throw missingError(missing, input);
2304
+ if (missing.length > 0) throw missingError(missing, branchId, input.stage);
2305
+ }
2306
+ const tokenRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for destroy teardown.");
2307
+ async function managementClient() {
2308
+ if ((process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError();
2309
+ return Effect.runPromise(Effect.gen(function* () {
2310
+ return yield* ManagementClient;
2311
+ }).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv())))));
2312
+ }
2313
+ /**
2314
+ * The Prisma Cloud extension's `teardown`. Removes the stage's state database,
2315
+ * with failure handling that differs by stage because the consequences do:
2316
+ *
2317
+ * - **Named stage: throw.** The Branch delete that follows would fail anyway —
2318
+ * the platform refuses a Branch that still has a database attached — so
2319
+ * failing here names the actual cause instead of a confusing symptom.
2320
+ * - **Production: warn and continue.** Nothing blocks the Project delete on
2321
+ * this; removing the database only stops production's state outliving
2322
+ * production and holding a quota slot. That is a cleanup step, and a cleanup
2323
+ * step must not fail the command.
2324
+ *
2325
+ * Accepts an injected client and ownership verifier for tests; otherwise
2326
+ * builds a client from env and verifies against the real database.
2327
+ */
2328
+ async function runTeardown(input, deps) {
2329
+ const { projectId, branchId } = prismaCloudContainerOf(input.container);
2330
+ const isNamedStage = branchId !== void 0;
2331
+ try {
2332
+ const client = deps?.client ?? await managementClient();
2333
+ await Effect.runPromise(deleteStateDatabaseWith({
2334
+ projectId,
2335
+ ...branchId !== void 0 ? { branchId } : {}
2336
+ }, deps?.verify ?? verifyOwnership).pipe(Effect.provideService(ManagementClient, client)));
2337
+ } catch (error) {
2338
+ const reason = error instanceof Error ? error.message : String(error);
2339
+ if (isNamedStage) throw new Error(`Failed to delete the deploy-state database: ${reason}`);
2340
+ console.warn(`Could not remove production's deploy-state database: ${reason}`);
2341
+ }
1743
2342
  }
1744
2343
  /** The Prisma Cloud–hosted deploy state store; its implementation lives in @internal/lowering. */
2344
+ /**
2345
+ * ADR-0031's registered provisioner for RPC_PEER_KEY: mints one `ServiceKey`
2346
+ * resource per edge (ADR-0030) and forwards its value as the opaque ref core
2347
+ * writes into the consumer's `serviceKey` param. The resource id keeps the
2348
+ * `servicekey-${edgeId}` scheme byte-identical to slice 2's, so an existing
2349
+ * deploy's keys are found, not re-minted. Defined here, not in service-keys.ts:
2350
+ * that module is also reachable from the runtime/authoring side, which must
2351
+ * never import `@internal/lowering` or `effect`.
2352
+ */
2353
+ const serviceKeyProvisioner = { provision: (edge) => Effect.gen(function* () {
2354
+ return (yield* ServiceKey(`servicekey-${edge.edgeId}`, {})).value;
2355
+ }) };
2356
+ /**
2357
+ * `ctx.provisioned`'s refs are typed `unknown` — core forwards a provisioner's
2358
+ * ref without inspecting it. Each provider param below is the sole reader of
2359
+ * its own provisioner's output, so the shape is asserted here, once, rather
2360
+ * than checked.
2361
+ */
2362
+ const asKeyOutputs = (refs) => refs.map((ref) => blindCast(ref));
2363
+ /**
2364
+ * RPC's deploy-side `value(refs)` (ADR-0030): the provider stores a SET — one
2365
+ * key per inbound edge — into the accepted-keys var `serve()` reads. Paired
2366
+ * with the provisioner above: mint per edge, aggregate every edge.
2367
+ *
2368
+ * Zero consumers still emits, and that is the whole point of #100: an ABSENT
2369
+ * var means "never provisioned" and passes every caller through, so a deployed
2370
+ * provider nobody wired must say "[]" — deny everything — rather than say
2371
+ * nothing. Written as a literal because `Output.all()` with no arguments has
2372
+ * nothing to resolve.
2373
+ */
2374
+ const rpcAcceptedKeysValue = (refs) => refs.length > 0 ? Output.all(...asKeyOutputs(refs)) : [];
2375
+ /**
2376
+ * ADR-0031's registered provisioner for STREAMS_API_KEY — the same `ServiceKey`
2377
+ * mint, keyed PER PROVIDER instead of per edge: the resource id is the
2378
+ * provider's address, so every consumer edge of one streams module resolves to
2379
+ * the same resource and therefore the same stable value. That is what
2380
+ * `@prisma/streams-server` requires (it authenticates a single `API_KEY`), and
2381
+ * cardinality is exactly what ADR-0031 leaves to the provisioner. Making it
2382
+ * per-edge later is this id's shape plus an accepted-set provider param — no
2383
+ * new resource, no core change.
2384
+ */
2385
+ const streamsApiKeyProvisioner = { provision: (edge) => Effect.gen(function* () {
2386
+ return (yield* ServiceKey(`streamskey-${edge.providerAddress}`, {})).value;
2387
+ }) };
2388
+ /**
2389
+ * Streams' deploy-side `value(refs)`: ONE value, not a set —
2390
+ * `@prisma/streams-server` authenticates a single `API_KEY`, which is why the
2391
+ * provisioner above mints per provider. That pairing is the invariant this
2392
+ * param depends on, so it asserts rather than trusts it: a future per-edge
2393
+ * flip without a paired accepted-set param here would otherwise ship
2394
+ * whichever key came first and leave every other consumer 401ing, silently.
2395
+ * The refs are lazy Outputs (not comparable at serialize time); inside
2396
+ * `Output.map` they are resolved strings, the same seam RPC's set aggregates
2397
+ * on.
2398
+ *
2399
+ * Zero consumers emits NOTHING — the streams counterpart to RPC's "[]".
2400
+ * `@prisma/streams-server` has no deny-all mode: it either authenticates a key
2401
+ * or runs --no-auth, so there is no value here that means "refuse everyone".
2402
+ * Writing no key is what fails closed — the entrypoint refuses to boot with
2403
+ * a named error rather than serve unauthenticated.
2404
+ */
2405
+ const streamsApiKeyValue = (refs) => {
2406
+ if (refs.length === 0) return void 0;
2407
+ return Output.map(Output.all(...asKeyOutputs(refs)), (vals) => {
2408
+ const distinct = [...new Set(vals)];
2409
+ if (distinct.length > 1) throw new Error(`a streams provider was provisioned ${distinct.length} distinct keys across its ${refs.length} inbound bindings, but it can only be given one (@prisma/streams-server authenticates a single API_KEY). Its provisioner must mint per provider, not per edge — or this param must store an accepted-key set, once the server accepts one.`);
2410
+ return distinct[0] ?? "";
2411
+ });
2412
+ };
2413
+ /** The user-facing state descriptor: `state: prismaState()` in `prisma-composer.config.ts` (ADR-0017). */
2414
+ const prismaState = () => ({
2415
+ extension: PRISMA_CLOUD_EXTENSION_ID,
2416
+ create: (container) => {
2417
+ const { projectId, branchId } = prismaCloudContainerOf(container);
2418
+ return prismaStateLayer(branchId !== void 0 ? {
2419
+ projectId,
2420
+ branchId
2421
+ } : { projectId });
2422
+ }
2423
+ });
1745
2424
  const KNOWN_REGION_SET = new Set(COMPUTE_REGIONS);
1746
2425
  function isComputeRegion(value) {
1747
2426
  return KNOWN_REGION_SET.has(value);
@@ -1751,64 +2430,109 @@ function asProvidersLayer(layer) {
1751
2430
  return layer;
1752
2431
  }
1753
2432
  /**
2433
+ * This extension's brands, each with the two halves ADR-0031 splits: the
2434
+ * PROVISIONER core resolves a mint through, and the reserved PROVIDER PARAM
2435
+ * that stores the minted values on the provider. So this file stays the only
2436
+ * place a brand is named — `descriptors/compute.ts` just looks a provider
2437
+ * param up by brand.
2438
+ *
2439
+ * `__tests__/provider-params.test.ts` asserts this map's brands are exactly
2440
+ * `PROVIDER_PARAMS`'s brands: a brand minted here with no provider param
2441
+ * below would leave the value it mints written to consumers while no
2442
+ * provider ever stores an accepted-keys row for it — `serve()` (or the
2443
+ * equivalent runtime reader) then sees an absent var and passes every caller
2444
+ * through.
2445
+ */
2446
+ const PROVISIONERS = /* @__PURE__ */ new Map([[RPC_PEER_KEY, serviceKeyProvisioner], [STREAMS_API_KEY, streamsApiKeyProvisioner]]);
2447
+ /**
2448
+ * Every brand's deploy-side `value(refs)` — the only per-brand thing this
2449
+ * file still holds directly. `PROVIDER_PARAMS` below is built by mapping
2450
+ * `RESERVED_PROVIDER_PARAMS` (`provider-params.ts`, the boot-side list) onto
2451
+ * this map, so a param can exist on the deploy side only if it already
2452
+ * exists on the boot side: `RESERVED_PROVIDER_PARAMS` is the single source of
2453
+ * which reserved provider params exist at all, closing the drift the old
2454
+ * name-comparison test only detected after the fact.
2455
+ */
2456
+ const PROVIDER_PARAM_VALUES = /* @__PURE__ */ new Map([[RPC_PEER_KEY, rpcAcceptedKeysValue], [STREAMS_API_KEY, streamsApiKeyValue]]);
2457
+ /**
2458
+ * Builds the deploy-side registry from the boot-side list, keyed by brand —
2459
+ * throws if a boot-side entry has no registered deploy-side `value(refs)`, so
2460
+ * `RESERVED_PROVIDER_PARAMS` stays the single source of which reserved
2461
+ * provider params exist: deploy can no longer write a row boot never
2462
+ * stashes. Exported (rather than inlined into `PROVIDER_PARAMS` below) so
2463
+ * `__tests__/provider-params.test.ts` can drive it directly with a
2464
+ * deliberately incomplete value map and watch it throw.
2465
+ */
2466
+ function buildProviderParams(entries, values) {
2467
+ return new Map(entries.map((entry) => {
2468
+ const value = values.get(entry.brand);
2469
+ if (value === void 0) throw new Error(`prisma-cloud: reserved provider param "${entry.name}" (provider-params.ts) has no registered deploy-side value() in control.ts's PROVIDER_PARAM_VALUES — every param in RESERVED_PROVIDER_PARAMS must have one.`);
2470
+ return [entry.brand, {
2471
+ ...entry,
2472
+ value
2473
+ }];
2474
+ }));
2475
+ }
2476
+ const PROVIDER_PARAMS = buildProviderParams(RESERVED_PROVIDER_PARAMS, PROVIDER_PARAM_VALUES);
2477
+ /**
1754
2478
  * Resolves the factory's env-or-option inputs, failing fast with the exact
1755
- * variable name. `projectId`/`branchId` aren't required here — `prismaCloud()`
1756
- * also runs in the CLI parent, before they're set; the required check lives in `application.provision`.
2479
+ * variable name.
1757
2480
  */
1758
2481
  function resolveOptions(opts) {
1759
2482
  const workspaceId = opts.workspaceId ?? process.env["PRISMA_WORKSPACE_ID"];
1760
2483
  if (workspaceId === void 0 || workspaceId.length === 0) throw new Error("prismaCloud(): environment variable PRISMA_WORKSPACE_ID is required.");
1761
- const projectId = process.env["PRISMA_PROJECT_ID"] || void 0;
1762
- const branchId = process.env["PRISMA_BRANCH_ID"] || void 0;
1763
2484
  if (opts.region !== void 0) return {
1764
2485
  workspaceId,
1765
2486
  region: opts.region,
1766
- projectId,
1767
- branchId
2487
+ providerParams: PROVIDER_PARAMS
1768
2488
  };
1769
2489
  const region = process.env["PRISMA_REGION"];
1770
2490
  if (region === void 0 || region.length === 0) return {
1771
2491
  workspaceId,
1772
- projectId,
1773
- branchId
2492
+ providerParams: PROVIDER_PARAMS
1774
2493
  };
1775
2494
  if (!isComputeRegion(region)) throw new Error(`prismaCloud(): environment variable PRISMA_REGION="${region}" is not a known region (expected one of: ${COMPUTE_REGIONS.join(", ")}).`);
1776
2495
  return {
1777
2496
  workspaceId,
1778
2497
  region,
1779
- projectId,
1780
- branchId
2498
+ providerParams: PROVIDER_PARAMS
1781
2499
  };
1782
2500
  }
1783
2501
  /** The Prisma Cloud extension descriptor — `prisma-composer.config.ts` lists it under `extensions`. */
1784
2502
  const prismaCloud = (opts = {}) => {
1785
2503
  const o = resolveOptions(opts);
1786
2504
  return {
1787
- id: "@prisma/composer-prisma-cloud",
1788
- providers: () => asProvidersLayer(Layer.mergeAll(providers(), PgWarmProvider(), PnMigrationProvider(), S3CredentialsProvider())),
2505
+ id: PRISMA_CLOUD_EXTENSION_ID,
2506
+ container: containerDescriptor(),
2507
+ providers: () => asProvidersLayer(Layer.mergeAll(providers(), PgWarmProvider(), PnMigrationProvider(), S3CredentialsProvider(), ServiceKeyProvider())),
1789
2508
  preflight: (input) => runPreflight(input),
1790
- application: { provision: () => Effect.gen(function* () {
1791
- const projectId = o.projectId;
1792
- if (projectId === void 0 || projectId.length === 0) throw new Error("prismaCloud(): environment variable PRISMA_PROJECT_ID is required (the CLI sets it — deploy via `prisma-composer deploy`).");
2509
+ teardown: (input) => runTeardown(input),
2510
+ application: { provision: (ctx) => Effect.gen(function* () {
2511
+ const { projectId, branchId } = prismaCloudContainerOf(ctx.container);
1793
2512
  for (const key of ["DATABASE_URL", "DATABASE_URL_POOLED"]) yield* EnvironmentVariable(`${key}-poison`, {
1794
2513
  projectId,
1795
2514
  key,
1796
2515
  value: "-",
1797
- class: o.branchId ? "preview" : "production",
1798
- ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
2516
+ class: branchId ? "preview" : "production",
2517
+ ...branchId !== void 0 ? { branchId } : {}
1799
2518
  });
1800
- return { outputs: { projectId } };
2519
+ return {
2520
+ projectId,
2521
+ branchId
2522
+ };
1801
2523
  }) },
2524
+ provisions: PROVISIONERS,
1802
2525
  nodes: {
1803
2526
  postgres: postgresDescriptor(o),
1804
2527
  "prisma-next": prismaNextDescriptor(o),
1805
2528
  compute: computeDescriptor(o),
1806
2529
  credentials: s3CredentialsDescriptor(o),
1807
- "s3-store": s3StoreDescriptor(o)
2530
+ "s3-store": s3StoreDescriptor(o),
2531
+ s3: bucketDescriptor(o)
1808
2532
  }
1809
2533
  };
1810
2534
  };
1811
2535
  //#endregion
1812
- export { prismaCloud, prismaState };
2536
+ export { PROVIDER_PARAMS, buildProviderParams, prismaCloud, prismaState };
1813
2537
 
1814
2538
  //# sourceMappingURL=control.mjs.map