@zackbart/connecta 0.24.3 → 0.25.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 (145) hide show
  1. package/AGENTS.md +58 -30
  2. package/CHANGELOG.md +317 -1
  3. package/README.md +5 -6
  4. package/dist/advertised-schema.js +53 -0
  5. package/dist/auth/downstream-oauth.js +490 -106
  6. package/dist/branding.d.ts +31 -2
  7. package/dist/branding.js +116 -8
  8. package/dist/call-admission.js +139 -113
  9. package/dist/catalog-service.js +141 -105
  10. package/dist/catalog.js +258 -33
  11. package/dist/concurrency.js +24 -23
  12. package/dist/connector-scope.js +6 -37
  13. package/dist/connectors/api.d.ts +3 -3
  14. package/dist/connectors/api.js +10 -2
  15. package/dist/connectors/guarded-fetch.js +34 -4
  16. package/dist/connectors/remote-mcp.d.ts +6 -5
  17. package/dist/connectors/remote-mcp.js +325 -245
  18. package/dist/credential-contract.d.ts +9 -0
  19. package/dist/credential-rules.js +2 -2
  20. package/dist/credentials.d.ts +6 -1
  21. package/dist/credentials.js +45 -3
  22. package/dist/errors.d.ts +11 -3
  23. package/dist/errors.js +2 -1
  24. package/dist/execute.js +363 -257
  25. package/dist/executor-admission.js +67 -50
  26. package/dist/executors/quickjs-runtime.js +4 -2
  27. package/dist/executors/quickjs.js +251 -210
  28. package/dist/fix-prompt.js +37 -0
  29. package/dist/index.d.ts +11 -2
  30. package/dist/index.js +63 -21
  31. package/dist/invocation.js +324 -285
  32. package/dist/meta-tools.js +304 -114
  33. package/dist/oauth-callback-outcome.js +94 -0
  34. package/dist/oauth-sealing.js +29 -0
  35. package/dist/operator-ui/fix-prompts.js +87 -0
  36. package/dist/operator-ui/generated.js +2 -2
  37. package/dist/operator-ui/model.d.ts +46 -1
  38. package/dist/operator-ui/setup-commands.js +53 -0
  39. package/dist/operator-ui/view.js +109 -2
  40. package/dist/program-source.js +205 -0
  41. package/dist/providers/cloudflare.d.ts +14 -2
  42. package/dist/providers/cloudflare.js +107 -16
  43. package/dist/providers/linear.d.ts +26 -4
  44. package/dist/providers/linear.js +19 -4
  45. package/dist/providers/mixpanel.d.ts +16 -3
  46. package/dist/providers/mixpanel.js +13 -2
  47. package/dist/providers/notion.d.ts +8 -1
  48. package/dist/providers/notion.js +85 -12
  49. package/dist/providers/revenuecat.d.ts +30 -4
  50. package/dist/providers/revenuecat.js +70 -5
  51. package/dist/providers/stripe.d.ts +10 -3
  52. package/dist/providers/stripe.js +30 -4
  53. package/dist/providers/vercel.js +12 -2
  54. package/dist/registry.d.ts +13 -5
  55. package/dist/registry.js +317 -258
  56. package/dist/routes/activity.js +120 -137
  57. package/dist/routes/credentials.js +174 -163
  58. package/dist/routes/mcp.js +129 -70
  59. package/dist/routes/oauth-management.js +60 -71
  60. package/dist/routes/oauth.js +44 -16
  61. package/dist/routes/operator.js +82 -0
  62. package/dist/routes/shared.d.ts +2 -0
  63. package/dist/routes/ui-data.js +210 -0
  64. package/dist/routes/ui.js +64 -32
  65. package/dist/runtime/admission.js +43 -0
  66. package/dist/runtime/call-admission.js +67 -0
  67. package/dist/runtime/connector-scope.js +72 -0
  68. package/dist/runtime/run.js +174 -0
  69. package/dist/runtime/services.js +130 -0
  70. package/dist/runtime/storage.js +66 -0
  71. package/dist/server.js +111 -89
  72. package/dist/skills.js +1 -1
  73. package/dist/storage/file.js +39 -2
  74. package/dist/storage/memory.js +28 -16
  75. package/dist/timeout.js +17 -47
  76. package/dist/types.d.ts +64 -7
  77. package/dist/ui.d.ts +16 -2
  78. package/dist/ui.js +68 -176
  79. package/dist/url-safety.js +157 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/documentation/architecture.md +368 -182
  83. package/documentation/auth.md +277 -176
  84. package/documentation/code-mode.md +479 -329
  85. package/documentation/meta-tools.md +494 -417
  86. package/ethos.md +44 -47
  87. package/examples/worker/AGENTS.md +2 -1
  88. package/examples/worker/README.md +62 -14
  89. package/examples/worker/src/d1-storage.ts +116 -0
  90. package/examples/worker/src/index.ts +18 -21
  91. package/examples/worker/wrangler.jsonc +16 -4
  92. package/package.json +11 -4
  93. package/templates/node/.env.example +3 -3
  94. package/templates/node/AGENTS.md +5 -4
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. package/templates/node/src/index.ts +23 -22
  98. package/dist/auth/downstream-oauth.d.ts +0 -200
  99. package/dist/catalog-fingerprint.d.ts +0 -19
  100. package/dist/catalog-limits.d.ts +0 -9
  101. package/dist/catalog-service.d.ts +0 -263
  102. package/dist/catalog.d.ts +0 -87
  103. package/dist/concurrency.d.ts +0 -3
  104. package/dist/connectors/guarded-fetch.d.ts +0 -92
  105. package/dist/credential-rules.d.ts +0 -85
  106. package/dist/execute.d.ts +0 -154
  107. package/dist/executor-result.d.ts +0 -10
  108. package/dist/executors/quickjs-child.d.ts +0 -1
  109. package/dist/executors/quickjs-protocol.d.ts +0 -67
  110. package/dist/favicon.d.ts +0 -1
  111. package/dist/identity.d.ts +0 -4
  112. package/dist/invocation.d.ts +0 -74
  113. package/dist/mcp-result.d.ts +0 -8
  114. package/dist/meta-tools.d.ts +0 -129
  115. package/dist/operator-ui/generated.d.ts +0 -2
  116. package/dist/operator-ui/view.d.ts +0 -151
  117. package/dist/result-shapes.d.ts +0 -13
  118. package/dist/retired-toolkits.d.ts +0 -2
  119. package/dist/routes/activity.d.ts +0 -2
  120. package/dist/routes/credentials.d.ts +0 -2
  121. package/dist/routes/mcp.d.ts +0 -10
  122. package/dist/routes/oauth-management.d.ts +0 -2
  123. package/dist/routes/oauth.d.ts +0 -2
  124. package/dist/routes/ui.d.ts +0 -2
  125. package/dist/server.d.ts +0 -9
  126. package/dist/skills.d.ts +0 -44
  127. package/dist/timeout.d.ts +0 -21
  128. package/dist/tool-safety.d.ts +0 -9
  129. package/documentation/call-admission.md +0 -158
  130. package/documentation/cloudflare.md +0 -471
  131. package/documentation/connector-guides.md +0 -176
  132. package/documentation/connectors.md +0 -431
  133. package/documentation/linear.md +0 -193
  134. package/documentation/mixpanel.md +0 -160
  135. package/documentation/notion.md +0 -308
  136. package/documentation/operations.md +0 -359
  137. package/documentation/operator-ui.md +0 -135
  138. package/documentation/optional-modules-upgrade.md +0 -243
  139. package/documentation/provider-conventions.md +0 -729
  140. package/documentation/request-admission.md +0 -204
  141. package/documentation/revenuecat.md +0 -305
  142. package/documentation/storage-and-credentials.md +0 -254
  143. package/documentation/stripe.md +0 -262
  144. package/documentation/upgrading.md +0 -768
  145. package/documentation/vercel.md +0 -241
