@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ProtocolError, INVALID_PARAMS } from "@modelcontextprotocol/server";
|
|
2
|
+
export const REQUEST_STATE_TTL_MS = 10 * 60_000;
|
|
3
|
+
export const MAX_INPUT_ROUNDS = 3;
|
|
4
|
+
export function stateObject(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function invalidRequestState() {
|
|
8
|
+
throw new ProtocolError(INVALID_PARAMS, "Invalid or expired requestState", { reason: "invalid_request_state" });
|
|
9
|
+
}
|
|
10
|
+
/** Object order is immaterial; array order and every submitted value are bound. */
|
|
11
|
+
export function canonicalState(value) {
|
|
12
|
+
return JSON.stringify(value, (_key, item) => stateObject(item)
|
|
13
|
+
? Object.fromEntries(Object.keys(item)
|
|
14
|
+
.sort()
|
|
15
|
+
.map((key) => [key, item[key]]))
|
|
16
|
+
: item);
|
|
17
|
+
}
|
|
18
|
+
export async function requestDigest(tool, args) {
|
|
19
|
+
const bytes = new TextEncoder().encode(canonicalState({ tool, args }));
|
|
20
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
21
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { classifyCallError, ConnectorCallError } from "./errors.js";
|
|
3
|
+
import { InvocationFailure, timed } from "./invocation.js";
|
|
4
|
+
import { withDeadlineEffect } from "./runtime/run.js";
|
|
5
|
+
import { sentSecretsFor } from "./sent-secrets.js";
|
|
6
|
+
import { hasControlCharacters } from "./tool-name.js";
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const URI_HINT = "Use resource://<connectorId>/<encodeURIComponent(downstreamUri)> with a whole-connector grant.";
|
|
9
|
+
/** Parse once without URL normalization or interpreting the downstream URI. */
|
|
10
|
+
function resourceTarget(registry, raw) {
|
|
11
|
+
const invalid = () => new ConnectorCallError("invalid_args", `Invalid resource URI. ${URI_HINT}`);
|
|
12
|
+
if (typeof raw !== "string" || raw.length > 8192 || encoder.encode(raw).length > 8192)
|
|
13
|
+
throw invalid();
|
|
14
|
+
const match = /^resource:\/\/([a-z0-9_-]+)\/(.+)$/.exec(raw);
|
|
15
|
+
if (!match)
|
|
16
|
+
throw invalid();
|
|
17
|
+
let uri;
|
|
18
|
+
try {
|
|
19
|
+
uri = decodeURIComponent(match[2]);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw invalid();
|
|
23
|
+
}
|
|
24
|
+
if (encodeURIComponent(uri) !== match[2] || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(uri) || hasControlCharacters(uri))
|
|
25
|
+
throw invalid();
|
|
26
|
+
const connector = registry.getResourceConnector(match[1]);
|
|
27
|
+
if (!connector?.readResource) {
|
|
28
|
+
throw new InvocationFailure({
|
|
29
|
+
code: "unknown_address",
|
|
30
|
+
message: `No resource reader is available for this address. ${URI_HINT}`,
|
|
31
|
+
retryable: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return { connector, uri };
|
|
35
|
+
}
|
|
36
|
+
/** A protocol read, under the same request scope, call permit and deadline as tools. */
|
|
37
|
+
export function readResource(registry, raw, options) {
|
|
38
|
+
let connector;
|
|
39
|
+
return withDeadlineEffect((signal) => Effect.scoped(Effect.gen(function* () {
|
|
40
|
+
const target = yield* Effect.try({ try: () => resourceTarget(registry, raw), catch: (error) => error });
|
|
41
|
+
connector = target.connector;
|
|
42
|
+
options.onResolved?.(target.connector);
|
|
43
|
+
// Await this Promise in the reader's request context, as invocation.ts does.
|
|
44
|
+
yield* Effect.acquireRelease(Effect.suspend(() => {
|
|
45
|
+
const pending = registry.admitCall(target.connector.id, {
|
|
46
|
+
toolName: "resources/read",
|
|
47
|
+
args: { uri: target.uri },
|
|
48
|
+
signal,
|
|
49
|
+
});
|
|
50
|
+
return Effect.tryPromise({ try: () => pending, catch: (error) => error }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
51
|
+
pending.then((permit) => permit.release(), () => { });
|
|
52
|
+
})));
|
|
53
|
+
}), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
|
|
54
|
+
// Downstream contents enter the guest here, so they take the full payload rules.
|
|
55
|
+
const contents = yield* timed((elapsed) => options.onConnectorTime?.(elapsed), Effect.tryPromise({
|
|
56
|
+
try: () => {
|
|
57
|
+
if (signal.aborted)
|
|
58
|
+
throw signal.reason;
|
|
59
|
+
const ctx = registry.contextFor(target.connector.id, options.baseUrl, options.requestScope, {
|
|
60
|
+
signal,
|
|
61
|
+
timeoutMs: options.timeoutMs,
|
|
62
|
+
});
|
|
63
|
+
options.sentSecrets.include(sentSecretsFor(ctx));
|
|
64
|
+
if (target.connector.credential && !ctx.credential) {
|
|
65
|
+
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured.");
|
|
66
|
+
}
|
|
67
|
+
return target.connector.readResource(target.uri, ctx);
|
|
68
|
+
},
|
|
69
|
+
catch: (error) => error,
|
|
70
|
+
}));
|
|
71
|
+
return options.sentSecrets.redact(contents);
|
|
72
|
+
})), {
|
|
73
|
+
timeoutMs: options.timeoutMs,
|
|
74
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
75
|
+
timeoutError: new ConnectorCallError("timeout", `Resource read timed out after ${options.timeoutMs}ms.`),
|
|
76
|
+
}).pipe(Effect.catch((error) => {
|
|
77
|
+
let details = options.signal?.aborted
|
|
78
|
+
? { code: "cancelled", message: "Resource read was cancelled because the run ended.", retryable: false }
|
|
79
|
+
: error instanceof InvocationFailure
|
|
80
|
+
? error.details
|
|
81
|
+
: classifyCallError(options.sentSecrets.redact(error));
|
|
82
|
+
if (connector && (details.code === "auth_required" || details.code === "downstream_oauth_required")) {
|
|
83
|
+
details = {
|
|
84
|
+
...details,
|
|
85
|
+
connector: connector.id,
|
|
86
|
+
recovery: connector.startAuth
|
|
87
|
+
? "oauth"
|
|
88
|
+
: registry.credentialUiAvailable() &&
|
|
89
|
+
connector.credential &&
|
|
90
|
+
registry.contextFor(connector.id, options.baseUrl, options.requestScope).credential
|
|
91
|
+
? "operator_config"
|
|
92
|
+
: "unavailable",
|
|
93
|
+
nextAction: {
|
|
94
|
+
tool: "authorize_connector",
|
|
95
|
+
arguments: { connector: connector.id },
|
|
96
|
+
operatorHandoff: "Give the URL and instructions it returns to the operator.",
|
|
97
|
+
},
|
|
98
|
+
retry: "Retry connecta.read with the same resource URI after the operator completes recovery.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return Effect.fail(new InvocationFailure(options.sentSecrets.redact(details)));
|
|
102
|
+
}));
|
|
103
|
+
}
|
package/dist/result-shapes.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
2
1
|
const OBSERVATION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3
2
|
const MAX_CACHED_SHAPES = 256;
|
|
4
3
|
const MAX_DEFINITION_BYTES = 64 * 1024;
|
|
@@ -8,11 +7,7 @@ const MAX_SCHEMA_NODES = 128;
|
|
|
8
7
|
const MAX_OBJECT_PROPERTIES = 48;
|
|
9
8
|
const MAX_ARRAY_ITEMS = 32;
|
|
10
9
|
const MAX_PROPERTY_NAME_BYTES = 128;
|
|
11
|
-
const UNSAFE_PROPERTY_NAMES = new Set([
|
|
12
|
-
"__proto__",
|
|
13
|
-
"constructor",
|
|
14
|
-
"prototype",
|
|
15
|
-
]);
|
|
10
|
+
const UNSAFE_PROPERTY_NAMES = new Set(["__proto__", "constructor", "prototype"]);
|
|
16
11
|
const encoder = new TextEncoder();
|
|
17
12
|
function broadType(value) {
|
|
18
13
|
if (value === null)
|
|
@@ -34,9 +29,7 @@ function broadType(value) {
|
|
|
34
29
|
function serializedSchema(schema) {
|
|
35
30
|
try {
|
|
36
31
|
const text = JSON.stringify(schema);
|
|
37
|
-
return encoder.encode(text).byteLength <= MAX_SCHEMA_BYTES
|
|
38
|
-
? text
|
|
39
|
-
: undefined;
|
|
32
|
+
return encoder.encode(text).byteLength <= MAX_SCHEMA_BYTES ? text : undefined;
|
|
40
33
|
}
|
|
41
34
|
catch {
|
|
42
35
|
return undefined;
|
|
@@ -56,9 +49,7 @@ function cloneSchema(schema) {
|
|
|
56
49
|
function definitionIdentity(definition) {
|
|
57
50
|
try {
|
|
58
51
|
const serialized = JSON.stringify(definition);
|
|
59
|
-
return encoder.encode(serialized).byteLength <= MAX_DEFINITION_BYTES
|
|
60
|
-
? serialized
|
|
61
|
-
: undefined;
|
|
52
|
+
return encoder.encode(serialized).byteLength <= MAX_DEFINITION_BYTES ? serialized : undefined;
|
|
62
53
|
}
|
|
63
54
|
catch {
|
|
64
55
|
return undefined;
|
|
@@ -68,8 +59,7 @@ function schemaType(schema) {
|
|
|
68
59
|
return typeof schema.type === "string" ? schema.type : undefined;
|
|
69
60
|
}
|
|
70
61
|
function safePropertyName(name) {
|
|
71
|
-
return
|
|
72
|
-
encoder.encode(name).byteLength <= MAX_PROPERTY_NAME_BYTES);
|
|
62
|
+
return !UNSAFE_PROPERTY_NAMES.has(name) && encoder.encode(name).byteLength <= MAX_PROPERTY_NAME_BYTES;
|
|
73
63
|
}
|
|
74
64
|
function boundedPropertyNames(value) {
|
|
75
65
|
const names = [];
|
|
@@ -83,9 +73,7 @@ function boundedPropertyNames(value) {
|
|
|
83
73
|
return names.sort();
|
|
84
74
|
}
|
|
85
75
|
function unionBranches(schema) {
|
|
86
|
-
return Array.isArray(schema.anyOf)
|
|
87
|
-
? schema.anyOf
|
|
88
|
-
: [schema];
|
|
76
|
+
return Array.isArray(schema.anyOf) ? schema.anyOf : [schema];
|
|
89
77
|
}
|
|
90
78
|
function branchOrder(schema) {
|
|
91
79
|
return ["null", "boolean", "number", "string", "array", "object"].indexOf(schemaType(schema) ?? "");
|
|
@@ -94,40 +82,25 @@ function mergeSchemas(left, right) {
|
|
|
94
82
|
const leftType = schemaType(left);
|
|
95
83
|
const rightType = schemaType(right);
|
|
96
84
|
if (leftType === "object" && rightType === "object") {
|
|
97
|
-
const leftProperties = left.properties && typeof left.properties === "object"
|
|
98
|
-
|
|
99
|
-
: {};
|
|
100
|
-
const rightProperties = right.properties && typeof right.properties === "object"
|
|
101
|
-
? right.properties
|
|
102
|
-
: {};
|
|
85
|
+
const leftProperties = left.properties && typeof left.properties === "object" ? left.properties : {};
|
|
86
|
+
const rightProperties = right.properties && typeof right.properties === "object" ? right.properties : {};
|
|
103
87
|
const properties = Object.create(null);
|
|
104
|
-
for (const key of [...new Set([
|
|
105
|
-
...Object.keys(leftProperties),
|
|
106
|
-
...Object.keys(rightProperties),
|
|
107
|
-
])].sort()) {
|
|
88
|
+
for (const key of [...new Set([...Object.keys(leftProperties), ...Object.keys(rightProperties)])].sort()) {
|
|
108
89
|
const leftProperty = leftProperties[key];
|
|
109
90
|
const rightProperty = rightProperties[key];
|
|
110
91
|
properties[key] =
|
|
111
|
-
leftProperty && rightProperty
|
|
112
|
-
? mergeSchemas(leftProperty, rightProperty)
|
|
113
|
-
: (leftProperty ?? rightProperty);
|
|
92
|
+
leftProperty && rightProperty ? mergeSchemas(leftProperty, rightProperty) : (leftProperty ?? rightProperty);
|
|
114
93
|
}
|
|
115
94
|
return { type: "object", properties };
|
|
116
95
|
}
|
|
117
96
|
if (leftType === "array" && rightType === "array") {
|
|
118
|
-
const leftItems = left.items && typeof left.items === "object"
|
|
119
|
-
|
|
120
|
-
: undefined;
|
|
121
|
-
const rightItems = right.items && typeof right.items === "object"
|
|
122
|
-
? right.items
|
|
123
|
-
: undefined;
|
|
97
|
+
const leftItems = left.items && typeof left.items === "object" ? left.items : undefined;
|
|
98
|
+
const rightItems = right.items && typeof right.items === "object" ? right.items : undefined;
|
|
124
99
|
return {
|
|
125
100
|
type: "array",
|
|
126
101
|
...(leftItems || rightItems
|
|
127
102
|
? {
|
|
128
|
-
items: leftItems && rightItems
|
|
129
|
-
? mergeSchemas(leftItems, rightItems)
|
|
130
|
-
: (leftItems ?? rightItems),
|
|
103
|
+
items: leftItems && rightItems ? mergeSchemas(leftItems, rightItems) : (leftItems ?? rightItems),
|
|
131
104
|
}
|
|
132
105
|
: {}),
|
|
133
106
|
};
|
|
@@ -212,8 +185,7 @@ function boundSchema(schema, budget, depth = 0) {
|
|
|
212
185
|
const properties = Object.create(null);
|
|
213
186
|
let included = 0;
|
|
214
187
|
for (const key of Object.keys(source).sort()) {
|
|
215
|
-
if (included >= MAX_OBJECT_PROPERTIES ||
|
|
216
|
-
!safePropertyName(key)) {
|
|
188
|
+
if (included >= MAX_OBJECT_PROPERTIES || !safePropertyName(key)) {
|
|
217
189
|
continue;
|
|
218
190
|
}
|
|
219
191
|
const property = boundSchema(source[key], budget, depth + 1);
|
|
@@ -231,9 +203,7 @@ function boundSchema(schema, budget, depth = 0) {
|
|
|
231
203
|
function observedSchema(value) {
|
|
232
204
|
try {
|
|
233
205
|
const inferred = inferSchema(value, { nodes: 0, seen: new WeakSet() });
|
|
234
|
-
const schema = inferred
|
|
235
|
-
? boundSchema(inferred, { nodes: 0 })
|
|
236
|
-
: undefined;
|
|
206
|
+
const schema = inferred ? boundSchema(inferred, { nodes: 0 }) : undefined;
|
|
237
207
|
return schema && serializedSchema(schema) ? schema : undefined;
|
|
238
208
|
}
|
|
239
209
|
catch {
|
|
@@ -267,7 +237,7 @@ export class ObservedOutputSchemas {
|
|
|
267
237
|
this.entries.delete(key);
|
|
268
238
|
return undefined;
|
|
269
239
|
}
|
|
270
|
-
if (
|
|
240
|
+
if (definition.classification !== "read") {
|
|
271
241
|
this.entries.delete(key);
|
|
272
242
|
return undefined;
|
|
273
243
|
}
|
|
@@ -298,7 +268,7 @@ export class ObservedOutputSchemas {
|
|
|
298
268
|
this.entries.delete(key);
|
|
299
269
|
return;
|
|
300
270
|
}
|
|
301
|
-
if (
|
|
271
|
+
if (definition.classification !== "read") {
|
|
302
272
|
this.entries.delete(key);
|
|
303
273
|
return;
|
|
304
274
|
}
|
|
@@ -311,9 +281,7 @@ export class ObservedOutputSchemas {
|
|
|
311
281
|
if (!inferred)
|
|
312
282
|
return;
|
|
313
283
|
const current = this.entries.get(key);
|
|
314
|
-
const merged = boundSchema(current &&
|
|
315
|
-
current.definition === identity &&
|
|
316
|
-
current.expiresAt > Date.now()
|
|
284
|
+
const merged = boundSchema(current && current.definition === identity && current.expiresAt > Date.now()
|
|
317
285
|
? mergeSchemas(current.schema, inferred)
|
|
318
286
|
: inferred, { nodes: 0 });
|
|
319
287
|
if (!merged || !serializedSchema(merged))
|
package/dist/retired-toolkits.js
CHANGED
|
@@ -4,5 +4,5 @@ export function assertNoRetiredToolkitOptions(source, options) {
|
|
|
4
4
|
if (!hasOwn(options, "toolkits") && !hasOwn(options, "unscoped"))
|
|
5
5
|
return;
|
|
6
6
|
throw new Error(`${source} options \`toolkits\` and \`unscoped\` were removed in issue ` +
|
|
7
|
-
"#178. Deploy one connecta instance per audience instead; see
|
|
7
|
+
"#178. Deploy one connecta instance per audience instead; see PRINCIPLES.md.");
|
|
8
8
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { authorizeUiIdentity, isSameOrigin, privateJson
|
|
1
|
+
import { authorizeUiIdentity, isSameOrigin, privateJson } from "./shared.js";
|
|
2
2
|
async function readName(request) {
|
|
3
|
-
if (!request.headers
|
|
4
|
-
.get("content-type")
|
|
5
|
-
?.toLowerCase()
|
|
6
|
-
.startsWith("application/json")) {
|
|
3
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
7
4
|
return {
|
|
8
5
|
ok: false,
|
|
9
6
|
response: privateJson({ error: "Content-Type must be application/json" }, { status: 415 }),
|
|
@@ -70,7 +67,7 @@ export async function routeAccessTokens(context, manager) {
|
|
|
70
67
|
if (mutating && !isSameOrigin(request, baseUrl)) {
|
|
71
68
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
72
69
|
}
|
|
73
|
-
const admin = await authorizeUiIdentity(request, baseUrl, opts.auth, "access token management", context.runtimeContext, opts.identity);
|
|
70
|
+
const admin = await authorizeUiIdentity(request, baseUrl, opts.config.auth, "access token management", context.runtimeContext, opts.config.identity);
|
|
74
71
|
if (!admin.ok)
|
|
75
72
|
return admin.response;
|
|
76
73
|
if (!admin.accessTokenManagement || !admin.identity.principal) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Attribute catalog refreshes initiated by an admitted operator request. */
|
|
2
|
+
export function operatorActivityContext(context, authz) {
|
|
3
|
+
const { opts, defer } = context;
|
|
4
|
+
const activity = opts.config.activity;
|
|
5
|
+
if (!activity)
|
|
6
|
+
return undefined;
|
|
7
|
+
const principal = authz.identity.principal;
|
|
8
|
+
return {
|
|
9
|
+
sink: activity.store,
|
|
10
|
+
recordTool: activity.recordTool,
|
|
11
|
+
actor: authz.actor,
|
|
12
|
+
...(principal
|
|
13
|
+
? { principalActor: { kind: authz.actor.kind, id: principal.id, namespace: principal.namespace } }
|
|
14
|
+
: {}),
|
|
15
|
+
requestId: crypto.randomUUID(),
|
|
16
|
+
serverInfo: opts.config.serverInfo,
|
|
17
|
+
...(activity.deploymentId ? { deploymentId: activity.deploymentId } : {}),
|
|
18
|
+
...(defer ? { defer } : {}),
|
|
19
|
+
logger: opts.config.logger,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { intersectAccess } from "../connector-access.js";
|
|
2
|
+
import { recordedToolName } from "../operator-record.js";
|
|
3
|
+
/** Historical disclosure uses the verdict at call time; old guarded rows fail closed. */
|
|
4
|
+
function activityToolVisible(authz, event) {
|
|
5
|
+
const allowed = authz.toolAccess?.get(event.connectorId);
|
|
6
|
+
return (recordedToolName({ name: event.toolName }) === event.toolName &&
|
|
7
|
+
(!allowed || allowed.has(event.toolName)) &&
|
|
8
|
+
(!authz.guardedToolAccess?.get(event.connectorId)?.has(event.toolName) || event.classification === "read"));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One disclosure rule for the timeline and the UI's last-call overlay.
|
|
12
|
+
* After Activity access and the read gate, operators and activityAccess machine
|
|
13
|
+
* callers may see other principals' shared-connector history when connector/tool
|
|
14
|
+
* and recorded-pool grants permit, including request IDs, client name/version,
|
|
15
|
+
* and actor kind/id/namespace. Interactive reads may add directory actor labels;
|
|
16
|
+
* machine reads do not. Personal history requires a matching principal owner.
|
|
17
|
+
*/
|
|
18
|
+
export function activityEventVisible(context, authz, registry, admittedPools, event) {
|
|
19
|
+
const connector = registry.getConnector(event.connectorId);
|
|
20
|
+
if (!connector || (event.pool !== undefined && !admittedPools.has(event.pool)))
|
|
21
|
+
return false;
|
|
22
|
+
if (connector.authScope === "personal" || (event.kind === "catalog_drift" && event.actorBasis === "principal")) {
|
|
23
|
+
const owner = authz.identity.principal;
|
|
24
|
+
if (!owner ||
|
|
25
|
+
event.actorBasis !== "principal" ||
|
|
26
|
+
event.actor.id !== owner.id ||
|
|
27
|
+
event.actor.namespace !== owner.namespace)
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const pool = event.pool ? context.opts.pools.get(event.pool) : undefined;
|
|
31
|
+
if (event.pool && !pool)
|
|
32
|
+
return false;
|
|
33
|
+
const access = pool ? intersectAccess(authz, pool.access) : authz;
|
|
34
|
+
if (access.connectorIds !== "all" && !access.connectorIds.includes(event.connectorId))
|
|
35
|
+
return false;
|
|
36
|
+
// Connector-wide counts reveal tools outside an address-only grant.
|
|
37
|
+
if (event.kind === "catalog_drift")
|
|
38
|
+
return !access.toolAccess?.has(event.connectorId);
|
|
39
|
+
return activityToolVisible(access, event);
|
|
40
|
+
}
|
package/dist/routes/activity.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { activityEventVisible } from "./activity-disclosure.js";
|
|
1
2
|
import { Duration, Effect } from "effect";
|
|
2
|
-
import { InvalidActivityCursorError, } from "../activity.js";
|
|
3
|
+
import { activityBehaviorFacts, activityPackageVersion, activityClientFact, InvalidActivityCursorError, } from "../activity.js";
|
|
4
|
+
import { failureRecord, logFailure } from "../operator-record.js";
|
|
3
5
|
import { ACTOR_LABEL_BUDGET_MS, ACTOR_LABEL_CONCURRENCY, actorKey, cleanActorLabel, labelLookups, } from "./actor-labels.js";
|
|
4
|
-
import { authorized, refuse, serveOperator } from "./operator.js";
|
|
6
|
+
import { authorized, refuse, serveOperator, visibleRegistry } from "./operator.js";
|
|
5
7
|
import { privateJson } from "./shared.js";
|
|
6
8
|
/**
|
|
7
9
|
* Add display-only actor labels to one authorized activity page. Resolution is
|
|
@@ -25,20 +27,25 @@ function enrichActivityActorLabels(page, auth) {
|
|
|
25
27
|
}).pipe(Effect.timeoutOption(Duration.millis(ACTOR_LABEL_BUDGET_MS)), Effect.map(() => ({
|
|
26
28
|
...page,
|
|
27
29
|
events: page.events.map((event) => {
|
|
28
|
-
const resolved = event.actor.id
|
|
29
|
-
? labels.get(actorKey(event.actor))
|
|
30
|
-
: undefined;
|
|
30
|
+
const resolved = event.actor.id ? labels.get(actorKey(event.actor)) : undefined;
|
|
31
31
|
// Never trust or echo a `label` supplied by storage. The persisted event
|
|
32
32
|
// schema has no label; only this authenticated read path may add one.
|
|
33
33
|
const actor = {
|
|
34
34
|
kind: event.actor.kind,
|
|
35
35
|
...(event.actor.id ? { id: event.actor.id } : {}),
|
|
36
|
-
...(event.actor.namespace
|
|
37
|
-
? { namespace: event.actor.namespace }
|
|
38
|
-
: {}),
|
|
36
|
+
...(event.actor.namespace ? { namespace: event.actor.namespace } : {}),
|
|
39
37
|
};
|
|
38
|
+
// Re-check stored client facts, including custom readers and old rows.
|
|
39
|
+
const { packageVersion: suppliedPackageVersion, clientName: suppliedName, clientVersion: suppliedVersion, classification: _classification, resultBytes: _resultBytes, kind: _kind, drift: _drift, pool: _pool, actorBasis: _actorBasis, ...record } = event;
|
|
40
|
+
const packageVersion = activityPackageVersion(suppliedPackageVersion);
|
|
41
|
+
const clientName = activityClientFact(suppliedName, "name");
|
|
42
|
+
const clientVersion = activityClientFact(suppliedVersion, "version");
|
|
40
43
|
return {
|
|
41
|
-
...
|
|
44
|
+
...record,
|
|
45
|
+
...activityBehaviorFacts(event),
|
|
46
|
+
...(packageVersion !== undefined ? { packageVersion } : {}),
|
|
47
|
+
...(clientName !== undefined ? { clientName } : {}),
|
|
48
|
+
...(clientVersion !== undefined ? { clientVersion } : {}),
|
|
42
49
|
actor: resolved ? { ...actor, label: resolved } : actor,
|
|
43
50
|
};
|
|
44
51
|
}),
|
|
@@ -48,41 +55,72 @@ function enrichActivityActorLabels(page, auth) {
|
|
|
48
55
|
function activityRead(context) {
|
|
49
56
|
const { url, opts } = context;
|
|
50
57
|
return Effect.gen(function* () {
|
|
51
|
-
const authz = yield* authorized(context
|
|
58
|
+
const authz = yield* authorized(context);
|
|
59
|
+
const registry = yield* visibleRegistry(context, authz);
|
|
52
60
|
if (!authz.operator)
|
|
53
|
-
return yield* refuse("
|
|
54
|
-
if (opts.
|
|
55
|
-
!(yield* Effect.promise(async () => opts.
|
|
61
|
+
return yield* refuse("activity access required", 403);
|
|
62
|
+
if (opts.config.activity?.readGate &&
|
|
63
|
+
!(yield* Effect.promise(async () => opts.config.activity?.readGate(authz.actor)))) {
|
|
56
64
|
return yield* refuse("forbidden", 403);
|
|
57
65
|
}
|
|
58
|
-
const list = opts.activity?.list?.bind(opts.activity);
|
|
66
|
+
const list = opts.config.activity?.store.list?.bind(opts.config.activity?.store);
|
|
59
67
|
if (!list)
|
|
60
68
|
return yield* refuse("activity history is not configured", 404);
|
|
69
|
+
const admittedPools = new Set();
|
|
70
|
+
for (const [name, pool] of opts.pools) {
|
|
71
|
+
if (yield* Effect.promise(async () => {
|
|
72
|
+
try {
|
|
73
|
+
return (await pool.grant(authz.identity)) === true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
admittedPools.add(name);
|
|
80
|
+
}
|
|
81
|
+
const visible = (event) => activityEventVisible(context, authz, registry, admittedPools, event);
|
|
61
82
|
const cursor = url.searchParams.get("cursor") ?? undefined;
|
|
62
83
|
if (cursor && cursor.length > 500)
|
|
63
84
|
return yield* refuse("invalid cursor", 400);
|
|
64
85
|
const requestedLimit = Number(url.searchParams.get("limit") ?? "50");
|
|
65
|
-
const limit = Number.isFinite(requestedLimit)
|
|
66
|
-
|
|
67
|
-
: 50;
|
|
68
|
-
return yield* Effect.tryPromise({
|
|
86
|
+
const limit = Number.isFinite(requestedLimit) ? Math.min(100, Math.max(1, Math.trunc(requestedLimit))) : 50;
|
|
87
|
+
const readPage = (cursor, limit) => Effect.tryPromise({
|
|
69
88
|
try: () => list({ ...(cursor !== undefined ? { cursor } : {}), limit }),
|
|
70
89
|
catch: (error) => error,
|
|
71
|
-
})
|
|
90
|
+
});
|
|
91
|
+
return yield* Effect.gen(function* () {
|
|
92
|
+
let page = yield* readPage(cursor, limit);
|
|
93
|
+
const events = page.events.filter(visible);
|
|
94
|
+
// Store cursors may encode their boundary row's timestamp and id. A
|
|
95
|
+
// rejected boundary must never reach the caller. Advance one row at a
|
|
96
|
+
// time until the boundary is visible or history is exhausted.
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
let boundaryReads = 0;
|
|
99
|
+
while (page.nextCursor && (!page.events.length || !visible(page.events[page.events.length - 1]))) {
|
|
100
|
+
if (boundaryReads++ >= 1000)
|
|
101
|
+
throw new Error("activity disclosure scan exhausted");
|
|
102
|
+
if (seen.has(page.nextCursor))
|
|
103
|
+
throw new Error("activity cursor did not advance");
|
|
104
|
+
seen.add(page.nextCursor);
|
|
105
|
+
page = yield* readPage(page.nextCursor, 1);
|
|
106
|
+
events.push(...page.events.filter(visible));
|
|
107
|
+
}
|
|
108
|
+
return { events, ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) };
|
|
109
|
+
}).pipe(Effect.flatMap((page) => enrichActivityActorLabels(page, authz.identity.interactive ? opts.config.auth : [])), Effect.map((page) => privateJson(page)),
|
|
72
110
|
// A page too malformed to label or serialize is the store's failure,
|
|
73
111
|
// like any other.
|
|
74
112
|
Effect.catchDefect((defect) => Effect.fail(defect)), Effect.catch((error) => {
|
|
75
113
|
if (error instanceof InvalidActivityCursorError) {
|
|
76
114
|
return refuse(error.message, 400);
|
|
77
115
|
}
|
|
78
|
-
opts.logger
|
|
116
|
+
logFailure(opts.config.logger, "activity read failed", failureRecord({}, error), "error");
|
|
79
117
|
return refuse("activity history is temporarily unavailable", 503);
|
|
80
118
|
}));
|
|
81
119
|
});
|
|
82
120
|
}
|
|
83
121
|
export async function routeActivity(context) {
|
|
84
122
|
const { path, request } = context;
|
|
85
|
-
if (path !== "/ui/activity")
|
|
123
|
+
if (path !== "/ui/activity" && path !== "/ui/api/activity")
|
|
86
124
|
return null;
|
|
87
125
|
if (request.method !== "GET") {
|
|
88
126
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Display labels for stored actors
|
|
2
|
-
// artifact library. An actor is stored as a stable id; a label is resolved at
|
|
1
|
+
// Display labels for stored activity actors. An actor is stored as a stable id; a label is resolved at
|
|
3
2
|
// read time from the one auth provider that owns the actor's directory, and
|
|
4
3
|
// only on an authorized read path. Nothing here is persisted, and a label a
|
|
5
4
|
// store happens to carry is never trusted or echoed.
|
|
@@ -43,11 +42,9 @@ export function labelLookups(actors, auth) {
|
|
|
43
42
|
const eligible = identity.namespace
|
|
44
43
|
? candidates.filter(({ provider }) => activityActorNamespace(provider) === identity.namespace)
|
|
45
44
|
: (() => {
|
|
46
|
-
const directoryKey = ({ provider, index
|
|
45
|
+
const directoryKey = ({ provider, index }) => {
|
|
47
46
|
const namespace = activityActorNamespace(provider);
|
|
48
|
-
return namespace === undefined
|
|
49
|
-
? `provider:${index}`
|
|
50
|
-
: `namespace:${namespace}`;
|
|
47
|
+
return namespace === undefined ? `provider:${index}` : `namespace:${namespace}`;
|
|
51
48
|
};
|
|
52
49
|
// Every same-kind provider participates in the ambiguity check,
|
|
53
50
|
// even if it cannot resolve labels. Otherwise a legacy ID owned
|
|
@@ -68,47 +65,3 @@ export function labelLookups(actors, auth) {
|
|
|
68
65
|
}
|
|
69
66
|
return lookups;
|
|
70
67
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Resolve labels for `actors`, best-effort: at most eight lookups at once,
|
|
73
|
-
* 1.5 seconds in all, and whatever has resolved by then is the answer. A
|
|
74
|
-
* provider outage yields no labels, never a failed read.
|
|
75
|
-
*/
|
|
76
|
-
export async function resolveActorLabels(actors, auth) {
|
|
77
|
-
const labels = new Map();
|
|
78
|
-
const queue = labelLookups(actors, auth);
|
|
79
|
-
let cursor = 0;
|
|
80
|
-
let expired = false;
|
|
81
|
-
const worker = async () => {
|
|
82
|
-
while (!expired && cursor < queue.length) {
|
|
83
|
-
const lookup = queue[cursor++];
|
|
84
|
-
if (!lookup)
|
|
85
|
-
return;
|
|
86
|
-
try {
|
|
87
|
-
const label = cleanActorLabel(await Promise.resolve(lookup.provider.activityActorLabel?.(lookup.id)));
|
|
88
|
-
if (label && !expired)
|
|
89
|
-
labels.set(lookup.key, label);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// A failed lookup leaves the actor unlabeled.
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
let timer;
|
|
97
|
-
const budget = new Promise((resolve) => {
|
|
98
|
-
timer = setTimeout(() => {
|
|
99
|
-
expired = true;
|
|
100
|
-
resolve();
|
|
101
|
-
}, ACTOR_LABEL_BUDGET_MS);
|
|
102
|
-
});
|
|
103
|
-
try {
|
|
104
|
-
await Promise.race([
|
|
105
|
-
Promise.all(Array.from({ length: Math.min(ACTOR_LABEL_CONCURRENCY, queue.length) }, worker)),
|
|
106
|
-
budget,
|
|
107
|
-
]);
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
if (timer !== undefined)
|
|
111
|
-
clearTimeout(timer);
|
|
112
|
-
}
|
|
113
|
-
return new Map(labels);
|
|
114
|
-
}
|