@zackbart/connecta 0.28.0 → 0.29.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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,48 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import type { ActivityStore } from "./activity.js";
3
+ import type { KVStorage } from "./types.js";
4
+ import { type SqlActivityOptions } from "./storage/sql.js";
5
+ export type SqliteActivityOptions = SqlActivityOptions;
6
+ /** A database path, or a `DatabaseSync` that `openSqlite` (or you) opened. */
7
+ export type SqliteDatabase = string | DatabaseSync;
8
+ /**
9
+ * Open (creating if needed) a SQLite database for connecta. The directory is
10
+ * created owner-only and the file is owner-only before SQLite first writes
11
+ * it, because it holds downstream OAuth state and sealed credentials; SQLite
12
+ * gives its `-wal` and `-shm` files the database file's mode. Write-ahead
13
+ * logging lets activity reads proceed beside writes, and a busy timeout lets a
14
+ * second process on the same file wait for a lock instead of failing.
15
+ *
16
+ * `":memory:"` opens a private in-memory database, for tests.
17
+ */
18
+ export declare function openSqlite(path: string): DatabaseSync;
19
+ /**
20
+ * `KVStorage` in a SQLite file, with the atomic `compareAndSet` every
21
+ * subsystem relies on. Each write commits one row; nothing rewrites the file.
22
+ */
23
+ export declare function sqliteStorage(database: SqliteDatabase): KVStorage;
24
+ /**
25
+ * Payload-free activity history in the same SQLite file, for
26
+ * `activityHistory({ store })`. Each write prunes a bounded batch of rows
27
+ * older than `retentionDays`.
28
+ */
29
+ export declare function sqliteActivityStore(database: SqliteDatabase, options?: SqliteActivityOptions): ActivityStore;
30
+ export interface StateFileImport {
31
+ /** Entries copied into the database. */
32
+ imported: number;
33
+ /** Entries left alone because the database already held their key. */
34
+ kept: number;
35
+ /** Entries whose TTL had already passed. */
36
+ expired: number;
37
+ }
38
+ /**
39
+ * One-shot migration from the 0.28 `fileStorage` JSON state file: copy every
40
+ * live entry, with its expiry, into the database's key-value table. Run it
41
+ * once, with the old deployment stopped, before the first start on SQLite.
42
+ *
43
+ * Keys the database already holds are kept, so a second run copies nothing
44
+ * twice and never overwrites state written since. The file is read, never
45
+ * changed; delete it once the deployment is verified. A file that is not a
46
+ * state file throws before anything is written.
47
+ */
48
+ export declare function importStateFile(database: SqliteDatabase, statePath: string, now?: number): StateFileImport;
package/dist/sqlite.js ADDED
@@ -0,0 +1,166 @@
1
+ // `@zackbart/connecta/sqlite`: connecta's storage on Node, in one SQLite file.
2
+ //
3
+ // Built on `node:sqlite` (Node 22.13 or later), so there is nothing to
4
+ // install. `sqliteStorage` is the deployment's `KVStorage` and
5
+ // `sqliteActivityStore` its activity history; both create their tables on
6
+ // first use. Node-only: nothing reachable from the root entry imports this.
7
+ import { chmodSync, closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
8
+ import { dirname, resolve } from "node:path";
9
+ import { DatabaseSync } from "node:sqlite";
10
+ import { validateStorageKey } from "./storage/keys.js";
11
+ import { KV_SCHEMA, sqlActivityStore, sqlStorage, } from "./storage/sql.js";
12
+ /** Milliseconds a write waits for another process's write lock. */
13
+ const BUSY_TIMEOUT_MS = 5_000;
14
+ /**
15
+ * Open (creating if needed) a SQLite database for connecta. The directory is
16
+ * created owner-only and the file is owner-only before SQLite first writes
17
+ * it, because it holds downstream OAuth state and sealed credentials; SQLite
18
+ * gives its `-wal` and `-shm` files the database file's mode. Write-ahead
19
+ * logging lets activity reads proceed beside writes, and a busy timeout lets a
20
+ * second process on the same file wait for a lock instead of failing.
21
+ *
22
+ * `":memory:"` opens a private in-memory database, for tests.
23
+ */
24
+ export function openSqlite(path) {
25
+ if (path !== ":memory:") {
26
+ path = resolve(path);
27
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
28
+ closeSync(openSync(path, "a", 0o600));
29
+ try {
30
+ chmodSync(path, 0o600);
31
+ }
32
+ catch {
33
+ // Non-POSIX filesystem — leave the mode as it is.
34
+ }
35
+ }
36
+ const db = new DatabaseSync(path);
37
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
38
+ if (path !== ":memory:")
39
+ db.exec("PRAGMA journal_mode = WAL");
40
+ return db;
41
+ }
42
+ const drivers = new WeakMap();
43
+ function sqliteDriver(database) {
44
+ const db = typeof database === "string" ? openSqlite(database) : database;
45
+ const existing = drivers.get(db);
46
+ if (existing)
47
+ return existing;
48
+ // The statement set is the fixed SQL in ./sql.ts, so the cache is bounded.
49
+ const statements = new Map();
50
+ const prepare = (statement) => {
51
+ let prepared = statements.get(statement.sql);
52
+ if (!prepared) {
53
+ prepared = db.prepare(statement.sql);
54
+ statements.set(statement.sql, prepared);
55
+ }
56
+ return prepared;
57
+ };
58
+ const run = (statement) => {
59
+ return Number(prepare(statement).run(...statement.params).changes);
60
+ };
61
+ const driver = {
62
+ async all(statement) {
63
+ return prepare(statement).all(...statement.params);
64
+ },
65
+ async run(statement) {
66
+ return run(statement);
67
+ },
68
+ async batch(batch) {
69
+ // Synchronous from BEGIN to COMMIT: no other call on this connection
70
+ // can interleave, and IMMEDIATE takes the write lock up front so
71
+ // another process cannot either.
72
+ db.exec("BEGIN IMMEDIATE");
73
+ try {
74
+ const changes = batch.map(run);
75
+ db.exec("COMMIT");
76
+ return changes;
77
+ }
78
+ catch (error) {
79
+ db.exec("ROLLBACK");
80
+ throw error;
81
+ }
82
+ },
83
+ };
84
+ drivers.set(db, driver);
85
+ return driver;
86
+ }
87
+ /**
88
+ * `KVStorage` in a SQLite file, with the atomic `compareAndSet` every
89
+ * subsystem relies on. Each write commits one row; nothing rewrites the file.
90
+ */
91
+ export function sqliteStorage(database) {
92
+ return sqlStorage(sqliteDriver(database), "sqlite");
93
+ }
94
+ /**
95
+ * Payload-free activity history in the same SQLite file, for
96
+ * `activityHistory({ store })`. Each write prunes a bounded batch of rows
97
+ * older than `retentionDays`.
98
+ */
99
+ export function sqliteActivityStore(database, options) {
100
+ return sqlActivityStore(sqliteDriver(database), "sqlite", options);
101
+ }
102
+ /**
103
+ * One-shot migration from the 0.28 `fileStorage` JSON state file: copy every
104
+ * live entry, with its expiry, into the database's key-value table. Run it
105
+ * once, with the old deployment stopped, before the first start on SQLite.
106
+ *
107
+ * Keys the database already holds are kept, so a second run copies nothing
108
+ * twice and never overwrites state written since. The file is read, never
109
+ * changed; delete it once the deployment is verified. A file that is not a
110
+ * state file throws before anything is written.
111
+ */
112
+ export function importStateFile(database, statePath, now = Date.now()) {
113
+ const text = readFileSync(statePath, "utf8");
114
+ // The file holds stored state (tokens, sealed credentials), and these
115
+ // errors reach the operator's terminal, so they name the file and an entry's
116
+ // position, never a key or the parser's account, which quotes the text.
117
+ let loaded;
118
+ try {
119
+ loaded = JSON.parse(text);
120
+ }
121
+ catch {
122
+ throw new TypeError(`${statePath} is not a connecta state file: it is not valid JSON`);
123
+ }
124
+ if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) {
125
+ throw new TypeError(`${statePath} is not a connecta state file`);
126
+ }
127
+ const entries = Object.entries(loaded);
128
+ for (const [index, [key, entry]] of entries.entries()) {
129
+ validateStorageKey(key);
130
+ const valid = entry !== null &&
131
+ typeof entry === "object" &&
132
+ typeof entry.value === "string" &&
133
+ (!("exp" in entry) || Number.isFinite(entry.exp));
134
+ if (!valid) {
135
+ throw new TypeError(`${statePath} is not a connecta state file: entry ${index + 1} has no string value`);
136
+ }
137
+ }
138
+ const db = typeof database === "string" ? openSqlite(database) : database;
139
+ const result = { imported: 0, kept: 0, expired: 0 };
140
+ db.exec("BEGIN IMMEDIATE");
141
+ try {
142
+ for (const statement of KV_SCHEMA)
143
+ db.exec(statement);
144
+ const insert = db.prepare(`INSERT INTO connecta_kv (key, value, expires_at_ms) VALUES (?, ?, ?)
145
+ ON CONFLICT (key) DO NOTHING`);
146
+ for (const [key, entry] of entries) {
147
+ const { value, exp } = entry;
148
+ if (exp !== undefined && exp <= now) {
149
+ result.expired += 1;
150
+ continue;
151
+ }
152
+ if (Number(insert.run(key, value, exp === undefined ? null : Math.ceil(exp)).changes) > 0) {
153
+ result.imported += 1;
154
+ }
155
+ else {
156
+ result.kept += 1;
157
+ }
158
+ }
159
+ db.exec("COMMIT");
160
+ }
161
+ catch (error) {
162
+ db.exec("ROLLBACK");
163
+ throw error;
164
+ }
165
+ return result;
166
+ }
@@ -0,0 +1,428 @@
1
+ // Every key connecta stores, in one module.
2
+ //
3
+ // A deployment has one KVStorage (D1 on Workers, SQLite on Node, memory in
4
+ // tests), and every subsystem shares it. Sharing is safe because no two
5
+ // families claim overlapping keys, and that is only checkable if every key is
6
+ // built here: `test/storage-keys.node.test.ts` fails when another source file
7
+ // spells one of these prefixes itself.
8
+ //
9
+ // Each family records:
10
+ //
11
+ // - scope: where its keys sit. `root` keys are physical. `partition` keys sit
12
+ // under a result partition (`results:`, or `subject:<key>:` for a scoped
13
+ // subject). `connector` keys sit under a connector's namespace
14
+ // (`conn:<id>:`), which a personal registry nests again under
15
+ // `principal:<key>:`.
16
+ // - version: the layout the keys and values follow. `in: "key"` families spell
17
+ // it in the key, so a new layout can be written beside the old one and
18
+ // migrated; `in: "value"` families carry it inside each record; `untagged`
19
+ // families predate tags, and their next layout change adds one. A version
20
+ // changes with a one-shot migration or with a family declared transient.
21
+ // - codec: how a value is written. `json` values are validated by the
22
+ // subsystem that reads them; nothing here trusts stored bytes.
23
+ // - ttl: `durable` (no expiry), `fixed` seconds, or `configured` by the
24
+ // deployment. Transient families can change layout freely, because their
25
+ // data expires within one TTL of a deploy.
26
+ /** Stored as given. */
27
+ const textCodec = {
28
+ name: "text",
29
+ encode: (value) => value,
30
+ decode: (raw) => raw,
31
+ };
32
+ /** JSON. Decoding returns `unknown`: the reader validates the shape. */
33
+ export const jsonCodec = {
34
+ name: "json",
35
+ encode: (value) => JSON.stringify(value),
36
+ decode: (raw) => JSON.parse(raw),
37
+ };
38
+ /** SQL TEXT keys must round-trip on every supported Node and D1 runtime. */
39
+ export function validateStorageKey(key) {
40
+ if (key.includes("\0")) {
41
+ throw new TypeError("Storage keys and list prefixes must not contain U+0000 (NUL)");
42
+ }
43
+ return key;
44
+ }
45
+ // --- scopes -------------------------------------------------------------
46
+ /** Namespaces core hands to the subsystems below. */
47
+ export const scopes = {
48
+ /** A personal registry's partition of the root store. */
49
+ principal: (principalKey) => validateStorageKey(`principal:${principalKey}:`),
50
+ /** One connector's namespace: its `ctx.storage`. */
51
+ connector: (connectorId) => validateStorageKey(`conn:${connectorId}:`),
52
+ /** The root result partition. */
53
+ results: "results:",
54
+ /** A scoped subject's result partition. */
55
+ subject: (subjectKey) => validateStorageKey(`subject:${subjectKey}:`),
56
+ };
57
+ // --- partition families ---------------------------------------------------
58
+ /** Seconds a stashed result stays readable through paging. */
59
+ export const RESULT_TTL_SECONDS = 900;
60
+ /**
61
+ * A stashed result: chunk 0 at `result:<id>`, chunk n at `result:<id>#<n>`.
62
+ * The value is the paging envelope `src/meta-tools.ts` writes and reads.
63
+ */
64
+ export const resultKeys = {
65
+ family: {
66
+ name: "result",
67
+ scope: "partition",
68
+ prefixes: ["result:"],
69
+ version: { number: 4, in: "value" },
70
+ codec: textCodec,
71
+ ttl: { kind: "fixed", seconds: RESULT_TTL_SECONDS },
72
+ durable: false,
73
+ },
74
+ chunk: (id, index) => validateStorageKey(index === 0 ? `result:${id}` : `result:${id}#${index}`),
75
+ };
76
+ // --- root families --------------------------------------------------------
77
+ /**
78
+ * The runtime-wide result stash ledger: every live stash entry's partition
79
+ * key, byte charge, and expiry. One record, swapped by compare-and-set, so the
80
+ * stash bounds hold across isolates and processes sharing the store.
81
+ */
82
+ export const stashLedgerKeys = {
83
+ family: {
84
+ name: "result-stash-ledger",
85
+ scope: "root",
86
+ prefixes: ["result-stash:v1:"],
87
+ version: { number: 1, in: "key" },
88
+ codec: jsonCodec,
89
+ // Durable as a record, self-pruning by content: an entry leaves the
90
+ // ledger once its result's TTL has passed.
91
+ ttl: { kind: "durable" },
92
+ durable: false,
93
+ },
94
+ ledger: "result-stash:v1:ledger",
95
+ };
96
+ /**
97
+ * A persisted downstream catalog: the manifest at `catalog:<id>`, then its
98
+ * chunks at `catalog:<id>:chunk:<revision>:<index>`.
99
+ */
100
+ export const catalogKeys = {
101
+ family: {
102
+ name: "catalog",
103
+ scope: "root",
104
+ prefixes: ["catalog:"],
105
+ // Retired v2/v3 listings are inventoried for migration; no runtime reads them.
106
+ version: { number: 3, in: "value" },
107
+ codec: textCodec,
108
+ ttl: {
109
+ kind: "configured",
110
+ by: "retired catalog layout; expires on its original TTL",
111
+ },
112
+ durable: false,
113
+ },
114
+ manifest: (connectorId) => validateStorageKey(`catalog:${connectorId}`),
115
+ chunk: (connectorId, revision, index) => validateStorageKey(`catalog:${connectorId}:chunk:${revision}:${index}`),
116
+ };
117
+ /** SDK catalog response cache, complete manifests published after their chunks. */
118
+ export const responseCacheKeys = {
119
+ family: {
120
+ name: "response-cache",
121
+ scope: "root",
122
+ prefixes: ["response-cache:v1:"],
123
+ version: { number: 1, in: "key" },
124
+ codec: textCodec,
125
+ ttl: {
126
+ kind: "configured",
127
+ by: "bounded downstream ttlMs; generations and hash refresh baselines expire after 48 hours",
128
+ },
129
+ durable: false,
130
+ },
131
+ prefix: (id) => validateStorageKey(`response-cache:v1:${id}:`),
132
+ generation: (id) => validateStorageKey(`response-cache:v1:${id}:generation`),
133
+ namespace: (id, config, generation) => validateStorageKey(`response-cache:v1:${id}:${config}:${generation}:`),
134
+ entry: (namespace, partition) => validateStorageKey(`${namespace}${partition}`),
135
+ refreshDigest: (id, config, partition) => validateStorageKey(`response-cache:v1:${id}:refresh-digest:${config}:${partition}`),
136
+ chunk: (entry, revision, index) => validateStorageKey(`${entry}:chunk:${revision}:${index}`),
137
+ };
138
+ /** Seconds an OAuth callback's owner binding stays claimable. */
139
+ export const OAUTH_HANDOFF_TTL_SECONDS = 15 * 60;
140
+ /** The principal that started an OAuth flow, by connector and state digest. */
141
+ export const oauthHandoffKeys = {
142
+ family: {
143
+ name: "oauth-handoff",
144
+ scope: "root",
145
+ prefixes: ["oauth-handoff:v1:"],
146
+ version: { number: 1, in: "key" },
147
+ codec: textCodec,
148
+ ttl: { kind: "fixed", seconds: OAUTH_HANDOFF_TTL_SECONDS },
149
+ durable: false,
150
+ },
151
+ handoff: (connectorId, stateHash) => validateStorageKey(`oauth-handoff:v1:${connectorId}:${stateHash}`),
152
+ };
153
+ /** Connecta-issued `cta_` access tokens: records, digest lookups, capacity. */
154
+ export const accessTokenKeys = {
155
+ family: {
156
+ name: "access-token",
157
+ scope: "root",
158
+ prefixes: ["access-token:v1:"],
159
+ version: { number: 1, in: "key" },
160
+ codec: jsonCodec,
161
+ ttl: { kind: "durable" },
162
+ durable: true,
163
+ },
164
+ recordPrefix: "access-token:v1:record:",
165
+ record: (id) => validateStorageKey(`access-token:v1:record:${id}`),
166
+ lookup: (tokenHash) => validateStorageKey(`access-token:v1:lookup:${tokenHash}`),
167
+ active: "access-token:v1:active",
168
+ };
169
+ /**
170
+ * A vault credential, sealed: `credential:v1` inside its connector's
171
+ * namespace, under the owner's principal partition when it is personal.
172
+ */
173
+ export const credentialKeys = {
174
+ family: {
175
+ name: "credential",
176
+ scope: "connector",
177
+ prefixes: ["credential:v1"],
178
+ version: { number: 1, in: "key" },
179
+ codec: jsonCodec,
180
+ ttl: { kind: "durable" },
181
+ durable: true,
182
+ },
183
+ credential: (connectorId, owner) => `${owner ? scopes.principal(owner) : ""}${scopes.connector(connectorId)}credential:v1`,
184
+ };
185
+ /** Seconds a Workers KV → D1 copy's resume point stays usable. */
186
+ export const KV_COPY_CURSOR_TTL_SECONDS = 7 * 24 * 60 * 60;
187
+ /**
188
+ * Where a one-shot Workers KV → D1 copy (`copyKvToD1` in `src/d1.ts`) resumes,
189
+ * held in D1 under a random token. Workers KV's own list cursor can spell the
190
+ * last key it listed, so the caller holds the token and never the cursor.
191
+ */
192
+ export const kvCopyKeys = {
193
+ family: {
194
+ name: "kv-copy",
195
+ scope: "root",
196
+ prefixes: ["kv-copy:v1:"],
197
+ version: { number: 1, in: "key" },
198
+ codec: textCodec,
199
+ ttl: { kind: "fixed", seconds: KV_COPY_CURSOR_TTL_SECONDS },
200
+ durable: false,
201
+ },
202
+ cursor: (token) => validateStorageKey(`kv-copy:v1:cursor:${token}`),
203
+ };
204
+ /** Once traffic uses D1, copying stale KV state is refused. */
205
+ export const kvCutoverKeys = {
206
+ family: {
207
+ name: "kv-cutover",
208
+ scope: "root",
209
+ prefixes: ["kv-cutover:v1:"],
210
+ version: { number: 1, in: "key" },
211
+ codec: textCodec,
212
+ ttl: { kind: "durable" },
213
+ durable: true,
214
+ },
215
+ source: (source) => validateStorageKey(`kv-cutover:v1:${encodeURIComponent(source)}`),
216
+ };
217
+ // --- connector families ---------------------------------------------------
218
+ /** Seconds a consent's flow record stays claimable, the same as its link. */
219
+ export const OAUTH_FLOW_TTL_SECONDS = 15 * 60;
220
+ /**
221
+ * Downstream OAuth's grant in a connector's namespace, layout 3: one record
222
+ * per owner at `oauth:grant`, holding the epoch, the latest consent's state
223
+ * digest, and the sealed client, tokens, and discovery. Every write is a
224
+ * compare-and-set against the exact record read, so a write a restart
225
+ * overtook cannot land.
226
+ */
227
+ export const oauthGrantKeys = {
228
+ family: {
229
+ name: "oauth-grant",
230
+ scope: "connector",
231
+ prefixes: ["oauth:grant"],
232
+ version: { number: 3, in: "value" },
233
+ codec: jsonCodec,
234
+ ttl: { kind: "durable" },
235
+ durable: true,
236
+ },
237
+ grant: "oauth:grant",
238
+ };
239
+ /**
240
+ * One consent: `oauth:flow:<sha256(state)>`, holding its epoch, consent URL,
241
+ * and sealed PKCE verifier. The callback claims it by compare-and-set before
242
+ * the code leaves, so consumption is recorded on that consent alone.
243
+ */
244
+ export const oauthFlowKeys = {
245
+ family: {
246
+ name: "oauth-flow",
247
+ scope: "connector",
248
+ prefixes: ["oauth:flow:"],
249
+ version: { number: 1, in: "value" },
250
+ codec: jsonCodec,
251
+ ttl: { kind: "fixed", seconds: OAUTH_FLOW_TTL_SECONDS },
252
+ durable: false,
253
+ },
254
+ prefix: "oauth:flow:",
255
+ flow: (stateDigest) => validateStorageKey(`oauth:flow:${stateDigest}`),
256
+ };
257
+ /** Shared-storage lifetime of a refresh holder, longer than its HTTP deadline. */
258
+ export const OAUTH_REFRESH_LEASE_SECONDS = 120;
259
+ export const oauthRefreshKeys = {
260
+ family: {
261
+ name: "oauth-refresh",
262
+ scope: "connector",
263
+ prefixes: ["oauth:refresh:"],
264
+ version: { number: 1, in: "value" },
265
+ codec: jsonCodec,
266
+ ttl: { kind: "durable" },
267
+ durable: true,
268
+ },
269
+ prefix: "oauth:refresh:",
270
+ lease: (epoch, tokenDigest) => validateStorageKey(`oauth:refresh:${epoch}:${tokenDigest}`),
271
+ };
272
+ /** One fingerprint's dispatch and resolution across epochs. Never expire or delete these records. */
273
+ export const oauthRefreshSpentKeys = {
274
+ family: {
275
+ name: "oauth-refresh-spent",
276
+ scope: "connector",
277
+ prefixes: ["oauth:refresh-spent:"],
278
+ version: { number: 1, in: "value" },
279
+ codec: jsonCodec,
280
+ ttl: { kind: "durable" },
281
+ durable: true,
282
+ },
283
+ spent: (tokenDigest) => validateStorageKey(`oauth:refresh-spent:${tokenDigest}`),
284
+ };
285
+ /** Expiry is storage-owned; dispatched fingerprints themselves never expire. */
286
+ export const oauthRefreshActiveKeys = {
287
+ family: {
288
+ name: "oauth-refresh-active",
289
+ scope: "connector",
290
+ prefixes: ["oauth:refresh-active:"],
291
+ version: { number: 1, in: "value" },
292
+ codec: jsonCodec,
293
+ ttl: { kind: "fixed", seconds: OAUTH_REFRESH_LEASE_SECONDS },
294
+ durable: false,
295
+ },
296
+ prefix: "oauth:refresh-active:",
297
+ holder: (epoch, holder) => validateStorageKey(`oauth:refresh-active:${epoch}:${holder}`),
298
+ };
299
+ /** The values layout 2 stored, each under its historical key. */
300
+ const oauthV2Field = {
301
+ client: "oauth:client",
302
+ tokens: "oauth:tokens",
303
+ pending: "oauth:pending",
304
+ verifier: "oauth:verifier",
305
+ state: "oauth:state",
306
+ discovery: "oauth:discovery",
307
+ };
308
+ /**
309
+ * Layout 2, read once to migrate it into a grant record and then deleted:
310
+ * each value at its field key, suffixed `:epoch:<generation>` once a modern
311
+ * generation owned it; the active generation at `oauth:generation`; a reset's
312
+ * cleanup lineage at `oauth:cleanup:` and `oauth:cleanup-at:`.
313
+ */
314
+ export const oauthV2Keys = {
315
+ family: {
316
+ name: "oauth-v2",
317
+ scope: "connector",
318
+ prefixes: [...Object.values(oauthV2Field), "oauth:generation", "oauth:cleanup:", "oauth:cleanup-at:"],
319
+ version: { number: 2, in: "value" },
320
+ codec: textCodec,
321
+ ttl: { kind: "durable" },
322
+ durable: true,
323
+ },
324
+ field: oauthV2Field,
325
+ /** One listing covers every layout 2 key; the reader keeps those above. */
326
+ scan: "oauth:",
327
+ /** A value's physical key; `epoch` null is the historical unsuffixed name. */
328
+ value: (key, epoch) => validateStorageKey(epoch === null ? key : `${key}:epoch:${epoch}`),
329
+ generation: "oauth:generation",
330
+ };
331
+ /** Single-use browser links (`used`/`started`/`failed`/`closed`) and MRTR rounds. */
332
+ export const oauthConnectKeys = {
333
+ family: {
334
+ name: "oauth-connect",
335
+ scope: "connector",
336
+ prefixes: ["oauth:connect-used:", "oauth:request-used:"],
337
+ version: { number: 1, in: "untagged" },
338
+ codec: textCodec,
339
+ // The link's remaining lifetime, or fifteen minutes when retiring it.
340
+ ttl: { kind: "fixed", seconds: 15 * 60 },
341
+ durable: false,
342
+ },
343
+ used: (nonce) => validateStorageKey(`oauth:connect-used:${nonce}`),
344
+ retry: (nonce) => validateStorageKey(`oauth:request-used:${nonce}`),
345
+ };
346
+ /** A bounded, credential-partitioned downstream protocol verdict. */
347
+ export const NEGOTIATION_TTL_SECONDS = 300;
348
+ export const negotiationKeys = {
349
+ family: {
350
+ name: "negotiation",
351
+ scope: "connector",
352
+ prefixes: ["negotiation:v1:"],
353
+ version: { number: 1, in: "key" },
354
+ codec: jsonCodec,
355
+ ttl: { kind: "fixed", seconds: NEGOTIATION_TTL_SECONDS },
356
+ durable: false,
357
+ },
358
+ verdict: (partitionDigest) => validateStorageKey(`negotiation:v1:${partitionDigest}`),
359
+ };
360
+ /** A downstream MRTR round is consumed once before continuation dispatch. */
361
+ export const inputRetryKeys = {
362
+ family: {
363
+ name: "downstream-input-retry",
364
+ scope: "connector",
365
+ prefixes: ["input-retry:v1:"],
366
+ version: { number: 1, in: "key" },
367
+ codec: textCodec,
368
+ ttl: { kind: "fixed", seconds: 10 * 60 },
369
+ durable: false,
370
+ },
371
+ used: (nonce) => validateStorageKey(`input-retry:v1:${nonce}`),
372
+ };
373
+ /**
374
+ * Every family, for the overlap and coverage checks and for migrations that
375
+ * copy only what a deployment cannot recreate. Keys a custom connector writes
376
+ * through its own `ctx.storage` belong to that connector, inside its
377
+ * namespace, and are not listed here.
378
+ */
379
+ export const KEY_FAMILIES = [
380
+ resultKeys.family,
381
+ stashLedgerKeys.family,
382
+ catalogKeys.family,
383
+ responseCacheKeys.family,
384
+ oauthHandoffKeys.family,
385
+ accessTokenKeys.family,
386
+ credentialKeys.family,
387
+ oauthGrantKeys.family,
388
+ oauthFlowKeys.family,
389
+ oauthRefreshKeys.family,
390
+ oauthRefreshSpentKeys.family,
391
+ oauthRefreshActiveKeys.family,
392
+ oauthV2Keys.family,
393
+ oauthConnectKeys.family,
394
+ negotiationKeys.family,
395
+ inputRetryKeys.family,
396
+ kvCopyKeys.family,
397
+ kvCutoverKeys.family,
398
+ ];
399
+ // Principal, connector, and subject segments are hex digests and connector
400
+ // ids, so none holds a colon.
401
+ const PRINCIPAL_SCOPE = /^principal:[^:]*:/;
402
+ const CONNECTOR_SCOPE = /^conn:[^:]*:/;
403
+ const PARTITION_SCOPE = /^(?:results:|subject:[^:]*:)/;
404
+ /**
405
+ * The family a physical key belongs to, for reports that count keys without
406
+ * naming them: one-shot migrations report per family, never per key. A key
407
+ * no family claims is `connector-owned` when a custom connector wrote it
408
+ * through its own `ctx.storage`, and `unclassified` otherwise (a key an older release wrote).
409
+ */
410
+ export function familyOfKey(key) {
411
+ let rest = key.replace(PRINCIPAL_SCOPE, "");
412
+ let scope = "root";
413
+ for (const [pattern, nested] of [
414
+ [CONNECTOR_SCOPE, "connector"],
415
+ [PARTITION_SCOPE, "partition"],
416
+ ]) {
417
+ const match = pattern.exec(rest);
418
+ if (match) {
419
+ rest = rest.slice(match[0].length);
420
+ scope = nested;
421
+ break;
422
+ }
423
+ }
424
+ const family = KEY_FAMILIES.find((candidate) => candidate.scope === scope && candidate.prefixes.some((prefix) => rest.startsWith(prefix)));
425
+ if (family)
426
+ return family.name;
427
+ return scope === "connector" ? "connector-owned" : "unclassified";
428
+ }
@@ -1,3 +1,4 @@
1
+ import { validateStorageKey } from "./keys.js";
1
2
  /** In-memory KV store with expiry. The default for dev and Node. */
2
3
  export function memoryStorage() {
3
4
  const map = new Map();
@@ -36,16 +37,21 @@ export function memoryStorage() {
36
37
  });
37
38
  };
38
39
  return {
40
+ describe: () => ({ kind: "memory" }),
39
41
  async get(key) {
42
+ validateStorageKey(key);
40
43
  return fresh(key)?.value ?? null;
41
44
  },
42
45
  async set(key, value, opts) {
46
+ validateStorageKey(key);
43
47
  write(key, value, opts?.ttlSeconds);
44
48
  },
45
49
  async delete(key) {
50
+ validateStorageKey(key);
46
51
  map.delete(key);
47
52
  },
48
53
  async list(prefix) {
54
+ validateStorageKey(prefix);
49
55
  return [...map.keys()]
50
56
  .filter((key) => {
51
57
  fresh(key);
@@ -56,6 +62,7 @@ export function memoryStorage() {
56
62
  // Atomic because nothing between the read and the write yields: the body
57
63
  // runs to completion before any other call on this map can start.
58
64
  async compareAndSet(key, expected, next, opts) {
65
+ validateStorageKey(key);
59
66
  if ((fresh(key)?.value ?? null) !== expected)
60
67
  return false;
61
68
  if (next === null)