@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -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/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,111 @@
1
+ import { boundedEchoText } from "./errors.js";
2
+ function oauthFollowUp(connectorId) {
3
+ return `Then retry the original call; connecta.search({ connector: ${JSON.stringify(connectorId)} }) inside execute_code confirms the catalog now loads.`;
4
+ }
5
+ /** Shared with explicit authorization; errors never request a forced restart. */
6
+ export async function authorizationHandoff(registry, baseUrl, requestScope, opts, args) {
7
+ const connector = registry.getConnector(args.connector);
8
+ if (!connector) {
9
+ throw new Error(`Unknown connector "${boundedEchoText(args.connector)}"`);
10
+ }
11
+ if (!connector.startAuth) {
12
+ if (!connector.credential) {
13
+ return {
14
+ connector: connector.id,
15
+ recovery: "unavailable",
16
+ message: `Connector "${connector.id}" declares neither downstream OAuth ` +
17
+ "nor an operator-managed credential slot. Update the connector " +
18
+ "or deployment configuration before retrying.",
19
+ };
20
+ }
21
+ const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
22
+ if (!ctx.credential || !opts.credentialHandoffUrl) {
23
+ return {
24
+ connector: connector.id,
25
+ recovery: "unavailable",
26
+ message: "Credential recovery needs both a vault and the optional UI. Configure " +
27
+ "vault and ui in deployment code, then call " +
28
+ "authorize_connector again.",
29
+ };
30
+ }
31
+ const fields = connector.credential.fields?.map((field) => ({
32
+ name: field.name,
33
+ guidance: field.description ?? field.label,
34
+ })) ?? [
35
+ {
36
+ name: "value",
37
+ guidance: connector.credential.description ?? connector.credential.label,
38
+ },
39
+ ];
40
+ return {
41
+ connector: connector.id,
42
+ recovery: "operator_config",
43
+ credential: {
44
+ label: connector.credential.label,
45
+ fields,
46
+ },
47
+ operatorUrl: opts.credentialHandoffUrl,
48
+ instructions: "Have the operator open operatorUrl, set and test the credential, " +
49
+ "then retry the original call. No redeploy is needed. " +
50
+ (connector.authScope === "personal"
51
+ ? "Credential mutation requires the signed-in principal who owns this connection."
52
+ : "Shared credential mutation requires a signed-in human with access to this connector."),
53
+ };
54
+ }
55
+ if (opts.oauthConnectUnavailable || !opts.oauthConnectUrl)
56
+ return {
57
+ connector: connector.id,
58
+ recovery: "unavailable",
59
+ message: opts.oauthConnectUnavailable ??
60
+ "OAuth connection requires an interactive provider and a credential vault with a handoff signing key.",
61
+ };
62
+ if (!opts.canManageAuth?.(connector.id))
63
+ return {
64
+ connector: connector.id,
65
+ recovery: "unavailable",
66
+ message: "Your identity is not permitted to manage authentication for this connection.",
67
+ };
68
+ return {
69
+ connector: connector.id,
70
+ recovery: "oauth",
71
+ status: "auth_required",
72
+ authorizationUrl: await opts.oauthConnectUrl(connector.id, args.force),
73
+ instructions: "Copy authorizationUrl exactly as returned; do not decode or re-encode it. Open it in a browser and sign in as the user who requested this connection. Connecta verifies your identity and permission before starting consent. " +
74
+ oauthFollowUp(connector.id),
75
+ };
76
+ }
77
+ const requests = new WeakMap();
78
+ /** Retain the admitted route's factory when program utilities share its scope. */
79
+ export function bindAuthorizationHandoffs(scope, create) {
80
+ if (!requests.has(scope))
81
+ requests.set(scope, { create, enabled: true, handoffs: new Map() });
82
+ }
83
+ export function enableAuthorizationHandoffs(scope, enabled) {
84
+ const request = requests.get(scope);
85
+ if (request)
86
+ request.enabled = enabled;
87
+ }
88
+ /** One promise per connector also bounds concurrent errors in a program. */
89
+ export async function withAuthorizationHandoff(scope, error) {
90
+ const request = requests.get(scope);
91
+ if (!request?.enabled ||
92
+ !error.nextAction ||
93
+ !("tool" in error.nextAction) ||
94
+ error.nextAction.tool !== "authorize_connector")
95
+ return error;
96
+ const connector = error.nextAction.arguments.connector;
97
+ let handoff = request.handoffs.get(connector);
98
+ if (!handoff) {
99
+ handoff = request.create(connector);
100
+ request.handoffs.set(connector, handoff);
101
+ }
102
+ try {
103
+ const value = await handoff;
104
+ return { ...error, ...value, message: error.message, ...(value.message ? { instructions: value.message } : {}) };
105
+ }
106
+ catch {
107
+ // A signing/configuration failure cannot replace the original call failure
108
+ // or leak an arbitrary vault exception into the recovery envelope.
109
+ return error;
110
+ }
111
+ }
@@ -48,25 +48,18 @@ export declare function droppedThemeTokens(theme?: ConnectaTheme): string[];
48
48
  * anything that would need escaping is dropped rather than rewritten.
