@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
@@ -6,11 +6,12 @@
6
6
  // than the payload's, and its connector scope closes as the slot ends,
7
7
  // however it ends.
8
8
  import { Effect } from "effect";
9
+ import { bindActivityRequest } from "../activity-request.js";
9
10
  import { resolveDiscoveryConcurrency } from "../concurrency.js";
10
- import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape, } from "../credential-rules.js";
11
+ import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape } from "../credential-rules.js";
12
+ import { failureRecord, failureStatus, logFailure, recordedToolName, statusFailure } from "../operator-record.js";
11
13
  import { closeScope } from "../runtime/connector-scope.js";
12
14
  import { withDeadlineEffect } from "../runtime/run.js";
13
- import { isApprovalExempt, NO_EXEMPTIONS, } from "../tool-safety.js";
14
15
  import { uiProblemFor, uiToolSafety } from "../ui.js";
15
16
  import { CONNECTA_VERSION } from "../version.js";
16
17
  const DETAIL_TIMEOUT_MS = 30_000;
@@ -21,24 +22,23 @@ function attempt(operation) {
21
22
  export function uiData(registry, baseUrl, options) {
22
23
  return Effect.suspend(() => {
23
24
  const requestScope = {};
25
+ if (options.activityContext)
26
+ bindActivityRequest(requestScope, options.activityContext);
24
27
  const vault = options.credentialVault;
25
28
  const owner = options.personalCredentialOwner;
26
29
  /**
27
- * A connector's status message stops here. It can quote a downstream error
28
- * body — and a downstream error body can quote the secret it just rejected —
29
- * so the payload carries only the classified `problem` and the raw text goes
30
- * to the deployment's log, where an operator debugging the failure already
31
- * looks. An `ok` status's message is informational and simply dropped.
30
+ * A connector's status message stops here: the payload carries only the
31
+ * classified `problem`, and the log a record of the state and, when
32
+ * connecta described the failure itself, that failure's typed facts. The
33
+ * message is never logged, whatever it looks like: a connector's own
34
+ * `status()` can quote a downstream error body, which can quote the
35
+ * secret it just rejected (INV-6). An `ok` status is simply dropped.
32
36
  */
33
- const logStatus = (id, state, message) => {
34
- if (!message || state === "ok")
37
+ const logStatus = (id, state, failure) => {
38
+ if (state === "ok")
35
39
  return;
36
40
  const logger = registry.contextFor(id, baseUrl, requestScope).logger;
37
- const line = `[connecta] connector "${id}" operator status ${state}: ${message}`;
38
- if (state === "auth_required")
39
- logger.info(line);
40
- else
41
- logger.warn(line);
41
+ logFailure(logger, "operator status", failureRecord({ connector: id, state }, failure), state === "auth_required" ? "info" : "warn");
42
42
  };
43
43
  // Status first, then the catalog only when status is ok. A failed status
44
44
  // read is an error status, not a failed row: the row still reports the
@@ -52,26 +52,31 @@ export function uiData(registry, baseUrl, options) {
52
52
  });
53
53
  }
54
54
  return Effect.gen(function* () {
55
- const status = yield* attempt(() => registry.statusFor(c.id, baseUrl, requestScope, { signal }));
55
+ const status = yield* attempt(() => registry.statusFor(c.id, baseUrl, requestScope, {
56
+ signal,
57
+ ...(options.defer ? { defer: options.defer } : {}),
58
+ }));
56
59
  if (status.state !== "ok" || signal.aborted) {
57
60
  return { status, tools: [], catalogFailed: false };
58
61
  }
59
- return yield* attempt(async () => (await registry.getTools(c.id, baseUrl, requestScope, { signal })).map((t) => ({
60
- name: t.name,
61
- address: `${c.id}.${t.name}`,
62
+ // A catalog is configuration the operator chose to load, and its
63
+ // descriptions are the agent's too, so the page shows them as text.
64
+ // A name outside MCP's tool-name grammar is withheld here as in every
65
+ // record (INV-6), and so is the address built from it.
66
+ return yield* attempt(async () => (await registry.getTools(c.id, baseUrl, requestScope, {
67
+ signal,
68
+ ...(options.defer ? { defer: options.defer } : {}),
69
+ })).map((t) => ({
70
+ name: recordedToolName(t),
71
+ address: `${c.id}.${recordedToolName(t)}`,
62
72
  ...(t.description ? { description: t.description } : {}),
63
- safety: uiToolSafety(t, isApprovalExempt(options.approval ?? NO_EXEMPTIONS, c, t.name, t)),
73
+ safety: uiToolSafety(t),
64
74
  }))).pipe(Effect.map((tools) => ({ status, tools, catalogFailed: false })),
65
75
  // The registry owns the failed catalog observation; the page only
66
76
  // needs to know there was one.
67
77
  Effect.catch(() => Effect.succeed({ status, tools: [], catalogFailed: !signal.aborted })));
68
78
  }).pipe(Effect.catch((error) => Effect.succeed({
69
- status: {
70
- state: "error",
71
- message: error instanceof Error
72
- ? error.message
73
- : "Connection details unavailable",
74
- },
79
+ status: failureStatus(c.id, error),
75
80
  tools: [],
76
81
  catalogFailed: false,
77
82
  })));
@@ -79,8 +84,7 @@ export function uiData(registry, baseUrl, options) {
79
84
  // The credential card, for someone who may manage this connector's auth.
80
85
  // Never fails: a vault that cannot be read is the card's own error.
81
86
  const credentialFor = (c) => {
82
- const mayManageAuth = options.mayManage?.(c.id) ??
83
- (c.authScope === "personal" ? Boolean(owner) : options.oauthManagement);
87
+ const mayManageAuth = options.mayManage?.(c.id) ?? (c.authScope === "personal" ? Boolean(owner) : options.oauthManagement);
84
88
  const declared = c.credential;
85
89
  if (!declared || !vault || !mayManageAuth)
86
90
  return Effect.succeed(undefined);
@@ -119,13 +123,9 @@ export function uiData(registry, baseUrl, options) {
119
123
  ...(fields?.length ? { fields } : {}),
120
124
  configured: shape.state === "valid",
121
125
  removable: Boolean(metadata),
122
- ...(metadata
123
- ? { lastFour: metadata.lastFour, updatedAt: metadata.updatedAt }
124
- : {}),
126
+ ...(metadata ? { lastFour: metadata.lastFour, updatedAt: metadata.updatedAt } : {}),
125
127
  testable: testRule.mode !== null && shape.state !== "mismatch",
126
- ...(shape.state === "mismatch"
127
- ? { error: shape.message, problem: "credential_mismatch" }
128
- : {}),
128
+ ...(shape.state === "mismatch" ? { error: shape.message, problem: "credential_mismatch" } : {}),
129
129
  // A dropped field leaves its secret in the vault, and the field
130
130
  // list below only renders fields the connector still declares —
131
131
  // so without this line there is nowhere an operator could see it.
@@ -153,7 +153,7 @@ export function uiData(registry, baseUrl, options) {
153
153
  // after this one had already ended the row.
154
154
  const { status, tools, catalogFailed } = yield* observe(c, drift, signal);
155
155
  const credential = yield* credentialFor(c);
156
- logStatus(c.id, status.state, status.message);
156
+ logStatus(c.id, status.state, statusFailure(status));
157
157
  const problem = uiProblemFor(c, status.state, {
158
158
  credentialDrift: Boolean(drift),
159
159
  catalogFailed,
@@ -173,12 +173,17 @@ export function uiData(registry, baseUrl, options) {
173
173
  // schema even if the plugin seam returned one.
174
174
  ...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
175
175
  ...(status.catalogAccess ? { catalogAccess: status.catalogAccess } : {}),
176
+ ...(status.resourceTemplateRefusals ? { resourceTemplateRefusals: status.resourceTemplateRefusals } : {}),
177
+ ...(status.state === "auth_required" && c.startAuth && options.mayManage?.(c.id) && options.oauthConnectUrl
178
+ ? { authorizationUrl: yield* attempt(() => options.oauthConnectUrl(c.id)) }
179
+ : {}),
176
180
  oauth: Boolean(c.startAuth && c.disconnectAuth),
181
+ ...(status.registrationPath ? { registrationPath: status.registrationPath } : {}),
177
182
  ...(credential ? { credential } : {}),
178
183
  };
179
184
  });
180
185
  const unavailable = (c, error) => Effect.sync(() => {
181
- logStatus(c.id, "failed", error instanceof Error ? error.message : String(error));
186
+ logStatus(c.id, "failed", error);
182
187
  return {
183
188
  id: c.id,
184
189
  ...(c.title ? { title: c.title } : {}),
package/dist/routes/ui.js CHANGED
@@ -1,9 +1,16 @@
1
+ import { operatorAsset } from "../operator-ui/assets.js";
2
+ import { operatorActivityContext } from "./activity-context.js";
3
+ import { isSafeHttpsUrl } from "../branding.js";
4
+ import { htmlSecurityHeaders } from "../html-security.js";
5
+ import { oauthConnectUrl, oauthConnectUnavailable } from "../oauth-handoff.js";
6
+ import { authorizationCredential } from "../inbound-credential.js";
1
7
  import { Effect } from "effect";
2
8
  import { CONNECTA_VERSION } from "../version.js";
3
9
  import { CONNECTA_FAVICON_ICO } from "../favicon.js";
4
- import { CONNECTA_FAVICON_SVG, operatorPageForPath, renderUiHtml, } from "../ui.js";
10
+ import { CONNECTA_FAVICON_SVG, operatorPageForPath, renderUiHtml } from "../ui.js";
5
11
  import { authorized, refuse, scopeFor, serveOperator, visibleRegistry, } from "./operator.js";
6
- import { mayManageConnector, mayViewArtifacts, privateJson, } from "./shared.js";
12
+ import { mayManageConnector, privateJson } from "./shared.js";
13
+ import { routeUiConfig } from "./ui-config.js";
7
14
  import { uiData } from "./ui-data.js";
8
15
  /**
9
16
  * Headers that make an operator-supplied favicon body inert on this origin.
@@ -32,18 +39,12 @@ const INERT_ICON_HEADERS = {
32
39
  "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
33
40
  "X-Content-Type-Options": "nosniff",
34
41
  };
35
- /** Per-request base64 nonce for an operator shell's scripts (Node 22+ and Workers). */
36
- function uiScriptNonce() {
37
- const bytes = crypto.getRandomValues(new Uint8Array(16));
38
- let binary = "";
39
- for (const byte of bytes)
40
- binary += String.fromCharCode(byte);
41
- return btoa(binary);
42
- }
43
42
  export async function routeUi(context) {
44
43
  const { request, url, path, baseUrl, opts, runtimeContext } = context;
44
+ if (path.startsWith("/ui/assets/"))
45
+ return operatorAsset(request, path);
45
46
  if (request.method === "GET" && path === "/favicon.svg") {
46
- return new Response(opts.branding?.favicon?.svg ?? CONNECTA_FAVICON_SVG, {
47
+ return new Response(opts.config.ui?.branding?.favicon?.svg ?? CONNECTA_FAVICON_SVG, {
47
48
  headers: {
48
49
  "Content-Type": "image/svg+xml",
49
50
  "Cache-Control": "public, max-age=86400",
@@ -52,7 +53,7 @@ export async function routeUi(context) {
52
53
  });
53
54
  }
54
55
  if (request.method === "GET" && path === "/favicon.ico") {
55
- return new Response(opts.branding?.favicon?.ico ?? CONNECTA_FAVICON_ICO, {
56
+ return new Response(opts.config.ui?.branding?.favicon?.ico ?? CONNECTA_FAVICON_ICO, {
56
57
  headers: {
57
58
  "Content-Type": "image/x-icon",
58
59
  "Cache-Control": "public, max-age=86400",
@@ -71,50 +72,36 @@ export async function routeUi(context) {
71
72
  });
72
73
  }
73
74
  const operatorPage = operatorPageForPath(path);
74
- const artifactPage = operatorPage === "artifacts" || operatorPage === "artifact";
75
75
  if (operatorPage &&
76
- (operatorPage !== "activity" || opts.activity?.list) &&
77
- (operatorPage !== "tokens" || opts.accessTokens) &&
78
- (!artifactPage || opts.artifactsModule)) {
76
+ (operatorPage !== "activity" || opts.config.activity?.store.list) &&
77
+ (operatorPage !== "tokens" || opts.config.accessTokens)) {
79
78
  if (request.method !== "GET" && request.method !== "HEAD") {
80
79
  return privateJson({ error: "method not allowed" }, { status: 405 });
81
80
  }
82
81
  // Open shell — carries no operator data; everything comes from the
83
82
  // authenticated /ui/* APIs after the browser establishes a session.
84
- const ambient = runtimeContext?.access
85
- ? opts.auth.find((provider) => provider.uiAuth?.kind === "cloudflare-access")?.uiAuth
83
+ const ambient = authorizationCredential(request).kind === "absent" && runtimeContext?.access
84
+ ? opts.config.auth.find((provider) => provider.uiAuth?.kind === "cloudflare-access")?.uiAuth
86
85
  : undefined;
87
- const uiAuth = ambient ?? opts.auth.find((provider) => provider.uiAuth && provider.uiAuth.kind !== "cloudflare-access")?.uiAuth;
86
+ const uiAuth = ambient ??
87
+ opts.config.auth.find((provider) => provider.uiAuth && provider.uiAuth.kind !== "cloudflare-access")?.uiAuth;
88
88
  const mcpUrl = new URL("/mcp", baseUrl).toString();
89
- // Nonce the page's inline script (and the Clerk loader). 'strict-dynamic'
90
- // lets scripts the nonced Clerk loader injects at runtime execute; the
91
- // https:/'unsafe-inline' fallbacks are ignored by CSP3 browsers that
92
- // honour the nonce and only cover legacy ones. No default-src, so Clerk's
93
- // style/font/network needs and the page's inline <style> stay unrestricted
94
- // — only script execution, the XSS sink, is gated.
95
- const nonce = uiScriptNonce();
96
- // An artifact shell frames the sandboxed page from its own origin and
97
- // nothing else; Connections stays on the deployment's public origin.
98
- const homeUrl = opts.publicUrl ? new URL("/", opts.publicUrl).toString() : "/";
99
- // The artifact host serves no favicon, so a shell there takes its icons
100
- // from the public origin, as the 404 does; on the main host they stay
101
- // root-relative.
102
- const onArtifactHost = Boolean(opts.artifactOrigin) &&
103
- url.origin === new URL(opts.artifactOrigin).origin;
104
- const iconOrigin = onArtifactHost ? opts.publicUrl : undefined;
89
+ const clerkOrigin = uiAuth?.kind === "clerk" && isSafeHttpsUrl(uiAuth.frontendApiUrl)
90
+ ? new URL(uiAuth.frontendApiUrl).origin
91
+ : undefined;
105
92
  return new Response(request.method === "HEAD"
106
93
  ? null
107
- : renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce, operatorPage, { homeUrl, iconOrigin }), {
94
+ : renderUiHtml(uiAuth, mcpUrl, opts.config.ui?.branding, undefined, operatorPage), {
108
95
  status: 200,
109
- headers: {
96
+ headers: htmlSecurityHeaders({
110
97
  "Content-Type": "text/html; charset=utf-8",
111
- "Content-Security-Policy": `script-src 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline'; ` +
112
- "object-src 'none'; base-uri 'none'; frame-ancestors 'none'" +
113
- (artifactPage ? "; frame-src 'self'" : ""),
114
- "X-Content-Type-Options": "nosniff",
115
- },
98
+ "Cache-Control": "no-store",
99
+ }, clerkOrigin ? { clerkOrigin } : {}),
116
100
  });
117
101
  }
102
+ const configResponse = await routeUiConfig(context);
103
+ if (configResponse)
104
+ return configResponse;
118
105
  const detail = /^\/ui\/connectors\/([a-z0-9_-]+)$/.exec(path);
119
106
  if (path !== "/ui/data" && !detail)
120
107
  return null;
@@ -126,16 +113,23 @@ export async function routeUi(context) {
126
113
  /** What this identity may see and do, for the summary and a detail alike. */
127
114
  function operatorView({ opts }, authz, registry) {
128
115
  const visible = registry.listConnectors();
129
- const mayManage = (id) => { const connector = registry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); };
116
+ const mayManage = (id) => {
117
+ const connector = registry.getConnector(id);
118
+ return Boolean(connector && mayManageConnector(authz, connector));
119
+ };
130
120
  const permissions = (connector) => ({
131
121
  use: true,
132
122
  manageSharedAuth: connector.authScope !== "personal" && mayManage(connector.id),
133
123
  connectPersonal: connector.authScope === "personal" && mayManage(connector.id),
134
124
  });
135
- const activityEnabled = Boolean(opts.activity?.list) && authz.operator;
136
- const credentialManagement = visible.some(c => c.credential && mayManage(c.id))
137
- ? opts.credentialVault ? "available" : "vault_not_configured"
138
- : authz.identity.interactive && !visible.some(c => c.credential) ? "no_slots" : "requires_operator";
125
+ const activityEnabled = Boolean(opts.config.activity?.store.list) && authz.operator;
126
+ const credentialManagement = visible.some((c) => c.credential && mayManage(c.id))
127
+ ? opts.config.vault
128
+ ? "available"
129
+ : "vault_not_configured"
130
+ : authz.identity.interactive && !visible.some((c) => c.credential)
131
+ ? "no_slots"
132
+ : "requires_operator";
139
133
  return { visible, mayManage, permissions, activityEnabled, credentialManagement };
140
134
  }
141
135
  /** One connector's probed row, the page's second request per card. */
@@ -149,18 +143,21 @@ function connectorDetail(context, id) {
149
143
  if (!connector)
150
144
  return yield* refuse("unknown connector", 404);
151
145
  const data = yield* uiData(opts.registry.scoped(scopeFor(authz, [connector.id])), baseUrl, {
152
- serverInfo: opts.serverInfo,
153
- credentialVault: opts.credentialVault,
146
+ serverInfo: opts.config.serverInfo,
147
+ activityContext: operatorActivityContext(context, authz),
148
+ credentialVault: opts.config.vault,
154
149
  activityEnabled,
155
150
  credentialManagement,
156
151
  defer,
157
152
  oauthManagement: false,
153
+ ...(!oauthConnectUnavailable(opts)
154
+ ? { oauthConnectUrl: (id) => oauthConnectUrl(opts, baseUrl, id, authz.principalKey) }
155
+ : {}),
158
156
  discoveryConcurrency: 1,
159
157
  personalCredentialOwner: authz.principalKey,
160
158
  mayManage,
161
- timeoutMs: opts.probeTimeoutMs ?? 30_000,
159
+ timeoutMs: opts.config.discovery.probeTimeoutMs,
162
160
  signal: request.signal,
163
- approval: opts.approval,
164
161
  });
165
162
  return privateJson({ ...data.connectors[0], permissions: permissions(connector) });
166
163
  });
@@ -185,14 +182,27 @@ function summary(context) {
185
182
  }
186
183
  return privateJson({
187
184
  ...(pools.length ? { pools } : {}),
188
- serverInfo: opts.serverInfo,
185
+ serverInfo: opts.config.serverInfo,
189
186
  connectaVersion: CONNECTA_VERSION,
190
187
  activityEnabled,
191
- ...(opts.accessTokens ? { accessTokenManagement: authz.accessTokenManagement && authz.identity.principal ? "available" : "requires_operator" } : {}),
192
- ...(opts.artifactsModule && mayViewArtifacts(authz, opts.registry) ? { artifactsEnabled: true } : {}),
188
+ ...(opts.config.accessTokens
189
+ ? {
190
+ accessTokenManagement: authz.accessTokenManagement && authz.identity.principal ? "available" : "requires_operator",
191
+ }
192
+ : {}),
193
193
  credentialManagement,
194
- oauthManagement: visible.some(c => mayManage(c.id)),
195
- connectors: visible.map(c => ({ id: c.id, ...(c.title ? { title: c.title } : {}), ...(c.description ? { description: c.description } : {}), authScope: c.authScope ?? "shared", status: "loading", toolCount: 0, tools: [], oauth: Boolean(c.startAuth && c.disconnectAuth), permissions: permissions(c) })),
194
+ oauthManagement: visible.some((c) => mayManage(c.id)),
195
+ connectors: visible.map((c) => ({
196
+ id: c.id,
197
+ ...(c.title ? { title: c.title } : {}),
198
+ ...(c.description ? { description: c.description } : {}),
199
+ authScope: c.authScope ?? "shared",
200
+ status: "loading",
201
+ toolCount: 0,
202
+ tools: [],
203
+ oauth: Boolean(c.startAuth && c.disconnectAuth),
204
+ permissions: permissions(c),
205
+ })),
196
206
  });
197
207
  });
198
208
  }
@@ -30,7 +30,7 @@ export function admit(controller, options = {}) {
30
30
  if (!program) {
31
31
  throw new Error("AdmissionController's admission program was never installed.");
32
32
  }
33
- return program(controller, options.signal);
33
+ return program(controller, options.signal, options.wait !== false);
34
34
  }
35
35
  /**
36
36
  * A lease owned by the enclosing Scope: released when the scope closes, on
@@ -39,5 +39,7 @@ export function admit(controller, options = {}) {
39
39
  * in line until the queue timeout.
40
40
  */
41
41
  export function acquireScoped(controller, options = {}) {
42
- return Effect.acquireRelease(admit(controller, options), (lease) => Effect.sync(() => lease.release()), { interruptible: true });
42
+ return Effect.acquireRelease(admit(controller, options), (lease) => Effect.sync(() => lease.release()), {
43
+ interruptible: true,
44
+ });
43
45
  }
@@ -63,5 +63,7 @@ export function admitCall(controller, request) {
63
63
  * the queue timeout.
64
64
  */
65
65
  export function acquireCallScoped(controller, request) {
66
- return Effect.acquireRelease(admitCall(controller, request), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
66
+ return Effect.acquireRelease(admitCall(controller, request), (permit) => Effect.sync(() => permit.release()), {
67
+ interruptible: true,
68
+ });
67
69
  }
@@ -23,10 +23,7 @@ const CONNECTOR_SCOPE_DEFER_BUDGET_MS = 2_000;
23
23
  * cancel a connector's close, only stop waiting for it.
24
24
  */
25
25
  function waitAtMost(work, budgetMs) {
26
- return Effect.raceAllFirst([
27
- Effect.promise(() => work),
28
- Effect.sleep(Duration.millis(budgetMs)),
29
- ]);
26
+ return Effect.raceAllFirst([Effect.promise(() => work), Effect.sleep(Duration.millis(budgetMs))]);
30
27
  }
31
28
  /**
32
29
  * Tell a connector that a scope owned by the core has ended, with the contract
@@ -3,7 +3,7 @@
3
3
  // Connecta's published surface is Promise-shaped and stays that way; Effect is
4
4
  // an implementation detail behind it. Every edge where a fiber starts runs
5
5
  // through this module, so the rules for that crossing live in one file instead
6
- // of being re-derived at each call site — and test/purity.test.ts fails if any
6
+ // of being re-derived at each call site — and test/purity.node.test.ts fails if any
7
7
  // other file under src/ calls Effect.run*, runFork, forkDaemon, or
8
8
  // ManagedRuntime.make.
9
9
  //
@@ -18,7 +18,7 @@
18
18
  // - Effect.runPromise rejects an interrupted fiber with a generic "All fibers
19
19
  // interrupted without error". Callers of withDeadline today see the abort
20
20
  // reason the caller chose, and runEdge keeps it that way.
21
- import { Cause, Context, Duration, Effect, Exit, ManagedRuntime, Scheduler, } from "effect";
21
+ import { Cause, Context, Duration, Effect, Exit, Scheduler } from "effect";
22
22
  /** Yield to the scheduler through the microtask queue, never a timer. */
23
23
  function setMicrotask(task) {
24
24
  let cancelled = false;
@@ -34,9 +34,7 @@ function setMicrotask(task) {
34
34
  export const microtaskScheduler = new Scheduler.MixedScheduler("async", setMicrotask);
35
35
  const runExit = Effect.runPromiseExitWith(Context.make(Scheduler.Scheduler, microtaskScheduler));
36
36
  function abortReason(signal) {
37
- return signal && signal.reason !== undefined
38
- ? signal.reason
39
- : new DOMException("aborted", "AbortError");
37
+ return signal && signal.reason !== undefined ? signal.reason : new DOMException("aborted", "AbortError");
40
38
  }
41
39
  /**
42
40
  * Turn a failed Exit back into the value a Promise caller would have seen.
@@ -59,43 +57,16 @@ function rethrow(cause, signal) {
59
57
  throw defect.defect;
60
58
  throw abortReason(signal);
61
59
  }
62
- const disposedRuntimes = new WeakSet();
63
- /** Wrap a layer as an EdgeRuntime without building it. */
64
- export function makeEdgeRuntime(layer) {
65
- return ManagedRuntime.make(layer);
66
- }
67
- /**
68
- * Release a runtime's layer resources. Idempotent, and never rejects: a
69
- * finalizer that fails has nobody left to report to.
70
- */
71
- export async function disposeEdgeRuntime(runtime) {
72
- if (disposedRuntimes.has(runtime))
73
- return;
74
- disposedRuntimes.add(runtime);
75
- // Through runExit rather than runtime.dispose(), whose runner yields on
76
- // Effect's default scheduler — the one vitest's fake timers freeze.
77
- await runExit(runtime.disposeEffect);
78
- }
79
60
  /**
80
- * The effect with the runtime's services provided, building them first when
81
- * no run has yet. The build runs on the calling fiber's scheduler, so it is
82
- * the microtask scheduler here, as everything else is.
61
+ * Run an effect at a Promise boundary.
62
+ *
63
+ * Resolves with the success value. Rejects with the original Fail error or Die
64
+ * defect, and with `signal.reason` (or an AbortError) when interrupted. The
65
+ * effect requires no services: a caller that needs some provides them first
66
+ * (src/runtime/storage.ts).
83
67
  */
84
- function provideRuntime(effect, runtime) {
85
- if (disposedRuntimes.has(runtime)) {
86
- return Effect.die(new Error("This Connecta has been closed."));
87
- }
88
- const built = runtime.cachedContext;
89
- if (built)
90
- return Effect.provideContext(effect, built);
91
- return Effect.flatMap(runtime.contextEffect, (context) => Effect.provideContext(effect, context));
92
- }
93
68
  export async function runEdge(effect, options = {}) {
94
- const exit = await runExit(options.runtime
95
- ? provideRuntime(effect, options.runtime)
96
- // Without a runtime the overloads admit only an effect that requires
97
- // nothing, so there is nothing to provide.
98
- : effect, options.signal ? { signal: options.signal } : undefined);
69
+ const exit = await runExit(effect, options.signal ? { signal: options.signal } : undefined);
99
70
  if (Exit.isSuccess(exit))
100
71
  return exit.value;
101
72
  return rethrow(exit.cause, options.signal);
@@ -1,130 +1,25 @@
1
1
  // The core's services, as Effect code sees them.
2
2
  //
3
- // createConnecta resolves storage, the vault, activity history, logging, and
4
- // the rest of its configuration once, at construction. This module gives each
5
- // of those a service key and assembles them into the layer behind that
6
- // Connecta's runtime (an EdgeRuntime, src/runtime/run.ts), so an Effect
7
- // program reads them from its context rather than from arguments threaded
8
- // through every call. The shells — Registry, CatalogService, and the rest —
9
- // stay constructible from plain arguments, as the tests construct them. The
10
- // registry's storage programs read Storage and Logger too, but from their own
11
- // partition rather than this runtime (see src/runtime/storage.ts for why).
12
- //
13
- // Every import from an optional module is type-only. An omitted activity
14
- // module is a no-op recorder that builds no event and calls nothing, and the
15
- // UI, vault, and activity implementations stay out of the root import graph
16
- // (test/purity.test.ts).
17
- import { Context, Effect, Layer } from "effect";
18
- import { makeEdgeRuntime } from "./run.js";
3
+ // A registry's storage programs read the deployment's KVStorage and Logger
4
+ // from their context rather than from arguments threaded through every call.
5
+ // Each Registry provides its own pair (`runOnPartition` in
6
+ // src/runtime/storage.ts), because a personal registry's storage is the
7
+ // root's namespaced to its principal. There is no per-Connecta runtime:
8
+ // createConnecta resolves its configuration into plain values once
9
+ // (src/config.ts), and the request pipeline runs on no runtime so `/health`
10
+ // and a closed deployment's 503 keep answering after `close()`.
11
+ import { Context } from "effect";
19
12
  /**
20
- * The deployment's KVStorage: `config.storage`, or the memoryStorage() that
21
- * stands in for it. `compareAndSet` is present exactly when the adapter
22
- * implements it, so a caller that needs atomicity checks for it here.
13
+ * A registry's KVStorage partition. Every adapter implements `list` and
14
+ * `compareAndSet`.
23
15
  */
24
16
  export class Storage extends Context.Service()("connecta/Storage") {
25
17
  }
26
- /**
27
- * The credential vault, or undefined when the deployment configures none —
28
- * a required service whose value may be absent, not a default, so a program
29
- * run without it provided fails to typecheck instead of finding no vault.
30
- */
31
- export class Vault extends Context.Service()("connecta/Vault") {
32
- }
33
18
  /**
34
19
  * Connecta's diagnostic logger, already resolved: the configured Logger, a
35
20
  * no-op for `"silent"`, or console output prefixed `[connecta]`. Effect's
36
- * own logger is never used (test/purity.test.ts), because it honors neither
21
+ * own logger is never used (test/purity.node.test.ts), because it honors neither
37
22
  * `"silent"` nor the line format a deployment greps for.
38
23
  */
39
24
  export class Logger extends Context.Service()("connecta/Logger") {
40
25
  }
41
- /** Payload-free activity history, or a recorder that records nothing. */
42
- export class ActivityRecorder extends Context.Service()("connecta/ActivityRecorder") {
43
- }
44
- /** What createConnecta resolved at construction. */
45
- export class ResolvedConfig extends Context.Service()("connecta/ResolvedConfig") {
46
- }
47
- /**
48
- * The request's hook for work that may outlive its response (a Worker's
49
- * `ctx.waitUntil`). Per request, not per Connecta, so it is never part of the
50
- * runtime: a request provides it with `Effect.provideService`, and a program
51
- * outside any request sees undefined and keeps its work in the foreground.
52
- */
53
- export const DeferredWork = Context.Reference("connecta/DeferredWork", { defaultValue: () => undefined });
54
- /**
55
- * Resolve `ConnectaConfig.logger`. `"silent"` discards every line; an omitted
56
- * logger writes to the console with the `[connecta]` prefix.
57
- */
58
- export function resolveLogger(logger) {
59
- if (logger === "silent") {
60
- return { debug() { }, info() { }, warn() { }, error() { } };
61
- }
62
- return logger ?? {
63
- debug: (...a) => console.debug("[connecta]", ...a),
64
- info: (...a) => console.info("[connecta]", ...a),
65
- warn: (...a) => console.warn("[connecta]", ...a),
66
- error: (...a) => console.error("[connecta]", ...a),
67
- };
68
- }
69
- const NO_ACTIVITY = {
70
- enabled: false,
71
- recordTool: () => Effect.void,
72
- recordDrift: () => Effect.void,
73
- };
74
- function activityRecorder(module, serverInfo, logger) {
75
- const deployment = module.deploymentId !== undefined
76
- ? { deploymentId: module.deploymentId }
77
- : {};
78
- // A custom module's recorder that throws is logged like a store that
79
- // throws: activity never changes the outcome of the work it describes.
80
- const bestEffort = (record) => Effect.sync(() => {
81
- try {
82
- record();
83
- }
84
- catch (error) {
85
- logger.warn("[connecta] activity record failed", error);
86
- }
87
- });
88
- return {
89
- enabled: true,
90
- recordTool: (request, input) => DeferredWork.use((defer) => bestEffort(() => module.recordTool({
91
- sink: module.store,
92
- recordTool: module.recordTool,
93
- actor: request.actor,
94
- requestId: request.requestId,
95
- serverInfo,
96
- ...deployment,
97
- ...(defer ? { defer } : {}),
98
- logger,
99
- }, input))),
100
- recordDrift: (input) => bestEffort(() => module.recordDrift({ sink: module.store, serverInfo, ...deployment, logger }, input)),
101
- };
102
- }
103
- /**
104
- * The layer behind a Connecta's runtime. Every service is a value resolved
105
- * before this is called, so building the layer only assembles a context: it
106
- * starts no fiber and touches no storage.
107
- */
108
- function coreServices(options) {
109
- const activity = options.activity
110
- ? activityRecorder(options.activity, options.config.serverInfo, options.logger)
111
- : NO_ACTIVITY;
112
- return Layer.succeedContext(Context.make(Storage, options.storage).pipe(Context.add(Vault, options.vault), Context.add(Logger, options.logger), Context.add(ActivityRecorder, activity), Context.add(ResolvedConfig, options.config)));
113
- }
114
- // Keyed by the root Registry: it is the per-Connecta object every internal
115
- // caller already holds, and a Registry built directly (as the tests build
116
- // them) simply has no runtime.
117
- const runtimes = new WeakMap();
118
- /**
119
- * Create the runtime for the Connecta that owns `registry`. Runs nothing:
120
- * the layer is built by the first run that needs it (see EdgeRuntime).
121
- */
122
- export function createCoreRuntime(registry, options) {
123
- const runtime = makeEdgeRuntime(coreServices(options));
124
- runtimes.set(registry, runtime);
125
- return runtime;
126
- }
127
- /** The runtime createConnecta made for this root registry, if it made one. */
128
- export function coreRuntime(registry) {
129
- return runtimes.get(registry);
130
- }
@@ -75,9 +75,7 @@ export class SharedRead {
75
75
  this.cancel(signal?.reason);
76
76
  });
77
77
  const answer = Deferred.await(this.outcome);
78
- const own = signal
79
- ? Effect.raceFirst(answer, this.leftBy(signal))
80
- : answer;
78
+ const own = signal ? Effect.raceFirst(answer, this.leftBy(signal)) : answer;
81
79
  return Effect.ensuring(own, leave);
82
80
  });
83
81
  }