@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.
- package/AGENTS.md +43 -12
- package/CHANGELOG.md +524 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/advertised-schema.js +53 -0
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +449 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +194 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +152 -106
- package/dist/catalog.js +297 -38
- package/dist/concurrency.js +24 -23
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +330 -240
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +745 -282
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +145 -63
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +255 -212
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +281 -26
- package/dist/invocation.js +371 -286
- package/dist/meta-tools.js +213 -94
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +103 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +128 -3
- package/dist/program-source.js +321 -0
- package/dist/providers/notion.js +2 -2
- package/dist/providers/revenuecat.js +32 -5
- package/dist/providers/stripe.d.ts +3 -2
- package/dist/providers/vercel.js +1 -1
- package/dist/registry.d.ts +43 -7
- package/dist/registry.js +434 -288
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +74 -154
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +196 -163
- package/dist/routes/mcp.js +228 -112
- package/dist/routes/oauth-management.js +73 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +83 -0
- package/dist/routes/shared.d.ts +25 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +211 -0
- package/dist/routes/ui.js +76 -35
- package/dist/run-journal.js +347 -0
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +137 -89
- package/dist/skills.js +28 -9
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +39 -43
- package/dist/tool-safety.d.ts +27 -1
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +45 -7
- package/dist/ui.d.ts +25 -4
- package/dist/ui.js +77 -167
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +367 -16
- package/documentation/auth.md +163 -2
- package/documentation/code-mode.md +274 -56
- package/documentation/meta-tools.md +200 -44
- package/ethos.md +46 -47
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +224 -14
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +42 -7
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +21 -5
- package/package.json +16 -4
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -96
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -188
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- 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 (`:`, `	`, …).
|
|
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;
|