@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.
- package/CHANGELOG.md +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/argument-redaction.js +230 -0
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +409 -225
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +633 -0
- package/dist/config.js +508 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +194 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +336 -56
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +146 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +126 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +140 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
package/dist/routes/ui-data.js
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// than the payload's, and its connector scope closes as the slot ends,
|
|
7
7
|
// however it ends.
|
|
8
8
|
import { Effect } from "effect";
|
|
9
|
+
import { bindActivityRequest } from "../activity-request.js";
|
|
9
10
|
import { resolveDiscoveryConcurrency } from "../concurrency.js";
|
|
10
|
-
import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape
|
|
11
|
+
import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape } from "../credential-rules.js";
|
|
12
|
+
import { failureRecord, failureStatus, logFailure, recordedToolName, statusFailure } from "../operator-record.js";
|
|
11
13
|
import { closeScope } from "../runtime/connector-scope.js";
|
|
12
14
|
import { withDeadlineEffect } from "../runtime/run.js";
|
|
13
|
-
import { isApprovalExempt, NO_EXEMPTIONS, } from "../tool-safety.js";
|
|
14
15
|
import { uiProblemFor, uiToolSafety } from "../ui.js";
|
|
15
16
|
import { CONNECTA_VERSION } from "../version.js";
|
|
16
17
|
const DETAIL_TIMEOUT_MS = 30_000;
|
|
@@ -21,24 +22,23 @@ function attempt(operation) {
|
|
|
21
22
|
export function uiData(registry, baseUrl, options) {
|
|
22
23
|
return Effect.suspend(() => {
|
|
23
24
|
const requestScope = {};
|
|
25
|
+
if (options.activityContext)
|
|
26
|
+
bindActivityRequest(requestScope, options.activityContext);
|
|
24
27
|
const vault = options.credentialVault;
|
|
25
28
|
const owner = options.personalCredentialOwner;
|
|
26
29
|
/**
|
|
27
|
-
* A connector's status message stops here
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* A connector's status message stops here: the payload carries only the
|
|
31
|
+
* classified `problem`, and the log a record of the state and, when
|
|
32
|
+
* connecta described the failure itself, that failure's typed facts. The
|
|
33
|
+
* message is never logged, whatever it looks like: a connector's own
|
|
34
|
+
* `status()` can quote a downstream error body, which can quote the
|
|
35
|
+
* secret it just rejected (INV-6). An `ok` status is simply dropped.
|
|
32
36
|
*/
|
|
33
|
-
const logStatus = (id, state,
|
|
34
|
-
if (
|
|
37
|
+
const logStatus = (id, state, failure) => {
|
|
38
|
+
if (state === "ok")
|
|
35
39
|
return;
|
|
36
40
|
const logger = registry.contextFor(id, baseUrl, requestScope).logger;
|
|
37
|
-
|
|
38
|
-
if (state === "auth_required")
|
|
39
|
-
logger.info(line);
|
|
40
|
-
else
|
|
41
|
-
logger.warn(line);
|
|
41
|
+
logFailure(logger, "operator status", failureRecord({ connector: id, state }, failure), state === "auth_required" ? "info" : "warn");
|
|
42
42
|
};
|
|
43
43
|
// Status first, then the catalog only when status is ok. A failed status
|
|
44
44
|
// read is an error status, not a failed row: the row still reports the
|
|
@@ -52,26 +52,31 @@ export function uiData(registry, baseUrl, options) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
return Effect.gen(function* () {
|
|
55
|
-
const status = yield* attempt(() => registry.statusFor(c.id, baseUrl, requestScope, {
|
|
55
|
+
const status = yield* attempt(() => registry.statusFor(c.id, baseUrl, requestScope, {
|
|
56
|
+
signal,
|
|
57
|
+
...(options.defer ? { defer: options.defer } : {}),
|
|
58
|
+
}));
|
|
56
59
|
if (status.state !== "ok" || signal.aborted) {
|
|
57
60
|
return { status, tools: [], catalogFailed: false };
|
|
58
61
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
// A catalog is configuration the operator chose to load, and its
|
|
63
|
+
// descriptions are the agent's too, so the page shows them as text.
|
|
64
|
+
// A name outside MCP's tool-name grammar is withheld here as in every
|
|
65
|
+
// record (INV-6), and so is the address built from it.
|
|
66
|
+
return yield* attempt(async () => (await registry.getTools(c.id, baseUrl, requestScope, {
|
|
67
|
+
signal,
|
|
68
|
+
...(options.defer ? { defer: options.defer } : {}),
|
|
69
|
+
})).map((t) => ({
|
|
70
|
+
name: recordedToolName(t),
|
|
71
|
+
address: `${c.id}.${recordedToolName(t)}`,
|
|
62
72
|
...(t.description ? { description: t.description } : {}),
|
|
63
|
-
safety: uiToolSafety(t
|
|
73
|
+
safety: uiToolSafety(t),
|
|
64
74
|
}))).pipe(Effect.map((tools) => ({ status, tools, catalogFailed: false })),
|
|
65
75
|
// The registry owns the failed catalog observation; the page only
|
|
66
76
|
// needs to know there was one.
|
|
67
77
|
Effect.catch(() => Effect.succeed({ status, tools: [], catalogFailed: !signal.aborted })));
|
|
68
78
|
}).pipe(Effect.catch((error) => Effect.succeed({
|
|
69
|
-
status:
|
|
70
|
-
state: "error",
|
|
71
|
-
message: error instanceof Error
|
|
72
|
-
? error.message
|
|
73
|
-
: "Connection details unavailable",
|
|
74
|
-
},
|
|
79
|
+
status: failureStatus(c.id, error),
|
|
75
80
|
tools: [],
|
|
76
81
|
catalogFailed: false,
|
|
77
82
|
})));
|
|
@@ -79,8 +84,7 @@ export function uiData(registry, baseUrl, options) {
|
|
|
79
84
|
// The credential card, for someone who may manage this connector's auth.
|
|
80
85
|
// Never fails: a vault that cannot be read is the card's own error.
|
|
81
86
|
const credentialFor = (c) => {
|
|
82
|
-
const mayManageAuth = options.mayManage?.(c.id) ??
|
|
83
|
-
(c.authScope === "personal" ? Boolean(owner) : options.oauthManagement);
|
|
87
|
+
const mayManageAuth = options.mayManage?.(c.id) ?? (c.authScope === "personal" ? Boolean(owner) : options.oauthManagement);
|
|
84
88
|
const declared = c.credential;
|
|
85
89
|
if (!declared || !vault || !mayManageAuth)
|
|
86
90
|
return Effect.succeed(undefined);
|
|
@@ -119,13 +123,9 @@ export function uiData(registry, baseUrl, options) {
|
|
|
119
123
|
...(fields?.length ? { fields } : {}),
|
|
120
124
|
configured: shape.state === "valid",
|
|
121
125
|
removable: Boolean(metadata),
|
|
122
|
-
...(metadata
|
|
123
|
-
? { lastFour: metadata.lastFour, updatedAt: metadata.updatedAt }
|
|
124
|
-
: {}),
|
|
126
|
+
...(metadata ? { lastFour: metadata.lastFour, updatedAt: metadata.updatedAt } : {}),
|
|
125
127
|
testable: testRule.mode !== null && shape.state !== "mismatch",
|
|
126
|
-
...(shape.state === "mismatch"
|
|
127
|
-
? { error: shape.message, problem: "credential_mismatch" }
|
|
128
|
-
: {}),
|
|
128
|
+
...(shape.state === "mismatch" ? { error: shape.message, problem: "credential_mismatch" } : {}),
|
|
129
129
|
// A dropped field leaves its secret in the vault, and the field
|
|
130
130
|
// list below only renders fields the connector still declares —
|
|
131
131
|
// so without this line there is nowhere an operator could see it.
|
|
@@ -153,7 +153,7 @@ export function uiData(registry, baseUrl, options) {
|
|
|
153
153
|
// after this one had already ended the row.
|
|
154
154
|
const { status, tools, catalogFailed } = yield* observe(c, drift, signal);
|
|
155
155
|
const credential = yield* credentialFor(c);
|
|
156
|
-
logStatus(c.id, status.state, status
|
|
156
|
+
logStatus(c.id, status.state, statusFailure(status));
|
|
157
157
|
const problem = uiProblemFor(c, status.state, {
|
|
158
158
|
credentialDrift: Boolean(drift),
|
|
159
159
|
catalogFailed,
|
|
@@ -173,12 +173,17 @@ export function uiData(registry, baseUrl, options) {
|
|
|
173
173
|
// schema even if the plugin seam returned one.
|
|
174
174
|
...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
|
|
175
175
|
...(status.catalogAccess ? { catalogAccess: status.catalogAccess } : {}),
|
|
176
|
+
...(status.resourceTemplateRefusals ? { resourceTemplateRefusals: status.resourceTemplateRefusals } : {}),
|
|
177
|
+
...(status.state === "auth_required" && c.startAuth && options.mayManage?.(c.id) && options.oauthConnectUrl
|
|
178
|
+
? { authorizationUrl: yield* attempt(() => options.oauthConnectUrl(c.id)) }
|
|
179
|
+
: {}),
|
|
176
180
|
oauth: Boolean(c.startAuth && c.disconnectAuth),
|
|
181
|
+
...(status.registrationPath ? { registrationPath: status.registrationPath } : {}),
|
|
177
182
|
...(credential ? { credential } : {}),
|
|
178
183
|
};
|
|
179
184
|
});
|
|
180
185
|
const unavailable = (c, error) => Effect.sync(() => {
|
|
181
|
-
logStatus(c.id, "failed", error
|
|
186
|
+
logStatus(c.id, "failed", error);
|
|
182
187
|
return {
|
|
183
188
|
id: c.id,
|
|
184
189
|
...(c.title ? { title: c.title } : {}),
|
package/dist/routes/ui.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { operatorAsset } from "../operator-ui/assets.js";
|
|
2
|
+
import { operatorActivityContext } from "./activity-context.js";
|
|
3
|
+
import { isSafeHttpsUrl } from "../branding.js";
|
|
4
|
+
import { htmlSecurityHeaders } from "../html-security.js";
|
|
5
|
+
import { oauthConnectUrl, oauthConnectUnavailable } from "../oauth-handoff.js";
|
|
6
|
+
import { authorizationCredential } from "../inbound-credential.js";
|
|
1
7
|
import { Effect } from "effect";
|
|
2
8
|
import { CONNECTA_VERSION } from "../version.js";
|
|
3
9
|
import { CONNECTA_FAVICON_ICO } from "../favicon.js";
|
|
4
|
-
import { CONNECTA_FAVICON_SVG, operatorPageForPath, renderUiHtml
|
|
10
|
+
import { CONNECTA_FAVICON_SVG, operatorPageForPath, renderUiHtml } from "../ui.js";
|
|
5
11
|
import { authorized, refuse, scopeFor, serveOperator, visibleRegistry, } from "./operator.js";
|
|
6
|
-
import { mayManageConnector,
|
|
12
|
+
import { mayManageConnector, privateJson } from "./shared.js";
|
|
13
|
+
import { routeUiConfig } from "./ui-config.js";
|
|
7
14
|
import { uiData } from "./ui-data.js";
|
|
8
15
|
/**
|
|
9
16
|
* Headers that make an operator-supplied favicon body inert on this origin.
|
|
@@ -32,18 +39,12 @@ const INERT_ICON_HEADERS = {
|
|
|
32
39
|
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
|
|
33
40
|
"X-Content-Type-Options": "nosniff",
|
|
34
41
|
};
|
|
35
|
-
/** Per-request base64 nonce for an operator shell's scripts (Node 22+ and Workers). */
|
|
36
|
-
function uiScriptNonce() {
|
|
37
|
-
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
38
|
-
let binary = "";
|
|
39
|
-
for (const byte of bytes)
|
|
40
|
-
binary += String.fromCharCode(byte);
|
|
41
|
-
return btoa(binary);
|
|
42
|
-
}
|
|
43
42
|
export async function routeUi(context) {
|
|
44
43
|
const { request, url, path, baseUrl, opts, runtimeContext } = context;
|
|
44
|
+
if (path.startsWith("/ui/assets/"))
|
|
45
|
+
return operatorAsset(request, path);
|
|
45
46
|
if (request.method === "GET" && path === "/favicon.svg") {
|
|
46
|
-
return new Response(opts.branding?.favicon?.svg ?? CONNECTA_FAVICON_SVG, {
|
|
47
|
+
return new Response(opts.config.ui?.branding?.favicon?.svg ?? CONNECTA_FAVICON_SVG, {
|
|
47
48
|
headers: {
|
|
48
49
|
"Content-Type": "image/svg+xml",
|
|
49
50
|
"Cache-Control": "public, max-age=86400",
|
|
@@ -52,7 +53,7 @@ export async function routeUi(context) {
|
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
if (request.method === "GET" && path === "/favicon.ico") {
|
|
55
|
-
return new Response(opts.branding?.favicon?.ico ?? CONNECTA_FAVICON_ICO, {
|
|
56
|
+
return new Response(opts.config.ui?.branding?.favicon?.ico ?? CONNECTA_FAVICON_ICO, {
|
|
56
57
|
headers: {
|
|
57
58
|
"Content-Type": "image/x-icon",
|
|
58
59
|
"Cache-Control": "public, max-age=86400",
|
|
@@ -71,50 +72,36 @@ export async function routeUi(context) {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
const operatorPage = operatorPageForPath(path);
|
|
74
|
-
const artifactPage = operatorPage === "artifacts" || operatorPage === "artifact";
|
|
75
75
|
if (operatorPage &&
|
|
76
|
-
(operatorPage !== "activity" || opts.activity?.list) &&
|
|
77
|
-
(operatorPage !== "tokens" || opts.accessTokens)
|
|
78
|
-
(!artifactPage || opts.artifactsModule)) {
|
|
76
|
+
(operatorPage !== "activity" || opts.config.activity?.store.list) &&
|
|
77
|
+
(operatorPage !== "tokens" || opts.config.accessTokens)) {
|
|
79
78
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
80
79
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
81
80
|
}
|
|
82
81
|
// Open shell — carries no operator data; everything comes from the
|
|
83
82
|
// authenticated /ui/* APIs after the browser establishes a session.
|
|
84
|
-
const ambient = runtimeContext?.access
|
|
85
|
-
? opts.auth.find((provider) => provider.uiAuth?.kind === "cloudflare-access")?.uiAuth
|
|
83
|
+
const ambient = authorizationCredential(request).kind === "absent" && runtimeContext?.access
|
|
84
|
+
? opts.config.auth.find((provider) => provider.uiAuth?.kind === "cloudflare-access")?.uiAuth
|
|
86
85
|
: undefined;
|
|
87
|
-
const uiAuth = ambient ??
|
|
86
|
+
const uiAuth = ambient ??
|
|
87
|
+
opts.config.auth.find((provider) => provider.uiAuth && provider.uiAuth.kind !== "cloudflare-access")?.uiAuth;
|
|
88
88
|
const mcpUrl = new URL("/mcp", baseUrl).toString();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// honour the nonce and only cover legacy ones. No default-src, so Clerk's
|
|
93
|
-
// style/font/network needs and the page's inline <style> stay unrestricted
|
|
94
|
-
// — only script execution, the XSS sink, is gated.
|
|
95
|
-
const nonce = uiScriptNonce();
|
|
96
|
-
// An artifact shell frames the sandboxed page from its own origin and
|
|
97
|
-
// nothing else; Connections stays on the deployment's public origin.
|
|
98
|
-
const homeUrl = opts.publicUrl ? new URL("/", opts.publicUrl).toString() : "/";
|
|
99
|
-
// The artifact host serves no favicon, so a shell there takes its icons
|
|
100
|
-
// from the public origin, as the 404 does; on the main host they stay
|
|
101
|
-
// root-relative.
|
|
102
|
-
const onArtifactHost = Boolean(opts.artifactOrigin) &&
|
|
103
|
-
url.origin === new URL(opts.artifactOrigin).origin;
|
|
104
|
-
const iconOrigin = onArtifactHost ? opts.publicUrl : undefined;
|
|
89
|
+
const clerkOrigin = uiAuth?.kind === "clerk" && isSafeHttpsUrl(uiAuth.frontendApiUrl)
|
|
90
|
+
? new URL(uiAuth.frontendApiUrl).origin
|
|
91
|
+
: undefined;
|
|
105
92
|
return new Response(request.method === "HEAD"
|
|
106
93
|
? null
|
|
107
|
-
: renderUiHtml(uiAuth, mcpUrl, opts.branding,
|
|
94
|
+
: renderUiHtml(uiAuth, mcpUrl, opts.config.ui?.branding, undefined, operatorPage), {
|
|
108
95
|
status: 200,
|
|
109
|
-
headers: {
|
|
96
|
+
headers: htmlSecurityHeaders({
|
|
110
97
|
"Content-Type": "text/html; charset=utf-8",
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
(artifactPage ? "; frame-src 'self'" : ""),
|
|
114
|
-
"X-Content-Type-Options": "nosniff",
|
|
115
|
-
},
|
|
98
|
+
"Cache-Control": "no-store",
|
|
99
|
+
}, clerkOrigin ? { clerkOrigin } : {}),
|
|
116
100
|
});
|
|
117
101
|
}
|
|
102
|
+
const configResponse = await routeUiConfig(context);
|
|
103
|
+
if (configResponse)
|
|
104
|
+
return configResponse;
|
|
118
105
|
const detail = /^\/ui\/connectors\/([a-z0-9_-]+)$/.exec(path);
|
|
119
106
|
if (path !== "/ui/data" && !detail)
|
|
120
107
|
return null;
|
|
@@ -126,16 +113,23 @@ export async function routeUi(context) {
|
|
|
126
113
|
/** What this identity may see and do, for the summary and a detail alike. */
|
|
127
114
|
function operatorView({ opts }, authz, registry) {
|
|
128
115
|
const visible = registry.listConnectors();
|
|
129
|
-
const mayManage = (id) => {
|
|
116
|
+
const mayManage = (id) => {
|
|
117
|
+
const connector = registry.getConnector(id);
|
|
118
|
+
return Boolean(connector && mayManageConnector(authz, connector));
|
|
119
|
+
};
|
|
130
120
|
const permissions = (connector) => ({
|
|
131
121
|
use: true,
|
|
132
122
|
manageSharedAuth: connector.authScope !== "personal" && mayManage(connector.id),
|
|
133
123
|
connectPersonal: connector.authScope === "personal" && mayManage(connector.id),
|
|
134
124
|
});
|
|
135
|
-
const activityEnabled = Boolean(opts.activity?.list) && authz.operator;
|
|
136
|
-
const credentialManagement = visible.some(c => c.credential && mayManage(c.id))
|
|
137
|
-
? opts.
|
|
138
|
-
|
|
125
|
+
const activityEnabled = Boolean(opts.config.activity?.store.list) && authz.operator;
|
|
126
|
+
const credentialManagement = visible.some((c) => c.credential && mayManage(c.id))
|
|
127
|
+
? opts.config.vault
|
|
128
|
+
? "available"
|
|
129
|
+
: "vault_not_configured"
|
|
130
|
+
: authz.identity.interactive && !visible.some((c) => c.credential)
|
|
131
|
+
? "no_slots"
|
|
132
|
+
: "requires_operator";
|
|
139
133
|
return { visible, mayManage, permissions, activityEnabled, credentialManagement };
|
|
140
134
|
}
|
|
141
135
|
/** One connector's probed row, the page's second request per card. */
|
|
@@ -149,18 +143,21 @@ function connectorDetail(context, id) {
|
|
|
149
143
|
if (!connector)
|
|
150
144
|
return yield* refuse("unknown connector", 404);
|
|
151
145
|
const data = yield* uiData(opts.registry.scoped(scopeFor(authz, [connector.id])), baseUrl, {
|
|
152
|
-
serverInfo: opts.serverInfo,
|
|
153
|
-
|
|
146
|
+
serverInfo: opts.config.serverInfo,
|
|
147
|
+
activityContext: operatorActivityContext(context, authz),
|
|
148
|
+
credentialVault: opts.config.vault,
|
|
154
149
|
activityEnabled,
|
|
155
150
|
credentialManagement,
|
|
156
151
|
defer,
|
|
157
152
|
oauthManagement: false,
|
|
153
|
+
...(!oauthConnectUnavailable(opts)
|
|
154
|
+
? { oauthConnectUrl: (id) => oauthConnectUrl(opts, baseUrl, id, authz.principalKey) }
|
|
155
|
+
: {}),
|
|
158
156
|
discoveryConcurrency: 1,
|
|
159
157
|
personalCredentialOwner: authz.principalKey,
|
|
160
158
|
mayManage,
|
|
161
|
-
timeoutMs: opts.probeTimeoutMs
|
|
159
|
+
timeoutMs: opts.config.discovery.probeTimeoutMs,
|
|
162
160
|
signal: request.signal,
|
|
163
|
-
approval: opts.approval,
|
|
164
161
|
});
|
|
165
162
|
return privateJson({ ...data.connectors[0], permissions: permissions(connector) });
|
|
166
163
|
});
|
|
@@ -185,14 +182,27 @@ function summary(context) {
|
|
|
185
182
|
}
|
|
186
183
|
return privateJson({
|
|
187
184
|
...(pools.length ? { pools } : {}),
|
|
188
|
-
serverInfo: opts.serverInfo,
|
|
185
|
+
serverInfo: opts.config.serverInfo,
|
|
189
186
|
connectaVersion: CONNECTA_VERSION,
|
|
190
187
|
activityEnabled,
|
|
191
|
-
...(opts.accessTokens
|
|
192
|
-
|
|
188
|
+
...(opts.config.accessTokens
|
|
189
|
+
? {
|
|
190
|
+
accessTokenManagement: authz.accessTokenManagement && authz.identity.principal ? "available" : "requires_operator",
|
|
191
|
+
}
|
|
192
|
+
: {}),
|
|
193
193
|
credentialManagement,
|
|
194
|
-
oauthManagement: visible.some(c => mayManage(c.id)),
|
|
195
|
-
connectors: visible.map(c => ({
|
|
194
|
+
oauthManagement: visible.some((c) => mayManage(c.id)),
|
|
195
|
+
connectors: visible.map((c) => ({
|
|
196
|
+
id: c.id,
|
|
197
|
+
...(c.title ? { title: c.title } : {}),
|
|
198
|
+
...(c.description ? { description: c.description } : {}),
|
|
199
|
+
authScope: c.authScope ?? "shared",
|
|
200
|
+
status: "loading",
|
|
201
|
+
toolCount: 0,
|
|
202
|
+
tools: [],
|
|
203
|
+
oauth: Boolean(c.startAuth && c.disconnectAuth),
|
|
204
|
+
permissions: permissions(c),
|
|
205
|
+
})),
|
|
196
206
|
});
|
|
197
207
|
});
|
|
198
208
|
}
|
|
@@ -30,7 +30,7 @@ export function admit(controller, options = {}) {
|
|
|
30
30
|
if (!program) {
|
|
31
31
|
throw new Error("AdmissionController's admission program was never installed.");
|
|
32
32
|
}
|
|
33
|
-
return program(controller, options.signal);
|
|
33
|
+
return program(controller, options.signal, options.wait !== false);
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* A lease owned by the enclosing Scope: released when the scope closes, on
|
|
@@ -39,5 +39,7 @@ export function admit(controller, options = {}) {
|
|
|
39
39
|
* in line until the queue timeout.
|
|
40
40
|
*/
|
|
41
41
|
export function acquireScoped(controller, options = {}) {
|
|
42
|
-
return Effect.acquireRelease(admit(controller, options), (lease) => Effect.sync(() => lease.release()), {
|
|
42
|
+
return Effect.acquireRelease(admit(controller, options), (lease) => Effect.sync(() => lease.release()), {
|
|
43
|
+
interruptible: true,
|
|
44
|
+
});
|
|
43
45
|
}
|
|
@@ -63,5 +63,7 @@ export function admitCall(controller, request) {
|
|
|
63
63
|
* the queue timeout.
|
|
64
64
|
*/
|
|
65
65
|
export function acquireCallScoped(controller, request) {
|
|
66
|
-
return Effect.acquireRelease(admitCall(controller, request), (permit) => Effect.sync(() => permit.release()), {
|
|
66
|
+
return Effect.acquireRelease(admitCall(controller, request), (permit) => Effect.sync(() => permit.release()), {
|
|
67
|
+
interruptible: true,
|
|
68
|
+
});
|
|
67
69
|
}
|
|
@@ -23,10 +23,7 @@ const CONNECTOR_SCOPE_DEFER_BUDGET_MS = 2_000;
|
|
|
23
23
|
* cancel a connector's close, only stop waiting for it.
|
|
24
24
|
*/
|
|
25
25
|
function waitAtMost(work, budgetMs) {
|
|
26
|
-
return Effect.raceAllFirst([
|
|
27
|
-
Effect.promise(() => work),
|
|
28
|
-
Effect.sleep(Duration.millis(budgetMs)),
|
|
29
|
-
]);
|
|
26
|
+
return Effect.raceAllFirst([Effect.promise(() => work), Effect.sleep(Duration.millis(budgetMs))]);
|
|
30
27
|
}
|
|
31
28
|
/**
|
|
32
29
|
* Tell a connector that a scope owned by the core has ended, with the contract
|
package/dist/runtime/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Connecta's published surface is Promise-shaped and stays that way; Effect is
|
|
4
4
|
// an implementation detail behind it. Every edge where a fiber starts runs
|
|
5
5
|
// through this module, so the rules for that crossing live in one file instead
|
|
6
|
-
// of being re-derived at each call site — and test/purity.test.ts fails if any
|
|
6
|
+
// of being re-derived at each call site — and test/purity.node.test.ts fails if any
|
|
7
7
|
// other file under src/ calls Effect.run*, runFork, forkDaemon, or
|
|
8
8
|
// ManagedRuntime.make.
|
|
9
9
|
//
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// - Effect.runPromise rejects an interrupted fiber with a generic "All fibers
|
|
19
19
|
// interrupted without error". Callers of withDeadline today see the abort
|
|
20
20
|
// reason the caller chose, and runEdge keeps it that way.
|
|
21
|
-
import { Cause, Context, Duration, Effect, Exit,
|
|
21
|
+
import { Cause, Context, Duration, Effect, Exit, Scheduler } from "effect";
|
|
22
22
|
/** Yield to the scheduler through the microtask queue, never a timer. */
|
|
23
23
|
function setMicrotask(task) {
|
|
24
24
|
let cancelled = false;
|
|
@@ -34,9 +34,7 @@ function setMicrotask(task) {
|
|
|
34
34
|
export const microtaskScheduler = new Scheduler.MixedScheduler("async", setMicrotask);
|
|
35
35
|
const runExit = Effect.runPromiseExitWith(Context.make(Scheduler.Scheduler, microtaskScheduler));
|
|
36
36
|
function abortReason(signal) {
|
|
37
|
-
return signal && signal.reason !== undefined
|
|
38
|
-
? signal.reason
|
|
39
|
-
: new DOMException("aborted", "AbortError");
|
|
37
|
+
return signal && signal.reason !== undefined ? signal.reason : new DOMException("aborted", "AbortError");
|
|
40
38
|
}
|
|
41
39
|
/**
|
|
42
40
|
* Turn a failed Exit back into the value a Promise caller would have seen.
|
|
@@ -59,43 +57,16 @@ function rethrow(cause, signal) {
|
|
|
59
57
|
throw defect.defect;
|
|
60
58
|
throw abortReason(signal);
|
|
61
59
|
}
|
|
62
|
-
const disposedRuntimes = new WeakSet();
|
|
63
|
-
/** Wrap a layer as an EdgeRuntime without building it. */
|
|
64
|
-
export function makeEdgeRuntime(layer) {
|
|
65
|
-
return ManagedRuntime.make(layer);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Release a runtime's layer resources. Idempotent, and never rejects: a
|
|
69
|
-
* finalizer that fails has nobody left to report to.
|
|
70
|
-
*/
|
|
71
|
-
export async function disposeEdgeRuntime(runtime) {
|
|
72
|
-
if (disposedRuntimes.has(runtime))
|
|
73
|
-
return;
|
|
74
|
-
disposedRuntimes.add(runtime);
|
|
75
|
-
// Through runExit rather than runtime.dispose(), whose runner yields on
|
|
76
|
-
// Effect's default scheduler — the one vitest's fake timers freeze.
|
|
77
|
-
await runExit(runtime.disposeEffect);
|
|
78
|
-
}
|
|
79
60
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* the
|
|
61
|
+
* Run an effect at a Promise boundary.
|
|
62
|
+
*
|
|
63
|
+
* Resolves with the success value. Rejects with the original Fail error or Die
|
|
64
|
+
* defect, and with `signal.reason` (or an AbortError) when interrupted. The
|
|
65
|
+
* effect requires no services: a caller that needs some provides them first
|
|
66
|
+
* (src/runtime/storage.ts).
|
|
83
67
|
*/
|
|
84
|
-
function provideRuntime(effect, runtime) {
|
|
85
|
-
if (disposedRuntimes.has(runtime)) {
|
|
86
|
-
return Effect.die(new Error("This Connecta has been closed."));
|
|
87
|
-
}
|
|
88
|
-
const built = runtime.cachedContext;
|
|
89
|
-
if (built)
|
|
90
|
-
return Effect.provideContext(effect, built);
|
|
91
|
-
return Effect.flatMap(runtime.contextEffect, (context) => Effect.provideContext(effect, context));
|
|
92
|
-
}
|
|
93
68
|
export async function runEdge(effect, options = {}) {
|
|
94
|
-
const exit = await runExit(options.
|
|
95
|
-
? provideRuntime(effect, options.runtime)
|
|
96
|
-
// Without a runtime the overloads admit only an effect that requires
|
|
97
|
-
// nothing, so there is nothing to provide.
|
|
98
|
-
: effect, options.signal ? { signal: options.signal } : undefined);
|
|
69
|
+
const exit = await runExit(effect, options.signal ? { signal: options.signal } : undefined);
|
|
99
70
|
if (Exit.isSuccess(exit))
|
|
100
71
|
return exit.value;
|
|
101
72
|
return rethrow(exit.cause, options.signal);
|
package/dist/runtime/services.js
CHANGED
|
@@ -1,130 +1,25 @@
|
|
|
1
1
|
// The core's services, as Effect code sees them.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
// Every import from an optional module is type-only. An omitted activity
|
|
14
|
-
// module is a no-op recorder that builds no event and calls nothing, and the
|
|
15
|
-
// UI, vault, and activity implementations stay out of the root import graph
|
|
16
|
-
// (test/purity.test.ts).
|
|
17
|
-
import { Context, Effect, Layer } from "effect";
|
|
18
|
-
import { makeEdgeRuntime } from "./run.js";
|
|
3
|
+
// A registry's storage programs read the deployment's KVStorage and Logger
|
|
4
|
+
// from their context rather than from arguments threaded through every call.
|
|
5
|
+
// Each Registry provides its own pair (`runOnPartition` in
|
|
6
|
+
// src/runtime/storage.ts), because a personal registry's storage is the
|
|
7
|
+
// root's namespaced to its principal. There is no per-Connecta runtime:
|
|
8
|
+
// createConnecta resolves its configuration into plain values once
|
|
9
|
+
// (src/config.ts), and the request pipeline runs on no runtime so `/health`
|
|
10
|
+
// and a closed deployment's 503 keep answering after `close()`.
|
|
11
|
+
import { Context } from "effect";
|
|
19
12
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* implements it, so a caller that needs atomicity checks for it here.
|
|
13
|
+
* A registry's KVStorage partition. Every adapter implements `list` and
|
|
14
|
+
* `compareAndSet`.
|
|
23
15
|
*/
|
|
24
16
|
export class Storage extends Context.Service()("connecta/Storage") {
|
|
25
17
|
}
|
|
26
|
-
/**
|
|
27
|
-
* The credential vault, or undefined when the deployment configures none —
|
|
28
|
-
* a required service whose value may be absent, not a default, so a program
|
|
29
|
-
* run without it provided fails to typecheck instead of finding no vault.
|
|
30
|
-
*/
|
|
31
|
-
export class Vault extends Context.Service()("connecta/Vault") {
|
|
32
|
-
}
|
|
33
18
|
/**
|
|
34
19
|
* Connecta's diagnostic logger, already resolved: the configured Logger, a
|
|
35
20
|
* no-op for `"silent"`, or console output prefixed `[connecta]`. Effect's
|
|
36
|
-
* own logger is never used (test/purity.test.ts), because it honors neither
|
|
21
|
+
* own logger is never used (test/purity.node.test.ts), because it honors neither
|
|
37
22
|
* `"silent"` nor the line format a deployment greps for.
|
|
38
23
|
*/
|
|
39
24
|
export class Logger extends Context.Service()("connecta/Logger") {
|
|
40
25
|
}
|
|
41
|
-
/** Payload-free activity history, or a recorder that records nothing. */
|
|
42
|
-
export class ActivityRecorder extends Context.Service()("connecta/ActivityRecorder") {
|
|
43
|
-
}
|
|
44
|
-
/** What createConnecta resolved at construction. */
|
|
45
|
-
export class ResolvedConfig extends Context.Service()("connecta/ResolvedConfig") {
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* The request's hook for work that may outlive its response (a Worker's
|
|
49
|
-
* `ctx.waitUntil`). Per request, not per Connecta, so it is never part of the
|
|
50
|
-
* runtime: a request provides it with `Effect.provideService`, and a program
|
|
51
|
-
* outside any request sees undefined and keeps its work in the foreground.
|
|
52
|
-
*/
|
|
53
|
-
export const DeferredWork = Context.Reference("connecta/DeferredWork", { defaultValue: () => undefined });
|
|
54
|
-
/**
|
|
55
|
-
* Resolve `ConnectaConfig.logger`. `"silent"` discards every line; an omitted
|
|
56
|
-
* logger writes to the console with the `[connecta]` prefix.
|
|
57
|
-
*/
|
|
58
|
-
export function resolveLogger(logger) {
|
|
59
|
-
if (logger === "silent") {
|
|
60
|
-
return { debug() { }, info() { }, warn() { }, error() { } };
|
|
61
|
-
}
|
|
62
|
-
return logger ?? {
|
|
63
|
-
debug: (...a) => console.debug("[connecta]", ...a),
|
|
64
|
-
info: (...a) => console.info("[connecta]", ...a),
|
|
65
|
-
warn: (...a) => console.warn("[connecta]", ...a),
|
|
66
|
-
error: (...a) => console.error("[connecta]", ...a),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
const NO_ACTIVITY = {
|
|
70
|
-
enabled: false,
|
|
71
|
-
recordTool: () => Effect.void,
|
|
72
|
-
recordDrift: () => Effect.void,
|
|
73
|
-
};
|
|
74
|
-
function activityRecorder(module, serverInfo, logger) {
|
|
75
|
-
const deployment = module.deploymentId !== undefined
|
|
76
|
-
? { deploymentId: module.deploymentId }
|
|
77
|
-
: {};
|
|
78
|
-
// A custom module's recorder that throws is logged like a store that
|
|
79
|
-
// throws: activity never changes the outcome of the work it describes.
|
|
80
|
-
const bestEffort = (record) => Effect.sync(() => {
|
|
81
|
-
try {
|
|
82
|
-
record();
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
logger.warn("[connecta] activity record failed", error);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
return {
|
|
89
|
-
enabled: true,
|
|
90
|
-
recordTool: (request, input) => DeferredWork.use((defer) => bestEffort(() => module.recordTool({
|
|
91
|
-
sink: module.store,
|
|
92
|
-
recordTool: module.recordTool,
|
|
93
|
-
actor: request.actor,
|
|
94
|
-
requestId: request.requestId,
|
|
95
|
-
serverInfo,
|
|
96
|
-
...deployment,
|
|
97
|
-
...(defer ? { defer } : {}),
|
|
98
|
-
logger,
|
|
99
|
-
}, input))),
|
|
100
|
-
recordDrift: (input) => bestEffort(() => module.recordDrift({ sink: module.store, serverInfo, ...deployment, logger }, input)),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* The layer behind a Connecta's runtime. Every service is a value resolved
|
|
105
|
-
* before this is called, so building the layer only assembles a context: it
|
|
106
|
-
* starts no fiber and touches no storage.
|
|
107
|
-
*/
|
|
108
|
-
function coreServices(options) {
|
|
109
|
-
const activity = options.activity
|
|
110
|
-
? activityRecorder(options.activity, options.config.serverInfo, options.logger)
|
|
111
|
-
: NO_ACTIVITY;
|
|
112
|
-
return Layer.succeedContext(Context.make(Storage, options.storage).pipe(Context.add(Vault, options.vault), Context.add(Logger, options.logger), Context.add(ActivityRecorder, activity), Context.add(ResolvedConfig, options.config)));
|
|
113
|
-
}
|
|
114
|
-
// Keyed by the root Registry: it is the per-Connecta object every internal
|
|
115
|
-
// caller already holds, and a Registry built directly (as the tests build
|
|
116
|
-
// them) simply has no runtime.
|
|
117
|
-
const runtimes = new WeakMap();
|
|
118
|
-
/**
|
|
119
|
-
* Create the runtime for the Connecta that owns `registry`. Runs nothing:
|
|
120
|
-
* the layer is built by the first run that needs it (see EdgeRuntime).
|
|
121
|
-
*/
|
|
122
|
-
export function createCoreRuntime(registry, options) {
|
|
123
|
-
const runtime = makeEdgeRuntime(coreServices(options));
|
|
124
|
-
runtimes.set(registry, runtime);
|
|
125
|
-
return runtime;
|
|
126
|
-
}
|
|
127
|
-
/** The runtime createConnecta made for this root registry, if it made one. */
|
|
128
|
-
export function coreRuntime(registry) {
|
|
129
|
-
return runtimes.get(registry);
|
|
130
|
-
}
|
|
@@ -75,9 +75,7 @@ export class SharedRead {
|
|
|
75
75
|
this.cancel(signal?.reason);
|
|
76
76
|
});
|
|
77
77
|
const answer = Deferred.await(this.outcome);
|
|
78
|
-
const own = signal
|
|
79
|
-
? Effect.raceFirst(answer, this.leftBy(signal))
|
|
80
|
-
: answer;
|
|
78
|
+
const own = signal ? Effect.raceFirst(answer, this.leftBy(signal)) : answer;
|
|
81
79
|
return Effect.ensuring(own, leave);
|
|
82
80
|
});
|
|
83
81
|
}
|