@takosjp/yurucommu-core 4.1.0 → 4.1.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/package.json +3 -2
- package/packages/api/package.json +1 -1
- package/src/backend/runtime/bun.ts +1 -1
- package/src/backend/runtime/edge-facades.ts +34 -17
- package/src/backend/runtime/edge-objects.ts +8 -4
- package/src/backend/runtime/edge-queue.ts +6 -6
- package/src/backend/runtime/lane.ts +36 -18
- package/src/backend/runtime/node-paths.ts +79 -0
- package/src/backend/runtime/shared.ts +9 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takosjp/yurucommu-core",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -59,8 +59,9 @@
|
|
|
59
59
|
"start": "bun src/backend/server.ts",
|
|
60
60
|
"dev": "bun src/backend/server.ts",
|
|
61
61
|
"dev:server": "bun src/backend/server.ts",
|
|
62
|
-
"check": "bun run fmt:check && tsc --noEmit && bun run check:no-opentofu-artifacts && bun run test",
|
|
62
|
+
"check": "bun run fmt:check && tsc --noEmit && bun run check:no-opentofu-artifacts && bun run check:worker-bundle && bun run test",
|
|
63
63
|
"check:no-opentofu-artifacts": "bun scripts/check-no-opentofu-artifacts.mjs",
|
|
64
|
+
"check:worker-bundle": "bun scripts/check-worker-bundle-portable.mjs",
|
|
64
65
|
"test": "bun run build:api && bun test test/ src/backend/ packages/api/src/ scripts/check-publish-version-discipline.test.ts scripts/publish-package-resumable.test.ts && bun run check:release-contents",
|
|
65
66
|
"test:backend": "bun test src/backend/",
|
|
66
67
|
"build": "bun run build:api",
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
assertPathChainWithinBasePath,
|
|
25
25
|
isPathWithinBasePath,
|
|
26
26
|
resolvePathWithinBasePath,
|
|
27
|
-
} from "./
|
|
27
|
+
} from "./node-paths.ts";
|
|
28
28
|
import { MemoryKV } from "./memory-kv.ts";
|
|
29
29
|
import { isBackendPath } from "../lib/backend-paths.ts";
|
|
30
30
|
import { loadBunSqlite } from "./compat-bun/types.ts";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* The managed Cloudflare backend and the self-host backend project the SAME
|
|
10
10
|
* facade: same methods, same option keys, same error names. Takoserver's
|
|
11
11
|
* ADR 0005 states this explicitly for object storage, and its self-host wrapper
|
|
12
|
-
* repeats it for
|
|
12
|
+
* repeats it for all four.
|
|
13
13
|
*
|
|
14
14
|
* This module is a TYPE MIRROR of that contract plus the structural probes the
|
|
15
15
|
* lane selector uses. It deliberately contains no behaviour: the adapters that
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* — `projectEnv`, `createKvAdapter`, `createSqlAdapter`,
|
|
22
22
|
* `createQueueAdapter`, `createEdgeObjectsR2Adapter`
|
|
23
23
|
* takoserver `src/providers/selfhost-worker-wrapper.ts`
|
|
24
|
-
* — `projectEnv`, `createKvAdapter`, `createSqlAdapter
|
|
24
|
+
* — `projectEnv`, `createKvAdapter`, `createSqlAdapter`,
|
|
25
|
+
* `createQueueAdapter`, `createObjectsAdapter`
|
|
25
26
|
*
|
|
26
27
|
* Every method rejects with an `Error` whose `name` is the portable error code
|
|
27
28
|
* (`invalid_key`, `invalid_value`, `value_too_large`, `metadata_too_large`,
|
|
@@ -235,22 +236,24 @@ function hasMethods(value: unknown, names: readonly string[]): boolean {
|
|
|
235
236
|
/**
|
|
236
237
|
* Structural probes.
|
|
237
238
|
*
|
|
238
|
-
* Only
|
|
239
|
+
* Only ONE binding can be told apart by shape, and the difference matters:
|
|
239
240
|
*
|
|
240
241
|
* decisive `DB` — `execute`/`query`/`transaction` (facade) against
|
|
241
242
|
* `prepare`/`batch` (D1). Disjoint method sets.
|
|
242
|
-
* decisive `MEDIA` — R2 carries the multipart helpers the facade omits.
|
|
243
243
|
* decisive a queue *batch* — `acknowledgeAll` (facade) against `ackAll`
|
|
244
244
|
* (Cloudflare `MessageBatch`).
|
|
245
245
|
* AMBIGUOUS `KV` — `edge.kv` and `KVNamespace` expose the same five
|
|
246
246
|
* method names.
|
|
247
247
|
* AMBIGUOUS a queue *producer* — both are `send`/`sendBatch`.
|
|
248
|
+
* AMBIGUOUS `MEDIA` — `edge.objects@1.0.0` is R2's method set by design,
|
|
249
|
+
* multipart helpers included. See below.
|
|
248
250
|
*
|
|
249
251
|
* That is why the lane is a DECLARED variable rather than something sniffed:
|
|
250
|
-
*
|
|
251
|
-
* cross-checked against the decisive bindings, so a Worker whose var and
|
|
252
|
-
* bindings disagree refuses to start instead of calling
|
|
253
|
-
* on a facade that would silently treat the options
|
|
252
|
+
* three of the five bindings cannot be identified at all. The declaration is
|
|
253
|
+
* then cross-checked against the decisive bindings, so a Worker whose var and
|
|
254
|
+
* whose bindings disagree refuses to start instead of calling
|
|
255
|
+
* `kv.get(key, {type})` on a facade that would silently treat the options
|
|
256
|
+
* object as nothing.
|
|
254
257
|
*/
|
|
255
258
|
export function isEdgeSqlBinding(value: unknown): value is EdgeSqlBinding {
|
|
256
259
|
return (
|
|
@@ -274,24 +277,38 @@ export function isEdgeQueueBatch(value: unknown): value is EdgeQueueBatch {
|
|
|
274
277
|
);
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
/**
|
|
281
|
+
* A bucket-shaped binding with `edge.objects@1.0.0`'s call signatures.
|
|
282
|
+
*
|
|
283
|
+
* NOT DECISIVE, and never a lane test. The facade is R2's method set on
|
|
284
|
+
* purpose — `head`, `get`, `put`, `delete`, `list`, and the four multipart
|
|
285
|
+
* calls — so that an app written against R2 ports over unchanged. A real
|
|
286
|
+
* `R2Bucket` therefore answers `true` here too. Use it to check that SOMETHING
|
|
287
|
+
* bucket-shaped arrived, never to decide which host projected it: that is what
|
|
288
|
+
* `YURUCOMMU_RUNTIME_LANE` is for.
|
|
289
|
+
*
|
|
290
|
+
* The arity check is the facade's own contract rather than a discriminator:
|
|
291
|
+
* the Host counts `arguments.length`, so `get` takes its options slot even when
|
|
292
|
+
* that slot is `undefined`.
|
|
293
|
+
*/
|
|
277
294
|
export function isEdgeObjectsBinding(
|
|
278
295
|
value: unknown,
|
|
279
296
|
): value is EdgeObjectsBinding {
|
|
280
297
|
return (
|
|
281
298
|
hasMethods(value, ["head", "get", "put", "delete", "list"]) &&
|
|
282
|
-
// R2 exposes multipart helpers on the binding itself; the facade does not
|
|
283
|
-
// give a bucket-shaped object those names.
|
|
284
|
-
typeof (value as Record<string, unknown>).createMultipartUpload !==
|
|
285
|
-
"function" &&
|
|
286
|
-
// Arity is part of the facade's contract and is asserted rather than
|
|
287
|
-
// assumed: the Host checks `arguments.length`, so `get` and `list` take
|
|
288
|
-
// their options slot even when it is `undefined`. Anything bucket-shaped
|
|
289
|
-
// whose `get` takes one argument is some other adapter, not this facade.
|
|
290
299
|
(value as { get: (...args: unknown[]) => unknown }).get.length === 2
|
|
291
300
|
);
|
|
292
301
|
}
|
|
293
302
|
|
|
294
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* Cloudflare's `R2Bucket` — and, unavoidably, the `edge.objects` facade.
|
|
305
|
+
*
|
|
306
|
+
* A method-name test cannot separate the two, because Takoserver's facade
|
|
307
|
+
* carries `createMultipartUpload` as well (`selfhost-worker-wrapper.ts`
|
|
308
|
+
* `createObjectsAdapter`). 4.1.0 used this function to refuse the portable
|
|
309
|
+
* lane and so refused every self-hosted deployment. Keep it for describing a
|
|
310
|
+
* binding; do not let it decide a lane.
|
|
311
|
+
*/
|
|
295
312
|
export function isNativeR2Bucket(value: unknown): boolean {
|
|
296
313
|
return hasMethods(value, [
|
|
297
314
|
"head",
|
|
@@ -21,10 +21,14 @@
|
|
|
21
21
|
* - NO ENUMERATION OR HEAD. The port does not carry them, so neither does the
|
|
22
22
|
* adapter, even though the Host projects both.
|
|
23
23
|
*
|
|
24
|
-
* AVAILABILITY: `edge.objects
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
24
|
+
* AVAILABILITY: BOTH wrapper backends project `edge.objects`. The managed
|
|
25
|
+
* Cloudflare backend does it over provider-private R2
|
|
26
|
+
* (`createEdgeObjectsR2Adapter`); the self-host backend realizes its own object
|
|
27
|
+
* store for a Version's `bucketBindings` and projects the same facade, byte for
|
|
28
|
+
* byte. A Worker on the `portable` lane therefore receives `env.MEDIA` on
|
|
29
|
+
* either host. What still leaves `MEDIA` unbound is a Version that declared no
|
|
30
|
+
* bucket at all, and the core's existing "object storage unavailable" (503)
|
|
31
|
+
* behaviour is what applies then.
|
|
28
32
|
*/
|
|
29
33
|
|
|
30
34
|
import type {
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
* and the body arrives as `{encoding:"base64", data}`. `retry` also refuses
|
|
15
15
|
* `delaySeconds: 0`, which Cloudflare accepts as "no delay".
|
|
16
16
|
*
|
|
17
|
-
* AVAILABILITY:
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* readiness surface — is what applies
|
|
17
|
+
* AVAILABILITY: both wrapper backends project queue bindings (see takoserver
|
|
18
|
+
* `selfhost-worker-wrapper.ts` `projectEnv`, whose data-binding kinds are
|
|
19
|
+
* `edge.kv`, `edge.objects`, `edge.queue` and `edge.sql`). What leaves
|
|
20
|
+
* `DELIVERY_QUEUE` unbound is a Version that declared no queue, and the core's
|
|
21
|
+
* existing behaviour for that — synchronous fallback delivery, reported by the
|
|
22
|
+
* readiness surface — is what applies then.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import {
|
|
@@ -28,8 +28,18 @@
|
|
|
28
28
|
* So the lane comes from `YURUCOMMU_RUNTIME_LANE`, which a self-host or managed
|
|
29
29
|
* Workers-for-Platforms deployment sets to `portable` and every raw-binding
|
|
30
30
|
* deployment leaves unset (or `cloudflare`). The declaration is then
|
|
31
|
-
* cross-checked against the
|
|
32
|
-
*
|
|
31
|
+
* cross-checked against the ONE binding that is decisive — `DB`. A
|
|
32
|
+
* disagreement refuses to start.
|
|
33
|
+
*
|
|
34
|
+
* `MEDIA` is NOT decisive and must never be cross-checked. Takoserver's
|
|
35
|
+
* `edge.objects@1.0.0` facade is method-for-method a bucket: `head`, `get`,
|
|
36
|
+
* `put`, `delete`, `list`, `createMultipartUpload`, `uploadPart`,
|
|
37
|
+
* `completeMultipartUpload`, `abortMultipartUpload` — the same names, the same
|
|
38
|
+
* option keys, deliberately, so that an app written against R2 ports over
|
|
39
|
+
* unchanged (ADR 0005/0007). 4.1.0 read that identity backwards and refused the
|
|
40
|
+
* portable lane whenever `MEDIA` looked R2-shaped, which is to say always: a
|
|
41
|
+
* self-hosted Yurucommu Worker could not boot on the lane its own README
|
|
42
|
+
* documents. The declaration decides `MEDIA`.
|
|
33
43
|
*/
|
|
34
44
|
|
|
35
45
|
import type {
|
|
@@ -47,7 +57,6 @@ import {
|
|
|
47
57
|
isEdgeQueueBatch,
|
|
48
58
|
isEdgeSqlBinding,
|
|
49
59
|
isNativeD1Database,
|
|
50
|
-
isNativeR2Bucket,
|
|
51
60
|
type EdgeKvBinding,
|
|
52
61
|
type EdgeObjectsBinding,
|
|
53
62
|
type EdgeQueueBatch,
|
|
@@ -114,23 +123,35 @@ export function resolveRuntimeLane(declared: unknown): RuntimeLane {
|
|
|
114
123
|
|
|
115
124
|
interface LaneBindings {
|
|
116
125
|
readonly DB?: unknown;
|
|
126
|
+
/**
|
|
127
|
+
* Accepted so a caller can pass the whole `env`, and deliberately not read:
|
|
128
|
+
* the bucket binding carries no evidence about the lane. See
|
|
129
|
+
* {@link assertRuntimeLaneBindings}.
|
|
130
|
+
*/
|
|
117
131
|
readonly MEDIA?: unknown;
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
/**
|
|
121
|
-
* Prove the declared lane against the
|
|
135
|
+
* Prove the declared lane against the ONE binding that can be identified.
|
|
136
|
+
*
|
|
137
|
+
* `DB` is decisive in both directions: `execute`/`query`/`transaction` and
|
|
138
|
+
* `prepare`/`batch` are disjoint method sets, so a Worker that was handed the
|
|
139
|
+
* wrong one would fail at its first query anyway and is better stopped here
|
|
140
|
+
* with a message that names the variable to fix.
|
|
122
141
|
*
|
|
123
|
-
* `
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
142
|
+
* `MEDIA` is checked against NOTHING. The portable `edge.objects@1.0.0` facade
|
|
143
|
+
* is intentionally indistinguishable from an `R2Bucket` — that identity is the
|
|
144
|
+
* point of the Interface — so a shape test on it can only produce false
|
|
145
|
+
* refusals. On `portable` the bucket is wrapped as the facade, on `cloudflare`
|
|
146
|
+
* as native R2, and the declaration is the whole of the evidence. Getting it
|
|
147
|
+
* wrong is loud and immediate (the first `MEDIA` call throws), not the silent
|
|
148
|
+
* misread that `KV`'s ambiguity would cause.
|
|
128
149
|
*/
|
|
129
150
|
export function assertRuntimeLaneBindings(
|
|
130
151
|
lane: RuntimeLane,
|
|
131
152
|
bindings: LaneBindings,
|
|
132
153
|
): void {
|
|
133
|
-
const { DB
|
|
154
|
+
const { DB } = bindings;
|
|
134
155
|
if (lane === "portable") {
|
|
135
156
|
if (isNativeD1Database(DB)) {
|
|
136
157
|
throw new RuntimeLaneError(
|
|
@@ -148,13 +169,6 @@ export function assertRuntimeLaneBindings(
|
|
|
148
169
|
`neither that nor D1's prepare/batch.`,
|
|
149
170
|
);
|
|
150
171
|
}
|
|
151
|
-
if (MEDIA !== undefined && isNativeR2Bucket(MEDIA)) {
|
|
152
|
-
throw new RuntimeLaneError(
|
|
153
|
-
`${RUNTIME_LANE_VAR}="portable" declares the portable-facade lane, ` +
|
|
154
|
-
`but env.MEDIA is a native R2Bucket. A portable bucket binding ` +
|
|
155
|
-
`arrives as the edge.objects@1.0.0 facade.`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
172
|
return;
|
|
159
173
|
}
|
|
160
174
|
if (isEdgeSqlBinding(DB)) {
|
|
@@ -287,5 +301,9 @@ export function wrapRuntimeMessageBatch<T>(
|
|
|
287
301
|
return wrapCloudflareMessageBatch(batch as MessageBatch<T>);
|
|
288
302
|
}
|
|
289
303
|
|
|
290
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* Re-exported so a Worker entry can assert that SOMETHING bucket-shaped
|
|
306
|
+
* arrived without importing internals. It does not identify the lane — a
|
|
307
|
+
* native `R2Bucket` satisfies it too — so never branch on it.
|
|
308
|
+
*/
|
|
291
309
|
export { isEdgeObjectsBinding };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem path containment for the Bun/Node runtime ONLY.
|
|
3
|
+
*
|
|
4
|
+
* These helpers need `node:path`, so they live apart from `shared.ts`. That
|
|
5
|
+
* separation is load-bearing rather than tidy: `shared.ts` is reached from
|
|
6
|
+
* `edge-kv.ts` and `edge-objects.ts`, which are on the portable Worker's
|
|
7
|
+
* import path. A `node:` specifier anywhere in that graph survives bundling as
|
|
8
|
+
* a real static import, and a wrapper host (self-hosted Takoserver, managed
|
|
9
|
+
* Workers-for-Platforms) runs the Worker with no `nodejs_compat` flag — the
|
|
10
|
+
* portable `WorkerVersion` form has nowhere to ask for one — so the module
|
|
11
|
+
* fails to load with `No such module "node:path"` before a single request is
|
|
12
|
+
* served. Nothing in this file may be imported from a module the Worker
|
|
13
|
+
* bundle reaches; `scripts/check-worker-bundle-portable.mjs` enforces that.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { hasNulByte } from "./shared.ts";
|
|
19
|
+
|
|
20
|
+
export function isPathWithinBasePath(
|
|
21
|
+
basePath: string,
|
|
22
|
+
candidatePath: string,
|
|
23
|
+
): boolean {
|
|
24
|
+
const relative = path.relative(basePath, candidatePath);
|
|
25
|
+
return (
|
|
26
|
+
relative === "" ||
|
|
27
|
+
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolvePathWithinBasePath(
|
|
32
|
+
basePath: string,
|
|
33
|
+
key: string,
|
|
34
|
+
): string {
|
|
35
|
+
if (hasNulByte(key)) {
|
|
36
|
+
throw new Error("Invalid path");
|
|
37
|
+
}
|
|
38
|
+
const resolvedPath = path.resolve(basePath, key);
|
|
39
|
+
if (!isPathWithinBasePath(basePath, resolvedPath)) {
|
|
40
|
+
throw new Error("Path escapes base directory");
|
|
41
|
+
}
|
|
42
|
+
return resolvedPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isNotFoundError(error: unknown): boolean {
|
|
46
|
+
return (
|
|
47
|
+
typeof error === "object" &&
|
|
48
|
+
error !== null &&
|
|
49
|
+
"code" in error &&
|
|
50
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function assertPathChainWithinBasePath(
|
|
55
|
+
basePath: string,
|
|
56
|
+
targetPath: string,
|
|
57
|
+
realpath: (path: string) => Promise<string>,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
let currentPath = targetPath;
|
|
60
|
+
|
|
61
|
+
while (true) {
|
|
62
|
+
try {
|
|
63
|
+
const realCurrentPath = await realpath(currentPath);
|
|
64
|
+
if (!isPathWithinBasePath(basePath, realCurrentPath)) {
|
|
65
|
+
throw new Error("Path escapes base directory");
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!isNotFoundError(error)) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const parentPath = path.dirname(currentPath);
|
|
73
|
+
if (parentPath === currentPath) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
currentPath = parentPath;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Runtime helpers shared by EVERY lane, including the portable Worker.
|
|
3
|
+
*
|
|
4
|
+
* `edge-kv.ts` and `edge-objects.ts` import this module, so it is part of the
|
|
5
|
+
* bundle a wrapper host loads with no `nodejs_compat` flag. It must therefore
|
|
6
|
+
* stay free of `node:` specifiers; the filesystem path helpers that need
|
|
7
|
+
* `node:path` live in `node-paths.ts`, which only the Bun/Node runtime
|
|
8
|
+
* imports.
|
|
9
|
+
*/
|
|
2
10
|
|
|
3
11
|
export const DEFAULT_LIST_LIMIT = 1000;
|
|
4
12
|
|
|
@@ -35,67 +43,6 @@ export function hasNulByte(value: string): boolean {
|
|
|
35
43
|
return value.includes("\0");
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
export function isPathWithinBasePath(
|
|
39
|
-
basePath: string,
|
|
40
|
-
candidatePath: string,
|
|
41
|
-
): boolean {
|
|
42
|
-
const relative = path.relative(basePath, candidatePath);
|
|
43
|
-
return (
|
|
44
|
-
relative === "" ||
|
|
45
|
-
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function resolvePathWithinBasePath(
|
|
50
|
-
basePath: string,
|
|
51
|
-
key: string,
|
|
52
|
-
): string {
|
|
53
|
-
if (hasNulByte(key)) {
|
|
54
|
-
throw new Error("Invalid path");
|
|
55
|
-
}
|
|
56
|
-
const resolvedPath = path.resolve(basePath, key);
|
|
57
|
-
if (!isPathWithinBasePath(basePath, resolvedPath)) {
|
|
58
|
-
throw new Error("Path escapes base directory");
|
|
59
|
-
}
|
|
60
|
-
return resolvedPath;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isNotFoundError(error: unknown): boolean {
|
|
64
|
-
return (
|
|
65
|
-
typeof error === "object" &&
|
|
66
|
-
error !== null &&
|
|
67
|
-
"code" in error &&
|
|
68
|
-
(error as { code?: unknown }).code === "ENOENT"
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function assertPathChainWithinBasePath(
|
|
73
|
-
basePath: string,
|
|
74
|
-
targetPath: string,
|
|
75
|
-
realpath: (path: string) => Promise<string>,
|
|
76
|
-
): Promise<void> {
|
|
77
|
-
let currentPath = targetPath;
|
|
78
|
-
|
|
79
|
-
while (true) {
|
|
80
|
-
try {
|
|
81
|
-
const realCurrentPath = await realpath(currentPath);
|
|
82
|
-
if (!isPathWithinBasePath(basePath, realCurrentPath)) {
|
|
83
|
-
throw new Error("Path escapes base directory");
|
|
84
|
-
}
|
|
85
|
-
return;
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (!isNotFoundError(error)) {
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
const parentPath = path.dirname(currentPath);
|
|
91
|
-
if (parentPath === currentPath) {
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
currentPath = parentPath;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
46
|
export async function readStream(
|
|
100
47
|
stream: ReadableStream<Uint8Array>,
|
|
101
48
|
): Promise<Uint8Array> {
|