@zackbart/connecta 0.28.1 → 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 (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  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 +1584 -1580
  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 +93 -71
  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 +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. package/templates/node/src/file-activity.ts +0 -182
@@ -1,15 +0,0 @@
1
- import type { KVStorage, Logger } from "../types.js";
2
- export interface FileStorageOptions {
3
- /** Destination for the corrupt-state-file recovery report. Default console. */
4
- logger?: Logger;
5
- }
6
- /**
7
- * JSON-file-backed KVStorage for Node. Loads once, persists on every write via
8
- * an exclusive temp-file + rename. Refuses a second holder of the same path.
9
- * Call close() when finished to release its lock; process exit also releases it.
10
- * Only reachable via the "@zackbart/connecta/node"
11
- * subpath so the main entry stays Workers-clean.
12
- */
13
- export declare function fileStorage(path: string, opts?: FileStorageOptions): KVStorage & {
14
- close(): void;
15
- };
@@ -1,455 +0,0 @@
1
- import { chmodSync, closeSync, existsSync, futimesSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync, } from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
- import { hostname } from "node:os";
4
- import { dirname, resolve } from "node:path";
5
- function validEntry(value) {
6
- return value !== null && typeof value === "object" && !Array.isArray(value) &&
7
- "value" in value && typeof value.value === "string" &&
8
- (!("exp" in value) || (typeof value.exp === "number" && Number.isFinite(value.exp)));
9
- }
10
- function makeEntry(value, ttlSeconds) {
11
- if (ttlSeconds !== undefined && !Number.isFinite(ttlSeconds)) {
12
- throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
13
- }
14
- if (!ttlSeconds)
15
- return { value };
16
- const exp = Date.now() + ttlSeconds * 1000;
17
- if (!Number.isFinite(exp)) {
18
- throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
19
- }
20
- return { value, exp };
21
- }
22
- const HEARTBEAT_MS = 15_000;
23
- const STALE_LOCK_MS = 60_000;
24
- const localLocks = new Map();
25
- // A pid is meaningful only in its own host/namespace. Container hostnames may
26
- // be shared, so Linux uses the kernel boot id and the actual PID namespace.
27
- const pidScope = (() => {
28
- if (process.platform !== "linux")
29
- return hostname();
30
- try {
31
- return `${readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim()}:${readlinkSync("/proc/self/ns/pid")}`;
32
- }
33
- catch {
34
- return undefined; // Without namespace evidence, rely on the heartbeat.
35
- }
36
- })();
37
- function stale(path) {
38
- return Date.now() - statSync(path).mtimeMs > STALE_LOCK_MS;
39
- }
40
- // One listener per module, removed when the last store closes. Node's listen()
41
- // drains requests before process.exit(), so the lock lasts through those writes.
42
- const openStores = new Set();
43
- const closeStores = () => {
44
- for (const close of openStores) {
45
- try {
46
- close();
47
- }
48
- catch {
49
- // A dead-pid or expired-heartbeat lock can be reclaimed next startup.
50
- }
51
- }
52
- };
53
- function hasCode(error, code) {
54
- return error?.code === code;
55
- }
56
- function lockFile(path) {
57
- const lockPath = `${path}.lock`;
58
- const contents = JSON.stringify({
59
- pid: process.pid,
60
- pidScope,
61
- createdAt: Date.now(),
62
- id: randomUUID(),
63
- });
64
- const readLock = () => {
65
- try {
66
- return readFileSync(lockPath, "utf8");
67
- }
68
- catch (error) {
69
- if (hasCode(error, "ENOENT"))
70
- return null;
71
- throw error;
72
- }
73
- };
74
- const refuseLiveHolder = () => {
75
- const raw = readLock();
76
- if (raw === null)
77
- return false;
78
- try {
79
- if (stale(lockPath))
80
- return true;
81
- }
82
- catch (error) {
83
- if (hasCode(error, "ENOENT"))
84
- return false;
85
- throw error;
86
- }
87
- let holder;
88
- try {
89
- holder = JSON.parse(raw);
90
- if (!holder || !Number.isInteger(holder.pid) || holder.pid <= 0 ||
91
- holder.pid > 2147483647 || !Number.isFinite(holder.createdAt)) {
92
- throw new Error("Invalid lock holder");
93
- }
94
- }
95
- catch {
96
- throw new Error(`[connecta] state file ${path} has an unreadable lock at ${lockPath}. ` +
97
- `Refusing to start. Retry after its heartbeat has been stale for 60 seconds.`);
98
- }
99
- if (pidScope !== undefined && holder.pidScope === pidScope) {
100
- if (holder.pid === process.pid) {
101
- // A new container process may inherit the old holder's pid. Only our
102
- // own registry can distinguish that incarnation from a second opener.
103
- if (localLocks.get(path) !== raw)
104
- return true;
105
- }
106
- else {
107
- try {
108
- process.kill(holder.pid, 0);
109
- }
110
- catch (error) {
111
- if (hasCode(error, "ESRCH"))
112
- return true;
113
- // Permission errors do not establish that the holder is dead.
114
- }
115
- }
116
- }
117
- throw new Error(`[connecta] state file ${path} is held by pid ${holder.pid} ` +
118
- `(lock timestamp ${holder.createdAt}). Close that store before opening another.`);
119
- };
120
- const acquire = () => {
121
- let fd;
122
- try {
123
- fd = openSync(lockPath, "wx", 0o600);
124
- }
125
- catch (error) {
126
- if (hasCode(error, "EEXIST"))
127
- return false;
128
- throw error;
129
- }
130
- try {
131
- writeFileSync(fd, contents);
132
- const now = new Date();
133
- futimesSync(fd, now, now);
134
- }
135
- catch (error) {
136
- unlinkSync(lockPath);
137
- throw error;
138
- }
139
- finally {
140
- closeSync(fd);
141
- }
142
- return true;
143
- };
144
- if (!acquire()) {
145
- refuseLiveHolder();
146
- // Serialize stale-lock removal. Without this guard, two reclaimers could
147
- // both observe the dead pid and the slower one unlink the new live lock.
148
- // Recovery is synchronous. A guard older than the lease is from a crashed
149
- // or paused reclaimer, which must recheck its ownership before continuing.
150
- const reclaimPath = `${lockPath}.reclaim`;
151
- const recoveryError = () => new Error(`[connecta] state file ${path} has a lock recovery in progress at ${reclaimPath}. ` +
152
- `Retry after its 60-second lease expires.`);
153
- try {
154
- const expired = statSync(reclaimPath);
155
- if (Date.now() - expired.mtimeMs > STALE_LOCK_MS) {
156
- const markers = readdirSync(reclaimPath);
157
- const current = statSync(reclaimPath);
158
- if (current.dev !== expired.dev || current.ino !== expired.ino ||
159
- current.birthtimeMs !== expired.birthtimeMs || !stale(reclaimPath)) {
160
- throw recoveryError();
161
- }
162
- // Delete only the expired owner's unique marker. A competing cleanup
163
- // cannot empty a replacement guard by deleting these old filenames.
164
- for (const marker of markers)
165
- rmSync(`${reclaimPath}/${marker}`, { force: true });
166
- rmdirSync(reclaimPath);
167
- }
168
- }
169
- catch (error) {
170
- if (hasCode(error, "ENOTEMPTY") || hasCode(error, "EEXIST"))
171
- throw recoveryError();
172
- if (!hasCode(error, "ENOENT"))
173
- throw error;
174
- }
175
- try {
176
- mkdirSync(reclaimPath, { mode: 0o700 });
177
- }
178
- catch (error) {
179
- if (!hasCode(error, "EEXIST"))
180
- throw error;
181
- throw recoveryError();
182
- }
183
- const guard = statSync(reclaimPath);
184
- const markerPath = `${reclaimPath}/${randomUUID()}`;
185
- const ownsGuard = () => {
186
- try {
187
- const current = statSync(reclaimPath);
188
- return current.dev === guard.dev && current.ino === guard.ino &&
189
- existsSync(markerPath);
190
- }
191
- catch (error) {
192
- if (hasCode(error, "ENOENT"))
193
- return false;
194
- throw error;
195
- }
196
- };
197
- const assertGuard = () => {
198
- if (!ownsGuard() || stale(reclaimPath))
199
- throw recoveryError();
200
- };
201
- try {
202
- writeFileSync(markerPath, "", { flag: "wx", mode: 0o600 });
203
- if (!ownsGuard())
204
- throw recoveryError();
205
- const now = new Date();
206
- utimesSync(reclaimPath, now, now);
207
- const removeStale = refuseLiveHolder();
208
- assertGuard();
209
- if (removeStale)
210
- unlinkSync(lockPath);
211
- if (!acquire()) {
212
- refuseLiveHolder();
213
- throw new Error(`[connecta] state file ${path} lock changed during recovery. Retry opening it.`);
214
- }
215
- }
216
- finally {
217
- const owned = ownsGuard();
218
- rmSync(markerPath, { force: true });
219
- if (owned)
220
- rmdirSync(reclaimPath);
221
- }
222
- }
223
- const assertHeld = () => {
224
- if (readLock() !== contents) {
225
- throw new Error(`[connecta] state file ${path} lock was lost. Refusing to write a stale snapshot.`);
226
- }
227
- };
228
- localLocks.set(path, contents);
229
- const heartbeat = setInterval(() => {
230
- try {
231
- assertHeld();
232
- const now = new Date();
233
- utimesSync(lockPath, now, now);
234
- }
235
- catch {
236
- // A replaced lock belongs to its new holder. Failed refreshes let the
237
- // lease expire; subsequent writes still have to prove ownership.
238
- clearInterval(heartbeat);
239
- }
240
- }, HEARTBEAT_MS);
241
- heartbeat.unref();
242
- return {
243
- assertHeld,
244
- release() {
245
- clearInterval(heartbeat);
246
- if (localLocks.get(path) === contents)
247
- localLocks.delete(path);
248
- if (readLock() === contents)
249
- unlinkSync(lockPath);
250
- },
251
- };
252
- }
253
- /**
254
- * JSON-file-backed KVStorage for Node. Loads once, persists on every write via
255
- * an exclusive temp-file + rename. Refuses a second holder of the same path.
256
- * Call close() when finished to release its lock; process exit also releases it.
257
- * Only reachable via the "@zackbart/connecta/node"
258
- * subpath so the main entry stays Workers-clean.
259
- */
260
- export function fileStorage(path, opts = {}) {
261
- path = resolve(path);
262
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
263
- const lock = lockFile(path);
264
- let closed = false;
265
- const close = () => {
266
- if (closed)
267
- return;
268
- closed = true;
269
- openStores.delete(close);
270
- if (!openStores.size)
271
- process.removeListener("exit", closeStores);
272
- lock.release();
273
- };
274
- const assertOpen = () => {
275
- if (closed)
276
- throw new Error(`[connecta] state file ${path} is closed.`);
277
- };
278
- const logger = opts.logger ?? console;
279
- // With a sealing vault, downstream OAuth tokens here are ciphertext; without
280
- // one they are cleartext, and OAuth flow state is plaintext either way. Keep
281
- // the file owner-only regardless. Repair is best-effort: chmod is a no-op or throws on
282
- // non-POSIX filesystems, and a loose mode must never keep the store from
283
- // starting.
284
- const tighten = () => {
285
- try {
286
- chmodSync(path, 0o600);
287
- }
288
- catch {
289
- // Non-POSIX filesystem or a race on the file — leave the mode as-is.
290
- }
291
- };
292
- let data = Object.create(null);
293
- try {
294
- if (existsSync(path)) {
295
- tighten();
296
- try {
297
- const loaded = JSON.parse(readFileSync(path, "utf8"));
298
- if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) {
299
- throw new TypeError("Expected a state object");
300
- }
301
- if (!Object.values(loaded).every(validEntry)) {
302
- throw new TypeError("Expected string values and finite expiry timestamps");
303
- }
304
- data = Object.assign(Object.create(null), loaded);
305
- }
306
- catch (error) {
307
- // Never let a damaged state file be silently replaced by an empty one:
308
- // the next set() would persist {} over irreplaceable downstream OAuth
309
- // tokens and credential-vault entries. Quarantine the bytes so they
310
- // survive for manual recovery, and refuse to start if even that fails —
311
- // losing the file loudly beats losing it quietly.
312
- const quarantine = `${path}.corrupt-${Date.now()}`;
313
- try {
314
- renameSync(path, quarantine);
315
- }
316
- catch (renameError) {
317
- throw new Error(`[connecta] state file ${path} is not valid JSON and could not be ` +
318
- `moved aside (${String(renameError)}). Refusing to start rather ` +
319
- `than overwrite it. Move or repair the file, then restart.`);
320
- }
321
- logger.error(`[connecta] state file ${path} is not valid JSON ` +
322
- `(${error instanceof Error ? error.message : String(error)}) — ` +
323
- `moved to ${quarantine}, starting from empty state. Downstream ` +
324
- `OAuth connectors must be re-authorized and stored credentials ` +
325
- `re-entered.`);
326
- data = Object.create(null);
327
- }
328
- }
329
- }
330
- catch (error) {
331
- close();
332
- throw error;
333
- }
334
- if (!openStores.size)
335
- process.on("exit", closeStores);
336
- openStores.add(close);
337
- const persist = () => {
338
- // Physical expiry rides on an operation that was already going to write.
339
- // Reads only prune memory; they do not rewrite the state file.
340
- const now = Date.now();
341
- for (const [key, entry] of Object.entries(data)) {
342
- if (entry.exp !== undefined && now >= entry.exp)
343
- delete data[key];
344
- }
345
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
346
- // 0o600 on the tmp file; the atomic rename below preserves it, so the live
347
- // state file is never briefly world-readable.
348
- const fd = openSync(tmp, "wx", 0o600);
349
- let committed = false;
350
- try {
351
- try {
352
- writeFileSync(fd, JSON.stringify(data));
353
- }
354
- finally {
355
- closeSync(fd);
356
- }
357
- lock.assertHeld();
358
- renameSync(tmp, path);
359
- committed = true;
360
- }
361
- finally {
362
- // A successful rename already removed the temp path. Cleanup must not
363
- // turn a committed snapshot into a failure that callers roll back.
364
- if (!committed)
365
- rmSync(tmp, { force: true });
366
- }
367
- tighten();
368
- };
369
- const fresh = (key) => {
370
- const e = data[key];
371
- if (!e)
372
- return null;
373
- if (e.exp !== undefined && Date.now() >= e.exp) {
374
- delete data[key];
375
- return null;
376
- }
377
- return e;
378
- };
379
- return {
380
- close,
381
- async get(key) {
382
- // Reads use the loaded snapshot; only writes need filesystem ownership
383
- // checks to prevent a reclaimed holder from overwriting newer state.
384
- assertOpen();
385
- return fresh(key)?.value ?? null;
386
- },
387
- async set(key, value, opts) {
388
- assertOpen();
389
- lock.assertHeld();
390
- const previous = fresh(key);
391
- data[key] = makeEntry(value, opts?.ttlSeconds);
392
- try {
393
- persist();
394
- }
395
- catch (error) {
396
- if (previous)
397
- data[key] = previous;
398
- else
399
- delete data[key];
400
- throw error;
401
- }
402
- },
403
- async delete(key) {
404
- assertOpen();
405
- lock.assertHeld();
406
- const previous = fresh(key);
407
- delete data[key];
408
- try {
409
- persist();
410
- }
411
- catch (error) {
412
- if (previous)
413
- data[key] = previous;
414
- throw error;
415
- }
416
- },
417
- async list(prefix) {
418
- assertOpen();
419
- return Object.keys(data)
420
- .filter((key) => Boolean(fresh(key)) && key.startsWith(prefix))
421
- .sort();
422
- },
423
- // Atomic twice over: the lock makes this process the only writer of the
424
- // file, and nothing between the comparison and the persisted rename
425
- // yields, so no other call on this store can interleave. A write that
426
- // cannot be persisted is rolled back so memory never claims a value the
427
- // file does not hold.
428
- async compareAndSet(key, expected, next, opts) {
429
- assertOpen();
430
- lock.assertHeld();
431
- const previous = fresh(key);
432
- if ((previous?.value ?? null) !== expected)
433
- return false;
434
- if (next === null) {
435
- if (!previous)
436
- return true;
437
- delete data[key];
438
- }
439
- else {
440
- data[key] = makeEntry(next, opts?.ttlSeconds);
441
- }
442
- try {
443
- persist();
444
- }
445
- catch (error) {
446
- if (previous)
447
- data[key] = previous;
448
- else
449
- delete data[key];
450
- throw error;
451
- }
452
- return true;
453
- },
454
- };
455
- }
package/ethos.md DELETED
@@ -1,93 +0,0 @@
1
- # connecta — ethos
2
-
3
- Connecta’s scope, refusals, and invariants. Contradictions require a design decision.
4
-
5
- ## What this is
6
-
7
- - **One MCP endpoint.** Agents reach configured connectors through JavaScript,
8
- a required executor, and seven meta-tools.
9
- - **Config-as-code.** One tenant and connector set; identities get
10
- config-derived views. Maintained providers, `remoteMcp()`, and `api()` share
11
- one connector contract.
12
- - **Safe by default.** Programs run only read-only or config-exempt tools;
13
- every other write crosses `call_destructive_tool`. The host owns approval.
14
- - **One fetch-native core.** Web APIs on Node and Workers; platform code and
15
- optional features behind explicit subpaths; Effect inside, Promises outside.
16
- - **Human auth management.** Optional pages show status and payload-free history.
17
- Explicit permissions allow credential changes and OAuth. Capabilities stay in code.
18
-
19
- ## What this isn't
20
-
21
- No runtime registration, admin-editable capabilities, or policy engine; config
22
- may only exempt a tool from asking. No schema ingestion, accounts, or groups;
23
- personal state remains within one tenant. Breaking changes remain acceptable;
24
- version numbers signal change, not stability.
25
-
26
- ## Decisions
27
-
28
- Revisiting a verdict requires a new argument. Accepted designs live in
29
- subsystem guides and the CHANGELOG; `planned` ones bind review before either
30
- carries them.
31
-
32
- | Decision | Verdict | Why |
33
- | --- | --- | --- |
34
- | OpenAPI / GraphQL ingestion | refused | the disease is a document-authored tool nobody chose; hand-written literals, even through a shared factory, are still authorship |
35
- | Multi-tenancy / account model | refused | one deployment per tenant; inbound auth owns identity |
36
- | Approvals and pauses | removed | hosts approve per call, so under always-allow the resume prompt was a rubber stamp that cost a round trip and a replay ([#672](https://github.com/zackbart/connecta/issues/672), superseding [#565](https://github.com/zackbart/connecta/issues/565)) |
37
- | Approval exemptions | accepted | config-only, program-only, never read-only elsewhere ([#566](https://github.com/zackbart/connecta/issues/566)) |
38
- | Runtime connector registration | refused | config-as-code is the security model |
39
- | Optional deployment modules | accepted | typed slots select UI, activity, vault, and inbound auth; core keeps discovery, execution, invocation, and enforcement |
40
- | Artifacts module | accepted | a built-in connector for team-only sandboxed pages over stored JSON; immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) |
41
- | Plugin lifecycle, provider registry, or marketplace | refused | modules are deployment code, not runtime installs; prebuilt connections are imports, discovered in docs ([#297](https://github.com/zackbart/connecta/issues/297)) |
42
- | OAuth on `api()` | accepted | OAuth-only APIs; static grants reuse `remoteMcp()` machinery, core's callback route |
43
- | Connecta-issued access tokens | accepted | optional /auth/access-tokens preserves v0.23 secrets; config owns grants, interactive management needs explicit permission ([#619](https://github.com/zackbart/connecta/issues/619)) |
44
- | Expanded Notion page create/update options | refused | different workflows, not missing fields; use `api()` ([#408](https://github.com/zackbart/connecta/issues/408)) |
45
- | Resources, prompts, and downstream MCP Apps templates | refused | tools only; clients own presentation ([#266](https://github.com/zackbart/connecta/issues/266)) |
46
- | Protocol sessions, server push, elicitation passthrough | refused | stateless per request; elicitation has no route |
47
- | Repository formatter | refused | style is authored, not enforced |
48
- | Host-side projection or paging of program results | refused | a program projects: heuristics drop fields invisibly, paging rewards unprojected returns ([#223](https://github.com/zackbart/connecta/issues/223)) |
49
- | Native Tasks for oversized results | refused | tasks solve duration, `get_result` solves size ([#176](https://github.com/zackbart/connecta/issues/176)) |
50
- | Widening the `Executor` result contract | refused | `{ result, error?, logs? }` is the `@cloudflare/codemode` parity guarantee ([#267](https://github.com/zackbart/connecta/issues/267)) |
51
- | Erasable TypeScript in `execute_code` | refused | a 24 MB core dependency, no measured agent benefit ([#419](https://github.com/zackbart/connecta/issues/419)) |
52
- | Guest-minted `resource` / `resource_link` blocks | refused | no program mints a URI a client may dereference ([#266](https://github.com/zackbart/connecta/issues/266)) |
53
- | Provenance tracking for emitted content | refused | everything a program emits is program output ([#267](https://github.com/zackbart/connecta/issues/267)) |
54
- | Result sampling on the catalog surface | refused | sampling is execution, not a catalog read ([#282](https://github.com/zackbart/connecta/issues/282)) |
55
- | Legacy embedded `UIResource` delivery | refused | superseded upstream, rendered by no client we face ([#266](https://github.com/zackbart/connecta/issues/266)) |
56
- | Effect as the core effect system | accepted | lifetimes held the bugs: the rewrite closed leaked permits, hung waits, and cross-request races. Overturns [#470](https://github.com/zackbart/connecta/issues/470) |
57
- | Shared bounded queue under both admission controllers | refused | built: −17 lines bought a hook-parameterised abstraction ([#453](https://github.com/zackbart/connecta/issues/453)) |
58
- | MCP Apps rendering and `connecta.ui` | removed | clients render returned data |
59
- | Connector shortcut globals | removed | canonical addresses need no sanitization |
60
- | `connecta.batch` | removed | JavaScript promises suffice |
61
- | Automatic direct-call retries | removed | callers own retry timing |
62
- | Connector HTTP routes | removed | deployments own custom routes; the operator UI's artifact viewer is the exception |
63
- | Caller-selected toolkits | removed | config derives every view, grant-gated `/mcp/<pool>` pools included ([#178](https://github.com/zackbart/connecta/issues/178), [#531](https://github.com/zackbart/connecta/issues/531)) |
64
- | Proactive credential liveness | removed | fail-at-use is enough ([#179](https://github.com/zackbart/connecta/issues/179)) |
65
- | Classic (executor-free) surface | removed | an executor is mandatory ([#273](https://github.com/zackbart/connecta/issues/273)) |
66
- | Per-result lexical query coverage | removed | failed a precommitted response-bytes gate ([#323](https://github.com/zackbart/connecta/issues/323)) |
67
- | Stabilized workflows | accepted | only as artifact refresh: deployment-triggered read-only programs, shared credentials ([#225](https://github.com/zackbart/connecta/issues/225), [#564](https://github.com/zackbart/connecta/issues/564)) |
68
- | Semantic tool search | gated | keyword search has not been shown to fail ([#27](https://github.com/zackbart/connecta/issues/27)) |
69
- | MRTR / `input_required` passthrough | gated | relayable statelessly; no host or downstream emits it yet ([#176](https://github.com/zackbart/connecta/issues/176)) |
70
- | Downstream `ttlMs` cache hints | gated | needs refresh-churn evidence ([#206](https://github.com/zackbart/connecta/issues/206)) |
71
- | Worker Access inbound auth | provisional | Managed OAuth and Clerk migration need production evidence ([#506](https://github.com/zackbart/connecta/issues/506)) |
72
- | Program UI tool calls | removed | duplicated calls without improving retrieval ([#287](https://github.com/zackbart/connecta/issues/287), [#484](https://github.com/zackbart/connecta/issues/484)) |
73
-
74
- ## Invariants
75
-
76
- Tests beside subsystem documentation enforce these.
77
-
78
- - **Fail-closed read-only.** A missing, false, or contradictory annotation never gets the benefit of the doubt.
79
- - **Generated code cannot mint capabilities.** Admission, credentials, and classification are enforced below the sandbox.
80
- - **Only explicitly read-only or config-exempt work runs in the sandbox.** Every other write crosses `call_destructive_tool`.
81
- - **Nothing request-bound survives a request.** No transport, stream, signal, or awaited promise outlives it.
82
- - **A downstream catalog is complete or it is a failure.** A partial one is never cached, persisted, or served.
83
- - **Activity is payload-free by construction.** The event type has nowhere to put arguments, results, code, or raw errors.
84
- - **An observed shape is never a declaration.** Names and broad types only, labeled, and gone behind any declared schema.
85
- - **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector (and principal, for personal auth), rendered by nothing.
86
- - **Import-graph purity.** Nothing reachable from the root entry imports a `node:` builtin or `effect/testing`.
87
- - **The published surface is a boundary.** Heavyweight or platform-bound code goes behind an optional-peer subpath; Effect is the one hard dependency, named by no published type.
88
- - **Human routes manage auth, never capability.** Visibility grants use, not administration. Auth mutations need separate config-derived permissions, denied by default; activity reads its own. Artifact viewing respects connector grants and cannot invoke tools.
89
- - **Omitted modules do no work.** Core imports no UI bundle, vault, activity, artifacts, or bearer implementation. OAuth callbacks work without UI.
90
- - **Status reads do not start authorization.** Only an explicit authorized action starts OAuth, never a page load.
91
- - **Structural mistakes throw at construction.** Booting into the wrong shape is worse than not booting.
92
-
93
- Connecta simplifies [executor](https://github.com/UsefulSoftwareCo/executor).
@@ -1,43 +0,0 @@
1
- import type { KVStorage } from "@zackbart/connecta";
2
-
3
- /**
4
- * KVStorage backed by a Cloudflare Workers KV namespace binding.
5
- * Note: Workers KV enforces a 60s minimum TTL; shorter TTLs are dropped
6
- * (stored without expiry) rather than rejected.
7
- *
8
- * Workers KV is eventually consistent across locations. It is suitable for
9
- * this example's durable state, but cannot promise immediate global OAuth
10
- * disconnect, credential rotation, or access-token issuance/revocation; use a
11
- * strongly consistent adapter when that is required.
12
- */
13
- export function cloudflareKvStorage(namespace: KVNamespace): KVStorage {
14
- return {
15
- async get(key) {
16
- return namespace.get(key);
17
- },
18
- async set(key, value, opts) {
19
- const ttl = opts?.ttlSeconds;
20
- await namespace.put(
21
- key,
22
- value,
23
- ttl && ttl >= 60 ? { expirationTtl: ttl } : undefined,
24
- );
25
- },
26
- async delete(key) {
27
- await namespace.delete(key);
28
- },
29
- async list(prefix) {
30
- const keys: string[] = [];
31
- let cursor: string | undefined;
32
- do {
33
- const page = await namespace.list({
34
- prefix,
35
- ...(cursor ? { cursor } : {}),
36
- });
37
- keys.push(...page.keys.map((key) => key.name));
38
- cursor = page.list_complete ? undefined : page.cursor;
39
- } while (cursor);
40
- return keys.sort();
41
- },
42
- };
43
- }