@prisma/composer-prisma-cloud 0.1.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.
@@ -0,0 +1,1814 @@
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";
4
+ import { blindCast } from "@prisma/composer/casts";
5
+ import pg from "pg";
6
+ import * as Layer from "effect/Layer";
7
+ import { createManagementApiClient } from "@prisma/management-api-sdk";
8
+ import * as Context from "effect/Context";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Redacted from "effect/Redacted";
11
+ import * as Config$1 from "effect/Config";
12
+ import * as Data from "effect/Data";
13
+ import * as Provider from "alchemy/Provider";
14
+ import { Resource, Stack } from "alchemy";
15
+ import * as Schedule from "effect/Schedule";
16
+ import * as fs from "node:fs";
17
+ import * as crypto$1 from "node:crypto";
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
20
+ import * as zlib from "node:zlib";
21
+ import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveStateRecursive } from "alchemy/State";
22
+ import postgres from "postgres";
23
+ import * as Output from "alchemy/Output";
24
+ import { loadConfig } from "@prisma-next/cli/config-loader";
25
+ import { resolve } from "pathe";
26
+ import { readRef } from "@prisma-next/migration-tools/refs";
27
+ import { APP_SPACE_ID, readContractSpaceHeadRef, spaceMigrationDirectory, spaceRefsDirectory } from "@prisma-next/migration-tools/spaces";
28
+ import { createPostgresControlClient } from "@prisma-next/postgres/control";
29
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CxGdfSAP.mjs
30
+ /**
31
+ * The Prisma service token used to authenticate Management API calls. Kept
32
+ * as a Redacted value so it never lands in logs or error output.
33
+ */
34
+ var PrismaCredentials = class extends Context.Service()("PrismaCredentials") {};
35
+ /** Resolve the token from the `PRISMA_SERVICE_TOKEN` environment variable. */
36
+ const fromEnv = () => Layer.effect(PrismaCredentials, Effect.gen(function* () {
37
+ return { token: yield* Config$1.redacted("PRISMA_SERVICE_TOKEN") };
38
+ }));
39
+ /**
40
+ * The typed Prisma Management API client, built once from the resolved
41
+ * credentials. Providers yield this in their outer Effect and call it inside
42
+ * `reconcile` / `delete`.
43
+ */
44
+ var ManagementClient = class extends Context.Service()("PrismaManagementClient") {};
45
+ const layer = () => Layer.effect(ManagementClient, Effect.gen(function* () {
46
+ const { token } = yield* PrismaCredentials;
47
+ return createManagementApiClient({ token: Redacted.value(token) });
48
+ }));
49
+ /** A non-2xx response from the Management API (or a transport failure). */
50
+ var PrismaApiError = class extends Data.TaggedError("PrismaApiError") {};
51
+ const attempt$1 = (f) => Effect.tryPromise({
52
+ try: f,
53
+ catch: (cause) => new PrismaApiError({
54
+ status: 0,
55
+ message: String(cause)
56
+ })
57
+ });
58
+ const fail = (r) => Effect.fail(new PrismaApiError({
59
+ status: r.response.status,
60
+ message: JSON.stringify(r.error)
61
+ }));
62
+ /** Unwrap `data`, failing on any API error. Preserves the SDK's response type. */
63
+ const call = (f) => attempt$1(f).pipe(Effect.flatMap((r) => r.error !== void 0 || r.data === void 0 ? fail(r) : Effect.succeed(r.data)));
64
+ /** Unwrap `data`, mapping a 404 to `undefined` (resource gone / not found). */
65
+ const callOptional = (f) => attempt$1(f).pipe(Effect.flatMap((r) => r.response.status === 404 ? Effect.succeed(void 0) : r.error !== void 0 ? fail(r) : Effect.succeed(r.data)));
66
+ /** Fire-and-forget a call, tolerating a 404 (already deleted). */
67
+ const callVoid = (f) => attempt$1(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
68
+ //#endregion
69
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/compute/index.mjs
70
+ /**
71
+ * Stopping a deployment before the compute service that owns it can be
72
+ * deleted is asynchronous on the platform's side: DELETE can 409 with this
73
+ * message while the deployment is still winding down. Retrying blindly on
74
+ * every API error would mask real failures (bad auth, a genuinely conflicting
75
+ * state, etc.), so this only matches the platform's specific "not delete-safe
76
+ * yet" wording — everything else fails immediately, as before.
77
+ */
78
+ const isDeleteNotSafeYet = (error) => error.message.includes("did not reach a delete-safe state");
79
+ /**
80
+ * Backs off exponentially from 2s, capped at 5 minutes total — long enough
81
+ * for the platform to finish stopping the deployment, short enough to still
82
+ * fail loudly (rather than hang forever) if it never does.
83
+ */
84
+ const deleteSafeRetrySchedule = Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
85
+ /** Every region Prisma Compute serves — the runtime source of truth; `ComputeRegion` is derived from it so the two can never drift. */
86
+ const COMPUTE_REGIONS = [
87
+ "us-east-1",
88
+ "us-west-1",
89
+ "eu-west-3",
90
+ "eu-central-1",
91
+ "ap-northeast-1",
92
+ "ap-southeast-1"
93
+ ];
94
+ /** A Prisma **Compute service** — the stable app identity behind a project. */
95
+ const ComputeService = Resource("Prisma.ComputeService");
96
+ const ComputeServiceProvider = () => Provider.effect(ComputeService, Effect.gen(function* () {
97
+ const client = yield* ManagementClient;
98
+ return {
99
+ stables: ["id"],
100
+ list: () => Effect.succeed([]),
101
+ 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;
103
+ if (observed) return {
104
+ id: observed.data.id,
105
+ name: observed.data.name,
106
+ endpointDomain: observed.data.serviceEndpointDomain
107
+ };
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
+ }));
116
+ return {
117
+ id: created.data.id,
118
+ name: created.data.name,
119
+ endpointDomain: created.data.serviceEndpointDomain
120
+ };
121
+ }),
122
+ delete: Effect.fn(function* ({ output }) {
123
+ yield* callVoid(() => client.DELETE("/v1/compute-services/{computeServiceId}", { params: { path: { computeServiceId: output.id } } })).pipe(Effect.retry({
124
+ schedule: deleteSafeRetrySchedule,
125
+ while: isDeleteNotSafeYet
126
+ }));
127
+ }),
128
+ read: Effect.fn(function* ({ output }) {
129
+ if (!output?.id) return void 0;
130
+ const s = yield* callOptional(() => client.GET("/v1/compute-services/{computeServiceId}", { params: { path: { computeServiceId: output.id } } }));
131
+ return s ? {
132
+ id: s.data.id,
133
+ name: s.data.name,
134
+ endpointDomain: s.data.serviceEndpointDomain
135
+ } : void 0;
136
+ })
137
+ };
138
+ }));
139
+ /**
140
+ * A **deployment** of a Prisma Compute service — creates a version, uploads
141
+ * its artifact, starts the VM, waits for it to run, then promotes it to the
142
+ * service's stable endpoint.
143
+ */
144
+ const Deployment = Resource("Prisma.Deployment");
145
+ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
146
+ 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({
148
+ status: 409,
149
+ message: `compute version ${versionId} is ${v.data.status}, not running`
150
+ }))), Effect.retry(Schedule.both(Schedule.spaced("2 seconds"), Schedule.during("2 minutes"))));
151
+ return {
152
+ stables: [],
153
+ list: () => Effect.succeed([]),
154
+ reconcile: Effect.fn(function* ({ news }) {
155
+ const created = yield* call(() => client.POST("/v1/compute-services/{computeServiceId}/versions", {
156
+ params: { path: { computeServiceId: news.computeServiceId } },
157
+ body: news.port !== void 0 ? { portMapping: { http: news.port } } : {}
158
+ }));
159
+ const versionId = created.data.id;
160
+ if (created.data.uploadUrl) {
161
+ const uploadUrl = created.data.uploadUrl;
162
+ const artifact = yield* Effect.try({
163
+ try: () => fs.readFileSync(news.artifactPath),
164
+ catch: (cause) => new PrismaApiError({
165
+ status: 0,
166
+ message: `failed to read artifact ${news.artifactPath}: ${String(cause)}`
167
+ })
168
+ });
169
+ yield* Effect.tryPromise({
170
+ try: async () => {
171
+ const res = await fetch(uploadUrl, {
172
+ method: "PUT",
173
+ body: artifact
174
+ });
175
+ if (!res.ok) throw new PrismaApiError({
176
+ status: res.status,
177
+ message: `artifact upload failed: ${res.status} ${res.statusText}`
178
+ });
179
+ },
180
+ catch: (cause) => cause instanceof PrismaApiError ? cause : new PrismaApiError({
181
+ status: 0,
182
+ message: String(cause)
183
+ })
184
+ });
185
+ }
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;
193
+ return {
194
+ versionId,
195
+ ...deployedUrl !== void 0 && { deployedUrl }
196
+ };
197
+ }),
198
+ delete: Effect.fn(function* () {}),
199
+ 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 } } }));
202
+ return v ? {
203
+ versionId: v.data.id,
204
+ ...v.data.previewDomain && { deployedUrl: v.data.previewDomain }
205
+ } : void 0;
206
+ })
207
+ };
208
+ }));
209
+ /**
210
+ * A project-scoped **environment variable** that Compute injects into the
211
+ * project's services from their attached branch (e.g. wiring one module's URL into
212
+ * another).
213
+ */
214
+ const EnvironmentVariable = Resource("Prisma.EnvironmentVariable");
215
+ const EnvironmentVariableProvider = () => Provider.effect(EnvironmentVariable, Effect.gen(function* () {
216
+ const client = yield* ManagementClient;
217
+ return {
218
+ stables: ["id"],
219
+ list: () => Effect.succeed([]),
220
+ reconcile: Effect.fn(function* ({ news, output }) {
221
+ const cls = news.class ?? "production";
222
+ let id = output?.id;
223
+ if (id !== void 0) {
224
+ const priorId = id;
225
+ if (!(yield* callOptional(() => client.GET("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: priorId } } })))) id = void 0;
226
+ }
227
+ if (id === void 0) {
228
+ const matchId = (yield* call(() => client.GET("/v1/environment-variables", { params: { query: {
229
+ projectId: news.projectId,
230
+ class: cls,
231
+ key: news.key
232
+ } } }))).data?.[0]?.id;
233
+ 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.`);
235
+ id = matchId;
236
+ }
237
+ }
238
+ if (id !== void 0) {
239
+ const targetId = id;
240
+ yield* call(() => client.PATCH("/v1/environment-variables/{envVarId}", {
241
+ params: { path: { envVarId: targetId } },
242
+ body: { value: news.value }
243
+ }));
244
+ return {
245
+ id,
246
+ key: news.key
247
+ };
248
+ }
249
+ const created = yield* call(() => client.POST("/v1/environment-variables", { body: {
250
+ projectId: news.projectId,
251
+ class: cls,
252
+ key: news.key,
253
+ value: news.value,
254
+ ...news.branchId ? { branchId: news.branchId } : {}
255
+ } }));
256
+ return {
257
+ id: created.data.id,
258
+ key: created.data.key
259
+ };
260
+ }),
261
+ delete: Effect.fn(function* ({ output }) {
262
+ yield* callVoid(() => client.DELETE("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: output.id } } }));
263
+ }),
264
+ read: Effect.fn(function* ({ output }) {
265
+ if (!output?.id) return void 0;
266
+ const v = yield* callOptional(() => client.GET("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: output.id } } }));
267
+ return v ? {
268
+ id: v.data.id,
269
+ key: v.data.key
270
+ } : void 0;
271
+ })
272
+ };
273
+ }));
274
+ /**
275
+ * Assembles a Prisma Compute artifact: the app-built bundle plus the
276
+ * extension-printed bootstrap and manifest, tarred and gzipped deterministically
277
+ * (fixed mtimes, sorted entry order) so an unchanged service noops on
278
+ * redeploy — a rebuild is the only thing that changes the hash. Lives here
279
+ * (not in @prisma/composer-prisma-cloud/control) because it needs node:fs/node:zlib,
280
+ * which the extension's shipped src may never import (invariant 5).
281
+ */
282
+ const MANIFEST_VERSION = "1";
283
+ /** Finds main.js/main.mjs in a bundle dir when no explicit entry is given. */
284
+ function resolveEntry(bundleDir, entry) {
285
+ if (entry !== void 0) return entry;
286
+ const found = fs.readdirSync(bundleDir).find((f) => /^main\.m?js$/.test(f));
287
+ if (found === void 0) throw new Error(`no main.js/main.mjs found in bundle dir ${bundleDir}`);
288
+ return found;
289
+ }
290
+ /** All files under `dir`, as dir-relative POSIX paths, in sorted order. A
291
+ * symlink is a hard error: deploy bundles must be flat (ADR-0005), and the
292
+ * user's build owns flattening — dereferencing here would relink the tree and
293
+ * risk packaging files from outside it. */
294
+ function walkFiles(dir) {
295
+ const out = [];
296
+ const visit = (sub) => {
297
+ for (const entry of fs.readdirSync(path.join(dir, sub), { withFileTypes: true })) {
298
+ const rel = sub.length > 0 ? `${sub}/${entry.name}` : entry.name;
299
+ if (entry.isSymbolicLink()) throw new Error(`bundle contains a symlink at ${rel} — deploy bundles must be flat; materialize links in your build (e.g. cp -RL) so the tree is self-contained.`);
300
+ if (entry.isDirectory()) visit(rel);
301
+ else out.push(rel);
302
+ }
303
+ };
304
+ visit("");
305
+ return out.sort();
306
+ }
307
+ function octal(value, length) {
308
+ return `${value.toString(8).padStart(length - 1, "0")}\0`;
309
+ }
310
+ /** Splits a path into ustar's name (<=100 bytes) + prefix (<=155 bytes) fields. */
311
+ function splitUstarPath(relPath) {
312
+ if (Buffer.byteLength(relPath, "utf8") <= 100) return {
313
+ name: relPath,
314
+ prefix: ""
315
+ };
316
+ for (let i = relPath.length - 1; i >= 0; i--) {
317
+ if (relPath[i] !== "/") continue;
318
+ const prefix = relPath.slice(0, i);
319
+ const name = relPath.slice(i + 1);
320
+ if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
321
+ name,
322
+ prefix
323
+ };
324
+ }
325
+ throw new Error(`path too long for a ustar tar entry: ${relPath}`);
326
+ }
327
+ function ustarHeader(relPath, size) {
328
+ const { name, prefix } = splitUstarPath(relPath);
329
+ const buf = Buffer.alloc(512);
330
+ buf.write(name, 0, 100, "utf8");
331
+ buf.write(octal(420, 8), 100, 8, "utf8");
332
+ buf.write(octal(0, 8), 108, 8, "utf8");
333
+ buf.write(octal(0, 8), 116, 8, "utf8");
334
+ buf.write(octal(size, 12), 124, 12, "utf8");
335
+ buf.write(octal(0, 12), 136, 12, "utf8");
336
+ buf.write(" ", 148, 8, "utf8");
337
+ buf.write("0", 156, 1, "utf8");
338
+ buf.write("ustar\0", 257, 6, "utf8");
339
+ buf.write("00", 263, 2, "utf8");
340
+ buf.write(prefix, 345, 155, "utf8");
341
+ let sum = 0;
342
+ for (const b of buf) sum += b;
343
+ buf.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "utf8");
344
+ return buf;
345
+ }
346
+ function createDeterministicTarGz(entries) {
347
+ const sorted = [...entries].sort((a, b) => a.relPath.localeCompare(b.relPath));
348
+ const chunks = [];
349
+ for (const entry of sorted) {
350
+ chunks.push(ustarHeader(entry.relPath, entry.content.length));
351
+ chunks.push(entry.content);
352
+ const pad = (512 - entry.content.length % 512) % 512;
353
+ if (pad > 0) chunks.push(Buffer.alloc(pad));
354
+ }
355
+ chunks.push(Buffer.alloc(1024));
356
+ return zlib.gzipSync(Buffer.concat(chunks));
357
+ }
358
+ /**
359
+ * Prints the bootstrap + manifest and tars them with the bundle into a
360
+ * deterministic artifact. If bundleDir doesn't exist (e.g. `alchemy destroy`
361
+ * run before any build), returns a placeholder rather than throwing — the
362
+ * artifact is never read on destroy.
363
+ */
364
+ function packageComputeArtifact(opts) {
365
+ if (!fs.existsSync(opts.bundleDir)) return {
366
+ path: "",
367
+ sha256: "absent"
368
+ };
369
+ const bootstrap = `import main from "./${resolveEntry(opts.bundleDir, opts.bundleEntry)}";\nawait main.run(${JSON.stringify(opts.address)}, () => import(${JSON.stringify(`./${opts.appEntry}`)}));\n`;
370
+ const manifest = `${JSON.stringify({
371
+ manifestVersion: MANIFEST_VERSION,
372
+ entrypoint: "bootstrap.js"
373
+ }, null, 2)}\n`;
374
+ const files = walkFiles(opts.bundleDir).map((relPath) => ({
375
+ relPath,
376
+ content: fs.readFileSync(path.join(opts.bundleDir, relPath))
377
+ }));
378
+ files.push({
379
+ relPath: "bootstrap.js",
380
+ content: Buffer.from(bootstrap, "utf8")
381
+ });
382
+ files.push({
383
+ relPath: "compute.manifest.json",
384
+ content: Buffer.from(manifest, "utf8")
385
+ });
386
+ files.push({
387
+ relPath: "bunfig.toml",
388
+ content: Buffer.from("[install]\nauto = \"disable\"\n", "utf8")
389
+ });
390
+ const gz = createDeterministicTarGz(files);
391
+ const sha256 = crypto$1.createHash("sha256").update(gz).digest("hex");
392
+ const outDir = path.join(os.tmpdir(), `prisma-composer-compute-${String(os.userInfo().uid)}`, sha256.slice(0, 16));
393
+ fs.mkdirSync(outDir, { recursive: true });
394
+ const outPath = path.join(outDir, `${opts.id}.tar.gz`);
395
+ const tmpPath = path.join(outDir, `.${opts.id}.${crypto$1.randomUUID()}.tmp`);
396
+ fs.writeFileSync(tmpPath, gz);
397
+ fs.renameSync(tmpPath, outPath);
398
+ return {
399
+ path: outPath,
400
+ sha256
401
+ };
402
+ }
403
+ //#endregion
404
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/postgres/index.mjs
405
+ /** A **connection** to a Prisma Postgres database — yields the connection string. */
406
+ const Connection = Resource("Prisma.Connection");
407
+ const ConnectionProvider = () => Provider.effect(Connection, Effect.gen(function* () {
408
+ const client = yield* ManagementClient;
409
+ return {
410
+ stables: ["id", "connectionString"],
411
+ list: () => Effect.succeed([]),
412
+ reconcile: Effect.fn(function* ({ news, output }) {
413
+ if (output?.id) return output;
414
+ const created = yield* call(() => client.POST("/v1/databases/{databaseId}/connections", {
415
+ params: { path: { databaseId: news.databaseId } },
416
+ body: { name: news.name }
417
+ }));
418
+ const endpoints = created.data.endpoints;
419
+ const dsn = endpoints?.direct?.connectionString ?? endpoints?.pooled?.connectionString;
420
+ if (dsn === void 0) return yield* Effect.fail(new PrismaApiError({
421
+ status: 0,
422
+ message: `connection ${created.data.id} returned no direct/pooled connection string`
423
+ }));
424
+ return {
425
+ id: created.data.id,
426
+ connectionString: Redacted.make(dsn)
427
+ };
428
+ }),
429
+ delete: Effect.fn(function* ({ output }) {
430
+ yield* callVoid(() => client.DELETE("/v1/connections/{id}", { params: { path: { id: output.id } } }));
431
+ })
432
+ };
433
+ }));
434
+ /** A Prisma **Postgres database** inside a project. */
435
+ const Database = Resource("Prisma.Database");
436
+ const DatabaseProvider = () => Provider.effect(Database, Effect.gen(function* () {
437
+ const client = yield* ManagementClient;
438
+ return {
439
+ stables: ["id"],
440
+ list: () => Effect.succeed([]),
441
+ reconcile: Effect.fn(function* ({ news, output }) {
442
+ 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 = {
458
+ id: created.data.id,
459
+ name: created.data.name
460
+ };
461
+ }
462
+ if (news.branchId !== void 0) {
463
+ const branchId = news.branchId;
464
+ yield* call(() => client.PATCH("/v1/databases/{databaseId}", {
465
+ params: { path: { databaseId: result.id } },
466
+ body: { branchId }
467
+ }));
468
+ }
469
+ return result;
470
+ }),
471
+ delete: Effect.fn(function* ({ output }) {
472
+ yield* callVoid(() => client.DELETE("/v1/databases/{databaseId}", { params: { path: { databaseId: output.id } } }));
473
+ }),
474
+ read: Effect.fn(function* ({ output }) {
475
+ if (!output?.id) return void 0;
476
+ const d = yield* callOptional(() => client.GET("/v1/databases/{databaseId}", { params: { path: { databaseId: output.id } } }));
477
+ return d ? {
478
+ id: d.data.id,
479
+ name: d.data.name
480
+ } : void 0;
481
+ })
482
+ };
483
+ }));
484
+ /** A Prisma Developer Platform **Project** — the container for databases and compute services. */
485
+ const Project = Resource("Prisma.Project");
486
+ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
487
+ const client = yield* ManagementClient;
488
+ return {
489
+ stables: ["id"],
490
+ list: () => Effect.succeed([]),
491
+ reconcile: Effect.fn(function* ({ news, output }) {
492
+ const observed = output?.id ? yield* callOptional(() => client.GET("/v1/projects/{id}", { params: { path: { id: output.id } } })) : void 0;
493
+ if (observed) return {
494
+ id: observed.data.id,
495
+ name: observed.data.name
496
+ };
497
+ const created = yield* call(() => client.POST("/v1/projects", { body: {
498
+ name: news.name,
499
+ workspaceId: news.workspaceId
500
+ } }));
501
+ return {
502
+ id: created.data.id,
503
+ name: created.data.name
504
+ };
505
+ }),
506
+ delete: Effect.fn(function* ({ output }) {
507
+ yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: output.id } } }));
508
+ }),
509
+ read: Effect.fn(function* ({ output }) {
510
+ if (!output?.id) return void 0;
511
+ const p = yield* callOptional(() => client.GET("/v1/projects/{id}", { params: { path: { id: output.id } } }));
512
+ return p ? {
513
+ id: p.data.id,
514
+ name: p.data.name
515
+ } : void 0;
516
+ })
517
+ };
518
+ }));
519
+ Data.TaggedError("ContainerNotFoundError");
520
+ /** The collection of Prisma resource providers. */
521
+ var Providers = class extends Provider.ProviderCollection()("Prisma") {};
522
+ /**
523
+ * The Prisma provider bundle: every resource provider, the Management API
524
+ * client, and env-based credentials. Plug into a stack with
525
+ * `{ providers: Prisma.providers() }`.
526
+ */
527
+ const providers = () => Layer.effect(Providers, Provider.collection([
528
+ Project,
529
+ Database,
530
+ Connection,
531
+ ComputeService,
532
+ Deployment,
533
+ EnvironmentVariable
534
+ ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DatabaseProvider(), ConnectionProvider(), ComputeServiceProvider(), DeploymentProvider(), EnvironmentVariableProvider())), Layer.provideMerge(layer()), Layer.provideMerge(fromEnv()), Layer.orDie);
535
+ //#endregion
536
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/state/index.mjs
537
+ /** Collapses any thrown value (postgres.js failures included) into a {@link StateStoreError}. */
538
+ const toStateStoreError = (cause) => cause instanceof Error ? new StateStoreError({
539
+ message: cause.message,
540
+ cause
541
+ }) : new StateStoreError({ message: String(cause) });
542
+ /**
543
+ * An operator-facing failure from the hosted-state bootstrap pipeline
544
+ * (project/database discovery, connection mint, schema migration, or lock
545
+ * acquisition) — what a deployer actually sees, instead of a raw Effect
546
+ * defect.
547
+ */
548
+ var HostedStateBootstrapError = class extends Data.TaggedError("HostedStateBootstrapError") {
549
+ get message() {
550
+ return `hosted-state bootstrap failed for workspace ${this.workspaceId}: ${this.step} — ${this.reason}`;
551
+ }
552
+ };
553
+ /**
554
+ * Builds a {@link HostedStateBootstrapError} from whatever the failed step
555
+ * threw. Never retains the raw driver/API error object as `cause`: a
556
+ * postgres.js connection failure's `.message`/properties are not verified to
557
+ * omit the DSN or credentials, so only the extracted message text survives
558
+ * into the operator-facing error.
559
+ */
560
+ const hostedStateBootstrapError = (workspaceId, step, cause) => new HostedStateBootstrapError({
561
+ workspaceId,
562
+ step,
563
+ reason: cause instanceof Error ? cause.message : String(cause)
564
+ });
565
+ /**
566
+ * The well-known marker row written into every database this store owns.
567
+ * Its presence proves the database is genuinely Prisma App's state store, not
568
+ * a same-named project squatting on the discovery query (see `bootstrap.ts`
569
+ * `verifyOwnership` — PDP allows duplicate project names).
570
+ */
571
+ const STATE_META_MARKER = "prisma-composer-state-v1";
572
+ /**
573
+ * Idempotent schema migration for the Prisma-hosted state store — safe to run
574
+ * on every deploy, since `create table if not exists` no-ops once the tables
575
+ * exist. Run this against `sql` before serving a {@link StateService} built
576
+ * by `makePrismaStateService` over the same client.
577
+ */
578
+ const migratePrismaState = (sql) => Effect.tryPromise({
579
+ try: async () => {
580
+ await sql`
581
+ create table if not exists alchemy_resource_state (
582
+ stack text not null,
583
+ stage text not null,
584
+ fqn text not null,
585
+ value jsonb not null,
586
+ updated_at timestamptz not null default now(),
587
+ primary key (stack, stage, fqn)
588
+ )
589
+ `;
590
+ await sql`
591
+ create table if not exists alchemy_stack_output (
592
+ stack text not null,
593
+ stage text not null,
594
+ value jsonb not null,
595
+ updated_at timestamptz not null default now(),
596
+ primary key (stack, stage)
597
+ )
598
+ `;
599
+ await sql`
600
+ create table if not exists prisma_app_state_meta (
601
+ marker text primary key,
602
+ created_at timestamptz not null default now()
603
+ )
604
+ `;
605
+ await sql`
606
+ insert into prisma_app_state_meta (marker) values (${STATE_META_MARKER})
607
+ on conflict (marker) do nothing
608
+ `;
609
+ },
610
+ catch: toStateStoreError
611
+ });
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
+ 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
+ /**
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.
643
+ */
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
+ });
691
+ const listAllConnections = (client, databaseId) => Effect.gen(function* () {
692
+ const connections = [];
693
+ let cursor;
694
+ for (;;) {
695
+ const query = cursor === void 0 ? {} : { cursor };
696
+ const page = yield* call(() => client.GET("/v1/databases/{databaseId}/connections", { params: {
697
+ path: { databaseId },
698
+ query
699
+ } }));
700
+ connections.push(...page.data);
701
+ if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
702
+ cursor = page.pagination.nextCursor;
703
+ }
704
+ return connections;
705
+ });
706
+ const deleteConnection = (client, connectionId) => callVoid(() => client.DELETE("/v1/connections/{id}", { params: { path: { id: connectionId } } }));
707
+ /**
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).
715
+ */
716
+ const cleanupAgedConnections = (client, databaseId) => Effect.gen(function* () {
717
+ const connections = yield* listAllConnections(client, databaseId);
718
+ 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);
720
+ yield* Effect.forEach(aged, (c) => deleteConnection(client, c.id), { discard: true });
721
+ }).pipe(Effect.ignore);
722
+ /**
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`
742
+ * 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:
745
+ *
746
+ * - our marker table with our marker row present → `ours`, adopt outright.
747
+ * - our state tables (`alchemy_resource_state`/`alchemy_stack_output`) but no
748
+ * marker → `legacy`: a database from before this ownership check existed.
749
+ * The real, currently-in-use workspace state is in this shape today, so it
750
+ * must keep working — adopt it, and `migratePrismaState` (idempotent)
751
+ * writes the marker on the way in.
752
+ * - no tables at all → `empty`, a freshly-created default database — adopt.
753
+ * - anything else → `squatter`: foreign data occupies the name; refuse it.
754
+ */
755
+ const verifyOwnership = (connectionString) => Effect.tryPromise({
756
+ try: async () => {
757
+ const sql = postgres(Redacted.value(connectionString), {
758
+ max: 1,
759
+ onnotice: () => {}
760
+ });
761
+ try {
762
+ const rows = await sql`
763
+ select tablename from pg_tables where schemaname = 'public'
764
+ `;
765
+ const tables = new Set(rows.map((row) => row.tablename));
766
+ if (tables.has("prisma_app_state_meta")) return (await sql`
767
+ select marker from prisma_app_state_meta where marker = ${"prisma-composer-state-v1"}
768
+ `).length > 0 ? { kind: "ours" } : {
769
+ kind: "squatter",
770
+ tables: [...tables]
771
+ };
772
+ if (tables.has("alchemy_resource_state") || tables.has("alchemy_stack_output")) return { kind: "legacy" };
773
+ return tables.size === 0 ? { kind: "empty" } : {
774
+ kind: "squatter",
775
+ tables: [...tables]
776
+ };
777
+ } finally {
778
+ await sql.end({ timeout: 5 });
779
+ }
780
+ },
781
+ catch: (cause) => new PrismaApiError({
782
+ status: 0,
783
+ message: `ownership verification failed: ${cause instanceof Error ? cause.message : String(cause)}`
784
+ })
785
+ });
786
+ /**
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);
801
+ 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}`);
806
+ return {
807
+ project,
808
+ database,
809
+ connectionString
810
+ };
811
+ }
812
+ const sorted = [...candidates].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
813
+ const rejected = [];
814
+ for (const candidate of sorted) {
815
+ const database = yield* findDefaultDatabase(client, candidate.id);
816
+ const connectionString = yield* mintConnection(client, database.id);
817
+ const verdict = yield* verify(connectionString);
818
+ if (verdict.kind === "squatter") {
819
+ rejected.push(`${candidate.id} (foreign tables: ${verdict.tables.join(", ") || "none named"})`);
820
+ continue;
821
+ }
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}`);
823
+ return {
824
+ project: candidate,
825
+ database,
826
+ connectionString
827
+ };
828
+ }
829
+ return yield* Effect.fail(new PrismaApiError({
830
+ 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).`
832
+ }));
833
+ });
834
+ /**
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.
839
+ */
840
+ const bootstrapStateConnection = (workspaceId) => bootstrapStateConnectionWith(workspaceId, verifyOwnership);
841
+ /**
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.
846
+ */
847
+ const bootstrapStateConnectionWith = (workspaceId, verify) => Effect.gen(function* () {
848
+ const client = yield* ManagementClient;
849
+ const { project, database, connectionString } = yield* resolveStateProject(client, workspaceId, verify);
850
+ yield* cleanupAgedConnections(client, database.id);
851
+ return {
852
+ projectId: project.id,
853
+ databaseId: database.id,
854
+ connectionString
855
+ };
856
+ });
857
+ /** Another deploy already holds the lock for this stack/stage. Never queued — fails immediately. */
858
+ var StateLockContentionError = class extends Data.TaggedError("StateLockContentionError") {
859
+ get message() {
860
+ return `another deploy holds the state lock for ${this.stack}/${this.stage}`;
861
+ }
862
+ };
863
+ const lockKey = (stack, stage) => `prisma-composer:${stack}/${stage}`;
864
+ /**
865
+ * Acquires a session-scoped Postgres advisory lock on a reserved
866
+ * connection pulled from `sql`'s pool — session (not transaction) scope,
867
+ * because a transaction-scoped lock releases at the first commit and a
868
+ * deploy spans many. Held for the run's whole lifetime; contention fails
869
+ * immediately rather than queuing. If the process crashes, the reserved
870
+ * connection drops and Postgres auto-releases the session lock — no
871
+ * explicit crash-recovery bookkeeping needed.
872
+ */
873
+ const acquireStateLock = (sql, stack, stage) => Effect.gen(function* () {
874
+ const key = lockKey(stack, stage);
875
+ const reserved = yield* Effect.tryPromise({
876
+ try: () => sql.reserve(),
877
+ catch: toStateStoreError
878
+ });
879
+ const acquired = yield* Effect.tryPromise({
880
+ try: async () => {
881
+ return (await reserved`
882
+ select
883
+ pg_try_advisory_lock(hashtextextended(${key}, 0)) as acquired,
884
+ pg_backend_pid() as pid
885
+ `)[0];
886
+ },
887
+ catch: toStateStoreError
888
+ });
889
+ if (acquired?.acquired !== true) {
890
+ reserved.release();
891
+ return yield* Effect.fail(new StateLockContentionError({
892
+ stack,
893
+ stage
894
+ }));
895
+ }
896
+ const lockPid = acquired.pid;
897
+ const checkLive = Effect.tryPromise({
898
+ try: async () => {
899
+ 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;
910
+ },
911
+ 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` }))));
913
+ const release = async () => {
914
+ try {
915
+ await reserved`select pg_advisory_unlock(hashtextextended(${key}, 0))`;
916
+ } catch {} finally {
917
+ reserved.release();
918
+ }
919
+ };
920
+ return {
921
+ checkLive,
922
+ release
923
+ };
924
+ });
925
+ const attempt = (f) => Effect.tryPromise({
926
+ try: f,
927
+ catch: toStateStoreError
928
+ });
929
+ /**
930
+ * Wraps an already-`encodeState`d value as a jsonb-typed bind parameter.
931
+ * Must go through `sql.json(...)` (not `JSON.stringify(...)::jsonb`) —
932
+ * postgres.js re-serializes the parameter once it learns the server-inferred
933
+ * type is jsonb, so a pre-stringified value passed through a `::jsonb` cast
934
+ * gets JSON-encoded *twice* and lands as a jsonb string instead of an
935
+ * object. `sql.json` gives postgres.js the raw value up front and declares
936
+ * the jsonb oid itself, so it is serialized exactly once.
937
+ */
938
+ const jsonParam = (sql, value) => sql.json(blindCast(encodeState(value)));
939
+ /**
940
+ * `reviveStateRecursive` is typed to return `unknown` — the caller is
941
+ * expected to know the shape it revived. Here the shape is known by
942
+ * construction: every row was written by `set()`, which persists a value
943
+ * through `encodeState` first, so reviving it recovers a `PersistedState`.
944
+ */
945
+ const revivePersistedState = (value) => blindCast(reviveStateRecursive(value));
946
+ /** Same reasoning as {@link revivePersistedState}, narrowed by the SQL status filter. */
947
+ const reviveReplacedResourceState = (value) => blindCast(reviveStateRecursive(value));
948
+ /**
949
+ * Builds alchemy's `StateService` over a caller-supplied postgres.js client,
950
+ * against the two-table schema `migratePrismaState` creates. The caller owns
951
+ * the client's lifecycle (connection pooling, reconnects, `.end()`); this
952
+ * factory only issues queries.
953
+ */
954
+ const makePrismaStateService = (sql) => ({
955
+ id: "prisma-postgres",
956
+ getVersion: () => Effect.succeed(STATE_STORE_VERSION),
957
+ listStacks: () => attempt(() => sql`
958
+ select stack from alchemy_resource_state
959
+ union
960
+ select stack from alchemy_stack_output
961
+ order by stack
962
+ `).pipe(Effect.map((rows) => rows.map((row) => row.stack))),
963
+ listStages: (stack) => attempt(() => sql`
964
+ select stage from alchemy_resource_state where stack = ${stack}
965
+ union
966
+ select stage from alchemy_stack_output where stack = ${stack}
967
+ order by stage
968
+ `).pipe(Effect.map((rows) => rows.map((row) => row.stage))),
969
+ get: (request) => attempt(() => sql`
970
+ select value from alchemy_resource_state
971
+ where stack = ${request.stack} and stage = ${request.stage} and fqn = ${request.fqn}
972
+ `).pipe(Effect.map((rows) => {
973
+ const row = rows[0];
974
+ return row === void 0 ? void 0 : revivePersistedState(row.value);
975
+ })),
976
+ getReplacedResources: (request) => attempt(() => sql`
977
+ select value from alchemy_resource_state
978
+ where stack = ${request.stack} and stage = ${request.stage}
979
+ and value ->> 'status' = 'replaced'
980
+ `).pipe(Effect.map((rows) => rows.map((row) => reviveReplacedResourceState(row.value)))),
981
+ set: (request) => attempt(() => sql`
982
+ insert into alchemy_resource_state (stack, stage, fqn, value, updated_at)
983
+ values (
984
+ ${request.stack}, ${request.stage}, ${request.fqn},
985
+ ${jsonParam(sql, request.value)}, now()
986
+ )
987
+ on conflict (stack, stage, fqn) do update
988
+ set value = excluded.value, updated_at = excluded.updated_at
989
+ `).pipe(Effect.map(() => request.value)),
990
+ delete: (request) => attempt(() => sql`
991
+ delete from alchemy_resource_state
992
+ where stack = ${request.stack} and stage = ${request.stage} and fqn = ${request.fqn}
993
+ `).pipe(Effect.asVoid),
994
+ deleteStack: (request) => attempt(async () => {
995
+ if (request.stage === void 0) {
996
+ await sql`delete from alchemy_resource_state where stack = ${request.stack}`;
997
+ await sql`delete from alchemy_stack_output where stack = ${request.stack}`;
998
+ } else {
999
+ await sql`
1000
+ delete from alchemy_resource_state
1001
+ where stack = ${request.stack} and stage = ${request.stage}
1002
+ `;
1003
+ await sql`
1004
+ delete from alchemy_stack_output
1005
+ where stack = ${request.stack} and stage = ${request.stage}
1006
+ `;
1007
+ }
1008
+ }),
1009
+ list: (request) => attempt(() => sql`
1010
+ select fqn from alchemy_resource_state
1011
+ where stack = ${request.stack} and stage = ${request.stage}
1012
+ order by fqn
1013
+ `).pipe(Effect.map((rows) => rows.map((row) => row.fqn))),
1014
+ getOutput: (request) => attempt(() => sql`
1015
+ select value from alchemy_stack_output
1016
+ where stack = ${request.stack} and stage = ${request.stage}
1017
+ `).pipe(Effect.map((rows) => {
1018
+ const row = rows[0];
1019
+ return row === void 0 ? void 0 : reviveStateRecursive(row.value);
1020
+ })),
1021
+ setOutput: (request) => attempt(() => sql`
1022
+ insert into alchemy_stack_output (stack, stage, value, updated_at)
1023
+ values (${request.stack}, ${request.stage}, ${jsonParam(sql, request.value)}, now())
1024
+ on conflict (stack, stage) do update
1025
+ set value = excluded.value, updated_at = excluded.updated_at
1026
+ `).pipe(Effect.map(() => request.value))
1027
+ });
1028
+ /**
1029
+ * How long a passing lease check is trusted before the next storage
1030
+ * operation re-verifies it. A deploy issues many state ops in quick
1031
+ * succession, and each raw `checkLive` is a `pg_locks` round-trip; without
1032
+ * amortization the guard roughly doubles the store's traffic. The cost of the
1033
+ * window is bounded: a lease lost mid-window is detected within this many ms,
1034
+ * not instantly — an accepted tradeoff on top of the already-accepted
1035
+ * non-atomic (TOCTOU) gap between the check and the operation.
1036
+ */
1037
+ const LEASE_CHECK_TTL_MS = 5e3;
1038
+ /**
1039
+ * Amortizes a lease check over a short TTL: a *passing* check is trusted for
1040
+ * `ttlMs`, so a burst of operations inside the window does one round-trip, not
1041
+ * one per op. A *failing* check is never cached — it propagates immediately
1042
+ * and leaves the last-good timestamp untouched, so the very next op re-checks.
1043
+ * The `lastOkAt` state is captured per call, so each store (each layer) gets
1044
+ * its own window — two stores in one process never share a cached success.
1045
+ */
1046
+ const amortizeCheck = (checkLive, ttlMs, now) => {
1047
+ let lastOkAt;
1048
+ return Effect.suspend(() => {
1049
+ if (lastOkAt !== void 0 && now() - lastOkAt < ttlMs) return Effect.void;
1050
+ return checkLive.pipe(Effect.tap(() => Effect.sync(() => {
1051
+ lastOkAt = now();
1052
+ })));
1053
+ });
1054
+ };
1055
+ /**
1056
+ * Wraps a {@link StateService} so every method that touches storage first
1057
+ * re-verifies the state lock's lease via `checkLive`. Used to enforce "a
1058
+ * dropped lock connection fails loudly" — see {@link ../lock.ts}. The check is
1059
+ * amortized over a short TTL (see {@link amortizeCheck}), so a run of
1060
+ * back-to-back operations does not fire one `pg_locks` round-trip per call.
1061
+ *
1062
+ * Reads are gated too, not just writes: a lost lease means a concurrent
1063
+ * deploy may already be mutating this stack's rows, so a read could return
1064
+ * stale or conflicting data — untrustworthy either way, not just the writes.
1065
+ * The check is best-effort and not atomic with the operation it guards (the
1066
+ * lease could be lost in the gap between `checkLive` passing and the wrapped
1067
+ * call executing, or within the TTL window); that residual race is accepted.
1068
+ *
1069
+ * `getVersion` is excluded: it returns a compile-time constant
1070
+ * (`STATE_STORE_VERSION`), so guarding it would only add a pointless
1071
+ * reserved-connection round-trip.
1072
+ *
1073
+ * `now` is injectable so tests can advance the clock deterministically; it
1074
+ * defaults to `Date.now` (fine in library runtime code — only Workflow
1075
+ * scripts forbid it).
1076
+ */
1077
+ const guardStateService = (service, checkLive, now = Date.now) => {
1078
+ const guard = amortizeCheck(checkLive, LEASE_CHECK_TTL_MS, now);
1079
+ return {
1080
+ id: service.id,
1081
+ getVersion: () => service.getVersion(),
1082
+ listStacks: () => guard.pipe(Effect.andThen(service.listStacks())),
1083
+ listStages: (stack) => guard.pipe(Effect.andThen(service.listStages(stack))),
1084
+ get: (request) => guard.pipe(Effect.andThen(service.get(request))),
1085
+ getReplacedResources: (request) => guard.pipe(Effect.andThen(service.getReplacedResources(request))),
1086
+ set: (request) => guard.pipe(Effect.andThen(service.set(request))),
1087
+ delete: (request) => guard.pipe(Effect.andThen(service.delete(request))),
1088
+ deleteStack: (request) => guard.pipe(Effect.andThen(service.deleteStack(request))),
1089
+ list: (request) => guard.pipe(Effect.andThen(service.list(request))),
1090
+ getOutput: (request) => guard.pipe(Effect.andThen(service.getOutput(request))),
1091
+ setOutput: (request) => guard.pipe(Effect.andThen(service.setOutput(request)))
1092
+ };
1093
+ };
1094
+ /**
1095
+ * 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`).
1103
+ *
1104
+ * Any bootstrap/lock/migration failure is wrapped into an operator-facing
1105
+ * `HostedStateBootstrapError` (naming the workspace and the step that
1106
+ * failed, never the raw driver/API error — see `errors.ts`) before dying the
1107
+ * layer (loud, immediate, unrecoverable) rather than surfacing as a typed
1108
+ * error — matching core's `LowerOptions.state: Layer.Layer<State, never,
1109
+ * StackServices>` contract and alchemy's own convention (e.g. a missing
1110
+ * state store is `Effect.die` in `Stack.make`).
1111
+ */
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.");
1115
+ return Layer.effect(State, Effect.gen(function* () {
1116
+ 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")));
1119
+ const sql = postgres(Redacted.value(connectionString), {
1120
+ max: 5,
1121
+ onnotice: () => {}
1122
+ });
1123
+ yield* Effect.addFinalizer(() => Effect.promise(() => sql.end({ timeout: 5 })));
1124
+ yield* migratePrismaState(sql).pipe(Effect.retry(Schedule.both(Schedule.spaced("5 seconds"), Schedule.during("2 minutes"))), Effect.mapError(bootstrapError("schema migration")));
1125
+ const lock = yield* acquireStateLock(sql, stack.name, stack.stage).pipe(Effect.mapError(bootstrapError("lock acquisition")));
1126
+ yield* Effect.addFinalizer(() => Effect.promise(() => lock.release()));
1127
+ const service = guardStateService(makePrismaStateService(sql), lock.checkLive);
1128
+ return Effect.succeed(service);
1129
+ })).pipe(Layer.orDie);
1130
+ };
1131
+ //#endregion
1132
+ //#region ../../1-prisma-cloud/1-extensions/target/dist/control.mjs
1133
+ const PRISMA_NAME_MIN = 3;
1134
+ const PRISMA_NAME_MAX = 65;
1135
+ function validateName(value, source) {
1136
+ 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
+ }
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"]);
1140
+ function computeDescriptor(o) {
1141
+ return {
1142
+ kind: "service",
1143
+ provision: ({ id, application }) => Effect.gen(function* () {
1144
+ validateName(id, "service name (from provision id)");
1145
+ return { outputs: {
1146
+ serviceId: (yield* ComputeService(`${id}-svc`, {
1147
+ projectId: projectIdOf(application),
1148
+ name: id,
1149
+ region: o.region ?? "us-east-1",
1150
+ ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
1151
+ })).id,
1152
+ projectId: application.outputs["projectId"]
1153
+ } };
1154
+ }),
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);
1159
+ const svc = node;
1160
+ const records = [];
1161
+ for (const d of paramEntries(svc)) {
1162
+ const value = d.owner === "service" ? config.service[d.name] : config.inputs[d.owner.input]?.[d.name];
1163
+ const key = configKey(address, d);
1164
+ records.push(yield* EnvironmentVariable(`${key}-var`, {
1165
+ projectId,
1166
+ key,
1167
+ value: encode(d.owner, value),
1168
+ class: cls,
1169
+ ...branch
1170
+ }));
1171
+ }
1172
+ for (const { key, name } of secretPointerRows(svc, address, graph.secrets)) records.push(yield* EnvironmentVariable(`${key}-var`, {
1173
+ projectId,
1174
+ key,
1175
+ value: name,
1176
+ class: cls,
1177
+ ...branch
1178
+ }));
1179
+ return { outputs: {
1180
+ environment: records,
1181
+ port: typeof config.service["port"] === "number" ? config.service["port"] : 3e3
1182
+ } };
1183
+ }),
1184
+ package: ({ id }, { assembled, address }) => Effect.try(() => packageComputeArtifact({
1185
+ id,
1186
+ bundleDir: assembled.dir,
1187
+ appEntry: assembled.entry,
1188
+ address
1189
+ })),
1190
+ 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
+ } };
1201
+ })
1202
+ };
1203
+ }
1204
+ /**
1205
+ * The `PgWarm` Alchemy resource (slice 3, FT-5226) — warm a freshly-provisioned
1206
+ * Prisma Postgres database at apply-time so it is ready by deploy-end, and the
1207
+ * first real connection (a service's runtime client, or the migration) doesn't
1208
+ * eat the cold-start reject.
1209
+ *
1210
+ * The DB `url` is a lazy `Output` at lowering time, so warming must be an
1211
+ * apply-time tracked resource (same pattern as `PnMigration`): its `reconcile`
1212
+ * receives the RESOLVED url and connects with `withConnectionRetry` + `select 1`,
1213
+ * riding out the cold-start. Shared by BOTH the bare-`postgres` and the
1214
+ * `prisma-next` lowerings; keyed on the connection `url`, so an unchanged
1215
+ * redeploy is a no-op (warming is idempotent anyway).
1216
+ *
1217
+ * Deploy-time only: imports `pg` directly + `alchemy`. Imported by `control.ts`
1218
+ * and tests, never by `index.ts` / the `./prisma-next` authoring entry — the
1219
+ * isolation invariants hold.
1220
+ */
1221
+ /** The `PgWarm` resource constructor — `yield* PgWarm(id, { url })` in a lowering. */
1222
+ const PgWarm = Resource("PrismaCloud.PgWarm");
1223
+ /** Connect (retrying the cold-start) and run `select 1`, then release the connection. */
1224
+ async function warmDatabase(url) {
1225
+ await withConnectionRetry(async () => {
1226
+ const client = new pg.Client({ connectionString: normalizeSslMode(url) });
1227
+ await client.connect();
1228
+ try {
1229
+ await client.query("select 1");
1230
+ } finally {
1231
+ await client.end();
1232
+ }
1233
+ });
1234
+ }
1235
+ /**
1236
+ * The `PgWarm` provider service. `reconcile` warms the DB (retrying the
1237
+ * cold-start) and echoes the `url` so a downstream resource that reads
1238
+ * `warm.url` runs only after the DB is warm. Idempotent — safe on redeploy;
1239
+ * nothing to enumerate (`list` → `[]`) or tear down (`delete` → no-op; the DB's
1240
+ * own deletion handles teardown). Exported so tests can drive it directly.
1241
+ */
1242
+ const pgWarmProviderService = {
1243
+ list: () => Effect.succeed([]),
1244
+ reconcile: ({ news }) => Effect.tryPromise({
1245
+ try: () => warmDatabase(news.url),
1246
+ catch: (error) => error
1247
+ }).pipe(Effect.map(() => ({ url: news.url }))),
1248
+ delete: () => Effect.void
1249
+ };
1250
+ /** The `PgWarm` provider layer — merged into the extension descriptor's `providers()`. */
1251
+ const PgWarmProvider = () => Provider.effect(PgWarm, Effect.succeed(pgWarmProviderService));
1252
+ /**
1253
+ * One Database per module-provisioned postgres resource — `id` is the
1254
+ * module provision id, so a resource shared by several consumers is created
1255
+ * exactly once.
1256
+ */
1257
+ function postgresDescriptor(o) {
1258
+ const lowering = ({ id, application }) => Effect.gen(function* () {
1259
+ validateName(id, "resource name (from provision id)");
1260
+ const db = yield* Database(`${id}-db`, {
1261
+ projectId: projectIdOf(application),
1262
+ name: id,
1263
+ region: o.region ?? "us-east-1",
1264
+ ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
1265
+ });
1266
+ const conn = yield* Connection(`${id}-conn`, {
1267
+ databaseId: db.id,
1268
+ name: id
1269
+ });
1270
+ const url = Output.map(conn.connectionString, (value) => Redacted.value(value));
1271
+ return { outputs: { url: (yield* PgWarm(`${id}-warm`, { url })).url } };
1272
+ });
1273
+ return Object.assign(lowering, { kind: "resource" });
1274
+ }
1275
+ /**
1276
+ * Resolves a `pnPostgres` resource's `prisma-next.config.ts` path to the
1277
+ * on-disk migrations directory the control client's `migrate`/`dbInit` needs
1278
+ * (ADR-0022, slice 2). Deploy-time only: loads PN's config (via c12) and
1279
+ * applies PN's own convention — `migrations.dir`, or the default `migrations/`,
1280
+ * relative to the config file's directory (mirrors the CLI's
1281
+ * `resolveMigrationPaths`). Imported by `control.ts` + tests, never by
1282
+ * `index.ts` / the `./prisma-next` authoring entry.
1283
+ *
1284
+ * `pathe` (not `node:path`) does the path work so the shipped source carries no
1285
+ * `node:` import — the same discipline `control.ts` already follows by
1286
+ * delegating fs/tar to `@internal/lowering` (invariant 5).
1287
+ */
1288
+ /** The absolute migrations directory PN reads authored migration packages from. */
1289
+ async function resolveMigrationsDir(configPath) {
1290
+ return resolve(configPath, "..", (await loadConfig(configPath)).migrations?.dir ?? "migrations");
1291
+ }
1292
+ /**
1293
+ * The Prisma Next migration step of the deploy lowering (ADR-0022, slice 2) —
1294
+ * the safety-critical decision that brings a live database to a target REF
1295
+ * using ONLY Prisma Next's authored migrations.
1296
+ *
1297
+ * Deploy-time only: this module imports `@prisma-next/postgres/control` (which
1298
+ * transitively pulls PN's control/migration machinery + `pg`). It is imported
1299
+ * by the deploy descriptors and this package's tests, NEVER by `index.ts` / the
1300
+ * `./prisma-next` authoring entry — so it never lands in an app runtime bundle
1301
+ * (the index-isolation invariant holds).
1302
+ *
1303
+ * The target is a ref `{ hash, invariants }` — not a bare `storageHash`. A
1304
+ * ref's `invariants` are named postconditions established by `data`-class
1305
+ * migration steps (e.g. a backfill), recorded monotonically on the live
1306
+ * marker. Keying on the hash alone would make a pure data-invariant change an
1307
+ * A→A self-edge the deploy wrongly skips. The decision, given the live marker
1308
+ * and the target ref (see {@link decideMigrationAction}):
1309
+ * - marker at ref.hash AND ref.invariants ⊆ marker.invariants → no-op
1310
+ * - no marker (fresh DB) AND no required invariants → `dbInit`
1311
+ * - otherwise → `migrate`
1312
+ *
1313
+ * `dbInit` is additive-only synthesis — it NEVER runs app-space data steps —
1314
+ * so it is only correct when the ref requires no invariants; a fresh DB whose
1315
+ * target carries invariants goes through `migrate`, which walks the AUTHORED
1316
+ * graph (including the invariant-bearing data migrations) from empty.
1317
+ *
1318
+ * Never `dbUpdate`: synthesized diff-and-apply plans are never run against a
1319
+ * deployed database. A no-authored-path (`MIGRATION_PATH_NOT_FOUND`) or a
1320
+ * runner failure fails the deploy as a typed `PnMigrationError` (not swallowed).
1321
+ * PN applies each migration in its own transaction, so a failed apply is atomic
1322
+ * and resume-safe — the marker and schema are left as the last committed step.
1323
+ */
1324
+ /** A deploy-failing migration error — surfaced, never swallowed. */
1325
+ var PnMigrationError = class extends Error {
1326
+ code;
1327
+ /** PN's structured explanation, when present. */
1328
+ why;
1329
+ constructor(code, summary, why) {
1330
+ super(`prisma-next migrate (${code}): ${summary}`);
1331
+ this.name = "PnMigrationError";
1332
+ this.code = code;
1333
+ this.why = why;
1334
+ }
1335
+ };
1336
+ /**
1337
+ * The target `storageHash` a contract heads to — `contractJson.storage.storageHash`.
1338
+ * Read defensively: `contractJson` crosses the boundary as `unknown`.
1339
+ */
1340
+ function targetStorageHash(contractJson) {
1341
+ if (typeof contractJson === "object" && contractJson !== null && "storage" in contractJson) {
1342
+ const storage = contractJson.storage;
1343
+ if (typeof storage === "object" && storage !== null && "storageHash" in storage) {
1344
+ const hash = storage.storageHash;
1345
+ if (typeof hash === "string" && hash.length > 0) return hash;
1346
+ }
1347
+ }
1348
+ throw new PnMigrationError("INIT_FAILED", "the contract has no storage.storageHash — cannot determine the target schema version");
1349
+ }
1350
+ /**
1351
+ * Resolve the deploy's target ref from the migrations dir.
1352
+ *
1353
+ * - `targetRef` named: read `migrations/app/refs/<name>.json` — fail loudly
1354
+ * (`TARGET_REF_NOT_FOUND`) when the ref doesn't exist or can't be parsed.
1355
+ * - Default: the app space's head. PN synthesizes the app head from the
1356
+ * emitted contract — `{ hash: contract.storage.storageHash, invariants: [] }`
1357
+ * (`contract emit` writes no app-space `refs/head.json` today; extension
1358
+ * spaces have one on disk). When a future PN version does emit one, the
1359
+ * on-disk `head.json` wins — read via `readContractSpaceHeadRef`, exactly
1360
+ * the loader PN's own migrate uses.
1361
+ */
1362
+ async function resolveTargetRef(migrationsDir, contractJson, targetRef) {
1363
+ if (targetRef !== void 0) {
1364
+ const refsDir = spaceRefsDirectory(spaceMigrationDirectory(migrationsDir, APP_SPACE_ID));
1365
+ try {
1366
+ const ref = await readRef(refsDir, targetRef);
1367
+ return {
1368
+ hash: ref.hash,
1369
+ invariants: ref.invariants
1370
+ };
1371
+ } catch (error) {
1372
+ throw new PnMigrationError("TARGET_REF_NOT_FOUND", `targetRef "${targetRef}" could not be read from ${refsDir}`, error instanceof Error ? error.message : String(error));
1373
+ }
1374
+ }
1375
+ const head = await readContractSpaceHeadRef(migrationsDir, APP_SPACE_ID);
1376
+ if (head !== null) return {
1377
+ hash: head.hash,
1378
+ invariants: head.invariants
1379
+ };
1380
+ return {
1381
+ hash: targetStorageHash(contractJson),
1382
+ invariants: []
1383
+ };
1384
+ }
1385
+ /**
1386
+ * The pure migration decision, mirroring PN's own verifier: the database is
1387
+ * AT the target when the marker's hash equals the ref's hash AND every ref
1388
+ * invariant is on the marker (marker invariants are monotonic). `dbInit` is
1389
+ * additive-only synthesis, so it is chosen only for a fresh DB whose
1390
+ * effective required invariants (`ref.invariants − marker.invariants`) are
1391
+ * empty; anything else — different hash, missing invariant (the A→A
1392
+ * data-only self-edge), or a fresh DB with required invariants — walks the
1393
+ * authored graph via `migrate`.
1394
+ */
1395
+ function decideMigrationAction(marker, ref) {
1396
+ const markerInvariants = new Set(marker?.invariants ?? []);
1397
+ const missing = ref.invariants.filter((id) => !markerInvariants.has(id));
1398
+ if (marker !== null && marker.storageHash === ref.hash && missing.length === 0) return "noop";
1399
+ if (marker === null && missing.length === 0) return "init";
1400
+ return "migrate";
1401
+ }
1402
+ /**
1403
+ * Bring the database at `url` to the target ref via PN's authored migrations.
1404
+ * Reads the live marker, decides no-op / init / migrate
1405
+ * ({@link decideMigrationAction}), applies, and throws a typed
1406
+ * {@link PnMigrationError} on a no-path or runner failure. `migrationsDir` is
1407
+ * the on-disk migrations root and `ref` the resolved target
1408
+ * ({@link resolveTargetRef} — both resolved by the lowering, which also keys
1409
+ * the PnMigration resource on them). `refName` (the resource's `targetRef`,
1410
+ * when set) is threaded into `migrate` so PN targets the named ref's hash and
1411
+ * plans an invariant-bearing path.
1412
+ */
1413
+ async function applyPnMigration(opts) {
1414
+ const connection = normalizeSslMode(opts.url);
1415
+ return withConnectionRetry(() => runMigration(connection, opts.contractJson, opts.migrationsDir, opts.ref, opts.refName), { shouldRetry: (error) => !(error instanceof PnMigrationError) });
1416
+ }
1417
+ async function runMigration(connection, contractJson, migrationsDir, ref, refName) {
1418
+ const client = createPostgresControlClient({ connection });
1419
+ await client.connect();
1420
+ try {
1421
+ const marker = await client.readMarker();
1422
+ const markerHashBefore = marker?.storageHash ?? null;
1423
+ const action = decideMigrationAction(marker, ref);
1424
+ if (action === "noop") return {
1425
+ action,
1426
+ targetHash: ref.hash,
1427
+ markerHashBefore
1428
+ };
1429
+ if (action === "init") {
1430
+ const result = await client.dbInit({
1431
+ contract: contractJson,
1432
+ mode: "apply",
1433
+ migrationsDir
1434
+ });
1435
+ if (!result.ok) throw new PnMigrationError("INIT_FAILED", result.failure.summary, result.failure.why);
1436
+ return {
1437
+ action,
1438
+ targetHash: ref.hash,
1439
+ markerHashBefore
1440
+ };
1441
+ }
1442
+ const result = await client.migrate({
1443
+ contract: contractJson,
1444
+ migrationsDir,
1445
+ ...refName !== void 0 ? {
1446
+ refHash: ref.hash,
1447
+ refInvariants: ref.invariants,
1448
+ refName
1449
+ } : {}
1450
+ });
1451
+ if (!result.ok) throw new PnMigrationError(result.failure.code === "MIGRATION_PATH_NOT_FOUND" ? "MIGRATION_PATH_NOT_FOUND" : "RUNNER_FAILED", result.failure.summary, result.failure.why);
1452
+ return {
1453
+ action,
1454
+ targetHash: ref.hash,
1455
+ markerHashBefore
1456
+ };
1457
+ } finally {
1458
+ await client.close();
1459
+ }
1460
+ }
1461
+ /**
1462
+ * The `PnMigration` Alchemy resource (ADR-0022, slice 2 D2) — the migration
1463
+ * step modeled as a tracked resource so it participates in deploy state: keyed
1464
+ * on the target REF identity (`targetHash` + sorted `invariants`), an
1465
+ * unchanged redeploy is an Alchemy-level no-op (on top of the marker read),
1466
+ * and a contract change — or a DATA-ONLY change that adds a ref invariant at
1467
+ * the same hash — re-runs the migration.
1468
+ *
1469
+ * Its provider's `reconcile` receives the RESOLVED props at apply-time — in
1470
+ * particular the concrete DB `url` (a lazy `Output` until the Connection
1471
+ * provisions) — and delegates to the proven `applyPnMigration` decision. The
1472
+ * provider is a standalone `Provider<PnMigration>` layer; the extension
1473
+ * descriptor merges it into its `providers()` (`Layer.merge(Prisma.providers(),
1474
+ * PnMigrationProvider())`), and Alchemy resolves it at apply via a direct
1475
+ * provider-tag lookup (`tryFindProviderByType`) — no change to `@internal/lowering`.
1476
+ *
1477
+ * Deploy-time only: imports `@prisma-next/postgres/control` (via the helper) +
1478
+ * `alchemy`. Imported by `control.ts` and tests, never by `index.ts` / the
1479
+ * `./prisma-next` authoring entry — index isolation holds.
1480
+ */
1481
+ /** The `PnMigration` resource constructor — `yield* PnMigration(id, props)` in the lowering. */
1482
+ const PnMigration = Resource("PrismaNext.Migration");
1483
+ /**
1484
+ * The `PnMigration` provider service. `reconcile` runs for both create and
1485
+ * update (Alchemy's unified lifecycle); `applyPnMigration` is idempotent via
1486
+ * the live marker read, so it is safe to run for either — the marker decides
1487
+ * no-op / init / migrate. A migration has nothing to enumerate (`list` → `[]`)
1488
+ * and nothing to tear down on its own (`delete` → no-op; the DB's own deletion
1489
+ * handles teardown). Exported so tests can drive `reconcile` directly, without
1490
+ * building an Effect layer.
1491
+ */
1492
+ const pnMigrationProviderService = {
1493
+ list: () => Effect.succeed([]),
1494
+ reconcile: ({ news }) => Effect.tryPromise({
1495
+ try: () => applyPnMigration({
1496
+ url: news.url,
1497
+ contractJson: news.contractJson,
1498
+ migrationsDir: news.migrationsDir,
1499
+ ref: {
1500
+ hash: news.targetHash,
1501
+ invariants: news.invariants
1502
+ },
1503
+ ...news.refName !== void 0 ? { refName: news.refName } : {}
1504
+ }),
1505
+ catch: (error) => error
1506
+ }).pipe(Effect.map((outcome) => ({
1507
+ storageHash: outcome.targetHash,
1508
+ invariants: news.invariants
1509
+ }))),
1510
+ delete: () => Effect.void
1511
+ };
1512
+ /** The `PnMigration` provider layer — merged into the extension descriptor's `providers()`. */
1513
+ const PnMigrationProvider = () => Provider.effect(PnMigration, Effect.succeed(pnMigrationProviderService));
1514
+ /**
1515
+ * The migration is a tracked `PnMigration` Alchemy resource keyed on the
1516
+ * target REF identity (hash + sorted invariants): unchanged redeploy is a
1517
+ * no-op, a contract or ref-invariant change re-migrates.
1518
+ */
1519
+ function prismaNextDescriptor(o) {
1520
+ const lowering = ({ id, node, application }) => Effect.gen(function* () {
1521
+ validateName(id, "resource name (from provision id)");
1522
+ const db = yield* Database(`${id}-db`, {
1523
+ projectId: projectIdOf(application),
1524
+ name: id,
1525
+ region: o.region ?? "us-east-1",
1526
+ ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
1527
+ });
1528
+ const conn = yield* Connection(`${id}-conn`, {
1529
+ databaseId: db.id,
1530
+ name: id
1531
+ });
1532
+ const url = Output.map(conn.connectionString, (value) => Redacted.value(value));
1533
+ if (!isPnPostgresResourceNode(node)) throw new Error(`prisma-next lowering received a non-prisma-next node (${id}).`);
1534
+ const contractJson = node.provides.__cmp.contractJson;
1535
+ const migrationsDir = yield* Effect.promise(() => resolveMigrationsDir(node.config));
1536
+ const ref = yield* Effect.promise(() => resolveTargetRef(migrationsDir, contractJson, node.targetRef));
1537
+ const warm = yield* PgWarm(`${id}-warm`, { url });
1538
+ yield* PnMigration(`${id}-migrate`, {
1539
+ url: warm.url,
1540
+ contractJson,
1541
+ migrationsDir,
1542
+ targetHash: ref.hash,
1543
+ invariants: [...ref.invariants].sort(),
1544
+ ...node.targetRef !== void 0 ? { refName: node.targetRef } : {}
1545
+ });
1546
+ return { outputs: { url: warm.url } };
1547
+ });
1548
+ return Object.assign(lowering, { kind: "resource" });
1549
+ }
1550
+ /**
1551
+ * The `S3Credentials` Alchemy resource (S5) — mints a random SigV4 key pair
1552
+ * ONCE at create and keeps it STABLE across deploys, so an unchanged module
1553
+ * no-ops on redeploy. The pair is generated with the Web Crypto global
1554
+ * (`crypto.getRandomValues` — no `node:` import, matching this package's
1555
+ * runtime-coupling invariant) and persisted in Alchemy state; on every later
1556
+ * apply the provider returns the persisted attributes (`reconcile`'s `output`)
1557
+ * unchanged — the same way the postgres resource keeps a Connection stable.
1558
+ * Rotation is destroy/recreate (a platform ask, not solved here).
1559
+ *
1560
+ * Deploy-time only: imports `alchemy`. Imported by `control.ts` and tests,
1561
+ * never by `index.ts` / the authoring entry.
1562
+ */
1563
+ /** The `S3Credentials` resource constructor — `yield* S3Credentials(id, {})` in the lowering. */
1564
+ const S3Credentials = Resource("PrismaCloud.S3Credentials");
1565
+ function randomBytes(n) {
1566
+ return crypto.getRandomValues(new Uint8Array(n));
1567
+ }
1568
+ function toHexUpper(bytes) {
1569
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
1570
+ }
1571
+ /** A fresh SigV4 key pair: an AKIA-prefixed id and a 40-char base64 secret. */
1572
+ function mintKeyPair() {
1573
+ return {
1574
+ accessKeyId: `AKIA${toHexUpper(randomBytes(8))}`,
1575
+ secretAccessKey: btoa(String.fromCharCode(...randomBytes(30)))
1576
+ };
1577
+ }
1578
+ /**
1579
+ * The `S3Credentials` provider service. `reconcile` runs for create and update;
1580
+ * it returns the persisted `output` when present (a redeploy reuses the stored
1581
+ * pair — the no-op property) and mints a fresh pair only on first create.
1582
+ * Nothing to enumerate (`list` → `[]`) or tear down (`delete` → no-op; the pair
1583
+ * lives only in state). Exported so tests can drive it directly.
1584
+ */
1585
+ const s3CredentialsProviderService = {
1586
+ list: () => Effect.succeed([]),
1587
+ reconcile: ({ output }) => Effect.sync(() => output ?? mintKeyPair()),
1588
+ delete: () => Effect.void
1589
+ };
1590
+ /** The `S3Credentials` provider layer — merged into the extension descriptor's `providers()`. */
1591
+ const S3CredentialsProvider = () => Provider.effect(S3Credentials, Effect.succeed(s3CredentialsProviderService));
1592
+ /**
1593
+ * One `S3Credentials` resource per provisioned credentials node — `id` is the
1594
+ * module provision id, so a pair shared by the storage service is minted once
1595
+ * and kept stable across deploys (the resource's provider preserves it).
1596
+ * `_o` is unused today (the mint needs no region/project) but kept for symmetry
1597
+ * with the other descriptors' signature.
1598
+ */
1599
+ function s3CredentialsDescriptor(_o) {
1600
+ const lowering = ({ id }) => Effect.gen(function* () {
1601
+ const creds = yield* S3Credentials(`${id}-creds`, {});
1602
+ return { outputs: {
1603
+ accessKeyId: creds.accessKeyId,
1604
+ secretAccessKey: creds.secretAccessKey
1605
+ } };
1606
+ });
1607
+ return Object.assign(lowering, { kind: "resource" });
1608
+ }
1609
+ function s3StoreDescriptor(o) {
1610
+ const base = computeDescriptor(o);
1611
+ if (base.kind !== "service") throw new Error("computeDescriptor must be a service descriptor");
1612
+ return {
1613
+ kind: "service",
1614
+ provision: base.provision,
1615
+ package: base.package,
1616
+ serialize: (ctx, provisioned, config) => Effect.gen(function* () {
1617
+ const serialized = yield* base.serialize(ctx, provisioned, config);
1618
+ const credentials = config.inputs["credentials"] ?? {};
1619
+ const bucket = config.service["bucket"];
1620
+ 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,
1623
+ bucket,
1624
+ accessKeyId: credentials["accessKeyId"],
1625
+ secretAccessKey: credentials["secretAccessKey"]
1626
+ } };
1627
+ }),
1628
+ 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
+ } };
1635
+ })
1636
+ };
1637
+ }
1638
+ /**
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.
1642
+ * A name absent on the platform but present in the deploy shell is provisioned
1643
+ * via a direct Management API POST — NEVER an Alchemy resource, so the value
1644
+ * never lands in hosted deploy state. A name absent from both fails the deploy,
1645
+ * listing exactly what is missing and where to set it.
1646
+ *
1647
+ * Control-plane only (imported by control.ts → prisma-composer.config.ts); runs
1648
+ * in the CLI parent, so it builds its own Management API client from env — the
1649
+ * same credential path `ensureContainers` uses.
1650
+ */
1651
+ /** production for the default stage; preview for a named stage — matching how the pack writes config rows. */
1652
+ const classFor = (branchId) => branchId === void 0 ? "production" : "preview";
1653
+ /**
1654
+ * One page of the env-var list. The query is `blindCast` to `never` because
1655
+ * openapi-fetch types this path's query as `never` (an SDK path/operation
1656
+ * mismatch); that same workaround defeats the client's response-type inference,
1657
+ * so the result is projected to the small shape we actually read.
1658
+ */
1659
+ async function listEnvVars(client, query) {
1660
+ return blindCast(await client.GET("/v1/environment-variables", { params: { query: blindCast(query) } }));
1661
+ }
1662
+ async function existsOnPlatform(client, projectId, branchId, key) {
1663
+ const cls = classFor(branchId);
1664
+ const visible = (row) => branchId === void 0 || row.branchId === null || row.branchId === branchId;
1665
+ let cursor = null;
1666
+ do {
1667
+ const res = await listEnvVars(client, cursor === null ? {
1668
+ projectId,
1669
+ class: cls,
1670
+ key
1671
+ } : {
1672
+ projectId,
1673
+ class: cls,
1674
+ key,
1675
+ cursor
1676
+ });
1677
+ if (res.error !== void 0) throw listFailedError(key, res.error);
1678
+ const page = res.data;
1679
+ if (page === void 0) return false;
1680
+ if (page.data.some(visible)) return true;
1681
+ cursor = page.pagination.hasMore ? page.pagination.nextCursor : null;
1682
+ } while (cursor !== null);
1683
+ return false;
1684
+ }
1685
+ /**
1686
+ * Provision `key`=`value` directly via the Management API for the target
1687
+ * stage's scope (a production template for the default stage; a preview branch
1688
+ * override for a named stage — the same scope the pack writes config rows to,
1689
+ * EnvironmentVariable.ts). A 409 means a concurrent deploy already provisioned
1690
+ * it — tolerated. The value is never logged.
1691
+ */
1692
+ async function fillMissing(client, input, key, value) {
1693
+ const res = await client.POST("/v1/environment-variables", { body: {
1694
+ projectId: input.projectId,
1695
+ class: classFor(input.branchId),
1696
+ key,
1697
+ value,
1698
+ ...input.branchId !== void 0 ? { branchId: input.branchId } : {}
1699
+ } });
1700
+ if (res.error !== void 0 && res.response.status !== 409) throw fillFailedError(key, res.error);
1701
+ }
1702
+ const tokenRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for deploy preflight.");
1703
+ const listFailedError = (key, error) => /* @__PURE__ */ new Error(`deploy preflight: Prisma Management API error listing "${key}": ${JSON.stringify(error)}.`);
1704
+ 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)`;
1707
+ 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}.`);
1709
+ }
1710
+ async function managementClient() {
1711
+ if ((process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError();
1712
+ return Effect.runPromise(Effect.gen(function* () {
1713
+ return yield* ManagementClient;
1714
+ }).pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv())))));
1715
+ }
1716
+ /**
1717
+ * The Prisma Cloud extension's `preflight`. Aggregates the target-agnostic
1718
+ * manifest (core's `provisionManifest`), checks each pointer secret against the
1719
+ * platform, fills from the shell where possible, and fails loudly on anything
1720
+ * absent from both. Accepts an injected client for tests; otherwise builds one
1721
+ * from env.
1722
+ */
1723
+ async function runPreflight(input, deps) {
1724
+ const manifest = provisionManifest(input.graph);
1725
+ if (manifest.length === 0) return;
1726
+ 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();
1732
+ const missing = [];
1733
+ for (const meta of names.values()) {
1734
+ if (await existsOnPlatform(client, input.projectId, input.branchId, meta.name)) continue;
1735
+ const shellValue = process.env[meta.name];
1736
+ if (shellValue !== void 0 && shellValue.length > 0) {
1737
+ await fillMissing(client, input, meta.name, shellValue);
1738
+ continue;
1739
+ }
1740
+ missing.push(meta);
1741
+ }
1742
+ if (missing.length > 0) throw missingError(missing, input);
1743
+ }
1744
+ /** The Prisma Cloud–hosted deploy state store; its implementation lives in @internal/lowering. */
1745
+ const KNOWN_REGION_SET = new Set(COMPUTE_REGIONS);
1746
+ function isComputeRegion(value) {
1747
+ return KNOWN_REGION_SET.has(value);
1748
+ }
1749
+ /** Prisma.providers()'s ProviderCollection doesn't structurally unify with Alchemy's inferred providers Layer (a @internal/lowering typings gap); it satisfies it at runtime. */
1750
+ function asProvidersLayer(layer) {
1751
+ return layer;
1752
+ }
1753
+ /**
1754
+ * 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`.
1757
+ */
1758
+ function resolveOptions(opts) {
1759
+ const workspaceId = opts.workspaceId ?? process.env["PRISMA_WORKSPACE_ID"];
1760
+ 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
+ if (opts.region !== void 0) return {
1764
+ workspaceId,
1765
+ region: opts.region,
1766
+ projectId,
1767
+ branchId
1768
+ };
1769
+ const region = process.env["PRISMA_REGION"];
1770
+ if (region === void 0 || region.length === 0) return {
1771
+ workspaceId,
1772
+ projectId,
1773
+ branchId
1774
+ };
1775
+ if (!isComputeRegion(region)) throw new Error(`prismaCloud(): environment variable PRISMA_REGION="${region}" is not a known region (expected one of: ${COMPUTE_REGIONS.join(", ")}).`);
1776
+ return {
1777
+ workspaceId,
1778
+ region,
1779
+ projectId,
1780
+ branchId
1781
+ };
1782
+ }
1783
+ /** The Prisma Cloud extension descriptor — `prisma-composer.config.ts` lists it under `extensions`. */
1784
+ const prismaCloud = (opts = {}) => {
1785
+ const o = resolveOptions(opts);
1786
+ return {
1787
+ id: "@prisma/composer-prisma-cloud",
1788
+ providers: () => asProvidersLayer(Layer.mergeAll(providers(), PgWarmProvider(), PnMigrationProvider(), S3CredentialsProvider())),
1789
+ 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`).");
1793
+ for (const key of ["DATABASE_URL", "DATABASE_URL_POOLED"]) yield* EnvironmentVariable(`${key}-poison`, {
1794
+ projectId,
1795
+ key,
1796
+ value: "-",
1797
+ class: o.branchId ? "preview" : "production",
1798
+ ...o.branchId !== void 0 ? { branchId: o.branchId } : {}
1799
+ });
1800
+ return { outputs: { projectId } };
1801
+ }) },
1802
+ nodes: {
1803
+ postgres: postgresDescriptor(o),
1804
+ "prisma-next": prismaNextDescriptor(o),
1805
+ compute: computeDescriptor(o),
1806
+ credentials: s3CredentialsDescriptor(o),
1807
+ "s3-store": s3StoreDescriptor(o)
1808
+ }
1809
+ };
1810
+ };
1811
+ //#endregion
1812
+ export { prismaCloud, prismaState };
1813
+
1814
+ //# sourceMappingURL=control.mjs.map