@zackbart/connecta 0.24.4 → 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.
- package/AGENTS.md +40 -10
- package/CHANGELOG.md +253 -0
- package/dist/advertised-schema.js +53 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +141 -105
- package/dist/catalog.js +258 -33
- package/dist/concurrency.js +24 -23
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +317 -241
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/execute.js +363 -257
- package/dist/executor-admission.js +67 -50
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +251 -210
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +51 -20
- package/dist/invocation.js +324 -285
- package/dist/meta-tools.js +206 -92
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +46 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +38 -2
- package/dist/program-source.js +205 -0
- package/dist/providers/notion.js +2 -2
- package/dist/providers/revenuecat.js +32 -5
- package/dist/providers/stripe.d.ts +3 -2
- package/dist/providers/vercel.js +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +305 -260
- package/dist/routes/activity.js +120 -137
- package/dist/routes/credentials.js +174 -163
- package/dist/routes/mcp.js +129 -70
- package/dist/routes/oauth-management.js +60 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +82 -0
- package/dist/routes/shared.d.ts +2 -0
- package/dist/routes/ui-data.js +210 -0
- package/dist/routes/ui.js +64 -32
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +111 -89
- package/dist/skills.js +1 -1
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +17 -47
- package/dist/types.d.ts +27 -7
- package/dist/ui.d.ts +16 -2
- package/dist/ui.js +50 -166
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +184 -10
- package/documentation/auth.md +80 -0
- package/documentation/code-mode.md +69 -24
- package/documentation/meta-tools.md +164 -27
- package/ethos.md +44 -47
- package/examples/worker/README.md +51 -5
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +6 -6
- package/examples/worker/wrangler.jsonc +16 -4
- package/package.json +11 -3
- package/templates/node/package.json +1 -1
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -96
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -188
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
- package/dist/tool-safety.d.ts +0 -9
|
@@ -1,180 +1,190 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { credentialTestRule, describeCredentialTestMismatch, storedCredentialShape, } from "../credential-rules.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
return
|
|
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
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
145
|
-
return
|
|
86
|
+
catch {
|
|
87
|
+
return yield* refuse("invalid JSON body", 400);
|
|
146
88
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
201
|
+
// No signal: a write that has started runs through to its invalidation.
|
|
202
|
+
return serveOperator(credentialRequest(context, connectorId, match[2]));
|
|
192
203
|
}
|