@mcsherrylabs/evolver-mcp 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/release.json +1 -1
- package/dist/application-signer.d.ts +16 -0
- package/dist/application-signer.d.ts.map +1 -0
- package/dist/application-signer.js +55 -0
- package/dist/application-signer.js.map +1 -0
- package/dist/approved-request.d.ts +30 -0
- package/dist/approved-request.d.ts.map +1 -0
- package/dist/approved-request.js +167 -0
- package/dist/approved-request.js.map +1 -0
- package/dist/attachment-tools.d.ts +3 -0
- package/dist/attachment-tools.d.ts.map +1 -0
- package/dist/attachment-tools.js +32 -0
- package/dist/attachment-tools.js.map +1 -0
- package/dist/attachments.d.ts +41 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +458 -0
- package/dist/attachments.js.map +1 -0
- package/dist/content-file-tools.d.ts +3 -0
- package/dist/content-file-tools.d.ts.map +1 -0
- package/dist/content-file-tools.js +67 -0
- package/dist/content-file-tools.js.map +1 -0
- package/dist/content-files.d.ts +32 -0
- package/dist/content-files.d.ts.map +1 -0
- package/dist/content-files.js +79 -0
- package/dist/content-files.js.map +1 -0
- package/dist/content-ranges.d.ts +41 -0
- package/dist/content-ranges.d.ts.map +1 -0
- package/dist/content-ranges.js +75 -0
- package/dist/content-ranges.js.map +1 -0
- package/dist/content.d.ts +62 -19
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +180 -25
- package/dist/content.js.map +1 -1
- package/dist/files.d.ts +60 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +355 -0
- package/dist/files.js.map +1 -0
- package/dist/group-crypto-tools.d.ts +3 -0
- package/dist/group-crypto-tools.d.ts.map +1 -0
- package/dist/group-crypto-tools.js +47 -0
- package/dist/group-crypto-tools.js.map +1 -0
- package/dist/group-crypto.d.ts +24 -0
- package/dist/group-crypto.d.ts.map +1 -0
- package/dist/group-crypto.js +135 -0
- package/dist/group-crypto.js.map +1 -0
- package/dist/group-key-provider.d.ts +30 -0
- package/dist/group-key-provider.d.ts.map +1 -0
- package/dist/group-key-provider.js +72 -0
- package/dist/group-key-provider.js.map +1 -0
- package/dist/group-keys.d.ts +34 -0
- package/dist/group-keys.d.ts.map +1 -0
- package/dist/group-keys.js +88 -0
- package/dist/group-keys.js.map +1 -0
- package/dist/group-ownership.d.ts +78 -0
- package/dist/group-ownership.d.ts.map +1 -0
- package/dist/group-ownership.js +324 -0
- package/dist/group-ownership.js.map +1 -0
- package/dist/group.d.ts +2 -2
- package/dist/group.js +3 -3
- package/dist/group.js.map +1 -1
- package/dist/http-profiles.d.ts +47 -0
- package/dist/http-profiles.d.ts.map +1 -0
- package/dist/http-profiles.js +161 -0
- package/dist/http-profiles.js.map +1 -0
- package/dist/human-approval.d.ts +14 -0
- package/dist/human-approval.d.ts.map +1 -0
- package/dist/human-approval.js +34 -0
- package/dist/human-approval.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +94 -40
- package/dist/index.js.map +1 -1
- package/dist/login-profiles.d.ts +25 -0
- package/dist/login-profiles.d.ts.map +1 -0
- package/dist/login-profiles.js +77 -0
- package/dist/login-profiles.js.map +1 -0
- package/dist/login-tools.d.ts +3 -0
- package/dist/login-tools.d.ts.map +1 -0
- package/dist/login-tools.js +27 -0
- package/dist/login-tools.js.map +1 -0
- package/dist/login.d.ts +36 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +144 -0
- package/dist/login.js.map +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +22 -6
- package/dist/mcp.js.map +1 -1
- package/dist/node-transport.d.ts +14 -0
- package/dist/node-transport.d.ts.map +1 -0
- package/dist/node-transport.js +54 -0
- package/dist/node-transport.js.map +1 -0
- package/dist/p2p-tools.d.ts +3 -0
- package/dist/p2p-tools.d.ts.map +1 -0
- package/dist/p2p-tools.js +52 -0
- package/dist/p2p-tools.js.map +1 -0
- package/dist/p2p.d.ts +67 -0
- package/dist/p2p.d.ts.map +1 -0
- package/dist/p2p.js +326 -0
- package/dist/p2p.js.map +1 -0
- package/dist/proof-attempts.d.ts +19 -0
- package/dist/proof-attempts.d.ts.map +1 -0
- package/dist/proof-attempts.js +30 -0
- package/dist/proof-attempts.js.map +1 -0
- package/dist/read.d.ts +60 -1
- package/dist/read.d.ts.map +1 -1
- package/dist/read.js +97 -5
- package/dist/read.js.map +1 -1
- package/dist/signed-http-tools.d.ts +3 -0
- package/dist/signed-http-tools.d.ts.map +1 -0
- package/dist/signed-http-tools.js +33 -0
- package/dist/signed-http-tools.js.map +1 -0
- package/dist/signed-http.d.ts +30 -0
- package/dist/signed-http.d.ts.map +1 -0
- package/dist/signed-http.js +114 -0
- package/dist/signed-http.js.map +1 -0
- package/dist/tools.d.ts +27 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +243 -32
- package/dist/tools.js.map +1 -1
- package/dist/transaction-interchange-tools.d.ts +3 -0
- package/dist/transaction-interchange-tools.d.ts.map +1 -0
- package/dist/transaction-interchange-tools.js +41 -0
- package/dist/transaction-interchange-tools.js.map +1 -0
- package/dist/transaction-interchange.d.ts +141 -0
- package/dist/transaction-interchange.d.ts.map +1 -0
- package/dist/transaction-interchange.js +502 -0
- package/dist/transaction-interchange.js.map +1 -0
- package/dist/transaction-policy.d.ts +32 -0
- package/dist/transaction-policy.d.ts.map +1 -0
- package/dist/transaction-policy.js +89 -0
- package/dist/transaction-policy.js.map +1 -0
- package/dist/write.d.ts +52 -13
- package/dist/write.d.ts.map +1 -1
- package/dist/write.js +95 -25
- package/dist/write.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// File-backed content is an explicit host-filesystem capability, never a model-context blob.
|
|
2
|
+
// The SDK still owns HTTP and streaming transport. The file layer owns byte bounds, incremental
|
|
3
|
+
// hashing and atomic output; neither this module nor the tools implement the node protocol.
|
|
4
|
+
import { parseAccess, OPEN_NOTE, RESTRICTED_NOTE } from "./content.js";
|
|
5
|
+
import { ContentHttpError } from "@mcsherrylabs/evolver-client";
|
|
6
|
+
import { FileAccessError, fileName } from "./files.js";
|
|
7
|
+
/** Deliberately do not echo exceptions/response text/host paths: a server can reflect content,
|
|
8
|
+
* and some TLS errors include certificate details. A completed HTTP refusal is not transport loss. */
|
|
9
|
+
export function contentFileFailure(e, url, operation) {
|
|
10
|
+
const aftermath = operation === "publish" ? "upload storage is unconfirmed" : "no completed output metadata returned";
|
|
11
|
+
if (e instanceof FileAccessError && e.code === "publication_unknown")
|
|
12
|
+
return { kind: "partial", what: operation, cause: "file_publication_unknown; target may exist or have been visible; this is NOT a no-write refusal. Do not retry or automatically delete the final target; operator inspection is required." +
|
|
13
|
+
(e.cleanupWarning ? " staging_cleanup_failed; protected staging may also remain." : "") };
|
|
14
|
+
if (e instanceof FileAccessError && e.code !== "transfer_failed")
|
|
15
|
+
return { kind: "partial", what: operation, cause: `file_${e.code}; ${aftermath}` +
|
|
16
|
+
(e.cleanupWarning ? "; staging_cleanup_failed; operator inspection of protected staging may be required" : "") };
|
|
17
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
18
|
+
return { kind: "partial", what: operation, cause: `cancelled; ${aftermath}` };
|
|
19
|
+
const message = e instanceof Error ? e.message : "";
|
|
20
|
+
const status = e instanceof ContentHttpError ? String(e.status) :
|
|
21
|
+
/^(?:postContent(?:Stream)?|getContentStream): HTTP ([1-5][0-9]{2})\b/.exec(message)?.[1];
|
|
22
|
+
if (status)
|
|
23
|
+
return { kind: "partial", what: operation,
|
|
24
|
+
cause: `content_http_${status}; not complete, not retried or republished open; ${aftermath}` };
|
|
25
|
+
return { kind: "unreachable", chain: url, cause: `content_transfer_failed; ${aftermath}` +
|
|
26
|
+
(e instanceof FileAccessError && e.cleanupWarning ? "; staging_cleanup_failed; operator inspection of protected staging may be required" : "") };
|
|
27
|
+
}
|
|
28
|
+
export const contentFileDisabled = () => ({ kind: "misconfigured", what: "file content access is disabled",
|
|
29
|
+
fix: "operator must opt in with EVOLVER_FILE_ROOT (dedicated server-local0700 directory) and reload; client roots do not authorize host access" });
|
|
30
|
+
export async function publishFile(deps, req, signal) {
|
|
31
|
+
// Share the INLINE ACL parser; do not trim/filter a malformed grant into open publication.
|
|
32
|
+
const access = parseAccess(req.access, req.authorizedPrincipals);
|
|
33
|
+
if ("kind" in access)
|
|
34
|
+
return access;
|
|
35
|
+
if (!deps)
|
|
36
|
+
return contentFileDisabled();
|
|
37
|
+
try {
|
|
38
|
+
const file = fileName(req.file);
|
|
39
|
+
return await deps.files.transfer(files => files.withInput(file, async (input) => {
|
|
40
|
+
const result = await deps.client.postContentStream(input.stream, { size: input.size, signal: files.signal,
|
|
41
|
+
...(access.mode === "restricted" ? { authorizedPrincipals: access.principals } : {}) });
|
|
42
|
+
const metadata = input.metadata();
|
|
43
|
+
if (result.size !== metadata.size || !/^[0-9a-f]{64}$/.test(result.key))
|
|
44
|
+
throw new FileAccessError("incomplete");
|
|
45
|
+
return { kind: "ok", value: { ...metadata, key: result.key, access: access.mode,
|
|
46
|
+
...(access.mode === "restricted" ? { authorizedPrincipals: access.principals } : {}),
|
|
47
|
+
note: access.mode === "restricted" ? RESTRICTED_NOTE : OPEN_NOTE } };
|
|
48
|
+
}), signal);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return contentFileFailure(e, deps.contentUrl, "publish");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function fetchFile(deps, req, signal) {
|
|
55
|
+
if (!deps)
|
|
56
|
+
return contentFileDisabled();
|
|
57
|
+
if (typeof req.key !== "string" || !/^[0-9a-f]{64}$/.test(req.key))
|
|
58
|
+
return { kind: "partial", what: "key", cause: "a content key must be exactly64 lowercase hex characters" };
|
|
59
|
+
const key = req.key;
|
|
60
|
+
try {
|
|
61
|
+
const file = fileName(req.file);
|
|
62
|
+
return await deps.files.transfer(async (files) => {
|
|
63
|
+
const result = await deps.client.getContentStream(key, { signal: files.signal });
|
|
64
|
+
if (result === null)
|
|
65
|
+
return { kind: "ok", value: null };
|
|
66
|
+
// A full-file tool never silently treats a partial response as the complete object.
|
|
67
|
+
if (result.status !== 200 || result.range !== null) {
|
|
68
|
+
await result.stream.cancel().catch(() => undefined);
|
|
69
|
+
throw new FileAccessError("incomplete");
|
|
70
|
+
}
|
|
71
|
+
const metadata = await files.writeNew(file, result.stream, result.size);
|
|
72
|
+
return { kind: "ok", value: { ...metadata, key } };
|
|
73
|
+
}, signal);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
return contentFileFailure(e, deps.contentUrl, "download");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=content-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-files.js","sourceRoot":"","sources":["../src/content-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FileContentDeps } from "./content-files.js";
|
|
2
|
+
import type { FileCleanupWarning } from "./files.js";
|
|
3
|
+
import type { ToolResult } from "./result.js";
|
|
4
|
+
export interface ByteInterval {
|
|
5
|
+
readonly from: number;
|
|
6
|
+
readonly toInclusive: number;
|
|
7
|
+
}
|
|
8
|
+
export type FileRangeResult = {
|
|
9
|
+
readonly status: "range";
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly file: string;
|
|
12
|
+
readonly size: number;
|
|
13
|
+
readonly sha256: string;
|
|
14
|
+
readonly hashScope: "served-range";
|
|
15
|
+
/** False refers to the ORIGINAL object, even if this interval happens to cover all of it. */
|
|
16
|
+
readonly complete: false;
|
|
17
|
+
readonly rangeComplete: true;
|
|
18
|
+
readonly fileComplete: true;
|
|
19
|
+
readonly requested: ByteInterval;
|
|
20
|
+
readonly served: ByteInterval & {
|
|
21
|
+
readonly totalSize: number;
|
|
22
|
+
};
|
|
23
|
+
readonly clipped: boolean;
|
|
24
|
+
readonly cleanupWarning?: FileCleanupWarning;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: "unsatisfiable";
|
|
27
|
+
readonly key: string;
|
|
28
|
+
readonly requested: ByteInterval;
|
|
29
|
+
readonly totalSize: number | null;
|
|
30
|
+
readonly complete: false;
|
|
31
|
+
readonly rangeComplete: false;
|
|
32
|
+
readonly fileComplete: false;
|
|
33
|
+
};
|
|
34
|
+
export declare function byteInterval(from: unknown, toInclusive: unknown, maxBytes: number): ByteInterval | ToolResult<never>;
|
|
35
|
+
export declare function fetchRangeFile(deps: FileContentDeps | undefined, req: {
|
|
36
|
+
key?: unknown;
|
|
37
|
+
file?: unknown;
|
|
38
|
+
from?: unknown;
|
|
39
|
+
toInclusive?: unknown;
|
|
40
|
+
}, signal?: AbortSignal): Promise<ToolResult<FileRangeResult | null>>;
|
|
41
|
+
//# sourceMappingURL=content-ranges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-ranges.d.ts","sourceRoot":"","sources":["../src/content-ranges.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,WAAW,YAAY;IAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE;AACrF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAC5F,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAC;IAC1D,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAC;CAC9C,GAAG;IACF,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACzF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC;CACvF,CAAC;AAGF,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC,KAAK,CAAC,CAOpH;AAWD,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,EAAE,GAAG,EAAE;IAC3E,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CACtE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC,CAyCpE"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Inclusive range semantics sit above the SDK's truthful HTTP metadata and reuse #11's
|
|
2
|
+
// exact-byte, atomic no-overwrite file boundary. No headers, HTTP request or wire encoder here.
|
|
3
|
+
import { contentFileDisabled, contentFileFailure } from "./content-files.js";
|
|
4
|
+
import { fileName } from "./files.js";
|
|
5
|
+
const nat = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
6
|
+
export function byteInterval(from, toInclusive, maxBytes) {
|
|
7
|
+
if (!nat(from) || !nat(toInclusive) || from > toInclusive)
|
|
8
|
+
return { kind: "partial", what: "range", cause: "invalid_range; inclusive bounds must be non-negative safe integers with from <= toInclusive" };
|
|
9
|
+
const count = toInclusive - from + 1;
|
|
10
|
+
if (!Number.isSafeInteger(count) || count > maxBytes)
|
|
11
|
+
return { kind: "partial", what: "range", cause: `range_too_large; requested inclusive interval must fit the ${maxBytes}-byte file bound` };
|
|
12
|
+
return { from, toInclusive };
|
|
13
|
+
}
|
|
14
|
+
/** SDK0.22 prerequisite: structured416, never regex-inferred from a transport exception.
|
|
15
|
+
* Unknown total stays null; malformed metadata is not an unsatisfiable interval result. */
|
|
16
|
+
function unsatisfiable(e) {
|
|
17
|
+
if (!(e instanceof Error) || e.name !== "ContentRangeError")
|
|
18
|
+
return undefined;
|
|
19
|
+
const value = e;
|
|
20
|
+
if (value.status !== 416 || (value.totalSize !== null && !nat(value.totalSize)))
|
|
21
|
+
return undefined;
|
|
22
|
+
return { totalSize: value.totalSize };
|
|
23
|
+
}
|
|
24
|
+
export async function fetchRangeFile(deps, req, signal) {
|
|
25
|
+
if (!deps)
|
|
26
|
+
return contentFileDisabled();
|
|
27
|
+
if (typeof req.key !== "string" || !/^[0-9a-f]{64}$/.test(req.key))
|
|
28
|
+
return { kind: "partial", what: "key", cause: "a content key must be exactly 64 lowercase hex characters" };
|
|
29
|
+
const key = req.key;
|
|
30
|
+
const requested = byteInterval(req.from, req.toInclusive, deps.files.maxBytes);
|
|
31
|
+
if ("kind" in requested)
|
|
32
|
+
return requested;
|
|
33
|
+
try {
|
|
34
|
+
const file = fileName(req.file);
|
|
35
|
+
return await deps.files.transfer(async (files) => {
|
|
36
|
+
let result;
|
|
37
|
+
try {
|
|
38
|
+
result = await deps.client.getContentStream(key, { range: requested, signal: files.signal });
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const refusal = unsatisfiable(e);
|
|
42
|
+
if (!refusal)
|
|
43
|
+
throw e;
|
|
44
|
+
return { kind: "ok", value: { status: "unsatisfiable", key, requested, totalSize: refusal.totalSize,
|
|
45
|
+
complete: false, rangeComplete: false, fileComplete: false } };
|
|
46
|
+
}
|
|
47
|
+
if (result === null)
|
|
48
|
+
return { kind: "ok", value: null };
|
|
49
|
+
if (result.status === 200) {
|
|
50
|
+
await result.stream.cancel().catch(() => undefined);
|
|
51
|
+
return { kind: "partial", what: "range", cause: "range_ignored; server returned a full object, discarded without writing or retry" };
|
|
52
|
+
}
|
|
53
|
+
const served = result.range;
|
|
54
|
+
// The SDK validates headers. Also refuse a collaborator lacking/corrupting that contract;
|
|
55
|
+
// a stream alone can never prove the node honored the requested range.
|
|
56
|
+
if (result.status !== 206 || !served || !nat(served.from) || !nat(served.toInclusive) ||
|
|
57
|
+
!nat(served.totalSize) || served.from > served.toInclusive || served.toInclusive >= served.totalSize ||
|
|
58
|
+
served.from !== requested.from || served.toInclusive !== Math.min(requested.toInclusive, served.totalSize - 1) ||
|
|
59
|
+
!nat(result.size) || result.size !== served.toInclusive - served.from + 1) {
|
|
60
|
+
await result.stream.cancel().catch(() => undefined);
|
|
61
|
+
return { kind: "partial", what: "range", cause: "range_metadata_invalid; validated SDK206/served interval/total/byte-count metadata is required; no output" };
|
|
62
|
+
}
|
|
63
|
+
const metadata = await files.writeNew(file, result.stream, result.size);
|
|
64
|
+
return { kind: "ok", value: { status: "range", key, file: metadata.file, size: metadata.size,
|
|
65
|
+
sha256: metadata.sha256, hashScope: "served-range", complete: false, rangeComplete: true, fileComplete: true,
|
|
66
|
+
requested, served: { from: served.from, toInclusive: served.toInclusive, totalSize: served.totalSize },
|
|
67
|
+
clipped: served.toInclusive !== requested.toInclusive,
|
|
68
|
+
...(metadata.cleanupWarning ? { cleanupWarning: metadata.cleanupWarning } : {}) } };
|
|
69
|
+
}, signal);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return contentFileFailure(e, deps.contentUrl, "range");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=content-ranges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-ranges.js","sourceRoot":"","sources":["../src/content-ranges.ts"],"names":[],"mappings":"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"}
|
package/dist/content.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { ToolResult } from "./result.js";
|
|
2
|
+
/** Inline bound on ORIGINAL (decoded) bytes, for both PUT and GET: 256 KiB. */
|
|
3
|
+
export declare const CONTENT_MAX_BYTES = 262144;
|
|
4
|
+
/** Longest canonical base64 accepted/returned for `CONTENT_MAX_BYTES` bytes (349 528 chars). */
|
|
5
|
+
export declare const CONTENT_MAX_BASE64_CHARS: number;
|
|
6
|
+
/** Most principal names one restricted publication may grant. */
|
|
7
|
+
export declare const CONTENT_MAX_PRINCIPALS = 32;
|
|
2
8
|
/** Only what the content verbs need from a client. `NodeClient` satisfies it structurally, which
|
|
3
9
|
* is what lets the rules be exercised against a fake — a rule only checked against a live node
|
|
4
|
-
* is a rule that drifts.
|
|
10
|
+
* is a rule that drifts. The GET goes through the stream so the declared size is checked BEFORE
|
|
11
|
+
* any body is buffered (the SDK's `getContent` buffers an object of any size). */
|
|
5
12
|
export interface ContentClient {
|
|
6
13
|
postContent(bytes: Uint8Array, opts?: {
|
|
7
14
|
authorizedPrincipals?: string[];
|
|
@@ -9,7 +16,10 @@ export interface ContentClient {
|
|
|
9
16
|
key: string;
|
|
10
17
|
size: number;
|
|
11
18
|
}>;
|
|
12
|
-
|
|
19
|
+
getContentStream(key: string): Promise<{
|
|
20
|
+
stream: ReadableStream<Uint8Array>;
|
|
21
|
+
size: number;
|
|
22
|
+
} | null>;
|
|
13
23
|
}
|
|
14
24
|
/** What the content verbs need from the world: the SEPARATE content client (built against
|
|
15
25
|
* `contentBaseUrl`, never the chain client) plus that content endpoint name for refusals.
|
|
@@ -19,33 +29,66 @@ export interface ContentDeps {
|
|
|
19
29
|
readonly client: ContentClient;
|
|
20
30
|
readonly contentUrl: string;
|
|
21
31
|
}
|
|
22
|
-
/** What a
|
|
23
|
-
|
|
32
|
+
/** What a publish is asked to do. Raw tool parameters: every field is validated here, never coerced. */
|
|
33
|
+
export interface PublishRequest {
|
|
34
|
+
readonly access?: unknown;
|
|
35
|
+
readonly authorizedPrincipals?: unknown;
|
|
36
|
+
readonly text?: unknown;
|
|
37
|
+
readonly base64?: unknown;
|
|
38
|
+
}
|
|
39
|
+
/** What a successful publish answers: the node's key, the ORIGINAL byte size, a locally-computed
|
|
40
|
+
* sha256 over those bytes, and the access actually requested. No `txId` — content has nothing to
|
|
41
|
+
* submit later. */
|
|
24
42
|
export interface Published {
|
|
25
43
|
readonly key: string;
|
|
26
44
|
readonly size: number;
|
|
27
45
|
readonly sha256: string;
|
|
46
|
+
readonly encoding: "text" | "base64";
|
|
47
|
+
readonly access: "open" | "restricted";
|
|
48
|
+
readonly authorizedPrincipals?: readonly string[];
|
|
49
|
+
readonly note: string;
|
|
28
50
|
}
|
|
29
|
-
/** What a successful fetch answers: the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
export
|
|
51
|
+
/** What a successful fetch answers: always the COMPLETE object (`complete: true`), its size and
|
|
52
|
+
* locally-recomputed sha256, and EXACTLY ONE of `text` (strict, lossless UTF-8) or `base64` (the
|
|
53
|
+
* actual fetched bytes). `null` is the DEFINITIVE "no reachable holder" negative (Pitfall 3). */
|
|
54
|
+
export type Fetched = {
|
|
33
55
|
readonly key: string;
|
|
34
56
|
readonly size: number;
|
|
35
57
|
readonly sha256: string;
|
|
58
|
+
readonly complete: true;
|
|
59
|
+
} & ({
|
|
60
|
+
readonly encoding: "text";
|
|
36
61
|
readonly text: string;
|
|
37
|
-
}
|
|
62
|
+
} | {
|
|
63
|
+
readonly encoding: "base64";
|
|
64
|
+
readonly base64: string;
|
|
65
|
+
});
|
|
38
66
|
/** SHA-256 of the given bytes, hex-encoded. Computed locally via Web Crypto — never trusted from
|
|
39
67
|
* the node (T-5-04). */
|
|
40
68
|
export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
export declare const OPEN_NOTE: string;
|
|
70
|
+
export declare const RESTRICTED_NOTE: string;
|
|
71
|
+
type Access = {
|
|
72
|
+
readonly mode: "open";
|
|
73
|
+
} | {
|
|
74
|
+
readonly mode: "restricted";
|
|
75
|
+
readonly principals: string[];
|
|
76
|
+
};
|
|
77
|
+
/** The access the caller DELIBERATELY asked for, or a refusal. Nothing is trimmed, filtered or
|
|
78
|
+
* defaulted: an entry the node would reinterpret (blank, whitespace, comma-joined) is refused,
|
|
79
|
+
* because the alternative is publishing to a set the caller did not name. */
|
|
80
|
+
export declare function parseAccess(access: unknown, grants: unknown): Access | ToolResult<never>;
|
|
81
|
+
/** Publish text or binary to the content cache (READ-11, #8). `access` must be stated; a
|
|
82
|
+
* restricted publication forwards its grants through the SDK's `authorizedPrincipals`. Every
|
|
83
|
+
* refusal happens BEFORE any call. A node answer that is not 201 (e.g. HTTP 400 for an unknown
|
|
84
|
+
* principal) is `partial publish` naming the node's answer — never `ok`, and never retried open;
|
|
85
|
+
* a transport throw is `unreachable`. */
|
|
86
|
+
export declare function publishBytes(deps: ContentDeps, req: PublishRequest): Promise<ToolResult<Published>>;
|
|
87
|
+
/** Fetch content from the cache by key (READ-10, #8), returning the COMPLETE object as strict
|
|
88
|
+
* UTF-8 `text` (default) or `base64` of the actual bytes. Empty/blank key or unknown encoding is
|
|
89
|
+
* refused locally BEFORE any call. `null` is the DEFINITIVE negative (Pitfall 3); a throw or a
|
|
90
|
+
* short/overlong transfer is `unreachable`. Over `CONTENT_MAX_BYTES`, or not valid UTF-8 in text
|
|
91
|
+
* mode, answers `partial` with NO content — never a preview, never a lossy decode. */
|
|
92
|
+
export declare function fetchBytes(deps: ContentDeps, key: string, encoding?: unknown): Promise<ToolResult<Fetched | null>>;
|
|
93
|
+
export {};
|
|
51
94
|
//# sourceMappingURL=content.d.ts.map
|
package/dist/content.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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"}
|
package/dist/content.js
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
// The content cache is a SECOND non-signing verb family, alongside `claimIdentity` (SIGN-08): it
|
|
2
|
-
// is a DIRECT node call through `NodeClient.postContent`/`
|
|
3
|
-
// key for the content it publishes or fetches — content is a loopback byte store on its OWN
|
|
2
|
+
// is a DIRECT node call through `NodeClient.postContent`/`getContentStream`, because the MCP holds
|
|
3
|
+
// no key for the content it publishes or fetches — content is a loopback byte store on its OWN
|
|
4
4
|
// listener (a different port from the chain API), and reaching that listener is the entire
|
|
5
5
|
// authorization. Nothing is recorded in the session stores, no `txId` is returned, and nothing
|
|
6
6
|
// reaches `evolver_submit`.
|
|
7
7
|
//
|
|
8
8
|
// The digest is NEVER trusted from the node: `sha256Hex` is computed locally over the exact
|
|
9
|
-
// bytes
|
|
10
|
-
// claims (a node-trusted digest would verify the
|
|
9
|
+
// ORIGINAL bytes (never a base64 transport string), so a reader verifies against what the
|
|
10
|
+
// PUBLISHER asserted, not against what the node claims (a node-trusted digest would verify the
|
|
11
|
+
// node against itself — T-5-04).
|
|
11
12
|
//
|
|
12
13
|
// A GET that returns `null` is a DEFINITIVE negative ("no reachable holder had it" — the node
|
|
13
14
|
// walked the overlay on a miss), NOT a throw: a throw is `unreachable` (transport failure), and
|
|
14
15
|
// the two are never collapsed (Pitfall 3).
|
|
16
|
+
//
|
|
17
|
+
// ⚠️ READ GRANTS (#8). `access` is REQUIRED and never inferred: "open" sends no grant, "restricted"
|
|
18
|
+
// sends `authorizedPrincipals` through the SDK's own grant header. Every malformed/blank/oversized
|
|
19
|
+
// grant list is refused BEFORE the node is asked, so an intended restriction can never become an
|
|
20
|
+
// open publish. The grant governs which authenticated remote peer NODE may read on the node's
|
|
21
|
+
// peer route — not the MCP signer, not the trusted local listener this module talks to — and it
|
|
22
|
+
// is a serving control, not encryption. Grants on identical bytes merge by union on the node, so a
|
|
23
|
+
// narrower re-publication cannot retract an earlier wider one.
|
|
24
|
+
//
|
|
25
|
+
// ⚠️ BOUNDS. Everything is inline in model context, so both directions are capped at
|
|
26
|
+
// `CONTENT_MAX_BYTES` of ORIGINAL bytes. An over-bound GET returns NO content: there is no preview
|
|
27
|
+
// and no truncated object reported as complete. Streaming/range reads are a follow-up.
|
|
28
|
+
import { ContentHttpError, isValidPrincipalName } from "@mcsherrylabs/evolver-client";
|
|
15
29
|
import { unreachableFrom } from "./result.js";
|
|
30
|
+
/** Inline bound on ORIGINAL (decoded) bytes, for both PUT and GET: 256 KiB. */
|
|
31
|
+
export const CONTENT_MAX_BYTES = 262_144;
|
|
32
|
+
/** Longest canonical base64 accepted/returned for `CONTENT_MAX_BYTES` bytes (349 528 chars). */
|
|
33
|
+
export const CONTENT_MAX_BASE64_CHARS = 4 * Math.ceil(CONTENT_MAX_BYTES / 3);
|
|
34
|
+
/** Most principal names one restricted publication may grant. */
|
|
35
|
+
export const CONTENT_MAX_PRINCIPALS = 32;
|
|
16
36
|
/** SHA-256 of the given bytes, hex-encoded. Computed locally via Web Crypto — never trusted from
|
|
17
37
|
* the node (T-5-04). */
|
|
18
38
|
export async function sha256Hex(bytes) {
|
|
@@ -21,40 +41,175 @@ export async function sha256Hex(bytes) {
|
|
|
21
41
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
22
42
|
.join("");
|
|
23
43
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
export const OPEN_NOTE = "OPEN: any peer node that can reach this node's content peer route and knows the key may read it. " +
|
|
45
|
+
"Not encryption; there is no retraction.";
|
|
46
|
+
export const RESTRICTED_NOTE = "RESTRICTED: remote reads are served only to authenticated peer NODES matching a granted identity, " +
|
|
47
|
+
"or currently in/owning a granted group. The trusted local content listener is NOT restricted, the " +
|
|
48
|
+
"storage operator and prior recipients are not excluded, and grants on identical bytes merge by union — " +
|
|
49
|
+
"an earlier open or wider publication is not narrowed. Not encryption, not an availability guarantee.";
|
|
50
|
+
/** Canonical, padded, standard-alphabet base64 — no whitespace, no URL-safe alphabet. */
|
|
51
|
+
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
52
|
+
/** A lone UTF-16 surrogate cannot be UTF-8 encoded losslessly (TextEncoder writes U+FFFD). */
|
|
53
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
54
|
+
/** The access the caller DELIBERATELY asked for, or a refusal. Nothing is trimmed, filtered or
|
|
55
|
+
* defaulted: an entry the node would reinterpret (blank, whitespace, comma-joined) is refused,
|
|
56
|
+
* because the alternative is publishing to a set the caller did not name. */
|
|
57
|
+
export function parseAccess(access, grants) {
|
|
58
|
+
if (access === "open") {
|
|
59
|
+
if (grants !== undefined)
|
|
60
|
+
return { kind: "partial", what: "authorizedPrincipals",
|
|
61
|
+
cause: 'access "open" takes no authorizedPrincipals — use access "restricted" to grant read access' };
|
|
62
|
+
return { mode: "open" };
|
|
63
|
+
}
|
|
64
|
+
if (access !== "restricted")
|
|
65
|
+
return { kind: "partial", what: "access",
|
|
66
|
+
cause: 'say access "open" (any peer with the key may read) or "restricted" (with authorizedPrincipals) — it is never inferred' };
|
|
67
|
+
if (!Array.isArray(grants) || grants.length === 0)
|
|
68
|
+
return { kind: "partial", what: "authorizedPrincipals",
|
|
69
|
+
cause: 'access "restricted" needs a non-empty list of identity/group names — nothing was published' };
|
|
70
|
+
if (grants.length > CONTENT_MAX_PRINCIPALS)
|
|
71
|
+
return { kind: "partial", what: "authorizedPrincipals",
|
|
72
|
+
cause: `at most ${CONTENT_MAX_PRINCIPALS} principals per publication — nothing was published` };
|
|
73
|
+
const principals = [];
|
|
74
|
+
for (const g of grants) {
|
|
75
|
+
if (typeof g !== "string" || !isValidPrincipalName(g))
|
|
76
|
+
return { kind: "partial", what: "authorizedPrincipals",
|
|
77
|
+
cause: `${JSON.stringify(g).slice(0, 80)} is not a principal name (1-128 of A-Z a-z 0-9 . _ -, no spaces or commas) — nothing was published` };
|
|
78
|
+
if (!principals.includes(g))
|
|
79
|
+
principals.push(g);
|
|
80
|
+
}
|
|
81
|
+
return { mode: "restricted", principals };
|
|
82
|
+
}
|
|
83
|
+
/** The ORIGINAL bytes to publish from exactly one of `text` / `base64`, or a refusal. */
|
|
84
|
+
function parsePayload(text, base64) {
|
|
85
|
+
if ((text === undefined) === (base64 === undefined))
|
|
86
|
+
return { kind: "partial", what: "content", cause: "give exactly one of `text` (UTF-8) or `base64` (binary)" };
|
|
87
|
+
if (text !== undefined) {
|
|
88
|
+
if (typeof text !== "string" || text.trim().length === 0)
|
|
89
|
+
return { kind: "partial", what: "text", cause: "publishing empty content is refused — write something first" };
|
|
90
|
+
if (LONE_SURROGATE.test(text))
|
|
91
|
+
return { kind: "partial", what: "text", cause: "text contains an unpaired surrogate that UTF-8 cannot carry losslessly — use `base64`" };
|
|
92
|
+
const bytes = new TextEncoder().encode(text);
|
|
93
|
+
if (bytes.length > CONTENT_MAX_BYTES)
|
|
94
|
+
return { kind: "partial", what: "text", cause: `${bytes.length} UTF-8 bytes exceeds the ${CONTENT_MAX_BYTES}-byte inline bound` };
|
|
95
|
+
return { bytes, encoding: "text" };
|
|
96
|
+
}
|
|
97
|
+
if (typeof base64 !== "string" || base64.length === 0)
|
|
98
|
+
return { kind: "partial", what: "base64", cause: "publishing empty content is refused — give non-empty base64" };
|
|
99
|
+
if (base64.length > CONTENT_MAX_BASE64_CHARS)
|
|
100
|
+
return { kind: "partial", what: "base64",
|
|
101
|
+
cause: `${base64.length} base64 characters exceeds the ${CONTENT_MAX_BASE64_CHARS}-character bound (${CONTENT_MAX_BYTES} bytes)` };
|
|
102
|
+
const buf = BASE64.test(base64) ? Buffer.from(base64, "base64") : undefined;
|
|
103
|
+
// Round-trip equality rejects non-zero pad bits, so exactly one string names each byte sequence.
|
|
104
|
+
if (buf === undefined || buf.toString("base64") !== base64)
|
|
105
|
+
return { kind: "partial", what: "base64",
|
|
106
|
+
cause: "not canonical standard base64 (A-Z a-z 0-9 + /, `=` padding, no whitespace or URL-safe alphabet)" };
|
|
107
|
+
if (buf.length > CONTENT_MAX_BYTES)
|
|
108
|
+
return { kind: "partial", what: "base64", cause: `${buf.length} bytes exceeds the ${CONTENT_MAX_BYTES}-byte inline bound` };
|
|
109
|
+
return { bytes: new Uint8Array(buf), encoding: "base64" };
|
|
110
|
+
}
|
|
111
|
+
/** Publish text or binary to the content cache (READ-11, #8). `access` must be stated; a
|
|
112
|
+
* restricted publication forwards its grants through the SDK's `authorizedPrincipals`. Every
|
|
113
|
+
* refusal happens BEFORE any call. A node answer that is not 201 (e.g. HTTP 400 for an unknown
|
|
114
|
+
* principal) is `partial publish` naming the node's answer — never `ok`, and never retried open;
|
|
115
|
+
* a transport throw is `unreachable`. */
|
|
116
|
+
export async function publishBytes(deps, req) {
|
|
117
|
+
const access = parseAccess(req.access, req.authorizedPrincipals);
|
|
118
|
+
if ("kind" in access)
|
|
119
|
+
return access;
|
|
120
|
+
const payload = parsePayload(req.text, req.base64);
|
|
121
|
+
if ("kind" in payload)
|
|
122
|
+
return payload;
|
|
123
|
+
const { bytes, encoding } = payload;
|
|
124
|
+
let r;
|
|
125
|
+
let sha256;
|
|
33
126
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
127
|
+
sha256 = await sha256Hex(bytes);
|
|
128
|
+
r = access.mode === "restricted"
|
|
129
|
+
? await deps.client.postContent(bytes, { authorizedPrincipals: access.principals })
|
|
130
|
+
: await deps.client.postContent(bytes);
|
|
37
131
|
}
|
|
38
132
|
catch (e) {
|
|
39
|
-
|
|
133
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
134
|
+
const status = e instanceof ContentHttpError ? String(e.status) : /^postContent(?:Stream)?: HTTP (\d{3})\b/.exec(message)?.[1];
|
|
135
|
+
if (status === undefined)
|
|
136
|
+
return unreachableFrom(e, deps.contentUrl);
|
|
137
|
+
return {
|
|
138
|
+
kind: "partial",
|
|
139
|
+
what: "publish",
|
|
140
|
+
cause: status.startsWith("4")
|
|
141
|
+
? `the content node refused it (${message}) — nothing was stored; an unknown principal name is answered HTTP 400. Not retried, never re-sent open.`
|
|
142
|
+
: `the content node answered ${message} — NOT a successful publication, and storage is unconfirmed. Not retried.`,
|
|
143
|
+
};
|
|
40
144
|
}
|
|
145
|
+
if (r.size !== bytes.length)
|
|
146
|
+
return { kind: "partial", what: "publish",
|
|
147
|
+
cause: `the node reported ${r.size} bytes for key ${r.key} but ${bytes.length} were sent — treat this publication as unverified` };
|
|
148
|
+
return {
|
|
149
|
+
kind: "ok",
|
|
150
|
+
value: access.mode === "restricted"
|
|
151
|
+
? { key: r.key, size: bytes.length, sha256, encoding, access: "restricted", authorizedPrincipals: access.principals, note: RESTRICTED_NOTE }
|
|
152
|
+
: { key: r.key, size: bytes.length, sha256, encoding, access: "open", note: OPEN_NOTE },
|
|
153
|
+
};
|
|
41
154
|
}
|
|
42
|
-
/** Fetch content from the cache by key
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `unreachable`.
|
|
46
|
-
|
|
155
|
+
/** Fetch content from the cache by key (READ-10, #8), returning the COMPLETE object as strict
|
|
156
|
+
* UTF-8 `text` (default) or `base64` of the actual bytes. Empty/blank key or unknown encoding is
|
|
157
|
+
* refused locally BEFORE any call. `null` is the DEFINITIVE negative (Pitfall 3); a throw or a
|
|
158
|
+
* short/overlong transfer is `unreachable`. Over `CONTENT_MAX_BYTES`, or not valid UTF-8 in text
|
|
159
|
+
* mode, answers `partial` with NO content — never a preview, never a lossy decode. */
|
|
160
|
+
export async function fetchBytes(deps, key, encoding = "text") {
|
|
47
161
|
const k = String(key ?? "").trim();
|
|
48
162
|
if (k.length === 0)
|
|
49
163
|
return { kind: "partial", what: "key", cause: "which content? a non-empty key is required" };
|
|
164
|
+
if (encoding !== "text" && encoding !== "base64")
|
|
165
|
+
return { kind: "partial", what: "encoding", cause: 'encoding is "text" (strict UTF-8) or "base64"' };
|
|
166
|
+
let bytes;
|
|
50
167
|
try {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
168
|
+
const got = await deps.client.getContentStream(k);
|
|
169
|
+
if (got === null)
|
|
53
170
|
return { kind: "ok", value: null };
|
|
54
|
-
|
|
171
|
+
if (got.size > CONTENT_MAX_BYTES) {
|
|
172
|
+
await got.stream.cancel().catch(() => undefined);
|
|
173
|
+
return { kind: "partial", what: "content",
|
|
174
|
+
cause: `the object is ${got.size} bytes, over the ${CONTENT_MAX_BYTES}-byte inline bound — nothing returned (no preview); streaming/range reads are not exposed` };
|
|
175
|
+
}
|
|
176
|
+
const reader = got.stream.getReader();
|
|
177
|
+
const parts = [];
|
|
178
|
+
let total = 0;
|
|
179
|
+
for (;;) {
|
|
180
|
+
const { done, value } = await reader.read();
|
|
181
|
+
if (done)
|
|
182
|
+
break;
|
|
183
|
+
total += value.length;
|
|
184
|
+
if (total > got.size) {
|
|
185
|
+
await reader.cancel().catch(() => undefined);
|
|
186
|
+
throw new Error(`content transfer overran its declared ${got.size} bytes — discarded`);
|
|
187
|
+
}
|
|
188
|
+
parts.push(value);
|
|
189
|
+
}
|
|
190
|
+
// The SDK errors the stream on a short read, so reaching here means exactly `got.size` bytes.
|
|
191
|
+
bytes = new Uint8Array(total);
|
|
192
|
+
let off = 0;
|
|
193
|
+
for (const p of parts) {
|
|
194
|
+
bytes.set(p, off);
|
|
195
|
+
off += p.length;
|
|
196
|
+
}
|
|
55
197
|
}
|
|
56
198
|
catch (e) {
|
|
57
199
|
return unreachableFrom(e, deps.contentUrl);
|
|
58
200
|
}
|
|
201
|
+
const sha256 = await sha256Hex(bytes);
|
|
202
|
+
if (encoding === "base64")
|
|
203
|
+
return { kind: "ok", value: { key: k, size: bytes.length, sha256, complete: true, encoding: "base64", base64: Buffer.from(bytes).toString("base64") } };
|
|
204
|
+
let text;
|
|
205
|
+
try {
|
|
206
|
+
// fatal: refuse rather than substitute U+FFFD; ignoreBOM: keep a leading BOM as content.
|
|
207
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { kind: "partial", what: "text",
|
|
211
|
+
cause: `the object (${bytes.length} bytes, sha256 ${sha256}) is not valid UTF-8 — nothing lossy is returned; fetch with encoding "base64"` };
|
|
212
|
+
}
|
|
213
|
+
return { kind: "ok", value: { key: k, size: bytes.length, sha256, complete: true, encoding: "text", text } };
|
|
59
214
|
}
|
|
60
215
|
//# sourceMappingURL=content.js.map
|