@@ -1,180 +1,190 @@
1
+ import { Effect } from "effect";
1
2
  import { credentialTestRule, describeCredentialTestMismatch, storedCredentialShape, } from "../credential-rules.js";
2
- import { authorizeUiIdentity, mayManageConnector, validateAuthPermissions, isSameOrigin, msg, privateJson, } from "./shared.js";
3
- async function readCredentialInput(request, config) {
4
- if (!request.headers
5
- .get("content-type")
6
- ?.toLowerCase()
7
- .startsWith("application/json")) {
8
- return {
9
- ok: false,
10
- response: privateJson({ error: "Content-Type must be application/json" }, { status: 415 }),
11
- };
12
- }
13
- const raw = await request.text();
14
- if (raw.length > 20_000) {
15
- return {
16
- ok: false,
17
- response: privateJson({ error: "request body is too large" }, { status: 413 }),
18
- };
19
- }
20
- try {
21
- const body = JSON.parse(raw);
22
- if (!config.fields?.length) {
23
- if (typeof body.value !== "string" || !body.value.trim()) {
24
- return {
25
- ok: false,
26
- response: privateJson({ error: "value must be a non-empty string" }, { status: 400 }),
27
- };
28
- }
29
- return { ok: true, input: { kind: "single", value: body.value } };
30
- }
31
- if (!body.values ||
32
- typeof body.values !== "object" ||
33
- Array.isArray(body.values)) {
34
- return {
35
- ok: false,
36
- response: privateJson({ error: "values must be an object" }, { status: 400 }),
37
- };
3
+ import { authorizedPerson, refuse, serveOperator, visibleRegistry, } from "./operator.js";
4
+ import { mayManageConnector, isSameOrigin, msg, privateJson, } from "./shared.js";
5
+ /** The longest body, in characters, a credential write accepts. */
6
+ const MAX_BODY_CHARS = 20_000;
7
+ /**
8
+ * The most UTF-8 a body within MAX_BODY_CHARS can take. No UTF-16 unit costs
9
+ * more than three bytes, and a malformed sequence decodes to one U+FFFD per
10
+ * at most three bytes, so a body past this many bytes is past the character
11
+ * limit too — and reading stops there, instead of buffering whatever a client
12
+ * cares to send before measuring it.
13
+ */
14
+ const MAX_BODY_BYTES = MAX_BODY_CHARS * 3;
15
+ /** The body as text, or undefined once it is longer than MAX_BODY_CHARS. */
16
+ async function boundedText(request) {
17
+ if (!request.body)
18
+ return "";
19
+ const reader = request.body.getReader();
20
+ const chunks = [];
21
+ let size = 0;
22
+ for (;;) {
23
+ const { done, value } = await reader.read();
24
+ if (done)
25
+ break;
26
+ size += value.byteLength;
27
+ if (size > MAX_BODY_BYTES) {
28
+ reader.cancel().catch(() => { });
29
+ return undefined;
38
30
  }
39
- const rawValues = body.values;
40
- const expected = new Set(config.fields.map((field) => field.name));
41
- const unexpected = Object.keys(rawValues).find((field) => !expected.has(field));
42
- if (unexpected) {
43
- return {
44
- ok: false,
45
- response: privateJson({ error: `unexpected credential field "${unexpected}"` }, { status: 400 }),
46
- };
47
- }
48
- const values = {};
49
- for (const field of config.fields) {
50
- const value = rawValues[field.name];
51
- if (typeof value !== "string" || !value.trim()) {
52
- return {
53
- ok: false,
54
- response: privateJson({ error: `${field.name} must be a non-empty string` }, { status: 400 }),
55
- };
56
- }
57
- values[field.name] = value;
58
- }
59
- return { ok: true, input: { kind: "multiple", values } };
31
+ chunks.push(value);
60
32
  }
61
- catch {
62
- return {
63
- ok: false,
64
- response: privateJson({ error: "invalid JSON body" }, { status: 400 }),
65
- };
33
+ const bytes = new Uint8Array(size);
34
+ let offset = 0;
35
+ for (const chunk of chunks) {
36
+ bytes.set(chunk, offset);
37
+ offset += chunk.byteLength;
66
38
  }
39
+ const text = new TextDecoder().decode(bytes);
40
+ return text.length > MAX_BODY_CHARS ? undefined : text;
67
41
  }
68
- async function handleCredentialRequest(context, connectorId, action) {
69
- const { request, baseUrl, opts } = context;
70
- if (!opts.credentialVault) {
71
- return privateJson({ error: "credential storage is not configured" }, { status: 503 });
72
- }
73
- if (!isSameOrigin(request, baseUrl)) {
74
- return privateJson({ error: "same-origin request required" }, { status: 403 });
75
- }
76
- const authz = await authorizeUiIdentity(request, baseUrl, opts.auth, "credential management", context.runtimeContext, opts.identity);
77
- if (!authz.ok)
78
- return authz.response;
79
- let registry;
80
- try {
81
- validateAuthPermissions(authz, opts.registry);
82
- registry = opts.registry.scoped({
83
- connectorIds: authz.connectorIds,
84
- ...(authz.toolAccess ? { toolAccess: authz.toolAccess } : {}),
85
- ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
86
- ...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
87
- });
88
- }
89
- catch (error) {
90
- return privateJson({ error: msg(error) }, { status: 403 });
42
+ /** Field-by-field checks, in words the Credentials page shows as written. */
43
+ function credentialInput(body, config) {
44
+ if (!config.fields?.length) {
45
+ if (typeof body.value !== "string" || !body.value.trim()) {
46
+ return refuse("value must be a non-empty string", 400);
47
+ }
48
+ return Effect.succeed({ kind: "single", value: body.value });
91
49
  }
92
- const connector = registry.getConnector(connectorId);
93
- if (!connector?.credential) {
94
- return privateJson({ error: "unknown credential slot" }, { status: 404 });
50
+ if (!body.values ||
51
+ typeof body.values !== "object" ||
52
+ Array.isArray(body.values)) {
53
+ return refuse("values must be an object", 400);
95
54
  }
96
- if (!mayManageConnector(authz, connector))
97
- return privateJson({ error: "credential management is not permitted" }, { status: 403 });
98
- const personal = connector.authScope === "personal";
99
- if (personal && !authz.principalKey) {
100
- return privateJson({ error: "forbidden" }, { status: 403 });
55
+ const rawValues = body.values;
56
+ const expected = new Set(config.fields.map((field) => field.name));
57
+ const unexpected = Object.keys(rawValues).find((field) => !expected.has(field));
58
+ if (unexpected) {
59
+ return refuse(`unexpected credential field "${unexpected}"`, 400);
101
60
  }
102
- const owner = personal ? authz.principalKey : undefined;
103
- const updatedBy = authz.identity.principal
104
- ? `${authz.identity.principal.namespace}:${authz.identity.principal.id}`
105
- : authz.actor.id ?? authz.actor.kind;
106
- if (action === "test") {
107
- if (request.method !== "POST") {
108
- return privateJson({ error: "method not allowed" }, { status: 405 });
61
+ const values = {};
62
+ for (const field of config.fields) {
63
+ const value = rawValues[field.name];
64
+ if (typeof value !== "string" || !value.trim()) {
65
+ return refuse(`${field.name} must be a non-empty string`, 400);
109
66
  }
110
- // The declared credential shape picks the hook — the same single rule the
111
- // Credentials page asks for its Test affordance, so a shown button reaches
112
- // that reads the shape the credential was stored in.
113
- const rule = credentialTestRule(connector);
114
- if (!rule.mode) {
115
- return privateJson({
116
- error: rule.mismatch
117
- ? "this connector cannot test its credential: " +
118
- describeCredentialTestMismatch(rule.mismatch)
119
- : "this connector does not support credential testing",
120
- }, { status: 400 });
67
+ values[field.name] = value;
68
+ }
69
+ return Effect.succeed({ kind: "multiple", values });
70
+ }
71
+ function readCredentialInput(request, config) {
72
+ return Effect.gen(function* () {
73
+ if (!request.headers
74
+ .get("content-type")
75
+ ?.toLowerCase()
76
+ .startsWith("application/json")) {
77
+ return yield* refuse("Content-Type must be application/json", 415);
121
78
  }
79
+ const raw = yield* Effect.promise(() => boundedText(request));
80
+ if (raw === undefined)
81
+ return yield* refuse("request body is too large", 413);
82
+ let body;
122
83
  try {
123
- const values = await opts.credentialVault.getAll(connectorId, owner);
124
- const shape = storedCredentialShape(connector.credential, values);
125
- if (shape.state === "missing") {
126
- return privateJson({
127
- error: rule.mode === "multiple"
128
- ? "configure the credentials before testing them"
129
- : "configure the credential before testing it",
130
- }, { status: 409 });
131
- }
132
- if (shape.state === "mismatch") {
133
- return privateJson({ error: shape.message }, { status: 409 });
134
- }
135
- const storedValues = values;
136
- const ctx = registry.contextFor(connectorId, baseUrl);
137
- const result = rule.mode === "multiple"
138
- ? await connector.testCredentials(storedValues, ctx)
139
- : await connector.testCredential(
140
- // The single-value shape check above guarantees this key.
141
- storedValues.value, ctx);
142
- return privateJson(result);
84
+ body = JSON.parse(raw);
143
85
  }
144
- catch (err) {
145
- return privateJson({ ok: false, message: msg(err) });
86
+ catch {
87
+ return yield* refuse("invalid JSON body", 400);
146
88
  }
147
- }
148
- if (action) {
149
- return privateJson({ error: "not found" }, { status: 404 });
150
- }
151
- if (request.method === "PUT") {
152
- const input = await readCredentialInput(request, connector.credential);
153
- if (!input.ok)
154
- return input.response;
155
- try {
156
- const metadata = input.input.kind === "single"
157
- ? await opts.credentialVault.set(connectorId, input.input.value, updatedBy, owner)
158
- : await opts.credentialVault.setAll(connectorId, input.input.values, updatedBy, owner);
159
- await registry.invalidateStored(connectorId);
160
- return privateJson({ credential: metadata });
89
+ // `null` parses, but a credential cannot be read from it.
90
+ if (body === null)
91
+ return yield* refuse("invalid JSON body", 400);
92
+ return yield* credentialInput(body, config);
93
+ });
94
+ }
95
+ function credentialRequest(context, connectorId, action) {
96
+ const { request, baseUrl, opts } = context;
97
+ return Effect.gen(function* () {
98
+ const vault = opts.credentialVault;
99
+ if (!vault)
100
+ return yield* refuse("credential storage is not configured", 503);
101
+ if (!isSameOrigin(request, baseUrl)) {
102
+ return yield* refuse("same-origin request required", 403);
161
103
  }
162
- catch (err) {
163
- return privateJson({ error: msg(err) }, { status: 400 });
104
+ const authz = yield* authorizedPerson(context, "credential management");
105
+ const registry = yield* visibleRegistry(context, authz);
106
+ const connector = registry.getConnector(connectorId);
107
+ const credential = connector?.credential;
108
+ if (!connector || !credential)
109
+ return yield* refuse("unknown credential slot", 404);
110
+ if (!mayManageConnector(authz, connector)) {
111
+ return yield* refuse("credential management is not permitted", 403);
164
112
  }
165
- }
166
- if (request.method === "DELETE") {
167
- await opts.credentialVault.delete(connectorId, owner);
168
- await registry.invalidateStored(connectorId);
169
- return new Response(null, {
170
- status: 204,
171
- headers: {
172
- "Cache-Control": "no-store",
173
- "Referrer-Policy": "no-referrer",
174
- },
175
- });
176
- }
177
- return privateJson({ error: "method not allowed" }, { status: 405 });
113
+ const personal = connector.authScope === "personal";
114
+ if (personal && !authz.principalKey)
115
+ return yield* refuse("forbidden", 403);
116
+ const owner = personal ? authz.principalKey : undefined;
117
+ const updatedBy = authz.identity.principal
118
+ ? `${authz.identity.principal.namespace}:${authz.identity.principal.id}`
119
+ : authz.actor.id ?? authz.actor.kind;
120
+ if (action === "test") {
121
+ if (request.method !== "POST")
122
+ return yield* refuse("method not allowed", 405);
123
+ // The declared credential shape picks the hook — the same single rule the
124
+ // Credentials page asks for its Test affordance, so a shown button reaches
125
+ // that reads the shape the credential was stored in.
126
+ const { mode, mismatch } = credentialTestRule(connector);
127
+ if (!mode) {
128
+ return yield* refuse(mismatch
129
+ ? "this connector cannot test its credential: " +
130
+ describeCredentialTestMismatch(mismatch)
131
+ : "this connector does not support credential testing", 400);
132
+ }
133
+ // Whatever goes wrong past this point is the test's result, not the
134
+ // route's: the page shows it beside the Test button.
135
+ return yield* Effect.tryPromise({
136
+ try: async () => {
137
+ const values = await vault.getAll(connectorId, owner);
138
+ const shape = storedCredentialShape(credential, values);
139
+ if (shape.state === "missing") {
140
+ return privateJson({
141
+ error: mode === "multiple"
142
+ ? "configure the credentials before testing them"
143
+ : "configure the credential before testing it",
144
+ }, { status: 409 });
145
+ }
146
+ if (shape.state === "mismatch") {
147
+ return privateJson({ error: shape.message }, { status: 409 });
148
+ }
149
+ const storedValues = values;
150
+ const ctx = registry.contextFor(connectorId, baseUrl);
151
+ return privateJson(mode === "multiple"
152
+ ? await connector.testCredentials(storedValues, ctx)
153
+ : await connector.testCredential(
154
+ // The single-value shape check above guarantees this key.
155
+ storedValues.value, ctx));
156
+ },
157
+ catch: (error) => error,
158
+ }).pipe(Effect.catch((error) => Effect.succeed(privateJson({ ok: false, message: msg(error) }))));
159
+ }
160
+ if (action)
161
+ return yield* refuse("not found", 404);
162
+ if (request.method === "PUT") {
163
+ const input = yield* readCredentialInput(request, credential);
164
+ return yield* Effect.tryPromise({
165
+ try: async () => {
166
+ const metadata = input.kind === "single"
167
+ ? await vault.set(connectorId, input.value, updatedBy, owner)
168
+ : await vault.setAll(connectorId, input.values, updatedBy, owner);
169
+ await registry.invalidateStored(connectorId);
170
+ return privateJson({ credential: metadata });
171
+ },
172
+ catch: (error) => error,
173
+ }).pipe(Effect.catch((error) => refuse(msg(error), 400)));
174
+ }
175
+ if (request.method === "DELETE") {
176
+ yield* Effect.promise(() => vault.delete(connectorId, owner));
177
+ yield* Effect.promise(() => registry.invalidateStored(connectorId));
178
+ return new Response(null, {
179
+ status: 204,
180
+ headers: {
181
+ "Cache-Control": "no-store",
182
+ "Referrer-Policy": "no-referrer",
183
+ },
184
+ });
185
+ }
186
+ return yield* refuse("method not allowed", 405);
187
+ });
178
188
  }
179
189
  export async function routeCredentials(context) {
180
190
  const match = /^\/ui\/credentials\/([a-z0-9_-]+)(?:\/([a-z]+))?$/.exec(context.path);
@@ -188,5 +198,6 @@ export async function routeCredentials(context) {
188
198
  if (context.request.method === "OPTIONS") {
189
199
  return privateJson({ error: "method not allowed" }, { status: 405 });
190
200
  }
191
- return handleCredentialRequest(context, connectorId, match[2]);
201
+ // No signal: a write that has started runs through to its invalidation.
202
+ return serveOperator(credentialRequest(context, connectorId, match[2]));
192
203
  }