49
49
  */
50
50
  export declare function themeCss(theme: ResolvedTheme): string;
51
- /**
52
- * The resolved scheme as the root element's attribute. The dark palette keys
53
- * off `html[data-scheme]`, so "system" leaves it off and the media query
54
- * decides.
55
- */
56
- export declare function schemeAttribute(theme: ResolvedTheme): string;
57
51
  /** Text or attribute value, escaped for any position in an HTML document. */
58
52
  export declare function escapeHtml(value: string): string;
59
53
  export interface PageLayout {
60
54
  /** The complete `<title>`. */
61
55
  title: string;
56
+ /** The SPA puts the masthead in its sidebar. */
57
+ operatorShell?: boolean;
62
58
  /** Whether the operator UI is mounted, and so serves `/favicon.*` and `/`. */
63
59
  uiMounted: boolean;
64
60
  /**
65
61
  * The origin that serves `/favicon.*`, for a page that may be rendered on
66
- * another host: root-relative icon hrefs resolve against it. A browser 404
67
- * on the artifact host passes `publicUrl`, since that host answers every
68
- * non-artifact path, the favicons included, with 404. Omitted, they stay
69
- * root-relative.
62
+ * another host: root-relative icon hrefs resolve against it. Omitted, they stay root-relative.
70
63
  */
71
64
  iconOrigin?: string | undefined;
72
65
  /** Markup after the masthead: the page's `<main>`. */
@@ -119,15 +112,14 @@ export declare const STATUS_ICONS: {
119
112
  /**
120
113
  * The 404 a browser gets for a path nothing serves, and everyone else's plain
121
114
  * `Not Found`. The body is built from deployment configuration alone and names
122
- * no path, so every unserved path — on the main host or an artifact host —
123
- * answers with the same bytes for the same `Accept`: a 404 must not tell one
115
+ * no path, so every unserved path answers with the same bytes for the same `Accept`: a 404 must not tell one
124
116
  * route from another. The home link appears only when the operator UI is
125
- * mounted to land on, and points at the public origin so an artifact host's
126
- * 404 does not link to another 404.
117
+ * mounted to land on, and points at the configured public origin.
127
118
  */
128
119
  export declare function notFoundResponse(request: Request, opts: {
129
- branding?: ConnectaBranding | undefined;
130
- ui?: object | undefined;
120
+ ui?: {
121
+ readonly branding?: ConnectaBranding | undefined;
122
+ } | undefined;
131
123
  publicUrl?: string | undefined;
132
124
  }): Response;
133
125
  /**
package/dist/branding.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PAGE_CSS, TOKENS_CSS } from "./page-styles.js";
2
+ import { htmlSecurityHeaders } from "./html-security.js";
2
3
  /** Connecta's default monochrome "C" mark. */
3
4
  export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
4
5
  <style>
@@ -33,11 +34,8 @@ export function resolveBranding(branding) {
33
34
  ...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
34
35
  description: trimmedString(branding?.description) ??
35
36
  `Manage the services this ${productName} instance makes available to agents.`,
36
- pageTitle: trimmedString(branding?.pageTitle) ??
37
- (ownerName ? `${productName} — ${ownerName}` : productName),
38
- faviconHref: faviconHref && isSafeIconHref(faviconHref)
39
- ? faviconHref
40
- : DEFAULT_FAVICON_HREF,
37
+ pageTitle: trimmedString(branding?.pageTitle) ?? (ownerName ? `${productName} — ${ownerName}` : productName),
38
+ faviconHref: faviconHref && isSafeIconHref(faviconHref) ? faviconHref : DEFAULT_FAVICON_HREF,
41
39
  themeColor: configuredThemeColor(branding) ?? "#ffffff",
42
40
  theme: resolveTheme(branding?.theme),
43
41
  };
@@ -63,8 +61,7 @@ function isFontStack(value) {
63
61
  if (value.length > 200)
64
62
  return false;
65
63
  const names = value.split(",");
66
- return names.length <= 12 &&
67
- names.every((name) => FONT_NAME.test(name.trim()));
64
+ return names.length <= 12 && names.every((name) => FONT_NAME.test(name.trim()));
68
65
  }
69
66
  const COLOR_SCHEMES = ["system", "light", "dark"];
70
67
  /**
@@ -96,9 +93,7 @@ export function resolveTheme(theme) {
96
93
  */
97
94
  function radiusLength(radius) {
98
95
  if (typeof radius === "number") {
99
- return Number.isFinite(radius) && radius >= 0 && radius <= 999
100
- ? `${radius}px`
101
- : undefined;
96
+ return Number.isFinite(radius) && radius >= 0 && radius <= 999 ? `${radius}px` : undefined;
102
97
  }
103
98
  const value = trimmedString(radius);
104
99
  if (!value || !CSS_LENGTH.test(value))
@@ -128,8 +123,7 @@ export function droppedThemeTokens(theme) {
128
123
  }
129
124
  // Compared against the trimmed value the resolver reads, so `" dark "` is
130
125
  // not reported as dropped when it was applied.
131
- if (isSetValue(theme.colorScheme) &&
132
- trimmedString(theme.colorScheme) !== resolved.colorScheme) {
126
+ if (isSetValue(theme.colorScheme) && trimmedString(theme.colorScheme) !== resolved.colorScheme) {
133
127
  dropped.push("colorScheme");
134
128
  }
135
129
  return dropped.map((token) => `theme.${token}`);
@@ -154,18 +148,12 @@ export function themeCss(theme) {
154
148
  * off `html[data-scheme]`, so "system" leaves it off and the media query
155
149
  * decides.
156
150
  */
157
- export function schemeAttribute(theme) {
158
- return theme.colorScheme === "system"
159
- ? ""
160
- : ` data-scheme="${theme.colorScheme}"`;
151
+ function schemeAttribute(theme) {
152
+ return theme.colorScheme === "system" ? "" : ` data-scheme="${theme.colorScheme}"`;
161
153
  }
162
154
  /** Text or attribute value, escaped for any position in an HTML document. */
163
155
  export function escapeHtml(value) {
164
- return value
165
- .replaceAll("&", "&amp;")
166
- .replaceAll('"', "&quot;")
167
- .replaceAll("<", "&lt;")
168
- .replaceAll(">", "&gt;");
156
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
169
157
  }
170
158
  /**
171
159
  * The masthead's `--surface` in each palette, for the browser chrome a
@@ -184,8 +172,8 @@ function themeColorMeta(branding, theme) {
184
172
  if (theme.colorScheme !== "system") {
185
173
  return `<meta name="theme-color" content="${SURFACE[theme.colorScheme]}">`;
186
174
  }
187
- return `<meta name="theme-color" content="${SURFACE.light}" media="(prefers-color-scheme: light)">\n` +
188
- `<meta name="theme-color" content="${SURFACE.dark}" media="(prefers-color-scheme: dark)">`;
175
+ return (`<meta name="theme-color" content="${SURFACE.light}" media="(prefers-color-scheme: light)">\n` +
176
+ `<meta name="theme-color" content="${SURFACE.dark}" media="(prefers-color-scheme: dark)">`);
189
177
  }
190
178
  /**
191
179
  * The one page layout every server-rendered page shares: the operator shell,
@@ -212,22 +200,16 @@ export function renderPage(branding, layout) {
212
200
  const owner = brand.ownerName
213
201
  ? label("brand", brand.ownerName, brand.ownerUrl)
214
202
  : label("brand", brand.productName, brand.productUrl);
215
- const product = brand.ownerName
216
- ? label("product", brand.productName, brand.productUrl)
217
- : "";
203
+ const product = brand.ownerName ? label("product", brand.productName, brand.productUrl) : "";
218
204
  const customIcon = brand.faviconHref !== DEFAULT_FAVICON_HREF;
219
205
  // `isSafeIconHref` already limited a relative href to one leading slash, so
220
206
  // resolving it can only land on `iconOrigin` itself.
221
- const iconHref = (href) => layout.iconOrigin && href.startsWith("/")
222
- ? new URL(href, layout.iconOrigin).toString()
223
- : href;
207
+ const iconHref = (href) => layout.iconOrigin && href.startsWith("/") ? new URL(href, layout.iconOrigin).toString() : href;
224
208
  const icons = [
225
209
  ...(customIcon || layout.uiMounted
226
210
  ? [`<link rel="icon" href="${escapeHtml(iconHref(brand.faviconHref))}" type="image/svg+xml">`]
227
211
  : []),
228
- ...(layout.uiMounted
229
- ? [`<link rel="shortcut icon" href="${escapeHtml(iconHref("/favicon.ico"))}">`]
230
- : []),
212
+ ...(layout.uiMounted ? [`<link rel="shortcut icon" href="${escapeHtml(iconHref("/favicon.ico"))}">`] : []),
231
213
  ];
232
214
  const skip = layout.skipTo
233
215
  ? `<a class="skip-link" href="#${escapeHtml(layout.skipTo.id)}">${escapeHtml(layout.skipTo.label)}</a>\n`
@@ -245,7 +227,7 @@ ${icons.join("\n")}
245
227
  ${layout.head ?? ""}
246
228
  <style>${styles}${themeCss(brand.theme)}</style>
247
229
  </head>
248
- <body>
230
+ <body${layout.operatorShell ? ' class="operator-shell"' : ""}>
249
231
  ${skip}<header class="masthead shell">
250
232
  <div class="masthead-inner">
251
233
  <div class="mast-nav">
@@ -278,17 +260,15 @@ function wantsHtml(request) {
278
260
  /**
279
261
  * The 404 a browser gets for a path nothing serves, and everyone else's plain
280
262
  * `Not Found`. The body is built from deployment configuration alone and names
281
- * no path, so every unserved path — on the main host or an artifact host —
282
- * answers with the same bytes for the same `Accept`: a 404 must not tell one
263
+ * no path, so every unserved path answers with the same bytes for the same `Accept`: a 404 must not tell one
283
264
  * route from another. The home link appears only when the operator UI is
284
- * mounted to land on, and points at the public origin so an artifact host's
285
- * 404 does not link to another 404.
265
+ * mounted to land on, and points at the configured public origin.
286
266
  */
287
267
  export function notFoundResponse(request, opts) {
288
268
  if (!wantsHtml(request)) {
289
269
  return new Response("Not Found", { status: 404, headers: { Vary: "Accept" } });
290
270
  }
291
- const brand = resolveBranding(opts.branding);
271
+ const brand = resolveBranding(opts.ui?.branding);
292
272
  const uiMounted = Boolean(opts.ui);
293
273
  const homeHref = opts.publicUrl ? new URL("/", opts.publicUrl).toString() : "/";
294
274
  const home = uiMounted
@@ -305,7 +285,7 @@ export function notFoundResponse(request, opts) {
305
285
  ${home}
306
286
  </section>
307
287
  </main>`;
308
- return new Response(renderPage(opts.branding, {
288
+ return new Response(renderPage(opts.ui?.branding, {
309
289
  title: `Page not found — ${brand.pageTitle}`,
310
290
  uiMounted,
311
291
  // Same bytes on either host: the icons, like the home link, come from
@@ -314,7 +294,7 @@ export function notFoundResponse(request, opts) {
314
294
  body,
315
295
  }), {
316
296
  status: 404,
317
- headers: { "Content-Type": "text/html; charset=utf-8", Vary: "Accept" },
297
+ headers: htmlSecurityHeaders({ "Content-Type": "text/html; charset=utf-8", Vary: "Accept" }),
318
298
  });
319
299
  }
320
300
  /**
@@ -326,9 +306,7 @@ export function notFoundResponse(request, opts) {
326
306
  * from leaving the field alone, and both take the default silently.
327
307
  */
328
308
  function isSetValue(value) {
329
- return typeof value === "string"
330
- ? trimmedString(value) !== undefined
331
- : value !== undefined && value !== null;
309
+ return typeof value === "string" ? trimmedString(value) !== undefined : value !== undefined && value !== null;
332
310
  }
333
311
  /**
334
312
  * Names of the branding URLs the operator set that failed their gate and were
@@ -342,16 +320,9 @@ export function droppedBrandingUrls(branding) {
342
320
  const resolved = resolveBranding(branding);
343
321
  const faviconHref = branding.favicon?.href;
344
322
  return [
345
- ...(isSetValue(branding.productUrl) && !resolved.productUrl
346
- ? ["productUrl"]
347
- : []),
348
- ...(isSetValue(branding.ownerUrl) && !resolved.ownerUrl
349
- ? ["ownerUrl"]
350
- : []),
351
- ...(isSetValue(faviconHref) &&
352
- trimmedString(faviconHref) !== resolved.faviconHref
353
- ? ["favicon.href"]
354
- : []),
323
+ ...(isSetValue(branding.productUrl) && !resolved.productUrl ? ["productUrl"] : []),
324
+ ...(isSetValue(branding.ownerUrl) && !resolved.ownerUrl ? ["ownerUrl"] : []),
325
+ ...(isSetValue(faviconHref) && trimmedString(faviconHref) !== resolved.faviconHref ? ["favicon.href"] : []),
355
326
  ];
356
327
  }
357
328
  /**
@@ -443,11 +414,7 @@ export function droppedUiAuthUrls(uiAuth) {
443
414
  return [];
444
415
  return [
445
416
  ...(isSafeHttpsUrl(uiAuth.frontendApiUrl) ? [] : ["uiAuth.frontendApiUrl"]),
446
- ...(isSetValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl)
447
- ? ["uiAuth.signInUrl"]
448
- : []),
449
- ...(isSetValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl)
450
- ? ["uiAuth.signUpUrl"]
451
- : []),
417
+ ...(isSetValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl) ? ["uiAuth.signInUrl"] : []),
418
+ ...(isSetValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl) ? ["uiAuth.signUpUrl"] : []),
452
419
  ];
453
420
  }
@@ -0,0 +1,39 @@
1
+ // Web-API only, like the rest of the core: Node and workerd read a body alike.
2
+ const decoder = new TextDecoder();
3
+ /**
4
+ * A downstream's response whose `text()` and `json()` read bytes.
5
+ *
6
+ * workerd, when `.text()` (and `.json()`, which reads text) meets a body whose
7
+ * Content-Type it does not parse as text, prints a native warning quoting that
8
+ * Content-Type whole: below every console and logger, so a downstream could
9
+ * write into the deployment's log (INV-6, #695). No filter on the header can
10
+ * agree with workerd's own parser for every value a downstream can send, so
11
+ * connecta never asks for text: `arrayBuffer()` carries no such warning, and
12
+ * the bytes are decoded here exactly as `text()` would decode them (UTF-8,
13
+ * BOM stripped, malformed sequences replaced).
14
+ *
15
+ * The response itself is returned, its readers shadowed by own properties, so
16
+ * `status`, `headers`, `url`, `body`, and `instanceof Response` are unchanged,
17
+ * and so is everything an SDK or a handler reads. A clone is shadowed too:
18
+ * the pinned SDK reads a token response's `clone().json()`.
19
+ */
20
+ export function byteReadResponse(response) {
21
+ if (!Object.isExtensible(response)) {
22
+ // A custom fetch that froze its response: shadow a copy instead, which
23
+ // keeps everything but `url` and `redirected`.
24
+ return byteReadResponse(new Response(response.body, {
25
+ status: response.status,
26
+ statusText: response.statusText,
27
+ headers: response.headers,
28
+ }));
29
+ }
30
+ const arrayBuffer = response.arrayBuffer.bind(response);
31
+ const clone = response.clone.bind(response);
32
+ const text = async () => decoder.decode(await arrayBuffer());
33
+ const own = (value) => ({ value, configurable: true, writable: true });
34
+ return Object.defineProperties(response, {
35
+ text: own(text),
36
+ json: own(async () => JSON.parse(await text())),
37
+ clone: own(() => byteReadResponse(clone())),
38
+ });
39
+ }
@@ -1,6 +1,6 @@
1
1
  import { Deferred, Duration, Effect } from "effect";
2
2
  import { ConnectorCallError } from "./errors.js";
3
- import { provideCallAdmissionProgram, startCallAdmission, } from "./runtime/call-admission.js";
3
+ import { provideCallAdmissionProgram, startCallAdmission } from "./runtime/call-admission.js";
4
4
  import { fromSignal, runEdge } from "./runtime/run.js";
5
5
  const DEFAULT_MAX_QUEUE_SIZE = 32;
6
6
  const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
@@ -28,7 +28,11 @@ export function isCallAdmissionError(error) {
28
28
  /** Sum gauges and counters without publishing connector or principal keys. */
29
29
  export function aggregateCallAdmissionSnapshots(snapshots) {
30
30
  const aggregate = {
31
- rules: 0, partitions: 0, active: 0, queued: 0, closed: snapshots.length > 0,
31
+ rules: 0,
32
+ partitions: 0,
33
+ active: 0,
34
+ queued: 0,
35
+ closed: snapshots.length > 0,
32
36
  totals: { admitted: 0, queued: 0, rejected: 0, rateLimited: 0, cancelled: 0 },
33
37
  queueWaitMs: { count: 0, total: 0, max: 0 },
34
38
  };
@@ -88,8 +92,7 @@ export class ConnectorCallAdmissionController {
88
92
  throw new TypeError(`connector "${connectorId}" callAdmission.rules must contain exactly one rule in this release.`);
89
93
  }
90
94
  const rule = policy.rules[0];
91
- if (rule.maxConcurrency === undefined &&
92
- rule.budget === undefined) {
95
+ if (rule.maxConcurrency === undefined && rule.budget === undefined) {
93
96
  throw new TypeError(`connector "${connectorId}" callAdmission rule must declare maxConcurrency or budget.`);
94
97
  }
95
98
  this.maxConcurrency =
@@ -97,9 +100,7 @@ export class ConnectorCallAdmissionController {
97
100
  ? undefined
98
101
  : positiveWhole(rule.maxConcurrency, `connector "${connectorId}" callAdmission maxConcurrency`);
99
102
  if (this.maxConcurrency === undefined &&
100
- (rule.maxQueueSize !== undefined ||
101
- rule.queueTimeoutMs !== undefined ||
102
- rule.retryAfterMs !== undefined)) {
103
+ (rule.maxQueueSize !== undefined || rule.queueTimeoutMs !== undefined || rule.retryAfterMs !== undefined)) {
103
104
  throw new TypeError(`connector "${connectorId}" callAdmission queue settings require maxConcurrency.`);
104
105
  }
105
106
  this.maxQueueSize = nonNegativeWhole(rule.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, `connector "${connectorId}" callAdmission maxQueueSize`);
@@ -193,8 +194,7 @@ export class ConnectorCallAdmissionController {
193
194
  pump(state) {
194
195
  if (this.closed || this.maxConcurrency === undefined)
195
196
  return;
196
- while (state.active < this.maxConcurrency &&
197
- state.waiters.length > 0) {
197
+ while (state.active < this.maxConcurrency && state.waiters.length > 0) {
198
198
  const waiter = state.waiters.shift();
199
199
  // The waiter's signal is not read here. This runs in whichever request
200
200
  // released a slot, and on Workers reading an AbortSignal that another
@@ -248,9 +248,7 @@ export class ConnectorCallAdmissionController {
248
248
  }
249
249
  maybeDeletePartition(key, state, now) {
250
250
  this.pruneBudget(state, now);
251
- if (state.active === 0 &&
252
- state.waiters.length === 0 &&
253
- state.admittedAt.length === 0) {
251
+ if (state.active === 0 && state.waiters.length === 0 && state.admittedAt.length === 0) {
254
252
  this.partitions.delete(key);
255
253
  }
256
254
  }
@@ -322,8 +320,7 @@ export class ConnectorCallAdmissionController {
322
320
  controller.rejectedTotal++;
323
321
  return Effect.fail(new CallAdmissionError("partition", "connector_call_failed", `Connector "${controller.connectorId}" call-admission partitionKey threw.`, { cause }));
324
322
  }
325
- if (typeof key !== "string" ||
326
- enc.encode(key).length > MAX_PARTITION_KEY_BYTES) {
323
+ if (typeof key !== "string" || enc.encode(key).length > MAX_PARTITION_KEY_BYTES) {
327
324
  controller.rejectedTotal++;
328
325
  return Effect.fail(new CallAdmissionError("partition", "connector_call_failed", `Connector "${controller.connectorId}" call-admission partitionKey must return a string of at most ${MAX_PARTITION_KEY_BYTES} UTF-8 bytes.`));
329
326
  }
@@ -352,8 +349,7 @@ export class ConnectorCallAdmissionController {
352
349
  controller.rateLimitedTotal++;
353
350
  return Effect.fail(controller.budgetLimited(budgetRetryAfterMs));
354
351
  }
355
- if (controller.maxConcurrency === undefined ||
356
- state.active < controller.maxConcurrency) {
352
+ if (controller.maxConcurrency === undefined || state.active < controller.maxConcurrency) {
357
353
  return Effect.succeed(controller.admit(state, queuedAt, 0));
358
354
  }
359
355
  if (state.waiters.length >= controller.maxQueueSize) {