@zackbart/connecta 0.24.4 → 0.26.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 (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
@@ -0,0 +1,76 @@
1
+ // The one document a page becomes: its source (or rendered Markdown) with the
2
+ // read-only `window.artifact` global injected, and the CSP it runs under.
3
+ //
4
+ // Built server-side, so a render check and the viewer see byte-for-byte the
5
+ // same page. The global goes in right after the doctype token — found by the
6
+ // tokenizer, never by a regex — so injecting it can never knock a page into
7
+ // quirks mode, and it runs before any script the author wrote.
8
+ import { scanHtml } from "./html-scan.js";
9
+ import { scriptSafeJson } from "./json.js";
10
+ import { markdownPage, MarkdownNestingError } from "./markdown.js";
11
+ /**
12
+ * The frame's Content-Security-Policy. `sandbox allow-scripts` without
13
+ * `allow-same-origin` gives the page an opaque origin — no cookies, no
14
+ * storage, nothing of the deployment's — and `connect-src 'none'` with every
15
+ * other fetch directive shut leaves it no network beyond the allowlisted
16
+ * script, style, and font origins. No `'unsafe-eval'`.
17
+ */
18
+ export function frameCsp(allowlist) {
19
+ const sources = (list) => (list.length ? ` ${list.join(" ")}` : "");
20
+ return [
21
+ "sandbox allow-scripts",
22
+ "default-src 'none'",
23
+ `script-src 'unsafe-inline'${sources(allowlist.scripts)}`,
24
+ `style-src 'unsafe-inline'${sources(allowlist.styles)}`,
25
+ `font-src data:${sources(allowlist.fonts)}`,
26
+ "img-src data: blob:",
27
+ "media-src data: blob:",
28
+ "connect-src 'none'",
29
+ "frame-src 'none'",
30
+ "worker-src 'none'",
31
+ "manifest-src 'none'",
32
+ "object-src 'none'",
33
+ "base-uri 'none'",
34
+ "form-action 'none'",
35
+ "frame-ancestors 'self'",
36
+ ].join("; ");
37
+ }
38
+ // Deep-frozen and non-configurable: the page reads its data and cannot
39
+ // replace it for a script that runs later.
40
+ const DEFINE_GLOBAL = '(()=>{const f=(o)=>{if(o!==null&&typeof o==="object"){for(const k of Object.keys(o))f(o[k]);Object.freeze(o)}return o};' +
41
+ 'Object.defineProperty(window,"artifact",{value:f(%VALUE%),writable:false,configurable:false,enumerable:false})})()';
42
+ /**
43
+ * The complete document a viewer or render check loads. `data` maps document
44
+ * names to their stored, script-safe JSON text, which is spliced in as-is.
45
+ */
46
+ export function buildFrameDocument(input) {
47
+ let page = input.source;
48
+ if (input.kind === "markdown") {
49
+ try {
50
+ page = markdownPage(input.source, input.global.title);
51
+ }
52
+ catch (error) {
53
+ if (!(error instanceof MarkdownNestingError))
54
+ throw error;
55
+ // Old or externally populated stores may hold a page that today's
56
+ // validator would reject. Render fixed text instead of throwing here.
57
+ page = markdownPage("This page has too many nested block quotes or lists to display.", input.global.title);
58
+ }
59
+ }
60
+ const meta = scriptSafeJson(input.global);
61
+ const data = Object.entries(input.data)
62
+ .map(([name, text]) => `${JSON.stringify(name)}:${text}`)
63
+ .join(",");
64
+ const value = `${meta.slice(0, -1)},"data":{${data}}}`;
65
+ const script = `<script>${DEFINE_GLOBAL.replace("%VALUE%", () => value)}</script>`;
66
+ let at = 0;
67
+ for (const token of scanHtml(page)) {
68
+ if (token.type === "doctype") {
69
+ at = token.end;
70
+ break;
71
+ }
72
+ if (token.type === "start")
73
+ break;
74
+ }
75
+ return `${page.slice(0, at)}${script}${page.slice(at)}`;
76
+ }
@@ -0,0 +1,140 @@
1
+ // The artifacts connector's usage guide: what an agent must know before its
2
+ // first write. Built once at construction with this deployment's allowlist
3
+ // and limits, so the numbers and origins it states are the ones enforced.
4
+ export const GUIDE_SUMMARY = "Publish team pages: validate first, keep data in documents, patch small edits, pass baseVersion, share the url.";
5
+ const size = (bytes) => bytes % (1024 * 1024) === 0
6
+ ? `${bytes / (1024 * 1024)} MiB`
7
+ : bytes % 1024 === 0
8
+ ? `${bytes / 1024} KiB`
9
+ : `${bytes.toLocaleString("en-US")} bytes`;
10
+ export function buildGuide(options) {
11
+ const { allowlist, limits } = options;
12
+ const origins = (list) => list.length ? list.map((origin) => `\`${origin}\``).join(", ") : "none";
13
+ return `# Artifacts
14
+
15
+ Artifacts are team pages that outlive a chat: dashboards, reports, plans. A page
16
+ is self-contained HTML or Markdown; the numbers it shows live in named JSON
17
+ **documents** that the page reads from a read-only global. Every write makes a
18
+ new immutable version, attributed to you, and any version can be rolled back.
19
+ Only people who can read this connector can open a page.
20
+
21
+ ## Writing a page
22
+
23
+ - **HTML** (\`kind: "html"\`): a complete document starting \`<!doctype html>\`,
24
+ with exactly one element \`id="artifact-root"\` (usually
25
+ \`<main id="artifact-root">\`) holding the content.
26
+ - **Markdown** (\`kind: "markdown"\`): headings, lists, tables, code, links, and
27
+ \`data:\` images. Raw HTML shows as text. Markdown pages cannot read data.
28
+ - **Data**: a script reads \`window.artifact.data.<name>\` — each document by
29
+ name, already parsed and frozen. \`window.artifact\` also has \`id\`, \`title\`,
30
+ \`view.version\`, \`documents.<name>.{version, updatedAt}\`, and \`snapshot\`.
31
+ Document names are identifiers: letters, digits, \`_\`.
32
+ - **Keep data out of the HTML.** Put every figure in a document and render it
33
+ from script; then fixing a number is \`set_documents\`, not a page rewrite,
34
+ and a refresh can replace the data without touching the view.
35
+
36
+ Pages run sandboxed in their own origin, so some things cannot work there:
37
+
38
+ - **No network.** \`fetch\`, XHR, WebSocket, and beacons fail. Read
39
+ \`window.artifact.data\` instead.
40
+ - **External scripts** load only from ${origins(allowlist.scripts)}. None are
41
+ allowed by default; an operator must opt in to each trusted origin. A script
42
+ can read this page's data. When allowed, use https and pin an exact version
43
+ (\`https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js\`).
44
+ Libraries that need \`eval\` or \`new Function\` do not run. Inline
45
+ \`<script>\` is fine.
46
+ - **External stylesheets** load only from ${origins(allowlist.styles)}; fonts
47
+ from ${origins(allowlist.fonts)} or \`data:\`. Both origin lists default to
48
+ empty. Trusted script, style, and font origins may receive page data in
49
+ request URLs. Inline \`<style>\` is fine.
50
+ - **Images** are \`data:\` URLs or inline SVG. Nothing relative: a page has no
51
+ files beside it.
52
+ - No \`<iframe>\`, \`<form>\`, \`<object>\`, \`<embed>\`, \`<base>\`, or
53
+ \`<meta http-equiv>\`. A link navigates the page's own frame and cannot open a
54
+ new window, so show URLs as text when readers need them elsewhere.
55
+
56
+ ## Tools
57
+
58
+ Reads run anywhere, including inside \`execute_code\`:
59
+
60
+ - \`list_artifacts { query?, includeArchived?, limit?, cursor? }\` — check for an
61
+ existing page before creating one.
62
+ - \`get_artifact { id, version?, includeSource? }\` — the view, its history, its
63
+ documents' versions, \`revision\`, \`url\`, and \`snapshotUrl\`.
64
+ - \`get_document { id, name, version? }\` — one document's value and history.
65
+ - \`get_refresh { id }\` — the configured program, schedule, freshness, and the
66
+ latest 20 run records. Logs and failure messages are for agents, not readers.
67
+ - \`validate_artifact { kind, source, documents? }\` — every check a save runs${options.renderCheck ? ", including this deployment's render check," : ""} without saving. **Validate before you save**; each error names a line and
68
+ the fix.
69
+
70
+ Writes make new versions and never ask for approval inside \`execute_code\`
71
+ (unless this deployment turned that off); each one spends the program's write
72
+ budget. At the top level, \`call_tool\` refuses them — use
73
+ \`call_destructive_tool\`.
74
+
75
+ - \`create_artifact { id, title, kind, source, documents? }\` — \`id\` is the
76
+ page's URL slug (lowercase letters, digits, hyphens). \`documents\` maps each
77
+ name straight to its value, \`{ "data": { "rows": [...] } }\`; supply
78
+ everything the first render needs in the same call.
79
+ - \`patch_artifact { id, baseVersion, edits: [{ find, replace }] }\` — **prefer
80
+ this for small edits** such as a title or a label. Each \`find\` must occur
81
+ exactly once in the current source (include surrounding text if it would
82
+ match twice); every edit applies at once, and if any fails nothing changes.
83
+ - \`update_artifact { id, baseVersion, source, title? }\` — replace the whole
84
+ source.
85
+ - \`set_documents { id, documents: { <name>: { baseVersion, value } } }\` — set
86
+ several documents atomically; \`baseVersion: 0\` creates one, \`value: null\`
87
+ removes one.
88
+ - \`set_refresh { id, document, program, schedule, baseVersion }\` — version a
89
+ complete \`execute_code\` async-arrow program returning the document's JSON
90
+ value. \`schedule\` is \`manual\`, \`daily\`, or \`weekly\`; use \`baseVersion: 0\`
91
+ for the first program, then the version from \`get_refresh\`. It can call
92
+ only explicitly read-only tools on shared connectors. Personal connectors
93
+ and writes fail the run even if the program catches the refusal. A failed
94
+ run keeps the last good document and marks the page stale. Each run rechecks
95
+ your current connector grants and pool access; removing permission to
96
+ configure refresh also stops your scheduled program.
97
+ - \`run_refresh { id }\` — trigger one run now through
98
+ \`call_destructive_tool\`. It cannot run inside \`execute_code\` because that
99
+ would hold an executor permit while asking for another.
100
+ - \`rollback_artifact { id, target: "view" | "document", name?, version, baseVersion }\`
101
+ — a new version with an old body; history is never rewritten.
102
+ - \`archive_artifact\` / \`restore_artifact { id, baseRevision }\` — hide a page
103
+ from the library, or bring it back. Archived pages refuse writes.
104
+
105
+ ## Versions and conflicts
106
+
107
+ Every write names what it expects to replace: \`baseVersion\` is the current
108
+ view version (or the document's version), \`baseRevision\` the artifact's
109
+ revision, all from \`get_artifact\`/\`get_document\`. If someone changed it
110
+ first, the write fails with code \`conflict\` and nothing is saved; the error's
111
+ \`current\` (\`err.details.current\` in a program) says where things stand.
112
+ Re-read, reapply your change, and retry with the new base. Never retry the
113
+ same call unchanged.
114
+
115
+ ## Sharing
116
+
117
+ \`url\` is the page as it is now; share that. \`snapshotUrl\` pins the view and
118
+ every document to their current versions, for "as of" links. Both are for the
119
+ team only.
120
+
121
+ ## Limits
122
+
123
+ | What | Limit |
124
+ | --- | --- |
125
+ | Page source | ${size(limits.sourceBytes)} |
126
+ | One document, serialized | ${size(limits.documentBytes)} |
127
+ | Documents per artifact | ${limits.documents} |
128
+ | Distinct document names over its lifetime | 64 (removed names keep their versions) |
129
+ | All documents together | ${size(limits.totalDocumentBytes)} |
130
+ | Page plus documents | ${size(limits.renderedBytes)} |
131
+ | Title | ${limits.titleChars} characters |
132
+ | Edits per patch | ${limits.patchEdits}, each \`find\` up to ${size(limits.findBytes)} |
133
+ | Document nesting | ${limits.jsonDepth} levels; numbers finite; no \`__proto__\` keys |
134
+
135
+ A program's host call returns at most 256 KiB on some executors: read a large
136
+ source with \`includeSource: false\` inside a program, or with \`call_tool\`.
137
+ Versions are kept forever, and a body written by a write that lost a conflict
138
+ stays stored unreferenced.
139
+ `;
140
+ }
@@ -0,0 +1,329 @@
1
+ // A small HTML tokenizer: exactly the HTML5 tokenizer states validation and
2
+ // the viewer need, and none of tree construction.
3
+ //
4
+ // It is a lint's tokenizer, not a sanitizer's. The viewer's CSP and sandbox are
5
+ // the security boundary; this finds the tags, attributes, and raw text a page
6
+ // author most plausibly got wrong, with a line and column for each, so an
7
+ // agent can fix a page from the message alone. Where it approximates the
8
+ // standard — foreign content, the rarer script escapes — the failure mode is a
9
+ // finding that is missing or extra, never a page that escapes its frame.
10
+ const RAW_TEXT = new Set([
11
+ "script",
12
+ "style",
13
+ "xmp",
14
+ "iframe",
15
+ "noembed",
16
+ "noframes",
17
+ "noscript",
18
+ "title",
19
+ "textarea",
20
+ ]);
21
+ const isSpace = (c) => c === " " || c === "\t" || c === "\n" || c === "\f" || c === "\r";
22
+ const isAlpha = (c) => c !== undefined && ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z"));
23
+ const NAMED = {
24
+ amp: "&",
25
+ lt: "<",
26
+ gt: ">",
27
+ quot: '"',
28
+ apos: "'",
29
+ nbsp: " ",
30
+ Tab: "\t",
31
+ NewLine: "\n",
32
+ colon: ":",
33
+ sol: "/",
34
+ lpar: "(",
35
+ rpar: ")",
36
+ period: ".",
37
+ comma: ",",
38
+ };
39
+ /**
40
+ * Decode the character references a URL check has to see through: numeric
41
+ * ones, and the named ones that spell a scheme (`&colon;`, `&Tab;`, …).
42
+ */
43
+ export function decodeEntities(value) {
44
+ if (!value.includes("&"))
45
+ return value;
46
+ return value.replace(/&(?:#(\d{1,8})|#[xX]([0-9a-fA-F]{1,7})|([A-Za-z]+));?/g, (match, dec, hex, name) => {
47
+ if (name !== undefined)
48
+ return NAMED[name] ?? match;
49
+ const code = dec !== undefined ? Number(dec) : Number.parseInt(hex ?? "", 16);
50
+ if (!Number.isFinite(code) || code === 0 || code > 0x10ffff)
51
+ return "�";
52
+ if (code >= 0xd800 && code <= 0xdfff)
53
+ return "�";
54
+ return String.fromCodePoint(code);
55
+ });
56
+ }
57
+ /** Tokenize `source`. Never throws; malformed markup tokenizes the way a browser would, approximately. */
58
+ export function scanHtml(source) {
59
+ const tokens = [];
60
+ // ASCII-only lowercasing keeps every offset valid; `toLowerCase` can change
61
+ // a string's length.
62
+ const lower = source.replace(/[A-Z]+/g, (run) => run.toLowerCase());
63
+ const n = source.length;
64
+ let i = 0;
65
+ let textStart = 0;
66
+ const flush = (end) => {
67
+ if (end > textStart)
68
+ tokens.push({ type: "text", start: textStart, end });
69
+ };
70
+ const until = (from, needle) => {
71
+ const at = source.indexOf(needle, from);
72
+ return at < 0 ? n : at + needle.length;
73
+ };
74
+ while (i < n) {
75
+ if (source[i] !== "<") {
76
+ i++;
77
+ continue;
78
+ }
79
+ const next = source[i + 1];
80
+ if (next === "!") {
81
+ flush(i);
82
+ const start = i;
83
+ if (source.startsWith("<!--", i)) {
84
+ if (source.startsWith("<!-->", i))
85
+ i += 5;
86
+ else if (source.startsWith("<!--->", i))
87
+ i += 6;
88
+ else
89
+ i = until(i + 4, "-->");
90
+ tokens.push({ type: "comment", start, end: i });
91
+ }
92
+ else if (source.slice(i + 2, i + 9).toLowerCase() === "doctype") {
93
+ i = until(i, ">");
94
+ tokens.push({ type: "doctype", start, end: i });
95
+ }
96
+ else if (source.startsWith("<![CDATA[", i)) {
97
+ i = until(i + 9, "]]>");
98
+ tokens.push({ type: "comment", start, end: i });
99
+ }
100
+ else {
101
+ i = until(i, ">");
102
+ tokens.push({ type: "comment", start, end: i });
103
+ }
104
+ textStart = i;
105
+ continue;
106
+ }
107
+ if (next === "?") {
108
+ flush(i);
109
+ const start = i;
110
+ i = until(i, ">");
111
+ tokens.push({ type: "comment", start, end: i });
112
+ textStart = i;
113
+ continue;
114
+ }
115
+ if (next === "/") {
116
+ const after = source[i + 2];
117
+ if (after === ">") {
118
+ // `</>` is dropped entirely.
119
+ flush(i);
120
+ i += 3;
121
+ textStart = i;
122
+ continue;
123
+ }
124
+ if (!isAlpha(after)) {
125
+ flush(i);
126
+ const start = i;
127
+ i = until(i, ">");
128
+ tokens.push({ type: "comment", start, end: i });
129
+ textStart = i;
130
+ continue;
131
+ }
132
+ const tag = readTag(source, i + 2);
133
+ if (!tag) {
134
+ // End of input inside a tag: the tag is never emitted.
135
+ flush(i);
136
+ i = n;
137
+ textStart = n;
138
+ break;
139
+ }
140
+ flush(i);
141
+ tokens.push({ type: "end", name: tag.name, start: i, end: tag.end });
142
+ i = tag.end;
143
+ textStart = i;
144
+ continue;
145
+ }
146
+ if (!isAlpha(next)) {
147
+ i++;
148
+ continue;
149
+ }
150
+ const tag = readTag(source, i + 1);
151
+ if (!tag) {
152
+ flush(i);
153
+ i = n;
154
+ textStart = n;
155
+ break;
156
+ }
157
+ flush(i);
158
+ tokens.push({
159
+ type: "start",
160
+ name: tag.name,
161
+ attrs: tag.attrs,
162
+ selfClosing: tag.selfClosing,
163
+ start: i,
164
+ end: tag.end,
165
+ });
166
+ i = tag.end;
167
+ textStart = i;
168
+ if (tag.name === "plaintext") {
169
+ if (n > i)
170
+ tokens.push({ type: "text", start: i, end: n, rawOf: "plaintext" });
171
+ i = n;
172
+ textStart = n;
173
+ break;
174
+ }
175
+ if (RAW_TEXT.has(tag.name)) {
176
+ const close = tag.name === "script"
177
+ ? scriptEnd(source, lower, i)
178
+ : rawTextEnd(source, lower, i, tag.name);
179
+ if (close > i) {
180
+ tokens.push({ type: "text", start: i, end: close, rawOf: tag.name });
181
+ }
182
+ i = close;
183
+ textStart = i;
184
+ }
185
+ }
186
+ flush(n);
187
+ return tokens;
188
+ }
189
+ /** Where `</name` followed by whitespace, `/`, or `>` starts, from `from`; the end of input when absent. */
190
+ function rawTextEnd(source, lower, from, name) {
191
+ let at = from;
192
+ for (;;) {
193
+ at = lower.indexOf(`</${name}`, at);
194
+ if (at < 0)
195
+ return source.length;
196
+ const c = source[at + 2 + name.length];
197
+ if (c === undefined || c === "/" || c === ">" || isSpace(c))
198
+ return at;
199
+ at += 2;
200
+ }
201
+ }
202
+ /**
203
+ * Script data, with its escaped and double-escaped states: inside `<!--`, a
204
+ * `<script` means the next `</script>` does not end the element.
205
+ */
206
+ function scriptEnd(source, lower, from) {
207
+ const tagAt = (at, prefix) => {
208
+ if (!lower.startsWith(prefix, at))
209
+ return false;
210
+ const c = source[at + prefix.length];
211
+ return c === undefined || c === "/" || c === ">" || isSpace(c);
212
+ };
213
+ let state = "data";
214
+ for (let i = from; i < source.length; i++) {
215
+ if (state === "data") {
216
+ if (tagAt(i, "</script"))
217
+ return i;
218
+ if (lower.startsWith("<!--", i)) {
219
+ state = "escaped";
220
+ i += 3;
221
+ }
222
+ }
223
+ else if (state === "escaped") {
224
+ if (tagAt(i, "</script"))
225
+ return i;
226
+ if (lower.startsWith("-->", i)) {
227
+ state = "data";
228
+ i += 2;
229
+ }
230
+ else if (tagAt(i, "<script")) {
231
+ state = "double";
232
+ i += 6;
233
+ }
234
+ }
235
+ else {
236
+ if (tagAt(i, "</script")) {
237
+ state = "escaped";
238
+ i += 7;
239
+ }
240
+ else if (lower.startsWith("-->", i)) {
241
+ state = "data";
242
+ i += 2;
243
+ }
244
+ }
245
+ }
246
+ return source.length;
247
+ }
248
+ /** Read a tag name and its attributes, from just after `<` or `</`. Null at end of input. */
249
+ function readTag(source, from) {
250
+ const n = source.length;
251
+ let i = from;
252
+ while (i < n && !isSpace(source[i]) && source[i] !== "/" && source[i] !== ">")
253
+ i++;
254
+ const name = source.slice(from, i).toLowerCase();
255
+ const attrs = [];
256
+ const seen = new Set();
257
+ let selfClosing = false;
258
+ for (;;) {
259
+ while (i < n && (isSpace(source[i]) || (source[i] === "/" && source[i + 1] !== ">")))
260
+ i++;
261
+ if (i >= n)
262
+ return null;
263
+ if (source[i] === ">")
264
+ return { name, attrs, selfClosing, end: i + 1 };
265
+ if (source[i] === "/") {
266
+ // "/>"
267
+ selfClosing = true;
268
+ return { name, attrs, selfClosing, end: i + 2 };
269
+ }
270
+ const nameStart = i;
271
+ // A leading "=" belongs to the name.
272
+ i++;
273
+ while (i < n &&
274
+ !isSpace(source[i]) &&
275
+ source[i] !== "/" &&
276
+ source[i] !== ">" &&
277
+ source[i] !== "=")
278
+ i++;
279
+ const attrName = source.slice(nameStart, i).toLowerCase();
280
+ let value = "";
281
+ let j = i;
282
+ while (j < n && isSpace(source[j]))
283
+ j++;
284
+ if (source[j] === "=") {
285
+ i = j + 1;
286
+ while (i < n && isSpace(source[i]))
287
+ i++;
288
+ const quote = source[i];
289
+ if (quote === '"' || quote === "'") {
290
+ const close = source.indexOf(quote, i + 1);
291
+ if (close < 0)
292
+ return null;
293
+ value = source.slice(i + 1, close);
294
+ i = close + 1;
295
+ }
296
+ else {
297
+ const valueStart = i;
298
+ while (i < n && !isSpace(source[i]) && source[i] !== ">")
299
+ i++;
300
+ value = source.slice(valueStart, i);
301
+ }
302
+ }
303
+ // A repeated attribute is dropped; the first one wins.
304
+ if (!seen.has(attrName)) {
305
+ seen.add(attrName);
306
+ attrs.push({ name: attrName, value: decodeEntities(value), offset: nameStart });
307
+ }
308
+ }
309
+ }
310
+ /** Map a source offset to a 1-based line and column. */
311
+ export function lineLocator(source) {
312
+ const starts = [0];
313
+ for (let i = 0; i < source.length; i++) {
314
+ if (source[i] === "\n")
315
+ starts.push(i + 1);
316
+ }
317
+ return (offset) => {
318
+ let lo = 0;
319
+ let hi = starts.length - 1;
320
+ while (lo < hi) {
321
+ const mid = (lo + hi + 1) >> 1;
322
+ if ((starts[mid] ?? 0) <= offset)
323
+ lo = mid;
324
+ else
325
+ hi = mid - 1;
326
+ }
327
+ return { line: lo + 1, column: offset - (starts[lo] ?? 0) + 1 };
328
+ };
329
+ }
@@ -0,0 +1,68 @@
1
+ // JSON as the artifacts module stores and injects it.
2
+ /**
3
+ * `JSON.stringify`, with `<`, U+2028, and U+2029 escaped. The result is the
4
+ * same JSON value, and it can sit inside a `<script>` element as a literal:
5
+ * no `</script>` or `<!--` can end or bend the element it lives in. Documents
6
+ * are stored in this form, so the bytes a limit counts are the bytes a page
7
+ * receives.
8
+ */
9
+ export function scriptSafeJson(value) {
10
+ return (JSON.stringify(value) ?? "null")
11
+ .replace(/</g, "\\u003c")
12
+ .replace(/[\u2028\u2029]/g, (c) => (c === "\u2028" ? "\\u2028" : "\\u2029"));
13
+ }
14
+ /**
15
+ * Why `value` cannot be a document, or undefined when it can: plain JSON with
16
+ * finite numbers, nested at most `maxDepth`, and no `__proto__` key — a page
17
+ * reads documents as JavaScript objects, where that key sets a prototype
18
+ * instead of holding data.
19
+ */
20
+ export function jsonProblem(value, maxDepth) {
21
+ const pointer = (path) => path.length
22
+ ? `/${path.map((part) => String(part).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`
23
+ : "/";
24
+ const stack = [
25
+ { value, path: [], depth: 0 },
26
+ ];
27
+ while (stack.length) {
28
+ const item = stack.pop();
29
+ if (!item)
30
+ break;
31
+ const current = item.value;
32
+ if (current === null || typeof current === "string" || typeof current === "boolean")
33
+ continue;
34
+ if (typeof current === "number") {
35
+ if (!Number.isFinite(current)) {
36
+ return `${pointer(item.path)} is ${String(current)}, which has no JSON form; use null or a string`;
37
+ }
38
+ continue;
39
+ }
40
+ if (typeof current !== "object") {
41
+ return `${pointer(item.path)} is ${current === undefined ? "undefined" : `a ${typeof current}`}, which has no JSON form`;
42
+ }
43
+ if (item.depth >= maxDepth) {
44
+ return `${pointer(item.path)} nests deeper than ${maxDepth} levels`;
45
+ }
46
+ if (Array.isArray(current)) {
47
+ for (let index = current.length - 1; index >= 0; index--) {
48
+ stack.push({ value: current[index], path: [...item.path, index], depth: item.depth + 1 });
49
+ }
50
+ continue;
51
+ }
52
+ const proto = Object.getPrototypeOf(current);
53
+ if (proto !== Object.prototype && proto !== null) {
54
+ return `${pointer(item.path)} is not a plain object`;
55
+ }
56
+ for (const key of Object.keys(current)) {
57
+ if (key === "__proto__") {
58
+ return `${pointer([...item.path, key])} is a "__proto__" key, which a page would read as a prototype; rename it`;
59
+ }
60
+ stack.push({
61
+ value: current[key],
62
+ path: [...item.path, key],
63
+ depth: item.depth + 1,
64
+ });
65
+ }
66
+ }
67
+ return undefined;
68
+ }
@@ -0,0 +1,20 @@
1
+ import type { KVStorage } from "../types.js";
2
+ import { type ArtifactBlobStore, type ArtifactStore } from "./types.js";
3
+ export interface KvArtifactStoreOptions {
4
+ /**
5
+ * Keep bodies here instead of in the key-value store — an R2 bucket on
6
+ * Workers. Heads, versions, and runs stay in the key-value store, which
7
+ * owns the compare-and-set.
8
+ */
9
+ blobs?: ArtifactBlobStore;
10
+ /** Key prefix for every record. Default `artifact:`. */
11
+ prefix?: string;
12
+ }
13
+ /**
14
+ * An artifact store over `KVStorage`. The storage must provide `compareAndSet`
15
+ * — a write commits by swapping one head record, and a read followed by a
16
+ * write cannot stand in for that — and `list`, which history and the library
17
+ * page through. Cloudflare Workers KV has neither guarantee and is refused
18
+ * here, at construction, rather than losing a write later.
19
+ */
20
+ export declare function kvArtifactStore(kv: KVStorage, options?: KvArtifactStoreOptions): ArtifactStore;