@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.
Files changed (137) hide show
  1. package/deploy/release.json +1 -1
  2. package/dist/application-signer.d.ts +16 -0
  3. package/dist/application-signer.d.ts.map +1 -0
  4. package/dist/application-signer.js +55 -0
  5. package/dist/application-signer.js.map +1 -0
  6. package/dist/approved-request.d.ts +30 -0
  7. package/dist/approved-request.d.ts.map +1 -0
  8. package/dist/approved-request.js +167 -0
  9. package/dist/approved-request.js.map +1 -0
  10. package/dist/attachment-tools.d.ts +3 -0
  11. package/dist/attachment-tools.d.ts.map +1 -0
  12. package/dist/attachment-tools.js +32 -0
  13. package/dist/attachment-tools.js.map +1 -0
  14. package/dist/attachments.d.ts +41 -0
  15. package/dist/attachments.d.ts.map +1 -0
  16. package/dist/attachments.js +458 -0
  17. package/dist/attachments.js.map +1 -0
  18. package/dist/content-file-tools.d.ts +3 -0
  19. package/dist/content-file-tools.d.ts.map +1 -0
  20. package/dist/content-file-tools.js +67 -0
  21. package/dist/content-file-tools.js.map +1 -0
  22. package/dist/content-files.d.ts +32 -0
  23. package/dist/content-files.d.ts.map +1 -0
  24. package/dist/content-files.js +79 -0
  25. package/dist/content-files.js.map +1 -0
  26. package/dist/content-ranges.d.ts +41 -0
  27. package/dist/content-ranges.d.ts.map +1 -0
  28. package/dist/content-ranges.js +75 -0
  29. package/dist/content-ranges.js.map +1 -0
  30. package/dist/content.d.ts +62 -19
  31. package/dist/content.d.ts.map +1 -1
  32. package/dist/content.js +180 -25
  33. package/dist/content.js.map +1 -1
  34. package/dist/files.d.ts +60 -0
  35. package/dist/files.d.ts.map +1 -0
  36. package/dist/files.js +355 -0
  37. package/dist/files.js.map +1 -0
  38. package/dist/group-crypto-tools.d.ts +3 -0
  39. package/dist/group-crypto-tools.d.ts.map +1 -0
  40. package/dist/group-crypto-tools.js +47 -0
  41. package/dist/group-crypto-tools.js.map +1 -0
  42. package/dist/group-crypto.d.ts +24 -0
  43. package/dist/group-crypto.d.ts.map +1 -0
  44. package/dist/group-crypto.js +135 -0
  45. package/dist/group-crypto.js.map +1 -0
  46. package/dist/group-key-provider.d.ts +30 -0
  47. package/dist/group-key-provider.d.ts.map +1 -0
  48. package/dist/group-key-provider.js +72 -0
  49. package/dist/group-key-provider.js.map +1 -0
  50. package/dist/group-keys.d.ts +34 -0
  51. package/dist/group-keys.d.ts.map +1 -0
  52. package/dist/group-keys.js +88 -0
  53. package/dist/group-keys.js.map +1 -0
  54. package/dist/group-ownership.d.ts +78 -0
  55. package/dist/group-ownership.d.ts.map +1 -0
  56. package/dist/group-ownership.js +324 -0
  57. package/dist/group-ownership.js.map +1 -0
  58. package/dist/group.d.ts +2 -2
  59. package/dist/group.js +3 -3
  60. package/dist/group.js.map +1 -1
  61. package/dist/http-profiles.d.ts +47 -0
  62. package/dist/http-profiles.d.ts.map +1 -0
  63. package/dist/http-profiles.js +161 -0
  64. package/dist/http-profiles.js.map +1 -0
  65. package/dist/human-approval.d.ts +14 -0
  66. package/dist/human-approval.d.ts.map +1 -0
  67. package/dist/human-approval.js +34 -0
  68. package/dist/human-approval.js.map +1 -0
  69. package/dist/index.d.ts +11 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +94 -40
  72. package/dist/index.js.map +1 -1
  73. package/dist/login-profiles.d.ts +25 -0
  74. package/dist/login-profiles.d.ts.map +1 -0
  75. package/dist/login-profiles.js +77 -0
  76. package/dist/login-profiles.js.map +1 -0
  77. package/dist/login-tools.d.ts +3 -0
  78. package/dist/login-tools.d.ts.map +1 -0
  79. package/dist/login-tools.js +27 -0
  80. package/dist/login-tools.js.map +1 -0
  81. package/dist/login.d.ts +36 -0
  82. package/dist/login.d.ts.map +1 -0
  83. package/dist/login.js +144 -0
  84. package/dist/login.js.map +1 -0
  85. package/dist/mcp.d.ts +1 -0
  86. package/dist/mcp.d.ts.map +1 -1
  87. package/dist/mcp.js +22 -6
  88. package/dist/mcp.js.map +1 -1
  89. package/dist/node-transport.d.ts +14 -0
  90. package/dist/node-transport.d.ts.map +1 -0
  91. package/dist/node-transport.js +54 -0
  92. package/dist/node-transport.js.map +1 -0
  93. package/dist/p2p-tools.d.ts +3 -0
  94. package/dist/p2p-tools.d.ts.map +1 -0
  95. package/dist/p2p-tools.js +52 -0
  96. package/dist/p2p-tools.js.map +1 -0
  97. package/dist/p2p.d.ts +67 -0
  98. package/dist/p2p.d.ts.map +1 -0
  99. package/dist/p2p.js +326 -0
  100. package/dist/p2p.js.map +1 -0
  101. package/dist/proof-attempts.d.ts +19 -0
  102. package/dist/proof-attempts.d.ts.map +1 -0
  103. package/dist/proof-attempts.js +30 -0
  104. package/dist/proof-attempts.js.map +1 -0
  105. package/dist/read.d.ts +60 -1
  106. package/dist/read.d.ts.map +1 -1
  107. package/dist/read.js +97 -5
  108. package/dist/read.js.map +1 -1
  109. package/dist/signed-http-tools.d.ts +3 -0
  110. package/dist/signed-http-tools.d.ts.map +1 -0
  111. package/dist/signed-http-tools.js +33 -0
  112. package/dist/signed-http-tools.js.map +1 -0
  113. package/dist/signed-http.d.ts +30 -0
  114. package/dist/signed-http.d.ts.map +1 -0
  115. package/dist/signed-http.js +114 -0
  116. package/dist/signed-http.js.map +1 -0
  117. package/dist/tools.d.ts +27 -1
  118. package/dist/tools.d.ts.map +1 -1
  119. package/dist/tools.js +243 -32
  120. package/dist/tools.js.map +1 -1
  121. package/dist/transaction-interchange-tools.d.ts +3 -0
  122. package/dist/transaction-interchange-tools.d.ts.map +1 -0
  123. package/dist/transaction-interchange-tools.js +41 -0
  124. package/dist/transaction-interchange-tools.js.map +1 -0
  125. package/dist/transaction-interchange.d.ts +141 -0
  126. package/dist/transaction-interchange.d.ts.map +1 -0
  127. package/dist/transaction-interchange.js +502 -0
  128. package/dist/transaction-interchange.js.map +1 -0
  129. package/dist/transaction-policy.d.ts +32 -0
  130. package/dist/transaction-policy.d.ts.map +1 -0
  131. package/dist/transaction-policy.js +89 -0
  132. package/dist/transaction-policy.js.map +1 -0
  133. package/dist/write.d.ts +52 -13
  134. package/dist/write.d.ts.map +1 -1
  135. package/dist/write.js +95 -25
  136. package/dist/write.js.map +1 -1
  137. 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
