@prisma/composer-prisma-cloud 0.2.0-dev.9 → 0.3.0-dev.1
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.
- package/dist/buckets-main.mjs +795 -0
- package/dist/buckets-main.mjs.map +1 -0
- package/dist/compute-main.mjs +559 -0
- package/dist/compute-main.mjs.map +1 -0
- package/dist/control.mjs +41 -1243
- package/dist/control.mjs.map +1 -1
- package/dist/cron/index.mjs +2 -3
- package/dist/cron/index.mjs.map +1 -1
- package/dist/cron/scheduler-entrypoint.mjs.map +1 -1
- package/dist/cron/scheduler-service.mjs +2 -3
- package/dist/cron/scheduler-service.mjs.map +1 -1
- package/dist/email/email-entrypoint.mjs.map +1 -1
- package/dist/email/email-service.mjs +2 -3
- package/dist/email/email-service.mjs.map +1 -1
- package/dist/email/index.mjs +2 -3
- package/dist/email/index.mjs.map +1 -1
- package/dist/email/testing.mjs.map +1 -1
- package/dist/get-port-BYnpbW5H.mjs +111 -0
- package/dist/get-port-BYnpbW5H.mjs.map +1 -0
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/local-target.d.mts +12 -0
- package/dist/local-target.d.mts.map +1 -0
- package/dist/local-target.mjs +6185 -0
- package/dist/local-target.mjs.map +1 -0
- package/dist/param-BTzY0ne5-CW6ojZ3E.mjs +66 -0
- package/dist/param-BTzY0ne5-CW6ojZ3E.mjs.map +1 -0
- package/dist/pg-connection-CadPZuEK-CF3FsSQu.mjs +79 -0
- package/dist/pg-connection-CadPZuEK-CF3FsSQu.mjs.map +1 -0
- package/dist/postgres-main.mjs +602 -0
- package/dist/postgres-main.mjs.map +1 -0
- package/dist/preflight-names-BPL-qsZF-BfoRgbgb.mjs +1252 -0
- package/dist/preflight-names-BPL-qsZF-BfoRgbgb.mjs.map +1 -0
- package/dist/{prisma-next-BvBwGCIt-CSauMX4l.mjs → prisma-next-BvBwGCIt-DssgnctU.mjs} +3 -78
- package/dist/prisma-next-BvBwGCIt-DssgnctU.mjs.map +1 -0
- package/dist/prisma-next.mjs +1 -1
- package/dist/{provisioned-edges-B_XS1Mz--NFJgcXK7.mjs → provisioned-edges-C1wSO3cx-YC5C19f9.mjs} +5 -65
- package/dist/provisioned-edges-C1wSO3cx-YC5C19f9.mjs.map +1 -0
- package/dist/secret-rz_ZqNeq-_xd97Jd9.mjs +47 -0
- package/dist/secret-rz_ZqNeq-_xd97Jd9.mjs.map +1 -0
- package/dist/segments-NpKN-46R-C_LQ2urs.mjs +1454 -0
- package/dist/segments-NpKN-46R-C_LQ2urs.mjs.map +1 -0
- package/dist/{serializer-D8_84XWO-DA2B6YbX.mjs → serializer-CJiOhF2d-DJUjQj1i.mjs} +4 -44
- package/dist/serializer-CJiOhF2d-DJUjQj1i.mjs.map +1 -0
- package/dist/storage/index.mjs +2 -3
- package/dist/storage/index.mjs.map +1 -1
- package/dist/storage/storage-entrypoint.mjs +4 -21
- package/dist/storage/storage-entrypoint.mjs.map +1 -1
- package/dist/storage/storage-service.mjs +2 -3
- package/dist/storage/storage-service.mjs.map +1 -1
- package/dist/storage/testing.d.mts +7 -6
- package/dist/storage/testing.d.mts.map +1 -1
- package/dist/storage/testing.mjs +4 -21
- package/dist/storage/testing.mjs.map +1 -1
- package/dist/streams/index.mjs +2 -3
- package/dist/streams/index.mjs.map +1 -1
- package/dist/streams/streams-entrypoint.mjs.map +1 -1
- package/dist/streams/streams-service.mjs +2 -3
- package/dist/streams/streams-service.mjs.map +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +21 -14
- package/src/exports/local-target.ts +1 -0
- package/dist/prisma-next-BvBwGCIt-CSauMX4l.mjs.map +0 -1
- package/dist/provisioned-edges-B_XS1Mz--NFJgcXK7.mjs.map +0 -1
- package/dist/serializer-D8_84XWO-DA2B6YbX.mjs.map +0 -1
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { a as readOwnVersion, i as readJsonFile, r as isValidSegment, t as StateFile } from "./segments-NpKN-46R-C_LQ2urs.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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|