@poe-platform/safe-fs 0.1.798 → 0.1.800
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/README.md +12 -6
- package/dist/safe-fs/contracts/io.js +26 -36
- package/dist/safe-fs/contracts/path.d.ts +1 -7
- package/dist/safe-fs/contracts/path.js +1 -7
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/memory/index.js +3 -3
- package/dist/safe-fs/fs/overlay/index.d.ts +4 -1
- package/dist/safe-fs/fs/overlay/index.js +57 -11
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +11 -4
- package/dist/safe-fs/fs/overlay/memory-publication.js +240 -14
- package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-fetch.js +74 -0
- package/dist/safe-fs/fs/s3/http/request-node.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-node.js +111 -0
- package/dist/safe-fs/fs/s3/http/request.d.ts +5 -4
- package/dist/safe-fs/fs/s3/http/request.js +1 -109
- package/dist/safe-fs/fs/s3/http/signature.js +7 -5
- package/dist/safe-fs/fs/s3/http/transport.js +6 -3
- package/dist/xml-ast/index.d.ts +2 -0
- package/dist/xml-ast/index.js +33 -13
- package/package.json +9 -2
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { concatBytes } from "./bytes.js";
|
|
2
|
-
import { once } from "node:events";
|
|
3
|
-
import { request as httpRequest } from "node:http";
|
|
4
|
-
import { request as httpsRequest } from "node:https";
|
|
5
2
|
import { S3ServiceError } from "../transport.js";
|
|
3
|
+
export { sendRequest } from "#safe-fs-s3-request";
|
|
6
4
|
export function scopeFor(signal, timeout) {
|
|
7
5
|
const controller = new AbortController();
|
|
8
6
|
const abort = () => controller.abort(signal?.reason);
|
|
@@ -31,112 +29,6 @@ export async function abortable(pending, signal) {
|
|
|
31
29
|
signal.removeEventListener("abort", abort);
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
|
-
export async function sendRequest(options, body, scope, factory) {
|
|
35
|
-
const signal = scope.signal;
|
|
36
|
-
signal.throwIfAborted();
|
|
37
|
-
return new Promise((resolve, reject) => {
|
|
38
|
-
let request;
|
|
39
|
-
let response;
|
|
40
|
-
let headersReceived = false;
|
|
41
|
-
let finished = false;
|
|
42
|
-
const cleanup = () => {
|
|
43
|
-
signal.removeEventListener("abort", abort);
|
|
44
|
-
scope.finish();
|
|
45
|
-
};
|
|
46
|
-
const close = () => {
|
|
47
|
-
if (finished)
|
|
48
|
-
return;
|
|
49
|
-
finished = true;
|
|
50
|
-
cleanup();
|
|
51
|
-
response?.destroy();
|
|
52
|
-
request?.destroy();
|
|
53
|
-
};
|
|
54
|
-
const fail = (error) => {
|
|
55
|
-
reject(signal.aborted ? signal.reason : error);
|
|
56
|
-
close();
|
|
57
|
-
};
|
|
58
|
-
const abort = () => fail(signal.reason);
|
|
59
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
60
|
-
try {
|
|
61
|
-
const create = factory ?? (options.protocol === "https:" ? httpsRequest : httpRequest);
|
|
62
|
-
request = create(options, message => {
|
|
63
|
-
response = message;
|
|
64
|
-
message.on("error", () => { });
|
|
65
|
-
if (finished || signal.aborted) {
|
|
66
|
-
close();
|
|
67
|
-
message.destroy();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
headersReceived = true;
|
|
71
|
-
message.once("close", cleanup);
|
|
72
|
-
let claimed = false;
|
|
73
|
-
const source = {
|
|
74
|
-
[Symbol.asyncIterator]() {
|
|
75
|
-
if (claimed)
|
|
76
|
-
throw new S3ServiceError("InvalidResponse", 502, "response body is single-use");
|
|
77
|
-
claimed = true;
|
|
78
|
-
const iterator = message[Symbol.asyncIterator]();
|
|
79
|
-
let ended = false;
|
|
80
|
-
return {
|
|
81
|
-
async next() {
|
|
82
|
-
try {
|
|
83
|
-
signal.throwIfAborted();
|
|
84
|
-
if (ended)
|
|
85
|
-
return { done: true, value: undefined };
|
|
86
|
-
const result = await abortable(iterator.next(), signal);
|
|
87
|
-
signal.throwIfAborted();
|
|
88
|
-
if (result.done) {
|
|
89
|
-
ended = true;
|
|
90
|
-
if (!message.complete)
|
|
91
|
-
throw new S3ServiceError("InvalidResponse", 502, "incomplete HTTP response");
|
|
92
|
-
close();
|
|
93
|
-
return { done: true, value: undefined };
|
|
94
|
-
}
|
|
95
|
-
if (!(result.value instanceof Uint8Array))
|
|
96
|
-
throw new S3ServiceError("InvalidResponse", 502, "nonbinary HTTP body");
|
|
97
|
-
return { done: false, value: new Uint8Array(result.value) };
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
ended = true;
|
|
101
|
-
close();
|
|
102
|
-
throw signal.aborted ? signal.reason : error;
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
async return() {
|
|
106
|
-
ended = true;
|
|
107
|
-
close();
|
|
108
|
-
return { done: true, value: undefined };
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
resolve({ message, body: source, close });
|
|
114
|
-
});
|
|
115
|
-
request.on("error", fail);
|
|
116
|
-
request.once("close", () => {
|
|
117
|
-
if (!headersReceived)
|
|
118
|
-
fail(new S3ServiceError("InvalidResponse", 502, "connection closed before response headers"));
|
|
119
|
-
});
|
|
120
|
-
if (signal.aborted || finished) {
|
|
121
|
-
abort();
|
|
122
|
-
request.destroy();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
void (async () => {
|
|
126
|
-
for (let offset = 0; offset < body.length; offset += 64 * 1024) {
|
|
127
|
-
signal.throwIfAborted();
|
|
128
|
-
if (!request.write(body.subarray(offset, offset + 64 * 1024)))
|
|
129
|
-
await once(request, "drain", { signal });
|
|
130
|
-
}
|
|
131
|
-
signal.throwIfAborted();
|
|
132
|
-
request.end();
|
|
133
|
-
})().catch(fail);
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
fail(error);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
32
|
export function limitedBody(response, maximum, expected) {
|
|
141
33
|
if (expected !== undefined && expected > maximum) {
|
|
142
34
|
response.close();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256 as hash } from "@noble/hashes/sha2.js";
|
|
2
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
3
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
2
4
|
import { S3ServiceError } from "../transport.js";
|
|
3
5
|
export function invalid(message) {
|
|
4
6
|
throw new S3ServiceError("InvalidArgument", 400, message);
|
|
@@ -20,7 +22,7 @@ export function canonicalQuery(entries) {
|
|
|
20
22
|
.map(([name, value]) => `${name}=${value}`).join("&");
|
|
21
23
|
}
|
|
22
24
|
export function sha256(bytes) {
|
|
23
|
-
return
|
|
25
|
+
return bytesToHex(hash(typeof bytes === "string" ? utf8ToBytes(bytes) : bytes));
|
|
24
26
|
}
|
|
25
27
|
export function headerValue(value) {
|
|
26
28
|
if (typeof value !== "string" || /[^\x20-\x7e\t]/.test(value))
|
|
@@ -63,10 +65,10 @@ export function signRequest(input) {
|
|
|
63
65
|
names.map(name => `${name}:${headers[name]}\n`).join(""), signedHeaders, payloadHash].join("\n");
|
|
64
66
|
const scope = `${date}/${input.region}/s3/aws4_request`;
|
|
65
67
|
const stringToSign = ["AWS4-HMAC-SHA256", timestamp, scope, sha256(canonicalRequest)].join("\n");
|
|
66
|
-
let key =
|
|
68
|
+
let key = hmac(hash, utf8ToBytes(`AWS4${credentials.secretAccessKey}`), utf8ToBytes(date));
|
|
67
69
|
for (const part of [input.region, "s3", "aws4_request"])
|
|
68
|
-
key =
|
|
69
|
-
const signature =
|
|
70
|
+
key = hmac(hash, key, utf8ToBytes(part));
|
|
71
|
+
const signature = bytesToHex(hmac(hash, key, utf8ToBytes(stringToSign)));
|
|
70
72
|
headers.authorization = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
71
73
|
if (Object.entries(headers).reduce((size, [name, value]) => size + name.length + value.length + 4, 0) > 16 * 1024)
|
|
72
74
|
invalid("request headers exceed 16 KiB");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { concatBytes } from "./bytes.js";
|
|
2
|
-
import { isIP } from "node:net";
|
|
3
2
|
import { S3ServiceError } from "../transport.js";
|
|
4
3
|
import { abortable, collect, limitedBody, scopeFor, sendRequest } from "./request.js";
|
|
5
4
|
import { canonicalQuery, headerValue, invalid, signRequest, uriEncode } from "./signature.js";
|
|
@@ -35,9 +34,13 @@ function boolean(value, fallback, name) {
|
|
|
35
34
|
invalid(`invalid ${name}`);
|
|
36
35
|
return value ?? fallback;
|
|
37
36
|
}
|
|
37
|
+
function isIPv4(value) {
|
|
38
|
+
const parts = value.split(".");
|
|
39
|
+
return parts.length === 4 && parts.every(part => String(Number(part)) === part && Number(part) >= 0 && Number(part) <= 255);
|
|
40
|
+
}
|
|
38
41
|
function bucketName(bucket) {
|
|
39
42
|
if (typeof bucket !== "string" || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket)
|
|
40
|
-
|| bucket.includes("..") || bucket.includes(".-") || bucket.includes("-.") ||
|
|
43
|
+
|| bucket.includes("..") || bucket.includes(".-") || bucket.includes("-.") || isIPv4(bucket))
|
|
41
44
|
invalid("expected a standard S3 bucket name");
|
|
42
45
|
}
|
|
43
46
|
function objectKey(key) {
|
|
@@ -179,7 +182,7 @@ export function createS3HttpTransport(options) {
|
|
|
179
182
|
const listEncoding = options.listUrlEncoding ?? "percent";
|
|
180
183
|
if (listEncoding !== "percent" && listEncoding !== "form")
|
|
181
184
|
invalid("unsupported LIST URL encoding");
|
|
182
|
-
if (addressing === "virtual-hosted" && (
|
|
185
|
+
if (addressing === "virtual-hosted" && (endpoint.hostname.includes(":") || isIPv4(endpoint.hostname) || endpoint.hostname === "localhost"))
|
|
183
186
|
invalid("virtual-hosted addressing requires a DNS endpoint");
|
|
184
187
|
const maxPut = limit(options.maxPutBytes, Infinity, "maxPutBytes");
|
|
185
188
|
const maxGet = limit(options.maxGetBytes, Infinity, "maxGetBytes");
|
package/dist/xml-ast/index.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface XmlElement extends XmlName {
|
|
|
26
26
|
readonly epilog?: readonly XmlContent[];
|
|
27
27
|
}
|
|
28
28
|
export interface XmlLimits {
|
|
29
|
+
/** Opt-in best-effort parsing; each repaired syntax error is reported. DTDs remain forbidden. */
|
|
30
|
+
readonly recover?: (message: string) => void;
|
|
29
31
|
readonly expectedEncoding?: "UTF-8" | "UTF-16" | "UTF-16LE" | "UTF-16BE";
|
|
30
32
|
readonly retainContent?: boolean;
|
|
31
33
|
readonly maxDepth?: number;
|
package/dist/xml-ast/index.js
CHANGED
|
@@ -74,7 +74,7 @@ function qualifiedNameSync(name, cache) {
|
|
|
74
74
|
}
|
|
75
75
|
return parts;
|
|
76
76
|
}
|
|
77
|
-
function* entities(text) {
|
|
77
|
+
function* entities(text, recover) {
|
|
78
78
|
let result = "";
|
|
79
79
|
let offset = 0;
|
|
80
80
|
while (offset < text.length) {
|
|
@@ -83,8 +83,12 @@ function* entities(text) {
|
|
|
83
83
|
return result + text.slice(offset);
|
|
84
84
|
result += text.slice(offset, start);
|
|
85
85
|
const end = yield* find(text, ";", start + 1);
|
|
86
|
-
if (end < 0)
|
|
87
|
-
|
|
86
|
+
if (end < 0) {
|
|
87
|
+
if (!recover)
|
|
88
|
+
invalid("unterminated entity");
|
|
89
|
+
recover("unterminated entity");
|
|
90
|
+
return result + text.slice(start + 1);
|
|
91
|
+
}
|
|
88
92
|
const entity = text.slice(start + 1, end);
|
|
89
93
|
const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
90
94
|
if (Object.hasOwn(predefined, entity))
|
|
@@ -92,8 +96,13 @@ function* entities(text) {
|
|
|
92
96
|
else {
|
|
93
97
|
const hexadecimal = entity.startsWith("#x");
|
|
94
98
|
const digits = entity.slice(hexadecimal ? 2 : 1);
|
|
95
|
-
if (!entity.startsWith("#") || !digits.length)
|
|
96
|
-
|
|
99
|
+
if (!entity.startsWith("#") || !digits.length) {
|
|
100
|
+
if (!recover)
|
|
101
|
+
invalid("undeclared entity");
|
|
102
|
+
recover("undeclared entity");
|
|
103
|
+
offset = end + 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
97
106
|
let point = 0;
|
|
98
107
|
for (let index = 0; index < digits.length; index++) {
|
|
99
108
|
const code = digits.charCodeAt(index);
|
|
@@ -296,7 +305,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
296
305
|
}
|
|
297
306
|
if (text.indexOf("]]>") >= 0)
|
|
298
307
|
invalid("CDATA terminator in text");
|
|
299
|
-
const resolved = stack.length ? (text.indexOf("&") < 0 ? (pendingWork += text.length, text) : yield* entities(text)) : text;
|
|
308
|
+
const resolved = stack.length ? (text.indexOf("&") < 0 ? (pendingWork += text.length, text) : yield* entities(text, limits.recover)) : text;
|
|
300
309
|
while (pendingWork >= 512) {
|
|
301
310
|
yield 512;
|
|
302
311
|
pendingWork -= 512;
|
|
@@ -412,8 +421,11 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
412
421
|
yield 512;
|
|
413
422
|
pendingWork -= 512;
|
|
414
423
|
}
|
|
415
|
-
if (source[offset++] !== ">" || stack.pop()?.name !== name)
|
|
416
|
-
|
|
424
|
+
if (source[offset++] !== ">" || stack.pop()?.name !== name) {
|
|
425
|
+
if (!limits.recover)
|
|
426
|
+
invalid("mismatched closing tag");
|
|
427
|
+
limits.recover("mismatched closing tag");
|
|
428
|
+
}
|
|
417
429
|
}
|
|
418
430
|
else {
|
|
419
431
|
offset++;
|
|
@@ -441,7 +453,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
441
453
|
pendingWork -= 512;
|
|
442
454
|
}
|
|
443
455
|
const ch = source.charCodeAt(offset);
|
|
444
|
-
if (ch === 47 || ch === 62)
|
|
456
|
+
if (ch === 47 || ch === 62 || limits.recover && offset === source.length)
|
|
445
457
|
break;
|
|
446
458
|
if (ws === 0)
|
|
447
459
|
invalid("attributes require whitespace");
|
|
@@ -482,7 +494,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
482
494
|
if (raw.indexOf("<") >= 0)
|
|
483
495
|
invalid("less-than in attribute");
|
|
484
496
|
const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
|
|
485
|
-
const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized);
|
|
497
|
+
const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
|
|
486
498
|
while (pendingWork >= 512) {
|
|
487
499
|
yield 512;
|
|
488
500
|
pendingWork -= 512;
|
|
@@ -571,8 +583,11 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
571
583
|
const empty = source[offset] === "/";
|
|
572
584
|
if (empty)
|
|
573
585
|
offset++;
|
|
574
|
-
if (source[offset++] !== ">")
|
|
575
|
-
|
|
586
|
+
if (source[offset++] !== ">") {
|
|
587
|
+
if (!limits.recover)
|
|
588
|
+
invalid("unterminated start tag");
|
|
589
|
+
limits.recover("unterminated start tag");
|
|
590
|
+
}
|
|
576
591
|
// Only cache compact, attribute-free spellings. Namespace resolution and
|
|
577
592
|
// every admission counter still run for each distinct physical element.
|
|
578
593
|
if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
|
|
@@ -585,8 +600,13 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
585
600
|
yield pendingWork;
|
|
586
601
|
pendingWork = 0;
|
|
587
602
|
}
|
|
588
|
-
if (
|
|
603
|
+
if (!root)
|
|
589
604
|
invalid("incomplete document");
|
|
605
|
+
if (stack.length) {
|
|
606
|
+
if (!limits.recover)
|
|
607
|
+
invalid("incomplete document");
|
|
608
|
+
limits.recover("incomplete document");
|
|
609
|
+
}
|
|
590
610
|
return root;
|
|
591
611
|
}
|
|
592
612
|
export function parseXml(input, limits = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poe-platform/safe-fs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.800",
|
|
4
4
|
"description": "Composable filesystem with a portable core and explicit Node adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -133,9 +133,16 @@
|
|
|
133
133
|
"access": "public"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@types/node": "^25.2.2"
|
|
136
|
+
"@types/node": "^25.2.2",
|
|
137
|
+
"@noble/hashes": "2.4.0"
|
|
137
138
|
},
|
|
138
139
|
"imports": {
|
|
140
|
+
"#safe-fs-s3-request": {
|
|
141
|
+
"types": "./dist/safe-fs/fs/s3/http/request-node.d.ts",
|
|
142
|
+
"workerd": "./dist/safe-fs/fs/s3/http/request-fetch.js",
|
|
143
|
+
"browser": "./dist/safe-fs/fs/s3/http/request-fetch.js",
|
|
144
|
+
"default": "./dist/safe-fs/fs/s3/http/request-node.js"
|
|
145
|
+
},
|
|
139
146
|
"#safe-fs-platform": {
|
|
140
147
|
"types": {
|
|
141
148
|
"workerd": "./dist/safe-fs/platform/browser.d.ts",
|