- getContent(key: string): Promise<Uint8Array | null>;
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 successful publish answers: the node's key and size, plus a locally-computed sha256
23
- * over the exact published bytes. No `txId` — content has nothing to submit later. */
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 key, size, locally-recomputed sha256, and the bytes
30
- * decoded to UTF-8 `text`. The raw `bytes` field is DROPPED — never surfaced to the model
31
- * (Pitfall 5). `null` is the DEFINITIVE "no reachable holder" negative (Pitfall 3). */
32
- export interface Fetched {
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
- /** Publish UTF-8 text to the content cache (READ-11). OPEN-only publish for v1: no
42
- * `authorizedPrincipals` is exposed, closing the blank-grant-list footgun (Pitfall 4). Empty or
43
- * blank text is refused locally as `partial` BEFORE any call. Returns `{ key, size, sha256 }`
44
- * with the sha256 computed locally over the exact bytes published. */
45
- export declare function publishBytes(deps: ContentDeps, text: string): Promise<ToolResult<Published>>;
46
- /** Fetch content from the cache by key and decode it to text (READ-10). Empty/blank key refused
47
- * locally as `partial` BEFORE any call. A `null` from the SDK is the DEFINITIVE negative — no
48
- * reachable holder — surfaced as `ok { value: null }`, NOT `not_found` (Pitfall 3); a throw is
49
- * `unreachable`. Raw bytes are decoded to `text` and dropped (Pitfall 5). */
50
- export declare function fetchBytes(deps: ContentDeps, key: string): Promise<ToolResult<Fetched | null>>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;6BAE6B;AAC7B,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE;QAAE,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;CACrD;AAED;;;0CAG0C;AAC1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;uFACuF;AACvF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;wFAEwF;AACxF,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;yBACyB;AACzB,wBAAsB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAKlE;AAED;;;uEAGuE;AACvE,wBAAsB,YAAY,CAChC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAchC;AAED;;;8EAG8E;AAC9E,wBAAsB,UAAU,CAC9B,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAYrC"}
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`/`getContent`, because the MCP holds no
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, so a reader verifies against what the PUBLISHER asserted, not against what the node
10
- // claims (a node-trusted digest would verify the node against itself — T-5-04).
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
- /** Publish UTF-8 text to the content cache (READ-11). OPEN-only publish for v1: no
25
- * `authorizedPrincipals` is exposed, closing the blank-grant-list footgun (Pitfall 4). Empty or
26
- * blank text is refused locally as `partial` BEFORE any call. Returns `{ key, size, sha256 }`
27
- * with the sha256 computed locally over the exact bytes published. */
28
- export async function publishBytes(deps, text) {
29
- const textStr = String(text ?? "");
30
- if (textStr.trim().length === 0)
31
- return { kind: "partial", what: "text", cause: "publishing empty content is refused — write something first" };
32
- const bytes = new TextEncoder().encode(textStr);
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
- const sha256 = await sha256Hex(bytes);
35
- const r = await deps.client.postContent(bytes);
36
- return { kind: "ok", value: { key: r.key, size: r.size, sha256 } };
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
- return unreachableFrom(e, deps.contentUrl);
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 and decode it to text (READ-10). Empty/blank key refused
43
- * locally as `partial` BEFORE any call. A `null` from the SDK is the DEFINITIVE negative — no
44
- * reachable holder — surfaced as `ok { value: null }`, NOT `not_found` (Pitfall 3); a throw is
45
- * `unreachable`. Raw bytes are decoded to `text` and dropped (Pitfall 5). */
46
- export async function fetchBytes(deps, key) {
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 bytes = await deps.client.getContent(k);
52
- if (bytes === null)
168
+ const got = await deps.client.getContentStream(k);
169
+ if (got === null)
53
170
  return { kind: "ok", value: null };
54
- return { kind: "ok", value: { key: k, size: bytes.length, sha256: await sha256Hex(bytes), text: new TextDecoder().decode(bytes) } };
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