@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,140 @@
1
+ // The reference ArtifactStore over KVStorage.
2
+ //
3
+ // Layout, under one prefix (default `artifact:`):
4
+ //
5
+ // head:<id> the one mutable record per artifact
6
+ // ver:<id>:<stream>:<n, 10 digits> every version but each stream's latest
7
+ // blob:<sha256> bodies, unless a blob store holds them
8
+ // run:<id>:<startedAt ms>:<runId> refresh run history, newest 50
9
+ //
10
+ // The head is the only key ever compared-and-set, and a write commits by
11
+ // swapping it. Everything else is written before that swap and is either
12
+ // content-addressed (bodies) or immutable once the head that describes it
13
+ // exists (versions), so a plain `set` of the same key twice writes the same
14
+ // bytes twice.
15
+ import { ARTIFACT_RUNS_RETAINED, } from "./types.js";
16
+ const VERSION_DIGITS = 10;
17
+ const pad = (n, digits) => String(n).padStart(digits, "0");
18
+ /**
19
+ * An artifact store over `KVStorage`. The storage must provide `compareAndSet`
20
+ * — a write commits by swapping one head record, and a read followed by a
21
+ * write cannot stand in for that — and `list`, which history and the library
22
+ * page through. Cloudflare Workers KV has neither guarantee and is refused
23
+ * here, at construction, rather than losing a write later.
24
+ */
25
+ export function kvArtifactStore(kv, options = {}) {
26
+ const { compareAndSet, list } = kv;
27
+ if (typeof compareAndSet !== "function" || typeof list !== "function") {
28
+ throw new TypeError("kvArtifactStore needs storage with compareAndSet and list: every " +
29
+ "artifact write commits by compare-and-set, and Cloudflare Workers KV " +
30
+ "cannot offer one. Use the Worker example's D1 store (d1Storage), " +
31
+ "fileStorage from @zackbart/connecta/node, or memoryStorage in tests.");
32
+ }
33
+ const cas = compareAndSet.bind(kv);
34
+ const keys = list.bind(kv);
35
+ const prefix = options.prefix ?? "artifact:";
36
+ if (typeof prefix !== "string") {
37
+ throw new TypeError("kvArtifactStore: prefix must be a string");
38
+ }
39
+ const blobs = options.blobs;
40
+ const headKey = (id) => `${prefix}head:${id}`;
41
+ const versionPrefix = (id, stream) => `${prefix}ver:${id}:${stream}:`;
42
+ const runPrefix = (id) => `${prefix}run:${id}:`;
43
+ const blobKey = (key) => `${prefix}blob:${key}`;
44
+ const scanKey = `${prefix}refresh:scan-cursor`;
45
+ const readJson = async (key) => {
46
+ const text = await kv.get(key);
47
+ return text === null ? null : JSON.parse(text);
48
+ };
49
+ return {
50
+ async head(id) {
51
+ const token = await kv.get(headKey(id));
52
+ if (token === null)
53
+ return null;
54
+ return { head: JSON.parse(token), token };
55
+ },
56
+ async swapHead(id, expected, next) {
57
+ return cas(headKey(id), expected, JSON.stringify(next));
58
+ },
59
+ async heads({ after, limit }) {
60
+ const base = `${prefix}head:`;
61
+ const ids = (await keys(base))
62
+ .map((key) => key.slice(base.length))
63
+ .filter((id) => after === undefined || id > after);
64
+ const page = ids.slice(0, Math.max(0, limit));
65
+ const heads = [];
66
+ for (const [index, head] of (await Promise.all(page.map((id) => readJson(headKey(id))))).entries()) {
67
+ const id = page[index];
68
+ // A head listed and then gone is not an error: nothing deletes one,
69
+ // but an adapter's listing may lag its reads.
70
+ if (id !== undefined && head !== null)
71
+ heads.push({ id, head });
72
+ }
73
+ const last = page.at(-1);
74
+ return ids.length > page.length && last !== undefined
75
+ ? { heads, next: last }
76
+ : { heads };
77
+ },
78
+ async putBody(key, body) {
79
+ if (blobs)
80
+ await blobs.put(key, body);
81
+ else
82
+ await kv.set(blobKey(key), body);
83
+ },
84
+ async body(key) {
85
+ return blobs ? blobs.get(key) : kv.get(blobKey(key));
86
+ },
87
+ async putVersion(id, stream, version, record) {
88
+ await kv.set(`${versionPrefix(id, stream)}${pad(version, VERSION_DIGITS)}`, JSON.stringify(record));
89
+ },
90
+ async versions(id, stream, { below, limit }) {
91
+ const base = versionPrefix(id, stream);
92
+ if (limit <= 0)
93
+ return [];
94
+ let wanted;
95
+ if (below === undefined) {
96
+ // Versions below each stream's latest are dense, but the latest
97
+ // itself lives only in the head, so an unbounded read lists.
98
+ wanted = (await keys(base))
99
+ .map((key) => Number(key.slice(base.length)))
100
+ .filter(Number.isInteger)
101
+ .sort((a, b) => b - a)
102
+ .slice(0, limit);
103
+ }
104
+ else {
105
+ wanted = [];
106
+ for (let n = below - 1; n >= 1 && wanted.length < limit; n--) {
107
+ wanted.push(n);
108
+ }
109
+ }
110
+ const records = await Promise.all(wanted.map((n) => readJson(`${base}${pad(n, VERSION_DIGITS)}`)));
111
+ return records.filter((record) => record !== null);
112
+ },
113
+ async putRun(id, run) {
114
+ const started = Date.parse(run.startedAt);
115
+ const key = `${runPrefix(id)}${pad(Number.isFinite(started) ? started : 0, 15)}:${run.runId}`;
116
+ await kv.set(key, JSON.stringify(run));
117
+ const all = await keys(runPrefix(id));
118
+ const excess = all.length - ARTIFACT_RUNS_RETAINED;
119
+ if (excess > 0) {
120
+ await Promise.all(all.slice(0, excess).map((old) => kv.delete(old)));
121
+ }
122
+ },
123
+ async runs(id, limit) {
124
+ if (limit <= 0)
125
+ return [];
126
+ const newest = (await keys(runPrefix(id))).slice(-limit).reverse();
127
+ const runs = await Promise.all(newest.map((key) => readJson(key)));
128
+ return runs.filter((run) => run !== null);
129
+ },
130
+ async refreshScanCursor() {
131
+ return (await kv.get(scanKey)) ?? undefined;
132
+ },
133
+ async setRefreshScanCursor(after) {
134
+ if (after === undefined)
135
+ await kv.delete(scanKey);
136
+ else
137
+ await kv.set(scanKey, after);
138
+ },
139
+ };
140
+ }
@@ -0,0 +1,449 @@
1
+ // A CommonMark subset for Markdown artifacts: ATX headings, paragraphs,
2
+ // nested lists, fenced code, block quotes, thematic breaks, GFM tables,
3
+ // emphasis, strong, strikethrough, code spans, links, autolinks, images, and
4
+ // hard breaks. Raw HTML is text: a Markdown page carries no markup of its own,
5
+ // so everything it renders is escaped here, and the only URLs that survive are
6
+ // ones a sandboxed page can use.
7
+ //
8
+ // Deliberately not a dependency. The subset is what documents and reports
9
+ // use; the viewer's CSP is the security boundary either way, and a full
10
+ // CommonMark implementation would outweigh the rest of this module.
11
+ //
12
+ // Every scan is bounded: brackets and parentheses are matched once per span
13
+ // of text, a delimiter that found no closer is never searched for again, and
14
+ // nesting stops at a fixed depth. A megabyte of `[[[[` or `**` renders in
15
+ // linear time, because this runs on every page view.
16
+ const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;");
17
+ const PUNCTUATION = /^[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]$/;
18
+ const MAX_INLINE_DEPTH = 8;
19
+ const MAX_BLOCK_DEPTH = 64;
20
+ export class MarkdownNestingError extends Error {
21
+ constructor() {
22
+ super(`Markdown blocks are nested more than ${MAX_BLOCK_DEPTH} levels deep. Flatten the block quotes or lists.`);
23
+ }
24
+ }
25
+ /** A link target a sandboxed page can follow; anything else renders as text. */
26
+ function safeHref(url) {
27
+ const trimmed = url.trim();
28
+ if (trimmed.startsWith("#"))
29
+ return trimmed;
30
+ return /^(?:https:\/\/|mailto:)/i.test(trimmed) ? trimmed : undefined;
31
+ }
32
+ /** Only embedded raster or SVG images load: pages have no network. */
33
+ function safeImage(url) {
34
+ const trimmed = url.trim();
35
+ return /^data:image\/(?:png|gif|jpeg|webp|svg\+xml);base64,[A-Za-z0-9+/=]*$/i.test(trimmed)
36
+ ? trimmed
37
+ : undefined;
38
+ }
39
+ /** Matching close for every open bracket, computed in one pass. */
40
+ function matchPairs(text, open, close) {
41
+ const pairs = new Map();
42
+ const stack = [];
43
+ for (let i = 0; i < text.length; i++) {
44
+ const c = text[i];
45
+ if (c === "\\")
46
+ i++;
47
+ else if (c === open)
48
+ stack.push(i);
49
+ else if (c === close) {
50
+ const start = stack.pop();
51
+ if (start !== undefined)
52
+ pairs.set(start, i);
53
+ }
54
+ }
55
+ return pairs;
56
+ }
57
+ function inline(text, depth = 0) {
58
+ return renderSpan({
59
+ text,
60
+ brackets: matchPairs(text, "[", "]"),
61
+ parens: matchPairs(text, "(", ")"),
62
+ unclosed: new Map(),
63
+ depth,
64
+ });
65
+ }
66
+ function renderSpan(span) {
67
+ const { text } = span;
68
+ let out = "";
69
+ let i = 0;
70
+ const n = text.length;
71
+ while (i < n) {
72
+ const c = text[i] ?? "";
73
+ if (c === "\\" && i + 1 < n) {
74
+ const next = text[i + 1] ?? "";
75
+ if (next === "\n") {
76
+ out += "<br>\n";
77
+ i += 2;
78
+ continue;
79
+ }
80
+ if (PUNCTUATION.test(next)) {
81
+ out += escapeHtml(next);
82
+ i += 2;
83
+ continue;
84
+ }
85
+ }
86
+ if (c === "`") {
87
+ const code = codeSpan(span, i);
88
+ if (code) {
89
+ out += code.html;
90
+ i = code.end;
91
+ continue;
92
+ }
93
+ let run = 0;
94
+ while (text[i + run] === "`")
95
+ run++;
96
+ out += "`".repeat(run);
97
+ i += run;
98
+ continue;
99
+ }
100
+ if (c === "<") {
101
+ const auto = /^<((?:https|mailto):[^\s<>]*)>/i.exec(text.slice(i, i + 2048));
102
+ if (auto?.[1]) {
103
+ const href = safeHref(auto[1]);
104
+ out += href
105
+ ? `<a href="${escapeHtml(href)}">${escapeHtml(auto[1])}</a>`
106
+ : escapeHtml(auto[0]);
107
+ i += auto[0].length;
108
+ continue;
109
+ }
110
+ }
111
+ if (c === "!" && text[i + 1] === "[") {
112
+ const link = parseLink(span, i + 1);
113
+ if (link) {
114
+ const src = safeImage(link.url);
115
+ out += src
116
+ ? `<img src="${escapeHtml(src)}" alt="${escapeHtml(link.label)}"${link.title ? ` title="${escapeHtml(link.title)}"` : ""}>`
117
+ : escapeHtml(link.label);
118
+ i = link.end;
119
+ continue;
120
+ }
121
+ }
122
+ if (c === "[") {
123
+ const link = parseLink(span, i);
124
+ if (link) {
125
+ const href = safeHref(link.url);
126
+ const label = span.depth < MAX_INLINE_DEPTH ? inline(link.label, span.depth + 1) : escapeHtml(link.label);
127
+ out += href
128
+ ? `<a href="${escapeHtml(href)}"${link.title ? ` title="${escapeHtml(link.title)}"` : ""}>${label}</a>`
129
+ : label;
130
+ i = link.end;
131
+ continue;
132
+ }
133
+ }
134
+ if ((c === "*" || c === "_" || c === "~") && span.depth < MAX_INLINE_DEPTH) {
135
+ const emphasis = parseEmphasis(span, i);
136
+ if (emphasis) {
137
+ out += emphasis.html;
138
+ i = emphasis.end;
139
+ continue;
140
+ }
141
+ }
142
+ if (c === "\n") {
143
+ // Two trailing spaces make a hard break.
144
+ if (out.endsWith(" "))
145
+ out = `${out.replace(/ +$/, "")}<br>`;
146
+ out += "\n";
147
+ i++;
148
+ continue;
149
+ }
150
+ out += escapeHtml(c);
151
+ i++;
152
+ }
153
+ return out;
154
+ }
155
+ function codeSpan(span, start) {
156
+ const { text } = span;
157
+ let run = 0;
158
+ while (text[start + run] === "`")
159
+ run++;
160
+ const key = `\`${run}`;
161
+ const known = span.unclosed.get(key);
162
+ if (known !== undefined && known <= start)
163
+ return undefined;
164
+ // The first run of exactly `run` backticks after this one closes it.
165
+ let at = start + run;
166
+ for (;;) {
167
+ const found = text.indexOf("`", at);
168
+ if (found < 0) {
169
+ span.unclosed.set(key, start);
170
+ return undefined;
171
+ }
172
+ let length = 0;
173
+ while (text[found + length] === "`")
174
+ length++;
175
+ if (length === run) {
176
+ let code = text.slice(start + run, found).replace(/\n/g, " ");
177
+ if (code.length > 2 && code.startsWith(" ") && code.endsWith(" ") && code.trim()) {
178
+ code = code.slice(1, -1);
179
+ }
180
+ return { html: `<code>${escapeHtml(code)}</code>`, end: found + run };
181
+ }
182
+ at = found + length;
183
+ }
184
+ }
185
+ /** `[label](url "title")` whose `[` is at `start`. */
186
+ function parseLink(span, start) {
187
+ const { text } = span;
188
+ const close = span.brackets.get(start);
189
+ if (close === undefined || text[close + 1] !== "(")
190
+ return undefined;
191
+ const end = span.parens.get(close + 1);
192
+ if (end === undefined)
193
+ return undefined;
194
+ const inner = text.slice(close + 2, end).trim();
195
+ const titled = /^(\S+)\s+(?:"([^"]*)"|'([^']*)')$/.exec(inner);
196
+ const rawUrl = titled?.[1] ?? inner;
197
+ const url = rawUrl.startsWith("<") && rawUrl.endsWith(">") ? rawUrl.slice(1, -1) : rawUrl;
198
+ if (/\s/.test(url))
199
+ return undefined;
200
+ const label = text.slice(start + 1, close);
201
+ const title = titled?.[2] ?? titled?.[3];
202
+ return title === undefined
203
+ ? { label, url, end: end + 1 }
204
+ : { label, url, title, end: end + 1 };
205
+ }
206
+ /** `**strong**`, `__strong__`, `*em*`, `_em_`, `~~del~~` opening at `start`. */
207
+ function parseEmphasis(span, start) {
208
+ const { text } = span;
209
+ const c = text[start] ?? "";
210
+ let run = 0;
211
+ while (text[start + run] === c)
212
+ run++;
213
+ if (c === "~" && run !== 2)
214
+ return undefined;
215
+ const width = c === "~" ? 2 : Math.min(run, 2);
216
+ const delimiter = c.repeat(width);
217
+ const after = text[start + width];
218
+ if (after === undefined || /\s/.test(after))
219
+ return undefined;
220
+ // `_` never opens inside a word.
221
+ if (c === "_" && start > 0 && /[A-Za-z0-9]/.test(text[start - 1] ?? ""))
222
+ return undefined;
223
+ // Whether a position closes depends only on that position, so an opener
224
+ // that found no closer proves every later opener finds none either.
225
+ const known = span.unclosed.get(delimiter);
226
+ if (known !== undefined && known <= start)
227
+ return undefined;
228
+ let search = start + width + 1;
229
+ for (;;) {
230
+ const close = text.indexOf(delimiter, search);
231
+ if (close < 0) {
232
+ span.unclosed.set(delimiter, start);
233
+ return undefined;
234
+ }
235
+ const before = text[close - 1] ?? "";
236
+ const next = text[close + width] ?? "";
237
+ const closes = !/\s/.test(before) &&
238
+ !(c === "_" && /[A-Za-z0-9]/.test(next)) &&
239
+ // A single delimiter must not be half of a double.
240
+ (width === 2 || next !== c);
241
+ if (closes) {
242
+ const inner = inline(text.slice(start + width, close), span.depth + 1);
243
+ const tag = c === "~" ? "del" : width === 2 ? "strong" : "em";
244
+ return { html: `<${tag}>${inner}</${tag}>`, end: close + width };
245
+ }
246
+ search = close + 1;
247
+ }
248
+ }
249
+ const FENCE = /^( {0,3})(`{3,}|~{3,})[ \t]*([^`\s]*)[^`]*$/;
250
+ const HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
251
+ const RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
252
+ const QUOTE = /^ {0,3}> ?/;
253
+ const ITEM = /^( {0,3})([-*+]|\d{1,9}[.)])(?:[ \t]+(.*))?$/;
254
+ const TABLE_DELIMITER = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
255
+ function splitRow(line) {
256
+ let row = line.trim();
257
+ if (row.startsWith("|"))
258
+ row = row.slice(1);
259
+ if (row.endsWith("|") && !row.endsWith("\\|"))
260
+ row = row.slice(0, -1);
261
+ const cells = [];
262
+ let cell = "";
263
+ for (let i = 0; i < row.length; i++) {
264
+ if (row[i] === "\\" && row[i + 1] === "|") {
265
+ cell += "|";
266
+ i++;
267
+ }
268
+ else if (row[i] === "|") {
269
+ cells.push(cell.trim());
270
+ cell = "";
271
+ }
272
+ else
273
+ cell += row[i];
274
+ }
275
+ cells.push(cell.trim());
276
+ return cells;
277
+ }
278
+ const startsBlock = (line) => FENCE.test(line) ||
279
+ HEADING.test(line) ||
280
+ RULE.test(line) ||
281
+ QUOTE.test(line) ||
282
+ /^ {0,3}(?:[-*+]|1[.)])[ \t]+\S/.test(line);
283
+ const leadingSpaces = (line) => /^ */.exec(line)?.[0].length ?? 0;
284
+ const dedent = (line, width) => line.slice(Math.min(width, leadingSpaces(line)));
285
+ /** Render block-level Markdown. `tight` renders paragraphs bare, as in a tight list. */
286
+ function blocks(lines, tight = false, depth = 0) {
287
+ if (depth > MAX_BLOCK_DEPTH)
288
+ throw new MarkdownNestingError();
289
+ const out = [];
290
+ let i = 0;
291
+ while (i < lines.length) {
292
+ const line = lines[i] ?? "";
293
+ if (!line.trim()) {
294
+ i++;
295
+ continue;
296
+ }
297
+ const fence = FENCE.exec(line);
298
+ if (fence) {
299
+ const indent = fence[1]?.length ?? 0;
300
+ const marker = fence[2] ?? "```";
301
+ const lang = fence[3] ?? "";
302
+ const body = [];
303
+ i++;
304
+ while (i < lines.length) {
305
+ const candidate = lines[i] ?? "";
306
+ const closing = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(candidate)?.[1];
307
+ i++;
308
+ if (closing && closing[0] === marker[0] && closing.length >= marker.length)
309
+ break;
310
+ body.push(dedent(candidate, indent));
311
+ }
312
+ const cls = lang ? ` class="language-${escapeHtml(lang)}"` : "";
313
+ out.push(`<pre><code${cls}>${escapeHtml(body.join("\n"))}${body.length ? "\n" : ""}</code></pre>`);
314
+ continue;
315
+ }
316
+ const heading = HEADING.exec(line);
317
+ if (heading) {
318
+ const level = heading[1]?.length ?? 1;
319
+ out.push(`<h${level}>${inline((heading[2] ?? "").trim())}</h${level}>`);
320
+ i++;
321
+ continue;
322
+ }
323
+ if (RULE.test(line)) {
324
+ out.push("<hr>");
325
+ i++;
326
+ continue;
327
+ }
328
+ if (QUOTE.test(line)) {
329
+ const inner = [];
330
+ while (i < lines.length && QUOTE.test(lines[i] ?? "")) {
331
+ inner.push((lines[i] ?? "").replace(QUOTE, ""));
332
+ i++;
333
+ }
334
+ out.push(`<blockquote>\n${blocks(inner, false, depth + 1)}\n</blockquote>`);
335
+ continue;
336
+ }
337
+ const next = lines[i + 1] ?? "";
338
+ if (line.includes("|") && next.includes("-") && TABLE_DELIMITER.test(next)) {
339
+ const header = splitRow(line);
340
+ const aligns = splitRow(next).map((cell) => cell.startsWith(":") && cell.endsWith(":")
341
+ ? "center"
342
+ : cell.endsWith(":")
343
+ ? "right"
344
+ : cell.startsWith(":")
345
+ ? "left"
346
+ : "");
347
+ const cell = (tag, text, index) => {
348
+ const align = aligns[index];
349
+ return `<${tag}${align ? ` style="text-align:${align}"` : ""}>${inline(text)}</${tag}>`;
350
+ };
351
+ const rows = [];
352
+ i += 2;
353
+ while (i < lines.length && (lines[i] ?? "").trim() && (lines[i] ?? "").includes("|")) {
354
+ const cells = splitRow(lines[i] ?? "");
355
+ rows.push(`<tr>${header.map((_, index) => cell("td", cells[index] ?? "", index)).join("")}</tr>`);
356
+ i++;
357
+ }
358
+ out.push(`<table>\n<thead><tr>${header.map((text, index) => cell("th", text, index)).join("")}</tr></thead>` +
359
+ (rows.length ? `\n<tbody>\n${rows.join("\n")}\n</tbody>` : "") +
360
+ "\n</table>");
361
+ continue;
362
+ }
363
+ const item = ITEM.exec(line);
364
+ if (item) {
365
+ const ordered = /\d/.test(item[2] ?? "");
366
+ const bullet = (item[2] ?? "").slice(-1);
367
+ const items = [];
368
+ let loose = false;
369
+ let sawBlank = false;
370
+ while (i < lines.length) {
371
+ const match = ITEM.exec(lines[i] ?? "");
372
+ if (!match ||
373
+ /\d/.test(match[2] ?? "") !== ordered ||
374
+ (match[2] ?? "").slice(-1) !== bullet)
375
+ break;
376
+ if (sawBlank)
377
+ loose = true;
378
+ const contentIndent = (match[1]?.length ?? 0) + (match[2]?.length ?? 0) + 1;
379
+ const body = [match[3] ?? ""];
380
+ i++;
381
+ sawBlank = false;
382
+ while (i < lines.length) {
383
+ const continuation = lines[i] ?? "";
384
+ if (!continuation.trim()) {
385
+ sawBlank = true;
386
+ body.push("");
387
+ i++;
388
+ continue;
389
+ }
390
+ const nested = leadingSpaces(continuation) >= Math.min(contentIndent, 4);
391
+ const lazy = !sawBlank && !startsBlock(continuation) && !ITEM.test(continuation);
392
+ if (!nested && !lazy)
393
+ break;
394
+ if (sawBlank)
395
+ loose = true;
396
+ body.push(dedent(continuation, contentIndent));
397
+ sawBlank = false;
398
+ i++;
399
+ }
400
+ while (body.length && !(body.at(-1) ?? "").trim())
401
+ body.pop();
402
+ items.push(body);
403
+ }
404
+ const start = ordered ? Number.parseInt(item[2] ?? "1", 10) : 1;
405
+ const tag = ordered ? "ol" : "ul";
406
+ out.push(`<${tag}${ordered && start !== 1 ? ` start="${start}"` : ""}>\n` +
407
+ items.map((body) => `<li>${blocks(body, !loose, depth + 1)}</li>`).join("\n") +
408
+ `\n</${tag}>`);
409
+ continue;
410
+ }
411
+ const paragraph = [line.trim()];
412
+ i++;
413
+ while (i < lines.length) {
414
+ const candidate = lines[i] ?? "";
415
+ if (!candidate.trim() || startsBlock(candidate))
416
+ break;
417
+ paragraph.push(candidate.replace(/^ +/, ""));
418
+ i++;
419
+ }
420
+ const html = inline(paragraph.join("\n"));
421
+ out.push(tight ? html : `<p>${html}</p>`);
422
+ }
423
+ return out.join("\n");
424
+ }
425
+ /** Render Markdown source to an HTML fragment. */
426
+ export function renderMarkdown(source) {
427
+ const lines = source
428
+ .replace(/\r\n?/g, "\n")
429
+ .split("\n")
430
+ .map((line) => line.replace(/^\t+/, (tabs) => " ".repeat(tabs.length)));
431
+ return blocks(lines);
432
+ }
433
+ const MARKDOWN_STYLE = ":root{color-scheme:light dark}body{margin:0;font:16px/1.6 system-ui,-apple-system,'Segoe UI',sans-serif;" +
434
+ "color:#1f2328;background:#fff}main{max-width:820px;margin:0 auto;padding:40px 24px}" +
435
+ "h1,h2,h3,h4{line-height:1.25;margin:1.6em 0 .6em}h1{font-size:2em}h2{font-size:1.5em}" +
436
+ "a{color:#0969da}code{font:0.9em ui-monospace,SFMono-Regular,Menlo,monospace;background:#f3f4f6;padding:.1em .3em;border-radius:4px}" +
437
+ "pre{overflow:auto;padding:16px;background:#f3f4f6;border-radius:8px}pre code{background:none;padding:0}" +
438
+ "blockquote{margin:0;padding:0 1em;color:#59636e;border-left:4px solid #d1d9e0}" +
439
+ "table{border-collapse:collapse;display:block;overflow:auto}th,td{padding:6px 12px;border:1px solid #d1d9e0}" +
440
+ "img{max-width:100%}hr{border:0;border-top:1px solid #d1d9e0}" +
441
+ "@media (prefers-color-scheme:dark){body{color:#e6edf3;background:#0d1117}a{color:#4493f8}" +
442
+ "code,pre{background:#161b22}blockquote{color:#9198a1;border-color:#3d444d}th,td,hr{border-color:#3d444d}}";
443
+ /** A complete page for a Markdown artifact: the rendered body in `<main id="artifact-root">`. */
444
+ export function markdownPage(source, title) {
445
+ return (`<!doctype html><html lang="en"><head><meta charset="utf-8">` +
446
+ `<meta name="viewport" content="width=device-width,initial-scale=1">` +
447
+ `<title>${escapeHtml(title)}</title><style>${MARKDOWN_STYLE}</style></head>` +
448
+ `<body><main id="artifact-root">\n${renderMarkdown(source)}\n</main></body></html>`);
449
+ }
@@ -0,0 +1,37 @@
1
+ import type { ArtifactsModule } from "../module-contracts.js";
2
+ import { type ArtifactRenderCheck } from "./connector.js";
3
+ import { ArtifactRefreshService, type ArtifactRefreshRuntime, type RefreshOutcome } from "./refresh.js";
4
+ import type { ArtifactAllowlist, ArtifactLimits, ArtifactStore } from "./types.js";
5
+ export interface ArtifactsOptions {
6
+ /**
7
+ * Where artifacts live: `kvArtifactStore(storage)` over storage with
8
+ * `compareAndSet` — the Worker example's D1 store (optionally with R2 for
9
+ * bodies), `fileStorage` on Node, `memoryStorage` in tests.
10
+ */
11
+ store: ArtifactStore;
12
+ /**
13
+ * Origins pages may load scripts, stylesheets, and fonts from, each an exact
14
+ * `https:` origin. Every list defaults to empty; a deployment must opt in
15
+ * to each origin it trusts with page data. The viewer's CSP names only these.
16
+ */
17
+ allowlist?: Partial<ArtifactAllowlist>;
18
+ /** Tighten any limit below its default; none can be raised. */
19
+ limits?: Partial<ArtifactLimits>;
20
+ /**
21
+ * A real render check, such as a headless browser, run on
22
+ * `validate_artifact` and every write that changes what a page renders.
23
+ * Without one, static validation is the floor.
24
+ */
25
+ renderCheck?: ArtifactRenderCheck;
26
+ }
27
+ /**
28
+ * Team pages over stored data, reached by agents through the built-in
29
+ * `artifacts` connector. Pass the result as `createConnecta({ artifacts })`.
30
+ * Structural mistakes throw here, before a deployment boots.
31
+ */
32
+ export interface RefreshableArtifacts extends ArtifactsModule {
33
+ bindRefresh(runtime: ArtifactRefreshRuntime): void;
34
+ refresh(id: string, by: import("./types.js").ArtifactActor): Promise<RefreshOutcome>;
35
+ runDue(): ReturnType<ArtifactRefreshService["runDue"]>;
36
+ }
37
+ export declare function artifacts(options: ArtifactsOptions): RefreshableArtifacts;
@@ -0,0 +1,60 @@
1
+ // artifacts(): the typed module a deployment passes as `ConnectaConfig.artifacts`.
2
+ import { artifactsConnector } from "./connector.js";
3
+ import { ArtifactOperations } from "./operations.js";
4
+ import { artifactRoutes } from "./routes.js";
5
+ import { ArtifactRefreshService } from "./refresh.js";
6
+ import { resolveAllowlist, resolveLimits } from "./validate.js";
7
+ const STORE_METHODS = [
8
+ "head",
9
+ "swapHead",
10
+ "heads",
11
+ "putBody",
12
+ "body",
13
+ "putVersion",
14
+ "versions",
15
+ "putRun",
16
+ "runs",
17
+ "refreshScanCursor",
18
+ "setRefreshScanCursor",
19
+ ];
20
+ const OPTIONS = new Set(["store", "allowlist", "limits", "renderCheck"]);
21
+ export function artifacts(options) {
22
+ if (!options || typeof options !== "object") {
23
+ throw new TypeError("artifacts() needs options with a store");
24
+ }
25
+ for (const key of Object.keys(options)) {
26
+ if (!OPTIONS.has(key))
27
+ throw new TypeError(`artifacts(): unknown option "${key}"`);
28
+ }
29
+ const store = options.store;
30
+ if (!store ||
31
+ typeof store !== "object" ||
32
+ STORE_METHODS.some((method) => typeof store[method] !== "function")) {
33
+ throw new TypeError("artifacts(): store must be an ArtifactStore, e.g. kvArtifactStore(storage)");
34
+ }
35
+ if (options.renderCheck !== undefined && typeof options.renderCheck !== "function") {
36
+ throw new TypeError("artifacts(): renderCheck must be a function");
37
+ }
38
+ const allowlist = resolveAllowlist(options.allowlist);
39
+ const limits = resolveLimits(options.limits);
40
+ const operations = new ArtifactOperations({
41
+ store: options.store,
42
+ allowlist,
43
+ limits,
44
+ });
45
+ const refresh = new ArtifactRefreshService(operations);
46
+ const connector = artifactsConnector({
47
+ operations,
48
+ refresh,
49
+ allowlist,
50
+ limits,
51
+ ...(options.renderCheck ? { renderCheck: options.renderCheck } : {}),
52
+ });
53
+ return Object.freeze({
54
+ connector,
55
+ handle: artifactRoutes({ operations, allowlist }),
56
+ bindRefresh: (runtime) => refresh.bind(runtime),
57
+ refresh: (id, by) => refresh.run(id, { manual: by }),
58
+ runDue: () => refresh.runDue(),
59
+ });
60
+ }