@zackbart/connecta 0.37.1 → 0.39.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 (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/sqlite.js CHANGED
@@ -4,12 +4,11 @@
4
4
  // install. `sqliteStorage` is the deployment's `KVStorage` and
5
5
  // `sqliteActivityStore` its activity history; both create their tables on
6
6
  // first use. Node-only: nothing reachable from the root entry imports this.
7
- import { chmodSync, closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
7
+ import { chmodSync, closeSync, mkdirSync, openSync } from "node:fs";
8
8
  import { dirname, resolve } from "node:path";
9
9
  import { DatabaseSync } from "node:sqlite";
10
10
  import { effectSqlDriver, sqlDriverEdge, sqlSync } from "./storage/sql-driver.js";
11
- import { validateStorageKey } from "./storage/keys.js";
12
- import { KV_SCHEMA, sqlActivityStore, sqlStorage, } from "./storage/sql.js";
11
+ import { sqlActivityStore, sqlStorage, } from "./storage/sql.js";
13
12
  /** Milliseconds a write waits for another process's write lock. */
14
13
  const BUSY_TIMEOUT_MS = 5_000;
15
14
  /**
@@ -100,83 +99,3 @@ export function sqliteStorage(database) {
100
99
  export function sqliteActivityStore(database, options) {
101
100
  return sqlActivityStore(sqliteDriver(database), "sqlite", options);
102
101
  }
103
- /**
104
- * One-shot migration from the 0.28 `fileStorage` JSON state file: copy every
105
- * live entry, with its expiry, into the database's key-value table. Run it
106
- * once, with the old deployment stopped, before the first start on SQLite.
107
- *
108
- * Keys the database already holds are kept, so a second run copies nothing
109
- * twice and never overwrites state written since. The file is read, never
110
- * changed; delete it once the deployment is verified. A file that is not a
111
- * state file throws before anything is written.
112
- */
113
- export function importStateFile(database, statePath, now = Date.now()) {
114
- const text = readFileSync(statePath, "utf8");
115
- // The file holds stored state (tokens, sealed credentials), and these
116
- // errors reach the operator's terminal, so they name the file and an entry's
117
- // position, never a key or the parser's account, which quotes the text.
118
- let loaded;
119
- try {
120
- loaded = JSON.parse(text);
121
- }
122
- catch {
123
- throw new TypeError(`${statePath} is not a connecta state file: it is not valid JSON`);
124
- }
125
- if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) {
126
- throw new TypeError(`${statePath} is not a connecta state file`);
127
- }
128
- const entries = Object.entries(loaded);
129
- for (const [index, [key, entry]] of entries.entries()) {
130
- validateStorageKey(key);
131
- const valid = entry !== null &&
132
- typeof entry === "object" &&
133
- typeof entry.value === "string" &&
134
- (!("exp" in entry) || Number.isFinite(entry.exp));
135
- if (!valid) {
136
- throw new TypeError(`${statePath} is not a connecta state file: entry ${index + 1} has no string value`);
137
- }
138
- }
139
- let db;
140
- let transaction = false;
141
- let step = "open the SQLite database";
142
- const result = { imported: 0, kept: 0, expired: 0 };
143
- try {
144
- db = typeof database === "string" ? openSqlite(database) : database;
145
- step = "begin the import transaction";
146
- db.exec("BEGIN IMMEDIATE");
147
- transaction = true;
148
- step = "prepare the key-value table";
149
- for (const statement of KV_SCHEMA)
150
- db.exec(statement);
151
- const insert = db.prepare(`INSERT INTO connecta_kv (key, value, expires_at_ms) VALUES (?, ?, ?)
152
- ON CONFLICT (key) DO NOTHING`);
153
- for (const [index, [key, entry]] of entries.entries()) {
154
- step = `import entry ${index + 1} into the key-value table`;
155
- const { value, exp } = entry;
156
- if (exp !== undefined && exp <= now) {
157
- result.expired += 1;
158
- continue;
159
- }
160
- if (Number(insert.run(key, value, exp === undefined ? null : Math.ceil(exp)).changes) > 0) {
161
- result.imported += 1;
162
- }
163
- else {
164
- result.kept += 1;
165
- }
166
- }
167
- step = "commit the import transaction";
168
- db.exec("COMMIT");
169
- }
170
- catch {
171
- if (transaction) {
172
- try {
173
- db.exec("ROLLBACK");
174
- }
175
- catch {
176
- // Rollback errors can contain stored text too. Report the failed step.
177
- }
178
- }
179
- throw new Error(`State import failed: could not ${step}`);
180
- }
181
- return result;
182
- }
@@ -141,7 +141,7 @@ export const catalogKeys = {
141
141
  name: "catalog",
142
142
  scope: "root",
143
143
  prefixes: ["catalog:"],
144
- // Retired v2/v3 listings are inventoried for migration; no runtime reads them.
144
+ // Retired v2/v3 listings expire on their original TTL; no runtime reads them.
145
145
  version: { number: 3, in: "value" },
146
146
  codec: textCodec,
147
147
  ttl: {
@@ -153,7 +153,33 @@ export const catalogKeys = {
153
153
  manifest: (connectorId) => validateStorageKey(`catalog:${connectorId}`),
154
154
  chunk: (connectorId, revision, index) => validateStorageKey(`catalog:${connectorId}:chunk:${revision}:${index}`),
155
155
  };
156
- /** SDK catalog response cache, complete manifests published after their chunks. */
156
+ /** How long a connector-health record outlives its retry time, fencing the next probe's claim. */
157
+ export const CONNECTOR_HEALTH_LINGER_MS = 60_000;
158
+ /**
159
+ * Bounded connector health by connector id and a digest of the owner
160
+ * partition and described configuration. `discovery` holds the latest failed
161
+ * discovery's typed code, observation time, retry time and the catalog
162
+ * generation it saw; `status` the operator page's latest status observation
163
+ * (state, problem code, tool count, observation time, generation). Values
164
+ * never carry downstream text, tools or credentials.
165
+ */
166
+ export const connectorHealthKeys = {
167
+ family: {
168
+ name: "connector-health",
169
+ scope: "root",
170
+ prefixes: ["connector-health:v1:"],
171
+ version: { number: 1, in: "key" },
172
+ codec: jsonCodec,
173
+ ttl: { kind: "configured", by: "discovery: retry time plus 60 seconds; status: 120 seconds" },
174
+ durable: false,
175
+ },
176
+ discovery: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:discovery`),
177
+ status: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:status`),
178
+ };
179
+ /**
180
+ * SDK catalog response cache, complete manifests published after their chunks.
181
+ * Secret-bearing inputs reach these keys only through `secretDigest`.
182
+ */
157
183
  export const responseCacheKeys = {
158
184
  family: {
159
185
  name: "response-cache",
@@ -242,38 +268,6 @@ export const credentialKeys = {
242
268
  },
243
269
  credential: (connectorId, owner) => `${owner ? scopes.principal(owner) : ""}${scopes.connector(connectorId)}credential:v1`,
244
270
  };
245
- /** Seconds a Workers KV → D1 copy's resume point stays usable. */
246
- export const KV_COPY_CURSOR_TTL_SECONDS = 7 * 24 * 60 * 60;
247
- /**
248
- * Where a one-shot Workers KV → D1 copy (`copyKvToD1` in `src/d1.ts`) resumes,
249
- * held in D1 under a random token. Workers KV's own list cursor can spell the
250
- * last key it listed, so the caller holds the token and never the cursor.
251
- */
252
- export const kvCopyKeys = {
253
- family: {
254
- name: "kv-copy",
255
- scope: "root",
256
- prefixes: ["kv-copy:v1:"],
257
- version: { number: 1, in: "key" },
258
- codec: textCodec,
259
- ttl: { kind: "fixed", seconds: KV_COPY_CURSOR_TTL_SECONDS },
260
- durable: false,
261
- },
262
- cursor: (token) => validateStorageKey(`kv-copy:v1:cursor:${token}`),
263
- };
264
- /** Once traffic uses D1, copying stale KV state is refused. */
265
- export const kvCutoverKeys = {
266
- family: {
267
- name: "kv-cutover",
268
- scope: "root",
269
- prefixes: ["kv-cutover:v1:"],
270
- version: { number: 1, in: "key" },
271
- codec: textCodec,
272
- ttl: { kind: "durable" },
273
- durable: true,
274
- },
275
- source: (source) => validateStorageKey(`kv-cutover:v1:${encodeURIComponent(source)}`),
276
- };
277
271
  // --- connector families ---------------------------------------------------
278
272
  /** Seconds a consent's flow record stays claimable, the same as its link. */
279
273
  export const OAUTH_FLOW_TTL_SECONDS = 15 * 60;
@@ -356,38 +350,6 @@ export const oauthRefreshActiveKeys = {
356
350
  prefix: "oauth:refresh-active:",
357
351
  holder: (epoch, holder) => validateStorageKey(`oauth:refresh-active:${epoch}:${holder}`),
358
352
  };
359
- /** The values layout 2 stored, each under its historical key. */
360
- const oauthV2Field = {
361
- client: "oauth:client",
362
- tokens: "oauth:tokens",
363
- pending: "oauth:pending",
364
- verifier: "oauth:verifier",
365
- state: "oauth:state",
366
- discovery: "oauth:discovery",
367
- };
368
- /**
369
- * Layout 2, read once to migrate it into a grant record and then deleted:
370
- * each value at its field key, suffixed `:epoch:<generation>` once a modern
371
- * generation owned it; the active generation at `oauth:generation`; a reset's
372
- * cleanup lineage at `oauth:cleanup:` and `oauth:cleanup-at:`.
373
- */
374
- export const oauthV2Keys = {
375
- family: {
376
- name: "oauth-v2",
377
- scope: "connector",
378
- prefixes: [...Object.values(oauthV2Field), "oauth:generation", "oauth:cleanup:", "oauth:cleanup-at:"],
379
- version: { number: 2, in: "value" },
380
- codec: textCodec,
381
- ttl: { kind: "durable" },
382
- durable: true,
383
- },
384
- field: oauthV2Field,
385
- /** One listing covers every layout 2 key; the reader keeps those above. */
386
- scan: "oauth:",
387
- /** A value's physical key; `epoch` null is the historical unsuffixed name. */
388
- value: (key, epoch) => validateStorageKey(epoch === null ? key : `${key}:epoch:${epoch}`),
389
- generation: "oauth:generation",
390
- };
391
353
  /** Single-use browser links (`used`/`started`/`failed`/`closed`) and MRTR rounds. */
392
354
  export const oauthConnectKeys = {
393
355
  family: {
@@ -403,8 +365,31 @@ export const oauthConnectKeys = {
403
365
  used: (nonce) => validateStorageKey(`oauth:connect-used:${nonce}`),
404
366
  retry: (nonce) => validateStorageKey(`oauth:request-used:${nonce}`),
405
367
  };
406
- /** A bounded, credential-partitioned downstream protocol verdict. */
368
+ /**
369
+ * The last verification of a connector's operator-managed credential:
370
+ * `verification:v1`, holding an outcome class, its time and the vault
371
+ * revision of the save it observed. Payload-free and long-lived; a lost
372
+ * record only makes the operator page say "not verified".
373
+ */
374
+ export const verificationKeys = {
375
+ family: {
376
+ name: "verification",
377
+ scope: "connector",
378
+ prefixes: ["verification:v1"],
379
+ version: { number: 1, in: "key" },
380
+ codec: jsonCodec,
381
+ ttl: { kind: "durable" },
382
+ durable: false,
383
+ },
384
+ record: "verification:v1",
385
+ };
386
+ /** A bounded, credential-partitioned downstream protocol verdict, keyed through `secretDigest`. */
407
387
  export const NEGOTIATION_TTL_SECONDS = 300;
388
+ /**
389
+ * A legacy verdict lives longer: a server that upgrades still answers the
390
+ * legacy handshake, and one that stops answering it is renegotiated at once.
391
+ */
392
+ export const LEGACY_NEGOTIATION_TTL_SECONDS = 6 * 60 * 60;
408
393
  export const negotiationKeys = {
409
394
  family: {
410
395
  name: "negotiation",
@@ -412,7 +397,8 @@ export const negotiationKeys = {
412
397
  prefixes: ["negotiation:v1:"],
413
398
  version: { number: 1, in: "key" },
414
399
  codec: jsonCodec,
415
- ttl: { kind: "fixed", seconds: NEGOTIATION_TTL_SECONDS },
400
+ // The longest verdict lifetime; a modern verdict expires sooner.
401
+ ttl: { kind: "fixed", seconds: LEGACY_NEGOTIATION_TTL_SECONDS },
416
402
  durable: false,
417
403
  },
418
404
  verdict: (partitionDigest) => validateStorageKey(`negotiation:v1:${partitionDigest}`),
@@ -430,63 +416,18 @@ export const inputRetryKeys = {
430
416
  },
431
417
  used: (nonce) => validateStorageKey(`input-retry:v1:${nonce}`),
432
418
  };
433
- /**
434
- * Every family, for the overlap and coverage checks and for migrations that
435
- * copy only what a deployment cannot recreate. Keys a custom connector writes
436
- * through its own `ctx.storage` belong to that connector, inside its
437
- * namespace, and are not listed here.
438
- */
439
- export const KEY_FAMILIES = [
440
- continuationKeys.family,
441
- operatorViewerKeys.family,
442
- resultKeys.family,
443
- stashLedgerKeys.family,
444
- stashExpiryKeys.family,
445
- catalogKeys.family,
446
- responseCacheKeys.family,
447
- oauthHandoffKeys.family,
448
- accessTokenKeys.family,
449
- agentObservationKeys.family,
450
- credentialKeys.family,
451
- oauthGrantKeys.family,
452
- oauthFlowKeys.family,
453
- oauthRefreshKeys.family,
454
- oauthRefreshSpentKeys.family,
455
- oauthRefreshActiveKeys.family,
456
- oauthV2Keys.family,
457
- oauthConnectKeys.family,
458
- negotiationKeys.family,
459
- inputRetryKeys.family,
460
- kvCopyKeys.family,
461
- kvCutoverKeys.family,
462
- ];
463
- // Principal, connector, and subject segments are hex digests and connector
464
- // ids, so none holds a colon.
465
- const PRINCIPAL_SCOPE = /^principal:[^:]*:/;
466
- const CONNECTOR_SCOPE = /^conn:[^:]*:/;
467
- const PARTITION_SCOPE = /^(?:results:|subject:[^:]*:)/;
468
- /**
469
- * The family a physical key belongs to, for reports that count keys without
470
- * naming them: one-shot migrations report per family, never per key. A key
471
- * no family claims is `connector-owned` when a custom connector wrote it
472
- * through its own `ctx.storage`, and `unclassified` otherwise (a key an older release wrote).
473
- */
474
- export function familyOfKey(key) {
475
- let rest = key.replace(PRINCIPAL_SCOPE, "");
476
- let scope = "root";
477
- for (const [pattern, nested] of [
478
- [CONNECTOR_SCOPE, "connector"],
479
- [PARTITION_SCOPE, "partition"],
480
- ]) {
481
- const match = pattern.exec(rest);
482
- if (match) {
483
- rest = rest.slice(match[0].length);
484
- scope = nested;
485
- break;
486
- }
487
- }
488
- const family = KEY_FAMILIES.find((candidate) => candidate.scope === scope && candidate.prefixes.some((prefix) => rest.startsWith(prefix)));
489
- if (family)
490
- return family.name;
491
- return scope === "connector" ? "connector-owned" : "unclassified";
492
- }
419
+ /** Retired OAuth names are inspected only to report that fresh consent is required. */
420
+ export const retiredOAuthKeys = {
421
+ scan: "oauth:",
422
+ prefixes: [
423
+ "oauth:client",
424
+ "oauth:tokens",
425
+ "oauth:pending",
426
+ "oauth:verifier",
427
+ "oauth:state",
428
+ "oauth:discovery",
429
+ "oauth:generation",
430
+ "oauth:cleanup:",
431
+ "oauth:cleanup-at:",
432
+ ],
433
+ };
@@ -1,4 +1,5 @@
1
1
  import { validateStorageKey } from "./keys.js";
2
+ import { noteStorageWrite, observeStorageWrites } from "../runtime/request-reads.js";
2
3
  /** In-memory KV store with expiry. The default for dev and Node. */
3
4
  export function memoryStorage() {
4
5
  const map = new Map();
@@ -40,7 +41,9 @@ export function memoryStorage() {
40
41
  ...(exp !== undefined ? { exp } : {}),
41
42
  });
42
43
  };
43
- return {
44
+ // Each write settles synchronously, so one note covers it (request-reads.ts).
45
+ const root = {};
46
+ return observeStorageWrites({
44
47
  capabilities: { absoluteExpiry: true },
45
48
  describe: () => ({ kind: "memory" }),
46
49
  async get(key) {
@@ -49,10 +52,12 @@ export function memoryStorage() {
49
52
  },
50
53
  async set(key, value, opts) {
51
54
  validateStorageKey(key);
55
+ noteStorageWrite(root, key);
52
56
  write(key, value, opts);
53
57
  },
54
58
  async delete(key) {
55
59
  validateStorageKey(key);
60
+ noteStorageWrite(root, key);
56
61
  map.delete(key);
57
62
  },
58
63
  async list(prefix) {
@@ -68,6 +73,7 @@ export function memoryStorage() {
68
73
  // runs to completion before any other call on this map can start.
69
74
  async compareAndSet(key, expected, next, opts) {
70
75
  validateStorageKey(key);
76
+ noteStorageWrite(root, key);
71
77
  if ((fresh(key)?.value ?? null) !== expected)
72
78
  return false;
73
79
  if (next === null)
@@ -76,5 +82,5 @@ export function memoryStorage() {
76
82
  write(key, next, opts);
77
83
  return true;
78
84
  },
79
- };
85
+ }, { root });
80
86
  }
@@ -61,6 +61,8 @@ function schemaOnce(driver, ddl) {
61
61
  });
62
62
  }
63
63
  // --- key-value ----------------------------------------------------------
64
+ /** Keys one `getMany` statement binds. */
65
+ const GET_MANY_KEYS = 64;
64
66
  /** Expired rows one write removes on its way. */
65
67
  const SWEEP_ROWS = 16;
66
68
  function ttlMillis(ttlSeconds) {
@@ -105,6 +107,31 @@ export function nativeSqlStorage(driver) {
105
107
  return yield* current(key);
106
108
  });
107
109
  },
110
+ getMany(keys) {
111
+ return Effect.gen(function* () {
112
+ for (const key of keys)
113
+ validateStorageKey(key);
114
+ if (keys.length === 0)
115
+ return [];
116
+ yield* ensure();
117
+ // One statement for every key: a chunked value costs one round trip.
118
+ // Rows answer by position, never by the key text a driver returns.
119
+ // Bound parameters stay under D1's per-statement limit of 100.
120
+ const values = keys.map(() => null);
121
+ for (let offset = 0; offset < keys.length; offset += GET_MANY_KEYS) {
122
+ const batch = keys.slice(offset, offset + GET_MANY_KEYS);
123
+ const rows = yield* driver.all({
124
+ sql: `WITH wanted (position, key) AS (VALUES ${batch.map((_, i) => `(${offset + i}, ?)`).join(", ")})
125
+ SELECT wanted.position AS position, ${textColumn("value")}
126
+ FROM wanted JOIN connecta_kv ON connecta_kv.key = wanted.key AND ${live}`,
127
+ params: batch,
128
+ });
129
+ for (const row of rows)
130
+ values[row.position] = textOf(row.value, row.value_bytes, `the stored value of ${JSON.stringify(keys[row.position])}`);
131
+ }
132
+ return values;
133
+ });
134
+ },
108
135
  set(key, value, opts) {
109
136
  return Effect.gen(function* () {
110
137
  validateStorageKey(key);
@@ -177,107 +204,6 @@ export function nativeSqlStorage(driver) {
177
204
  },
178
205
  };
179
206
  }
180
- /** Keys one read compares: D1 binds at most 100 parameters, one is `now`. */
181
- const COPY_READ_KEYS = 99;
182
- /** Writes one transaction carries, by count and by UTF-8 bytes. */
183
- const COPY_BATCH_STATEMENTS = 50;
184
- const COPY_BATCH_BYTES = 4 * 1024 * 1024;
185
- /**
186
- * Copy entries into the key-value table, verbatim: same key, same value,
187
- * same absolute expiry. A live row holding a different value is kept unless
188
- * `overwrite` is set, so a second copy of the same entries writes nothing.
189
- * Writes go out in bounded transactions; the outcomes follow `entries`.
190
- */
191
- export function copyIntoSqlEffect(driver, entries, options) {
192
- return Effect.gen(function* () {
193
- const { overwrite, now } = options;
194
- for (const entry of entries)
195
- validateStorageKey(entry.key);
196
- yield* driver.batch(KV_SCHEMA.map((statement) => sql(statement)));
197
- const live = "(expires_at_ms IS NULL OR expires_at_ms > ?)";
198
- const currentValues = (keys) => Effect.gen(function* () {
199
- const values = new Map();
200
- for (let start = 0; start < keys.length; start += COPY_READ_KEYS) {
201
- const chunk = keys.slice(start, start + COPY_READ_KEYS);
202
- const rows = yield* driver.all(sql(`SELECT key, ${textColumn("value")}, expires_at_ms FROM connecta_kv
203
- WHERE key IN (${chunk.map(() => "?").join(", ")}) AND ${live}`, ...chunk, now));
204
- // The message names no key: a copy's errors reach operator output.
205
- for (const row of rows) {
206
- values.set(row.key, {
207
- value: textOf(row.value, row.value_bytes, "a stored value") ?? "",
208
- expiresAtMs: row.expires_at_ms,
209
- });
210
- }
211
- }
212
- return values;
213
- });
214
- const outcomes = Array.from({ length: entries.length });
215
- const writes = [];
216
- const existing = yield* currentValues(entries.map((entry) => entry.key));
217
- for (const [index, entry] of entries.entries()) {
218
- const current = existing.get(entry.key);
219
- if (current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs) {
220
- outcomes[index] = "unchanged";
221
- }
222
- else if (current !== undefined && !overwrite) {
223
- outcomes[index] = "conflict";
224
- }
225
- else {
226
- outcomes[index] = current === undefined ? "copied" : "overwritten";
227
- writes.push({
228
- index,
229
- bytes: new TextEncoder().encode(entry.key).byteLength + new TextEncoder().encode(entry.value).byteLength + 32,
230
- statement: overwrite
231
- ? sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
232
- VALUES (?, ?, ?)
233
- ON CONFLICT (key) DO UPDATE SET
234
- value = excluded.value, expires_at_ms = excluded.expires_at_ms`, entry.key, entry.value, entry.expiresAtMs)
235
- : // Absent when read; a live row written since is kept, not replaced.
236
- sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
237
- VALUES (?, ?, ?)
238
- ON CONFLICT (key) DO UPDATE SET
239
- value = excluded.value, expires_at_ms = excluded.expires_at_ms
240
- WHERE connecta_kv.expires_at_ms IS NOT NULL
241
- AND connecta_kv.expires_at_ms <= ?`, entry.key, entry.value, entry.expiresAtMs, now),
242
- });
243
- }
244
- }
245
- const raced = [];
246
- let batch = [];
247
- let batchBytes = 0;
248
- const flush = () => Effect.gen(function* () {
249
- if (batch.length === 0)
250
- return;
251
- const changes = yield* driver.batch(batch.map((write) => write.statement));
252
- for (const [position, write] of batch.entries()) {
253
- if (!(changes[position] > 0))
254
- raced.push(write.index);
255
- }
256
- batch = [];
257
- batchBytes = 0;
258
- });
259
- for (const write of writes) {
260
- if (batch.length > 0 && (batch.length >= COPY_BATCH_STATEMENTS || batchBytes + write.bytes > COPY_BATCH_BYTES)) {
261
- yield* flush();
262
- }
263
- batch.push(write);
264
- batchBytes += write.bytes;
265
- }
266
- yield* flush();
267
- // A row another writer added between the read and the write: report what
268
- // it holds, and leave it.
269
- if (raced.length > 0) {
270
- const after = yield* currentValues(raced.map((index) => entries[index].key));
271
- for (const index of raced) {
272
- const entry = entries[index];
273
- const current = after.get(entry.key);
274
- outcomes[index] =
275
- current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs ? "unchanged" : "conflict";
276
- }
277
- }
278
- return outcomes;
279
- });
280
- }
281
207
  // --- activity -----------------------------------------------------------
282
208
  /**
283
209
  * The activity table, with keyset paging on `(occurred_at_ms, id)`. Its shape
@@ -1,7 +1,5 @@
1
1
  import type { ActivityStore } from "../activity.js";
2
2
  import type { KVStorage } from "../types.js";
3
- /** The 0.28-compatible key-value table and expiry index. */
4
- export { KV_SCHEMA } from "./sql-schema.js";
5
3
  type SqlValue = string | number | null;
6
4
  export interface SqlStatement {
7
5
  readonly sql: string;
@@ -21,18 +19,6 @@ export interface SqlDriver {
21
19
  }
22
20
  /** The two drivers, as `describe()` names them. */
23
21
  export type SqlKind = "d1" | "sqlite";
24
- export interface SqlCopyEntry {
25
- readonly key: string;
26
- readonly value: string;
27
- /** Epoch milliseconds; null never expires. */
28
- readonly expiresAtMs: number | null;
29
- }
30
- /**
31
- * What a copy did with one entry: written where no live row was, left alone
32
- * because the live row already held the value, refused because it held
33
- * another (`conflict`), or replaced under `overwrite` (`overwritten`).
34
- */
35
- export type SqlCopyOutcome = "copied" | "unchanged" | "conflict" | "overwritten";
36
22
  export interface SqlActivityOptions {
37
23
  /**
38
24
  * Days of history kept. Each write removes a bounded batch of older rows,
@@ -48,13 +34,4 @@ export declare function sqlStorage(driver: SqlDriver, kind: SqlKind): KVStorage;
48
34
  * payload to leak.
49
35
  */
50
36
  export declare function sqlActivityStore(driver: SqlDriver, kind: SqlKind, options?: SqlActivityOptions): ActivityStore;
51
- /**
52
- * Copy entries into the key-value table, verbatim: same key, same value,
53
- * same absolute expiry. A live row holding a different value is kept unless
54
- * `overwrite` is set, so a second copy of the same entries writes nothing.
55
- * Writes go out in bounded transactions; the outcomes follow `entries`.
56
- */
57
- export declare function copyIntoSql(driver: SqlDriver, entries: readonly SqlCopyEntry[], options: {
58
- overwrite: boolean;
59
- now: number;
60
- }): Promise<SqlCopyOutcome[]>;
37
+ export {};
@@ -1,13 +1,20 @@
1
1
  import { runEdge } from "../runtime/run.js";
2
2
  import { registerNativeStorage, registerNativeActivity } from "../runtime/native-storage.js";
3
+ import { observedWrite, observeStorageWrites } from "../runtime/request-reads.js";
3
4
  import { nativeSqlDriver } from "./sql-driver.js";
4
- import { nativeSqlStorage, nativeSqlActivityStore, copyIntoSqlEffect } from "./sql-native.js";
5
- /** The 0.28-compatible key-value table and expiry index. */
6
- export { KV_SCHEMA } from "./sql-schema.js";
5
+ import { nativeSqlStorage, nativeSqlActivityStore } from "./sql-native.js";
7
6
  /** `KVStorage` over one `connecta_kv` table. */
8
7
  export function sqlStorage(driver, kind) {
9
- const native = nativeSqlStorage(nativeSqlDriver(driver));
10
- return registerNativeStorage({
8
+ const store = nativeSqlStorage(nativeSqlDriver(driver));
9
+ // Request read memos see every write begin and settle (request-reads.ts).
10
+ const root = {};
11
+ const native = {
12
+ ...store,
13
+ set: (key, value, options) => observedWrite(root, key, store.set(key, value, options)),
14
+ delete: (key) => observedWrite(root, key, store.delete(key)),
15
+ compareAndSet: (key, expected, next, options) => observedWrite(root, key, store.compareAndSet(key, expected, next, options)),
16
+ };
17
+ const edge = registerNativeStorage({
11
18
  capabilities: { absoluteExpiry: true },
12
19
  describe: () => ({ kind }),
13
20
  get: (key) => runEdge(native.get(key)),
@@ -16,6 +23,7 @@ export function sqlStorage(driver, kind) {
16
23
  list: (prefix) => runEdge(native.list(prefix)),
17
24
  compareAndSet: (key, expected, next, options) => runEdge(native.compareAndSet(key, expected, next, options)),
18
25
  }, native);
26
+ return observeStorageWrites(edge, { root });
19
27
  }
20
28
  /**
21
29
  * One append-only row per completed downstream call. Rows carry no arguments,
@@ -30,12 +38,3 @@ export function sqlActivityStore(driver, kind, options = {}) {
30
38
  list: (options) => runEdge(native.list(options)),
31
39
  }, native);
32
40
  }
33
- /**
34
- * Copy entries into the key-value table, verbatim: same key, same value,
35
- * same absolute expiry. A live row holding a different value is kept unless
36
- * `overwrite` is set, so a second copy of the same entries writes nothing.
37
- * Writes go out in bounded transactions; the outcomes follow `entries`.
38
- */
39
- export function copyIntoSql(driver, entries, options) {
40
- return runEdge(copyIntoSqlEffect(nativeSqlDriver(driver), entries, options));
41
- }
@@ -0,0 +1,32 @@
1
+ import { Context } from "effect";
2
+ import { bytesToHex } from "./bytes.js";
3
+ import { serviceFor, setViewService } from "./runtime/call.js";
4
+ class StorageDigestKey extends Context.Service()("connecta/operation/StorageDigestKey") {
5
+ }
6
+ const importHmac = (raw) => crypto.subtle.importKey("raw", new Uint8Array(raw), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
7
+ // Drawn synchronously on first use, so concurrent first digests share one key.
8
+ let processBytes;
9
+ let processKey;
10
+ const fallbackKey = () => (processKey ??= importHmac((processBytes ??= crypto.getRandomValues(new Uint8Array(32)))));
11
+ const vaultKeys = new WeakMap();
12
+ /** Bind the vault's storage digest key to a registry-built context, when it has one. */
13
+ export function attachStorageDigestKey(ctx, vault) {
14
+ const derive = vault?.storageDigestKey;
15
+ if (typeof derive !== "function")
16
+ return;
17
+ let key = vaultKeys.get(vault);
18
+ if (!key) {
19
+ let pending;
20
+ key = () => (pending ??= derive.call(vault));
21
+ vaultKeys.set(vault, key);
22
+ }
23
+ setViewService(ctx, StorageDigestKey, key);
24
+ }
25
+ /**
26
+ * `hmac:<hex>` of a secret-bearing value, for a stored key only. Stable for one
27
+ * deployment key, so equal inputs still address the same entry.
28
+ */
29
+ export async function secretDigest(ctx, value) {
30
+ const key = await (serviceFor(ctx, StorageDigestKey) ?? fallbackKey)();
31
+ return `hmac:${bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value))))}`;
32
+ }
package/dist/timeout.js CHANGED
@@ -9,6 +9,14 @@ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
9
9
  * to trip only on a pathological hang, not a realistically slow probe.
10
10
  */
11
11
  export const DEFAULT_PROBE_TIMEOUT_MS = CONFIG_DEFAULTS.discovery.probeTimeoutMs;
12
+ /**
13
+ * Each connector's probe deadline in an unscoped search, or
14
+ * `discovery.probeTimeoutMs` when that is shorter. Scoped search, describe
15
+ * and calls keep the full probe deadline.
16
+ */
17
+ export const UNSCOPED_PROBE_TIMEOUT_MS = 5_000;
18
+ /** One unscoped search's whole discovery fan-out ends within this many ms. */
19
+ export const UNSCOPED_DISCOVERY_BUDGET_MS = 9_000;
12
20
  /** A finite, positive integer number of milliseconds, or undefined. */
13
21
  export function normalizeTimeoutMs(value) {
14
22
  if (value === undefined || !Number.isFinite(value) || !(value > 0)) {
@@ -0,0 +1,4 @@
1
+ export function toolError(error, fields = {}) {
2
+ const structuredContent = { ...fields, error };
3
+ return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
4
+ }