@prisma/composer-prisma-cloud 0.2.0-dev.12 → 0.2.0-dev.14

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 (61) hide show
  1. package/dist/buckets-main.mjs +795 -0
  2. package/dist/buckets-main.mjs.map +1 -0
  3. package/dist/compute-main.mjs +532 -0
  4. package/dist/compute-main.mjs.map +1 -0
  5. package/dist/control.mjs +41 -1257
  6. package/dist/control.mjs.map +1 -1
  7. package/dist/cron/index.mjs +2 -3
  8. package/dist/cron/index.mjs.map +1 -1
  9. package/dist/cron/scheduler-entrypoint.mjs.map +1 -1
  10. package/dist/cron/scheduler-service.mjs +2 -3
  11. package/dist/cron/scheduler-service.mjs.map +1 -1
  12. package/dist/email/email-entrypoint.mjs.map +1 -1
  13. package/dist/email/email-service.mjs +2 -3
  14. package/dist/email/email-service.mjs.map +1 -1
  15. package/dist/email/index.mjs +2 -3
  16. package/dist/email/index.mjs.map +1 -1
  17. package/dist/email/testing.mjs.map +1 -1
  18. package/dist/get-port-BYnpbW5H.mjs +111 -0
  19. package/dist/get-port-BYnpbW5H.mjs.map +1 -0
  20. package/dist/index.mjs +4 -2
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/local-target.d.mts +12 -0
  23. package/dist/local-target.d.mts.map +1 -0
  24. package/dist/local-target.mjs +6181 -0
  25. package/dist/local-target.mjs.map +1 -0
  26. package/dist/param-BTzY0ne5-CW6ojZ3E.mjs +66 -0
  27. package/dist/param-BTzY0ne5-CW6ojZ3E.mjs.map +1 -0
  28. package/dist/pg-connection-CadPZuEK-CF3FsSQu.mjs +79 -0
  29. package/dist/pg-connection-CadPZuEK-CF3FsSQu.mjs.map +1 -0
  30. package/dist/postgres-main.mjs +492 -0
  31. package/dist/postgres-main.mjs.map +1 -0
  32. package/dist/preflight-names-BPL-qsZF-BfoRgbgb.mjs +1252 -0
  33. package/dist/preflight-names-BPL-qsZF-BfoRgbgb.mjs.map +1 -0
  34. package/dist/{prisma-next-BvBwGCIt-CSauMX4l.mjs → prisma-next-BvBwGCIt-DssgnctU.mjs} +3 -78
  35. package/dist/prisma-next-BvBwGCIt-DssgnctU.mjs.map +1 -0
  36. package/dist/prisma-next.mjs +1 -1
  37. package/dist/{provisioned-edges-B_XS1Mz--NFJgcXK7.mjs → provisioned-edges-C1wSO3cx-YC5C19f9.mjs} +5 -65
  38. package/dist/provisioned-edges-C1wSO3cx-YC5C19f9.mjs.map +1 -0
  39. package/dist/secret-rz_ZqNeq-_xd97Jd9.mjs +47 -0
  40. package/dist/secret-rz_ZqNeq-_xd97Jd9.mjs.map +1 -0
  41. package/dist/segments-NpKN-46R-B01DmCqy.mjs +1441 -0
  42. package/dist/segments-NpKN-46R-B01DmCqy.mjs.map +1 -0
  43. package/dist/{serializer-D8_84XWO-DA2B6YbX.mjs → serializer-CJiOhF2d-DJUjQj1i.mjs} +4 -44
  44. package/dist/serializer-CJiOhF2d-DJUjQj1i.mjs.map +1 -0
  45. package/dist/storage/index.mjs +2 -3
  46. package/dist/storage/index.mjs.map +1 -1
  47. package/dist/storage/storage-entrypoint.mjs.map +1 -1
  48. package/dist/storage/storage-service.mjs +2 -3
  49. package/dist/storage/storage-service.mjs.map +1 -1
  50. package/dist/streams/index.mjs +2 -3
  51. package/dist/streams/index.mjs.map +1 -1
  52. package/dist/streams/streams-entrypoint.mjs.map +1 -1
  53. package/dist/streams/streams-service.mjs +2 -3
  54. package/dist/streams/streams-service.mjs.map +1 -1
  55. package/dist/testing.mjs +1 -1
  56. package/dist/testing.mjs.map +1 -1
  57. package/package.json +21 -15
  58. package/src/exports/local-target.ts +1 -0
  59. package/dist/prisma-next-BvBwGCIt-CSauMX4l.mjs.map +0 -1
  60. package/dist/provisioned-edges-B_XS1Mz--NFJgcXK7.mjs.map +0 -1
  61. package/dist/serializer-D8_84XWO-DA2B6YbX.mjs.map +0 -1
