@zackbart/connecta 0.24.4 → 0.26.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 +43 -12
- package/CHANGELOG.md +524 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/advertised-schema.js +53 -0
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +449 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +194 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +152 -106
- package/dist/catalog.js +297 -38
- package/dist/concurrency.js +24 -23
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- 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 +330 -240
- 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/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +745 -282
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +145 -63
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +255 -212
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +281 -26
- package/dist/invocation.js +371 -286
- package/dist/meta-tools.js +213 -94
- package/dist/module-contracts.d.ts +21 -1
- 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 +103 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +128 -3
- package/dist/program-source.js +321 -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 +43 -7
- package/dist/registry.js +434 -288
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +74 -154
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +196 -163
- package/dist/routes/mcp.js +228 -112
- package/dist/routes/oauth-management.js +73 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +83 -0
- package/dist/routes/shared.d.ts +25 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +211 -0
- package/dist/routes/ui.js +76 -35
- package/dist/run-journal.js +347 -0
- 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/shared-read.js +106 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +137 -89
- package/dist/skills.js +28 -9
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +39 -43
- package/dist/tool-safety.d.ts +27 -1
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +45 -7
- package/dist/ui.d.ts +25 -4
- package/dist/ui.js +77 -167
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +367 -16
- package/documentation/auth.md +163 -2
- package/documentation/code-mode.md +274 -56
- package/documentation/meta-tools.md +200 -44
- package/ethos.md +46 -47
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +224 -14
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +42 -7
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +21 -5
- package/package.json +16 -4
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
- 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
|
@@ -1,180 +1,212 @@
|
|
|
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
|
-
};
|
|
38
|
-
}
|
|
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;
|
|
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;
|
|
58
30
|
}
|
|
59
|
-
|
|
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. Only `ok` leaves
|
|
135
|
+
// the host. A hook's message, and anything it throws, can quote the
|
|
136
|
+
// downstream's reply, and that reply can quote the credential it just
|
|
137
|
+
// rejected — so the text goes to the deployment's log and the page
|
|
138
|
+
// says a fixed sentence for the outcome instead.
|
|
139
|
+
const logged = (ok, detail) => {
|
|
140
|
+
if (!detail)
|
|
141
|
+
return;
|
|
142
|
+
const line = `[connecta] connector "${connectorId}" credential test ${ok ? "passed" : "failed"}: ${detail}`;
|
|
143
|
+
if (ok)
|
|
144
|
+
opts.logger.info(line);
|
|
145
|
+
else
|
|
146
|
+
opts.logger.warn(line);
|
|
147
|
+
};
|
|
148
|
+
return yield* Effect.tryPromise({
|
|
149
|
+
try: async () => {
|
|
150
|
+
const values = await vault.getAll(connectorId, owner);
|
|
151
|
+
const shape = storedCredentialShape(credential, values);
|
|
152
|
+
// Both refusals are the route's own words, and each carries the
|
|
153
|
+
// problem kind the page keys its copy and fix prompt off.
|
|
154
|
+
if (shape.state === "missing") {
|
|
155
|
+
return privateJson({
|
|
156
|
+
error: mode === "multiple"
|
|
157
|
+
? "configure the credentials before testing them"
|
|
158
|
+
: "configure the credential before testing it",
|
|
159
|
+
problem: "credential_required",
|
|
160
|
+
}, { status: 409 });
|
|
161
|
+
}
|
|
162
|
+
if (shape.state === "mismatch") {
|
|
163
|
+
return privateJson({ error: shape.message, problem: "credential_mismatch" }, { status: 409 });
|
|
164
|
+
}
|
|
165
|
+
const storedValues = values;
|
|
166
|
+
const ctx = registry.contextFor(connectorId, baseUrl);
|
|
167
|
+
const result = mode === "multiple"
|
|
168
|
+
? await connector.testCredentials(storedValues, ctx)
|
|
169
|
+
: await connector.testCredential(
|
|
170
|
+
// The single-value shape check above guarantees this key.
|
|
171
|
+
storedValues.value, ctx);
|
|
172
|
+
const ok = result?.ok === true;
|
|
173
|
+
logged(ok, result?.message);
|
|
174
|
+
return privateJson({ ok });
|
|
175
|
+
},
|
|
176
|
+
catch: (error) => error,
|
|
177
|
+
}).pipe(Effect.catch((error) => {
|
|
178
|
+
logged(false, msg(error));
|
|
179
|
+
return Effect.succeed(privateJson({ ok: false }));
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
if (action)
|
|
183
|
+
return yield* refuse("not found", 404);
|
|
184
|
+
if (request.method === "PUT") {
|
|
185
|
+
const input = yield* readCredentialInput(request, credential);
|
|
186
|
+
return yield* Effect.tryPromise({
|
|
187
|
+
try: async () => {
|
|
188
|
+
const metadata = input.kind === "single"
|
|
189
|
+
? await vault.set(connectorId, input.value, updatedBy, owner)
|
|
190
|
+
: await vault.setAll(connectorId, input.values, updatedBy, owner);
|
|
191
|
+
await registry.invalidateStored(connectorId);
|
|
192
|
+
return privateJson({ credential: metadata });
|
|
193
|
+
},
|
|
194
|
+
catch: (error) => error,
|
|
195
|
+
}).pipe(Effect.catch((error) => refuse(msg(error), 400)));
|
|
196
|
+
}
|
|
197
|
+
if (request.method === "DELETE") {
|
|
198
|
+
yield* Effect.promise(() => vault.delete(connectorId, owner));
|
|
199
|
+
yield* Effect.promise(() => registry.invalidateStored(connectorId));
|
|
200
|
+
return new Response(null, {
|
|
201
|
+
status: 204,
|
|
202
|
+
headers: {
|
|
203
|
+
"Cache-Control": "no-store",
|
|
204
|
+
"Referrer-Policy": "no-referrer",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return yield* refuse("method not allowed", 405);
|
|
209
|
+
});
|
|
178
210
|
}
|
|
179
211
|
export async function routeCredentials(context) {
|
|
180
212
|
const match = /^\/ui\/credentials\/([a-z0-9_-]+)(?:\/([a-z]+))?$/.exec(context.path);
|
|
@@ -188,5 +220,6 @@ export async function routeCredentials(context) {
|
|
|
188
220
|
if (context.request.method === "OPTIONS") {
|
|
189
221
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
190
222
|
}
|
|
191
|
-
|
|
223
|
+
// No signal: a write that has started runs through to its invalidation.
|
|
224
|
+
return serveOperator(credentialRequest(context, connectorId, match[2]));
|
|
192
225
|
}
|