@@ -0,0 +1,795 @@
1
+ import { i as readOwnVersion, n as isValidSegment, r as readJsonFile, t as StateFile } from "./segments-NpKN-46R-B01DmCqy.mjs";
2
+ import * as fs from "node:fs";
3
+ import * as fs$1 from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
6
+ import * as http from "node:http";
7
+ //#region ../../1-prisma-cloud/0-lowering/s3-protocol/dist/index.mjs
8
+ const DEFAULT_CONTENT_TYPE$1 = "application/octet-stream";
9
+ const TMP_DIR_NAME = ".tmp";
10
+ const META_DIR_NAME = ".meta";
11
+ const RESERVED_SEGMENTS = /* @__PURE__ */ new Set([TMP_DIR_NAME, META_DIR_NAME]);
12
+ const BUCKET_NAME_RE = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
13
+ function etagOf$1(bytes) {
14
+ return `"${createHash("sha256").update(bytes).digest("hex")}"`;
15
+ }
16
+ function isErrnoException(err) {
17
+ return err instanceof Error && "code" in err;
18
+ }
19
+ function isEnoent(err) {
20
+ return isErrnoException(err) && err.code === "ENOENT";
21
+ }
22
+ /**
23
+ * Splits a key into its `/` segments, rejecting anything that could escape
24
+ * the bucket dir or collide with the reserved `.tmp`/`.meta` namespaces:
25
+ * `..` segments, an empty segment (leading/trailing/doubled `/`, which also
26
+ * catches a leading-slash "absolute" key), or a segment literally `.tmp` or
27
+ * `.meta` at any depth.
28
+ */
29
+ function keySegments(key) {
30
+ const segments = key.split("/");
31
+ for (const segment of segments) if (segment === "" || segment === "." || segment === ".." || RESERVED_SEGMENTS.has(segment)) throw new Error(`invalid object key: "${key}"`);
32
+ return segments;
33
+ }
34
+ /** `<bucketDir>/<key's segments>`, double-checked to still resolve inside `bucketDir`. */
35
+ function objectPath(bucketDir, key) {
36
+ const target = path.join(bucketDir, ...keySegments(key));
37
+ const resolvedRoot = path.resolve(bucketDir) + path.sep;
38
+ if (!(path.resolve(target) + path.sep).startsWith(resolvedRoot)) throw new Error(`invalid object key: "${key}"`);
39
+ return target;
40
+ }
41
+ /** `<bucketDir>/.meta/<key's segments>.json` — mirrors `objectPath`'s tree under `.meta`. */
42
+ function sidecarPath(bucketDir, key) {
43
+ const segments = keySegments(key);
44
+ const last = segments[segments.length - 1];
45
+ const dirs = segments.slice(0, -1);
46
+ return path.join(bucketDir, META_DIR_NAME, ...dirs, `${last}.json`);
47
+ }
48
+ /** Write-temp-then-rename: never leaves a reader observing a partial file. */
49
+ async function writeAtomic(finalPath, tmpRoot, bytes) {
50
+ await fs$1.mkdir(tmpRoot, { recursive: true });
51
+ const tmpPath = path.join(tmpRoot, randomUUID());
52
+ await fs$1.writeFile(tmpPath, bytes);
53
+ await fs$1.mkdir(path.dirname(finalPath), { recursive: true });
54
+ await fs$1.rename(tmpPath, finalPath);
55
+ }
56
+ function isSidecar(value) {
57
+ return typeof value === "object" && value !== null && "contentType" in value && typeof value.contentType === "string" && "etag" in value && typeof value.etag === "string";
58
+ }
59
+ async function readSidecar(sidecar) {
60
+ let raw;
61
+ try {
62
+ raw = await fs$1.readFile(sidecar, "utf8");
63
+ } catch (err) {
64
+ if (isEnoent(err)) return null;
65
+ throw err;
66
+ }
67
+ try {
68
+ const parsed = JSON.parse(raw);
69
+ if (isSidecar(parsed)) return parsed;
70
+ } catch {}
71
+ return null;
72
+ }
73
+ /** Read the object's bytes plus its metadata — adopting a sidecar-less file (dropped in by a developer) by computing and lazily persisting one. `null` when the object is missing. */
74
+ async function readObject(bucketDir, key) {
75
+ const target = objectPath(bucketDir, key);
76
+ let bytes;
77
+ try {
78
+ bytes = await fs$1.readFile(target);
79
+ } catch (err) {
80
+ if (isEnoent(err)) return null;
81
+ throw err;
82
+ }
83
+ const existing = await readSidecar(sidecarPath(bucketDir, key));
84
+ if (existing) return {
85
+ bytes,
86
+ meta: existing
87
+ };
88
+ const meta = {
89
+ contentType: DEFAULT_CONTENT_TYPE$1,
90
+ etag: etagOf$1(bytes)
91
+ };
92
+ await writeAtomic(sidecarPath(bucketDir, key), path.join(bucketDir, TMP_DIR_NAME), Buffer.from(JSON.stringify(meta)));
93
+ return {
94
+ bytes,
95
+ meta
96
+ };
97
+ }
98
+ /** Remove now-empty directories from `startDir` up to (never including) `root`. */
99
+ async function pruneEmptyDirs(root, startDir) {
100
+ const resolvedRoot = path.resolve(root);
101
+ let dir = path.resolve(startDir);
102
+ while (dir !== resolvedRoot && dir.startsWith(resolvedRoot + path.sep)) {
103
+ let entries;
104
+ try {
105
+ entries = await fs$1.readdir(dir);
106
+ } catch (err) {
107
+ if (isEnoent(err)) {
108
+ dir = path.dirname(dir);
109
+ continue;
110
+ }
111
+ throw err;
112
+ }
113
+ if (entries.length > 0) break;
114
+ try {
115
+ await fs$1.rmdir(dir);
116
+ } catch (err) {
117
+ if (!isEnoent(err)) throw err;
118
+ }
119
+ dir = path.dirname(dir);
120
+ }
121
+ }
122
+ /** Recursively lists every object key under `dir`, skipping `.tmp`/`.meta` at any depth. */
123
+ async function walkKeys(dir) {
124
+ const keys = [];
125
+ async function walk(current, prefix) {
126
+ let entries;
127
+ try {
128
+ entries = await fs$1.readdir(current, { withFileTypes: true });
129
+ } catch (err) {
130
+ if (isEnoent(err)) return;
131
+ throw err;
132
+ }
133
+ for (const entry of entries) {
134
+ if (RESERVED_SEGMENTS.has(entry.name)) continue;
135
+ const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
136
+ if (entry.isDirectory()) await walk(path.join(current, entry.name), rel);
137
+ else if (entry.isFile()) keys.push(rel);
138
+ }
139
+ }
140
+ await walk(dir, "");
141
+ return keys;
142
+ }
143
+ /**
144
+ * A disk-backed `ObjectStore`: object bytes and a metadata sidecar under a
145
+ * per-bucket directory the caller resolves. See the module doc for the
146
+ * unknown-bucket / invalid-key failure shapes.
147
+ */
148
+ function fsStore(resolveBucketDir) {
149
+ function resolveDir(bucket) {
150
+ if (!BUCKET_NAME_RE.test(bucket)) return void 0;
151
+ return resolveBucketDir(bucket);
152
+ }
153
+ return {
154
+ async put(bucket, key, bytes, opts = {}) {
155
+ const dir = resolveDir(bucket);
156
+ if (dir === void 0) throw new Error(`no such bucket: "${bucket}"`);
157
+ const target = objectPath(dir, key);
158
+ const tmpRoot = path.join(dir, TMP_DIR_NAME);
159
+ const etag = etagOf$1(bytes);
160
+ const contentType = opts.contentType ?? DEFAULT_CONTENT_TYPE$1;
161
+ await writeAtomic(target, tmpRoot, bytes);
162
+ await writeAtomic(sidecarPath(dir, key), tmpRoot, Buffer.from(JSON.stringify({
163
+ contentType,
164
+ etag
165
+ })));
166
+ return { etag };
167
+ },
168
+ async get(bucket, key, opts = {}) {
169
+ const dir = resolveDir(bucket);
170
+ if (dir === void 0) return null;
171
+ const found = await readObject(dir, key);
172
+ if (!found) return null;
173
+ const { bytes, meta } = found;
174
+ const size = bytes.byteLength;
175
+ if (opts.range) {
176
+ const start = opts.range.start;
177
+ const end = opts.range.end === void 0 ? size - 1 : Math.min(opts.range.end, size - 1);
178
+ return {
179
+ bytes: start > end ? /* @__PURE__ */ new Uint8Array(0) : bytes.subarray(start, end + 1),
180
+ etag: meta.etag,
181
+ contentType: meta.contentType,
182
+ size
183
+ };
184
+ }
185
+ return {
186
+ bytes: new Uint8Array(bytes),
187
+ etag: meta.etag,
188
+ contentType: meta.contentType,
189
+ size
190
+ };
191
+ },
192
+ async head(bucket, key) {
193
+ const dir = resolveDir(bucket);
194
+ if (dir === void 0) return null;
195
+ const found = await readObject(dir, key);
196
+ if (!found) return null;
197
+ return {
198
+ etag: found.meta.etag,
199
+ size: found.bytes.byteLength,
200
+ contentType: found.meta.contentType
201
+ };
202
+ },
203
+ async delete(bucket, key) {
204
+ const dir = resolveDir(bucket);
205
+ if (dir === void 0) return;
206
+ const target = objectPath(dir, key);
207
+ const sidecar = sidecarPath(dir, key);
208
+ try {
209
+ await fs$1.unlink(target);
210
+ } catch (err) {
211
+ if (!isEnoent(err)) throw err;
212
+ }
213
+ try {
214
+ await fs$1.unlink(sidecar);
215
+ } catch (err) {
216
+ if (!isEnoent(err)) throw err;
217
+ }
218
+ await pruneEmptyDirs(dir, path.dirname(target));
219
+ await pruneEmptyDirs(path.join(dir, META_DIR_NAME), path.dirname(sidecar));
220
+ },
221
+ async list(bucket, opts = {}) {
222
+ const dir = resolveDir(bucket);
223
+ if (dir === void 0) return {
224
+ keys: [],
225
+ isTruncated: false
226
+ };
227
+ const prefix = opts.prefix ?? "";
228
+ const maxKeys = opts.maxKeys ?? 1e3;
229
+ const token = opts.continuationToken;
230
+ const matching = (await walkKeys(dir)).filter((key) => key.startsWith(prefix)).sort().filter((key) => token === void 0 ? true : key > token);
231
+ const page = matching.slice(0, maxKeys);
232
+ const isTruncated = matching.length > maxKeys;
233
+ const last = page.at(-1);
234
+ return {
235
+ keys: page,
236
+ isTruncated,
237
+ ...isTruncated && last !== void 0 ? { nextContinuationToken: last } : {}
238
+ };
239
+ }
240
+ };
241
+ }
242
+ const ALGORITHM = "AWS4-HMAC-SHA256";
243
+ const UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
244
+ function sha256Hex(data) {
245
+ return createHash("sha256").update(data).digest("hex");
246
+ }
247
+ function hmac(key, data) {
248
+ return createHmac("sha256", key).update(data).digest();
249
+ }
250
+ /** AWS canonical URI encoding: every byte except the unreserved set is %XX. */
251
+ function awsUriEncode(value) {
252
+ return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%7E/g, "~");
253
+ }
254
+ function parseCredential(credential) {
255
+ const parts = credential.split("/");
256
+ if (parts.length !== 5 || parts[4] !== "aws4_request") return null;
257
+ const [accessKeyId, date, region, service] = parts;
258
+ if (!accessKeyId || !date || !region || !service) return null;
259
+ return {
260
+ accessKeyId,
261
+ date,
262
+ region,
263
+ service
264
+ };
265
+ }
266
+ function signingKey(secret, scope) {
267
+ return hmac(hmac(hmac(hmac(`AWS4${secret}`, scope.date), scope.region), scope.service), "aws4_request");
268
+ }
269
+ function canonicalHeaders(url, req, signedHeaders) {
270
+ return signedHeaders.map((name) => {
271
+ return `${name}:${(name === "host" ? url.host : req.headers.get(name) ?? "").trim().replace(/\s+/g, " ")}\n`;
272
+ }).join("");
273
+ }
274
+ function canonicalQuery(url, exclude) {
275
+ const entries = [];
276
+ for (const [key, value] of url.searchParams.entries()) {
277
+ if (exclude !== void 0 && key === exclude) continue;
278
+ entries.push([awsUriEncode(key), awsUriEncode(value)]);
279
+ }
280
+ const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
281
+ entries.sort(([ak, av], [bk, bv]) => cmp(ak, bk) || cmp(av, bv));
282
+ return entries.map(([k, v]) => `${k}=${v}`).join("&");
283
+ }
284
+ function stringToSign(amzDate, scope, canonicalRequest) {
285
+ const scopeString = `${scope.date}/${scope.region}/${scope.service}/aws4_request`;
286
+ return [
287
+ ALGORITHM,
288
+ amzDate,
289
+ scopeString,
290
+ sha256Hex(canonicalRequest)
291
+ ].join("\n");
292
+ }
293
+ function signatureMatches(expected, provided) {
294
+ const a = Buffer.from(expected, "hex");
295
+ const b = Buffer.from(provided, "hex");
296
+ return a.length === b.length && a.length > 0 && timingSafeEqual(a, b);
297
+ }
298
+ /** `YYYYMMDDTHHMMSSZ` → epoch ms, or null when malformed. */
299
+ function parseAmzDate(amzDate) {
300
+ const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(amzDate);
301
+ if (!match) return null;
302
+ const [, y, mo, d, h, mi, s] = match;
303
+ return Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s));
304
+ }
305
+ function parseAuthorizationHeader(header) {
306
+ if (!header.startsWith(`${ALGORITHM} `)) return null;
307
+ const rest = header.slice(17);
308
+ const fields = /* @__PURE__ */ new Map();
309
+ for (const part of rest.split(",")) {
310
+ const eq = part.indexOf("=");
311
+ if (eq === -1) continue;
312
+ fields.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
313
+ }
314
+ const credential = fields.get("Credential");
315
+ const signedHeaders = fields.get("SignedHeaders");
316
+ const signature = fields.get("Signature");
317
+ if (!credential || !signedHeaders || !signature) return null;
318
+ return {
319
+ credential,
320
+ signedHeaders: signedHeaders.split(";"),
321
+ signature
322
+ };
323
+ }
324
+ /** The one signing core both auth forms share: check the access key, rebuild the canonical request, derive the key, compare in constant time. */
325
+ function verifySignature(req, url, credentials, params) {
326
+ if (params.scope.accessKeyId !== credentials.accessKeyId) return {
327
+ ok: false,
328
+ reason: "unknown access key"
329
+ };
330
+ const canonicalRequest = [
331
+ req.method,
332
+ url.pathname,
333
+ canonicalQuery(url, params.excludeQuery),
334
+ canonicalHeaders(url, req, params.signedHeaders),
335
+ params.signedHeaders.join(";"),
336
+ params.payloadHash
337
+ ].join("\n");
338
+ return signatureMatches(hmac(signingKey(credentials.secretAccessKey, params.scope), stringToSign(params.amzDate, params.scope, canonicalRequest)).toString("hex"), params.signature) ? { ok: true } : {
339
+ ok: false,
340
+ reason: "signature mismatch"
341
+ };
342
+ }
343
+ function verifyHeader(req, url, credentials) {
344
+ const auth = parseAuthorizationHeader(req.headers.get("authorization") ?? "");
345
+ if (!auth) return {
346
+ ok: false,
347
+ reason: "malformed Authorization header"
348
+ };
349
+ const scope = parseCredential(auth.credential);
350
+ if (!scope) return {
351
+ ok: false,
352
+ reason: "malformed credential scope"
353
+ };
354
+ const amzDate = req.headers.get("x-amz-date");
355
+ if (!amzDate) return {
356
+ ok: false,
357
+ reason: "missing x-amz-date"
358
+ };
359
+ const payloadHash = req.headers.get("x-amz-content-sha256");
360
+ if (!payloadHash) return {
361
+ ok: false,
362
+ reason: "missing x-amz-content-sha256"
363
+ };
364
+ return verifySignature(req, url, credentials, {
365
+ scope,
366
+ amzDate,
367
+ signedHeaders: auth.signedHeaders,
368
+ payloadHash,
369
+ signature: auth.signature
370
+ });
371
+ }
372
+ function verifyPresigned(req, url, credentials, now) {
373
+ const q = url.searchParams;
374
+ if (q.get("X-Amz-Algorithm") !== ALGORITHM) return {
375
+ ok: false,
376
+ reason: "unsupported presign algorithm"
377
+ };
378
+ const credentialRaw = q.get("X-Amz-Credential");
379
+ const amzDate = q.get("X-Amz-Date");
380
+ const expiresRaw = q.get("X-Amz-Expires");
381
+ const signedHeadersRaw = q.get("X-Amz-SignedHeaders");
382
+ const signature = q.get("X-Amz-Signature");
383
+ if (!credentialRaw || !amzDate || !expiresRaw || !signedHeadersRaw || !signature) return {
384
+ ok: false,
385
+ reason: "incomplete presign parameters"
386
+ };
387
+ const scope = parseCredential(credentialRaw);
388
+ if (!scope) return {
389
+ ok: false,
390
+ reason: "malformed credential scope"
391
+ };
392
+ const signedAt = parseAmzDate(amzDate);
393
+ const expires = Number(expiresRaw);
394
+ if (signedAt === null || !Number.isFinite(expires)) return {
395
+ ok: false,
396
+ reason: "malformed presign date"
397
+ };
398
+ if (now.getTime() > signedAt + expires * 1e3) return {
399
+ ok: false,
400
+ reason: "presign expired"
401
+ };
402
+ return verifySignature(req, url, credentials, {
403
+ scope,
404
+ amzDate,
405
+ signedHeaders: signedHeadersRaw.split(";"),
406
+ payloadHash: UNSIGNED_PAYLOAD,
407
+ signature,
408
+ excludeQuery: "X-Amz-Signature"
409
+ });
410
+ }
411
+ /**
412
+ * Verify a request's SigV4 signature against a single credential pair. Picks
413
+ * the presigned form when `X-Amz-Signature` is present, otherwise the
414
+ * `Authorization`-header form. `now` is injectable for deterministic
415
+ * expiry tests.
416
+ */
417
+ function verifyRequest(req, credentials, now = /* @__PURE__ */ new Date()) {
418
+ const url = new URL(req.url);
419
+ if (url.searchParams.has("X-Amz-Signature")) return verifyPresigned(req, url, credentials, now);
420
+ if (req.headers.has("authorization")) return verifyHeader(req, url, credentials);
421
+ return {
422
+ ok: false,
423
+ reason: "unsigned request"
424
+ };
425
+ }
426
+ const DEFAULT_CONTENT_TYPE = "application/octet-stream";
427
+ /** Path-style: `/{bucket}/{key…}`. Each segment is percent-decoded. */
428
+ function parseTarget(url) {
429
+ const segments = url.pathname.split("/").filter((s) => s.length > 0);
430
+ if (segments.length === 0) return null;
431
+ const [bucket, ...keyParts] = segments;
432
+ return {
433
+ bucket: decodeURIComponent(bucket ?? ""),
434
+ key: keyParts.map(decodeURIComponent).join("/")
435
+ };
436
+ }
437
+ /** `bytes=a-b` (inclusive) or `bytes=a-` (open-ended). Null when absent/malformed. */
438
+ function parseRange(header) {
439
+ if (!header) return null;
440
+ const match = /^bytes=(\d+)-(\d*)$/.exec(header.trim());
441
+ if (!match) return null;
442
+ const start = Number(match[1]);
443
+ return match[2] ? {
444
+ start,
445
+ end: Number(match[2])
446
+ } : { start };
447
+ }
448
+ function xmlEscape(value) {
449
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
450
+ }
451
+ function listXml(bucket, prefix, maxKeys, result) {
452
+ const contents = result.keys.map((k) => `<Contents><Key>${xmlEscape(k)}</Key></Contents>`).join("");
453
+ const next = result.isTruncated && result.nextContinuationToken !== void 0 ? `<NextContinuationToken>${xmlEscape(result.nextContinuationToken)}</NextContinuationToken>` : "";
454
+ return `<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Name>${xmlEscape(bucket)}</Name><Prefix>${xmlEscape(prefix)}</Prefix><KeyCount>${result.keys.length}</KeyCount><MaxKeys>${maxKeys}</MaxKeys><IsTruncated>${result.isTruncated}</IsTruncated>` + contents + next + "</ListBucketResult>";
455
+ }
456
+ const DEFAULT_MAX_KEYS$1 = 1e3;
457
+ async function handleList(store, bucket, url) {
458
+ const prefix = url.searchParams.get("prefix") ?? "";
459
+ const continuationToken = url.searchParams.get("continuation-token");
460
+ const maxKeysRaw = url.searchParams.get("max-keys");
461
+ const maxKeys = maxKeysRaw !== null && Number.isFinite(Number(maxKeysRaw)) ? Number(maxKeysRaw) : DEFAULT_MAX_KEYS$1;
462
+ const result = await store.list(bucket, {
463
+ prefix,
464
+ maxKeys,
465
+ ...continuationToken !== null ? { continuationToken } : {}
466
+ });
467
+ return new Response(listXml(bucket, prefix, maxKeys, result), {
468
+ status: 200,
469
+ headers: { "content-type": "application/xml" }
470
+ });
471
+ }
472
+ /** aws-chunked / flexible-checksum PUTs frame the body as chunks + a trailer (signalled by `x-amz-content-sha256: STREAMING-…` or `content-encoding: aws-chunked`); the seed signature still verifies, so reject them (501) rather than store the raw framing as the object bytes. Decoding is out of scope. */
473
+ function isStreamingPut(req) {
474
+ const contentSha = req.headers.get("x-amz-content-sha256") ?? "";
475
+ const contentEncoding = req.headers.get("content-encoding") ?? "";
476
+ return contentSha.startsWith("STREAMING-") || contentEncoding.split(",").some((e) => e.trim() === "aws-chunked");
477
+ }
478
+ async function handlePut(store, t, req) {
479
+ if (isStreamingPut(req)) return new Response("aws-chunked / flexible checksums not supported; set requestChecksumCalculation: 'WHEN_REQUIRED'", { status: 501 });
480
+ const body = new Uint8Array(await req.arrayBuffer());
481
+ const contentType = req.headers.get("content-type") ?? DEFAULT_CONTENT_TYPE;
482
+ const { etag } = await store.put(t.bucket, t.key, body, { contentType });
483
+ return new Response(null, {
484
+ status: 200,
485
+ headers: { etag }
486
+ });
487
+ }
488
+ /** The etag/content-type/content-length/accept-ranges headers GET and HEAD share — `contentLength` is the slice length for GET, the total object size for HEAD. */
489
+ function metaHeaders(meta) {
490
+ return new Headers({
491
+ etag: meta.etag,
492
+ "content-type": meta.contentType,
493
+ "content-length": String(meta.contentLength),
494
+ "accept-ranges": "bytes"
495
+ });
496
+ }
497
+ async function handleGet(store, t, req) {
498
+ const range = parseRange(req.headers.get("range"));
499
+ const object = await store.get(t.bucket, t.key, range ? { range } : void 0);
500
+ if (!object) return new Response(null, { status: 404 });
501
+ const headers = metaHeaders({
502
+ etag: object.etag,
503
+ contentType: object.contentType,
504
+ contentLength: object.bytes.byteLength
505
+ });
506
+ if (!range) return new Response(object.bytes, {
507
+ status: 200,
508
+ headers
509
+ });
510
+ if (range.start >= object.size && object.size > 0) return new Response(null, {
511
+ status: 416,
512
+ headers: { "content-range": `bytes */${object.size}` }
513
+ });
514
+ const end = range.end === void 0 ? object.size - 1 : Math.min(range.end, object.size - 1);
515
+ headers.set("content-range", `bytes ${range.start}-${end}/${object.size}`);
516
+ return new Response(object.bytes, {
517
+ status: 206,
518
+ headers
519
+ });
520
+ }
521
+ async function handleHead(store, t) {
522
+ const meta = await store.head(t.bucket, t.key);
523
+ if (!meta) return new Response(null, { status: 404 });
524
+ return new Response(null, {
525
+ status: 200,
526
+ headers: metaHeaders({
527
+ etag: meta.etag,
528
+ contentType: meta.contentType,
529
+ contentLength: meta.size
530
+ })
531
+ });
532
+ }
533
+ async function handleDelete(store, t) {
534
+ await store.delete(t.bucket, t.key);
535
+ return new Response(null, { status: 204 });
536
+ }
537
+ function createS3Handler(opts) {
538
+ const { store, credentials } = opts;
539
+ return async (req) => {
540
+ if (!verifyRequest(req, credentials).ok) return new Response(null, { status: 403 });
541
+ const url = new URL(req.url);
542
+ const target = parseTarget(url);
543
+ if (!target) return new Response(null, { status: 400 });
544
+ if (req.method === "GET" && url.searchParams.get("list-type") === "2" && target.key === "") return handleList(store, target.bucket, url);
545
+ if (target.key === "") return new Response(null, { status: 400 });
546
+ switch (req.method) {
547
+ case "PUT": return handlePut(store, target, req);
548
+ case "GET": return handleGet(store, target, req);
549
+ case "HEAD": return handleHead(store, target);
550
+ case "DELETE": return handleDelete(store, target);
551
+ default: return new Response(null, { status: 405 });
552
+ }
553
+ };
554
+ }
555
+ //#endregion
556
+ //#region ../../1-prisma-cloud/0-lowering/dev-emulators/dist/buckets-main.mjs
557
+ /**
558
+ * The bucket emulator daemon (local-dev spec § 2 `buckets-main.ts`): the S3
559
+ * wire protocol (`@internal/s3-protocol`'s handler + `fsStore`) over
560
+ * registered per-app, per-bucket directories, multi-tenant via physical
561
+ * names `<app>--<name>`. Admin lives under `/_pcdev/` (the underscore can't
562
+ * collide with a valid bucket name). Plain `node:http`.
563
+ *
564
+ * Runs as its own OS process, started by `daemon.ts`'s `ensureDaemon` via
565
+ * `process.execPath <this file> --port <n> --state-dir <dir>`.
566
+ */
567
+ const STATE_MODE = 384;
568
+ const PHYSICAL_BUCKET_NAME_RE = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
569
+ const MULTIPART_MESSAGE = "multipart upload is not supported by the local dev bucket emulator yet";
570
+ function isBucketRegistration(value) {
571
+ return typeof value === "object" && value !== null && "app" in value && typeof value.app === "string" && "name" in value && typeof value.name === "string" && "dir" in value && typeof value.dir === "string";
572
+ }
573
+ function isCredentialRecord(value) {
574
+ return typeof value === "object" && value !== null && "app" in value && typeof value.app === "string" && "secretAccessKey" in value && typeof value.secretAccessKey === "string";
575
+ }
576
+ function isBucketsState(value) {
577
+ return typeof value === "object" && value !== null && "buckets" in value && typeof value.buckets === "object" && value.buckets !== null && Object.values(value.buckets).every(isBucketRegistration) && "credentials" in value && typeof value.credentials === "object" && value.credentials !== null && Object.values(value.credentials).every(isCredentialRecord);
578
+ }
579
+ function isBucketBody(value) {
580
+ return typeof value === "object" && value !== null && "dir" in value && typeof value.dir === "string";
581
+ }
582
+ function isCredentialsBody(value) {
583
+ return typeof value === "object" && value !== null && "accessKeyId" in value && typeof value.accessKeyId === "string" && "secretAccessKey" in value && typeof value.secretAccessKey === "string";
584
+ }
585
+ function parseArgs(argv) {
586
+ let port;
587
+ let stateDir;
588
+ for (let i = 0; i < argv.length; i++) if (argv[i] === "--port") port = Number(argv[i + 1]);
589
+ else if (argv[i] === "--state-dir") stateDir = argv[i + 1];
590
+ if (port === void 0 || Number.isNaN(port)) throw new Error("buckets-main: --port <n> is required");
591
+ if (stateDir === void 0) throw new Error("buckets-main: --state-dir <dir> is required");
592
+ return {
593
+ port,
594
+ stateDir
595
+ };
596
+ }
597
+ function readBody(req) {
598
+ return new Promise((resolve, reject) => {
599
+ const chunks = [];
600
+ req.on("data", (chunk) => chunks.push(chunk));
601
+ req.on("end", () => resolve(Buffer.concat(chunks)));
602
+ req.on("error", reject);
603
+ });
604
+ }
605
+ function toWebHeaders(nodeHeaders) {
606
+ const headers = new Headers();
607
+ for (const [key, value] of Object.entries(nodeHeaders)) {
608
+ if (value === void 0) continue;
609
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
610
+ else headers.set(key, value);
611
+ }
612
+ return headers;
613
+ }
614
+ async function sendWebResponse(res, webRes) {
615
+ const headers = {};
616
+ webRes.headers.forEach((value, key) => {
617
+ headers[key] = value;
618
+ });
619
+ const buf = Buffer.from(await webRes.arrayBuffer());
620
+ res.writeHead(webRes.status, headers);
621
+ res.end(buf);
622
+ }
623
+ function main() {
624
+ const { port, stateDir } = parseArgs(process.argv.slice(2));
625
+ const ownVersion = readOwnVersion();
626
+ const stateJsonPath = path.join(stateDir, "state.json");
627
+ const stateFile = new StateFile(stateJsonPath, STATE_MODE);
628
+ let state = {
629
+ buckets: {},
630
+ credentials: {}
631
+ };
632
+ function schedulePersist() {
633
+ stateFile.write(state);
634
+ }
635
+ const store = fsStore((physicalName) => state.buckets[physicalName]?.dir);
636
+ function json(res, status, body) {
637
+ res.writeHead(status, { "content-type": "application/json" });
638
+ res.end(JSON.stringify(body));
639
+ }
640
+ function text(res, status, body) {
641
+ res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
642
+ res.end(body);
643
+ }
644
+ function badSegment(res, segment) {
645
+ text(res, 400, `invalid path segment "${segment}": must match /^[a-z0-9][a-z0-9-]*$/ and be at most 63 characters`);
646
+ }
647
+ async function handlePutBucket(req, res, app, name) {
648
+ const physicalName = `${app}--${name}`;
649
+ if (!PHYSICAL_BUCKET_NAME_RE.test(physicalName)) return text(res, 400, `invalid bucket "${physicalName}" (app "${app}", name "${name}"): the physical bucket name must match /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/ and be at most 63 characters`);
650
+ const raw = await readBody(req);
651
+ let parsed;
652
+ try {
653
+ parsed = JSON.parse(raw.toString("utf8"));
654
+ } catch {
655
+ return text(res, 400, "malformed JSON body");
656
+ }
657
+ if (!isBucketBody(parsed)) return text(res, 400, "malformed bucket body: expected { \"dir\": string }");
658
+ await fs.promises.mkdir(parsed.dir, { recursive: true });
659
+ state.buckets[physicalName] = {
660
+ app,
661
+ name,
662
+ dir: parsed.dir
663
+ };
664
+ schedulePersist();
665
+ res.writeHead(204);
666
+ res.end();
667
+ }
668
+ async function handlePutCredentials(req, res, app) {
669
+ const raw = await readBody(req);
670
+ let parsed;
671
+ try {
672
+ parsed = JSON.parse(raw.toString("utf8"));
673
+ } catch {
674
+ return text(res, 400, "malformed JSON body");
675
+ }
676
+ if (!isCredentialsBody(parsed)) return text(res, 400, "malformed credentials body: expected { \"accessKeyId\", \"secretAccessKey\" }");
677
+ const existing = state.credentials[parsed.accessKeyId];
678
+ if (existing && existing.app !== app) return text(res, 409, `accessKeyId "${parsed.accessKeyId}" is already registered by a different app`);
679
+ state.credentials[parsed.accessKeyId] = {
680
+ app,
681
+ secretAccessKey: parsed.secretAccessKey
682
+ };
683
+ schedulePersist();
684
+ res.writeHead(204);
685
+ res.end();
686
+ }
687
+ function handleDeleteApp(res, app) {
688
+ for (const [key, reg] of Object.entries(state.buckets)) if (reg.app === app) delete state.buckets[key];
689
+ for (const [key, cred] of Object.entries(state.credentials)) if (cred.app === app) delete state.credentials[key];
690
+ schedulePersist();
691
+ res.writeHead(204);
692
+ res.end();
693
+ }
694
+ /** Path-style addressing: the bucket is the first non-empty path segment. Mirrors the handler's own `parseTarget`, which isn't exported. */
695
+ function bucketFromPath(pathname) {
696
+ const first = pathname.split("/").find((s) => s.length > 0);
697
+ return first !== void 0 ? decodeURIComponent(first) : void 0;
698
+ }
699
+ /**
700
+ * Credentials accepted for THIS request's target bucket only — its
701
+ * owning app, derived from the bucket's registration (the authoritative
702
+ * source for the `<app>--<name>` split; unregistered apps/names may
703
+ * themselves contain `--`, so the registration is trusted over re-parsing
704
+ * the string). An unknown bucket or an app with no registered credentials
705
+ * yields no candidates, which still resolves through the real 403 path
706
+ * below — never a bespoke "unknown bucket" shortcut that would reveal
707
+ * anything.
708
+ */
709
+ function credentialsForBucket(bucket) {
710
+ const owningApp = bucket !== void 0 ? state.buckets[bucket]?.app : void 0;
711
+ if (owningApp === void 0) return [];
712
+ return Object.entries(state.credentials).filter(([, rec]) => rec.app === owningApp).map(([accessKeyId, rec]) => ({
713
+ accessKeyId,
714
+ secretAccessKey: rec.secretAccessKey
715
+ }));
716
+ }
717
+ async function handleS3Wire(req, res, url) {
718
+ if (url.searchParams.has("uploads") || url.searchParams.has("uploadId") || url.searchParams.has("partNumber")) return text(res, 501, MULTIPART_MESSAGE);
719
+ const method = req.method ?? "GET";
720
+ const bodyBuf = method !== "GET" && method !== "HEAD" ? await readBody(req) : void 0;
721
+ const fullUrl = `http://${req.headers.host ?? `127.0.0.1:${String(port)}`}${req.url ?? "/"}`;
722
+ const headers = toWebHeaders(req.headers);
723
+ const candidates = credentialsForBucket(bucketFromPath(url.pathname));
724
+ if (candidates.length === 0) candidates.push({
725
+ accessKeyId: "",
726
+ secretAccessKey: ""
727
+ });
728
+ let last;
729
+ for (const credentials of candidates) {
730
+ const webReq = new Request(fullUrl, {
731
+ method,
732
+ headers,
733
+ ...bodyBuf && bodyBuf.length > 0 ? { body: bodyBuf } : {}
734
+ });
735
+ const webRes = await createS3Handler({
736
+ store,
737
+ credentials
738
+ })(webReq);
739
+ if (webRes.status !== 403) return sendWebResponse(res, webRes);
740
+ last = webRes;
741
+ }
742
+ if (last) await sendWebResponse(res, last);
743
+ }
744
+ async function handleRequest(req, res) {
745
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${String(port)}`);
746
+ const method = req.method ?? "GET";
747
+ if (method === "GET" && url.pathname === "/health") return json(res, 200, { version: ownVersion });
748
+ if (url.pathname === "/_pcdev/health" || url.pathname.startsWith("/_pcdev/")) {
749
+ const segments = url.pathname.split("/").filter((s) => s.length > 0).map((s) => decodeURIComponent(s));
750
+ if (method === "GET" && segments.length === 2 && segments[1] === "health") return json(res, 200, { version: ownVersion });
751
+ if (segments[1] === "apps" && segments.length >= 3) {
752
+ const app = segments[2];
753
+ if (app === void 0 || !isValidSegment(app)) return badSegment(res, app ?? "");
754
+ if (method === "PUT" && segments.length === 5 && segments[3] === "buckets") {
755
+ const name = segments[4];
756
+ if (name === void 0 || !isValidSegment(name)) return badSegment(res, name ?? "");
757
+ return handlePutBucket(req, res, app, name);
758
+ }
759
+ if (method === "PUT" && segments.length === 4 && segments[3] === "credentials") return handlePutCredentials(req, res, app);
760
+ if (method === "DELETE" && segments.length === 3) return handleDeleteApp(res, app);
761
+ }
762
+ res.writeHead(404);
763
+ res.end();
764
+ return;
765
+ }
766
+ return handleS3Wire(req, res, url);
767
+ }
768
+ const server = http.createServer((req, res) => {
769
+ handleRequest(req, res).catch((err) => {
770
+ if (!res.headersSent) res.writeHead(500, { "content-type": "text/plain" });
771
+ res.end(err instanceof Error ? err.message : String(err));
772
+ });
773
+ });
774
+ async function shutdown() {
775
+ await stateFile.flush();
776
+ server.close();
777
+ process.exit(0);
778
+ }
779
+ process.on("SIGTERM", () => void shutdown());
780
+ process.on("SIGINT", () => void shutdown());
781
+ readJsonFile(stateJsonPath, isBucketsState).then((loaded) => {
782
+ if (loaded) state = loaded;
783
+ server.listen(port, "127.0.0.1", () => {
784
+ console.log(`[dev-emulators] buckets-main listening on 127.0.0.1:${String(port)}`);
785
+ });
786
+ }).catch((err) => {
787
+ console.error("buckets-main: failed to load state", err);
788
+ process.exit(1);
789
+ });
790
+ }
791
+ main();
792
+ //#endregion
793
+ export {};
794
+
795
+ //# sourceMappingURL=buckets-main.mjs.map