@zackbart/connecta 0.24.4 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +43 -12
- package/CHANGELOG.md +524 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/advertised-schema.js +53 -0
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +449 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +194 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +152 -106
- package/dist/catalog.js +297 -38
- package/dist/concurrency.js +24 -23
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +330 -240
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +745 -282
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +145 -63
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +255 -212
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +281 -26
- package/dist/invocation.js +371 -286
- package/dist/meta-tools.js +213 -94
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +103 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +128 -3
- package/dist/program-source.js +321 -0
- package/dist/providers/notion.js +2 -2
- package/dist/providers/revenuecat.js +32 -5
- package/dist/providers/stripe.d.ts +3 -2
- package/dist/providers/vercel.js +1 -1
- package/dist/registry.d.ts +43 -7
- package/dist/registry.js +434 -288
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +74 -154
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +196 -163
- package/dist/routes/mcp.js +228 -112
- package/dist/routes/oauth-management.js +73 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +83 -0
- package/dist/routes/shared.d.ts +25 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +211 -0
- package/dist/routes/ui.js +76 -35
- package/dist/run-journal.js +347 -0
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +137 -89
- package/dist/skills.js +28 -9
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +39 -43
- package/dist/tool-safety.d.ts +27 -1
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +45 -7
- package/dist/ui.d.ts +25 -4
- package/dist/ui.js +77 -167
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +367 -16
- package/documentation/auth.md +163 -2
- package/documentation/code-mode.md +274 -56
- package/documentation/meta-tools.md +200 -44
- package/ethos.md +46 -47
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +224 -14
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +42 -7
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +21 -5
- package/package.json +16 -4
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -96
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -188
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
package/dist/routes/activity.js
CHANGED
|
@@ -1,172 +1,92 @@
|
|
|
1
|
+
import { Duration, Effect } from "effect";
|
|
1
2
|
import { InvalidActivityCursorError, } from "../activity.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const ACTIVITY_LABEL_MAX_LENGTH = 160;
|
|
6
|
-
function cleanActivityActorLabel(value) {
|
|
7
|
-
if (typeof value !== "string")
|
|
8
|
-
return undefined;
|
|
9
|
-
const compact = value.replace(/\s+/gu, " ").trim();
|
|
10
|
-
if (!compact)
|
|
11
|
-
return undefined;
|
|
12
|
-
return Array.from(compact).slice(0, ACTIVITY_LABEL_MAX_LENGTH).join("");
|
|
13
|
-
}
|
|
14
|
-
async function boundedActivityActorLabel(hook, id, budgetMs) {
|
|
15
|
-
let timer;
|
|
16
|
-
try {
|
|
17
|
-
return await Promise.race([
|
|
18
|
-
Promise.resolve(hook(id))
|
|
19
|
-
.then(cleanActivityActorLabel)
|
|
20
|
-
.catch(() => undefined),
|
|
21
|
-
new Promise((resolve) => {
|
|
22
|
-
timer = setTimeout(resolve, budgetMs);
|
|
23
|
-
}),
|
|
24
|
-
]);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
finally {
|
|
30
|
-
if (timer !== undefined)
|
|
31
|
-
clearTimeout(timer);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
3
|
+
import { ACTOR_LABEL_BUDGET_MS, ACTOR_LABEL_CONCURRENCY, actorKey, cleanActorLabel, labelLookups, } from "./actor-labels.js";
|
|
4
|
+
import { authorized, refuse, serveOperator } from "./operator.js";
|
|
5
|
+
import { privateJson } from "./shared.js";
|
|
34
6
|
/**
|
|
35
7
|
* Add display-only actor labels to one authorized activity page. Resolution is
|
|
36
8
|
* best-effort, bounded, and read-time only: stored events retain stable ids and
|
|
37
9
|
* a profile-provider outage falls back to those ids without failing the page.
|
|
10
|
+
*
|
|
11
|
+
* At most eight lookups run at once, and the page waits for them for 1.5
|
|
12
|
+
* seconds in all: whatever has resolved by then is used, and the rest are
|
|
13
|
+
* abandoned, as they are when the reader leaves.
|
|
38
14
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
continue;
|
|
44
|
-
identities.set(JSON.stringify([
|
|
45
|
-
event.actor.kind,
|
|
46
|
-
event.actor.namespace,
|
|
47
|
-
event.actor.id,
|
|
48
|
-
]), {
|
|
49
|
-
kind: event.actor.kind,
|
|
50
|
-
id: event.actor.id,
|
|
51
|
-
...(event.actor.namespace
|
|
52
|
-
? { namespace: event.actor.namespace }
|
|
53
|
-
: {}),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
const queue = [...identities.entries()];
|
|
57
|
-
const labels = new Map();
|
|
58
|
-
let next = 0;
|
|
59
|
-
const deadline = Date.now() + ACTIVITY_LABEL_PAGE_BUDGET_MS;
|
|
60
|
-
const workers = Array.from({ length: Math.min(ACTIVITY_LABEL_CONCURRENCY, queue.length) }, async () => {
|
|
61
|
-
while (next < queue.length) {
|
|
62
|
-
const entry = queue[next++];
|
|
63
|
-
if (!entry)
|
|
64
|
-
return;
|
|
65
|
-
const [key, identity] = entry;
|
|
66
|
-
const sameKindProviders = auth
|
|
67
|
-
.map((provider, index) => ({ provider, index }))
|
|
68
|
-
.filter(({ provider }) => provider.kind === identity.kind);
|
|
69
|
-
const candidates = sameKindProviders.filter(({ provider }) => Boolean(provider.activityActorLabel));
|
|
70
|
-
const eligible = identity.namespace
|
|
71
|
-
? candidates.filter(({ provider }) => activityActorNamespace(provider) === identity.namespace)
|
|
72
|
-
: (() => {
|
|
73
|
-
const directoryKey = ({ provider, index, }) => {
|
|
74
|
-
const namespace = activityActorNamespace(provider);
|
|
75
|
-
return namespace === undefined
|
|
76
|
-
? `provider:${index}`
|
|
77
|
-
: `namespace:${namespace}`;
|
|
78
|
-
};
|
|
79
|
-
// Every same-kind provider participates in the ambiguity check,
|
|
80
|
-
// even if it cannot resolve labels. Otherwise a legacy ID owned
|
|
81
|
-
// by a provider without a resolver could be disclosed to a
|
|
82
|
-
// different provider that happens to have one.
|
|
83
|
-
const directories = new Set(sameKindProviders.map(directoryKey));
|
|
84
|
-
if (directories.size !== 1)
|
|
85
|
-
return [];
|
|
86
|
-
const [directory] = directories;
|
|
87
|
-
return candidates.filter((candidate) => directoryKey(candidate) === directory);
|
|
88
|
-
})();
|
|
89
|
-
// One namespace is one directory. Use its first configured resolver so
|
|
90
|
-
// duplicate gate adapters over the same Clerk instance do not multiply
|
|
91
|
-
// the provider-level concurrency cap.
|
|
92
|
-
const provider = eligible[0]?.provider;
|
|
93
|
-
if (!provider)
|
|
94
|
-
continue;
|
|
95
|
-
const remaining = deadline - Date.now();
|
|
96
|
-
if (remaining <= 0)
|
|
97
|
-
return;
|
|
98
|
-
const label = await boundedActivityActorLabel(provider.activityActorLabel.bind(provider), identity.id, remaining);
|
|
15
|
+
function enrichActivityActorLabels(page, auth) {
|
|
16
|
+
return Effect.suspend(() => {
|
|
17
|
+
const labels = new Map();
|
|
18
|
+
const resolve = ({ key, id, provider }) => Effect.tryPromise(() => Promise.resolve(provider.activityActorLabel(id))).pipe(Effect.map(cleanActorLabel), Effect.orElseSucceed(() => undefined), Effect.map((label) => {
|
|
99
19
|
if (label)
|
|
100
20
|
labels.set(key, label);
|
|
21
|
+
}));
|
|
22
|
+
return Effect.forEach(labelLookups(page.events.map((event) => event.actor), auth), resolve, {
|
|
23
|
+
concurrency: ACTOR_LABEL_CONCURRENCY,
|
|
24
|
+
discard: true,
|
|
25
|
+
}).pipe(Effect.timeoutOption(Duration.millis(ACTOR_LABEL_BUDGET_MS)), Effect.map(() => ({
|
|
26
|
+
...page,
|
|
27
|
+
events: page.events.map((event) => {
|
|
28
|
+
const resolved = event.actor.id
|
|
29
|
+
? labels.get(actorKey(event.actor))
|
|
30
|
+
: undefined;
|
|
31
|
+
// Never trust or echo a `label` supplied by storage. The persisted event
|
|
32
|
+
// schema has no label; only this authenticated read path may add one.
|
|
33
|
+
const actor = {
|
|
34
|
+
kind: event.actor.kind,
|
|
35
|
+
...(event.actor.id ? { id: event.actor.id } : {}),
|
|
36
|
+
...(event.actor.namespace
|
|
37
|
+
? { namespace: event.actor.namespace }
|
|
38
|
+
: {}),
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
...event,
|
|
42
|
+
actor: resolved ? { ...actor, label: resolved } : actor,
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
})));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function activityRead(context) {
|
|
49
|
+
const { url, opts } = context;
|
|
50
|
+
return Effect.gen(function* () {
|
|
51
|
+
const authz = yield* authorized(context, false);
|
|
52
|
+
if (!authz.operator)
|
|
53
|
+
return yield* refuse("operator access required", 403);
|
|
54
|
+
if (opts.activityReadGate &&
|
|
55
|
+
!(yield* Effect.promise(async () => opts.activityReadGate(authz.actor)))) {
|
|
56
|
+
return yield* refuse("forbidden", 403);
|
|
101
57
|
}
|
|
58
|
+
const list = opts.activity?.list?.bind(opts.activity);
|
|
59
|
+
if (!list)
|
|
60
|
+
return yield* refuse("activity history is not configured", 404);
|
|
61
|
+
const cursor = url.searchParams.get("cursor") ?? undefined;
|
|
62
|
+
if (cursor && cursor.length > 500)
|
|
63
|
+
return yield* refuse("invalid cursor", 400);
|
|
64
|
+
const requestedLimit = Number(url.searchParams.get("limit") ?? "50");
|
|
65
|
+
const limit = Number.isFinite(requestedLimit)
|
|
66
|
+
? Math.min(100, Math.max(1, Math.trunc(requestedLimit)))
|
|
67
|
+
: 50;
|
|
68
|
+
return yield* Effect.tryPromise({
|
|
69
|
+
try: () => list({ ...(cursor !== undefined ? { cursor } : {}), limit }),
|
|
70
|
+
catch: (error) => error,
|
|
71
|
+
}).pipe(Effect.flatMap((page) => enrichActivityActorLabels(page, opts.auth)), Effect.map((page) => privateJson(page)),
|
|
72
|
+
// A page too malformed to label or serialize is the store's failure,
|
|
73
|
+
// like any other.
|
|
74
|
+
Effect.catchDefect((defect) => Effect.fail(defect)), Effect.catch((error) => {
|
|
75
|
+
if (error instanceof InvalidActivityCursorError) {
|
|
76
|
+
return refuse(error.message, 400);
|
|
77
|
+
}
|
|
78
|
+
opts.logger.error("[connecta] activity read failed", error);
|
|
79
|
+
return refuse("activity history is temporarily unavailable", 503);
|
|
80
|
+
}));
|
|
102
81
|
});
|
|
103
|
-
await Promise.all(workers);
|
|
104
|
-
return {
|
|
105
|
-
...page,
|
|
106
|
-
events: page.events.map((event) => {
|
|
107
|
-
const resolved = event.actor.id
|
|
108
|
-
? labels.get(JSON.stringify([
|
|
109
|
-
event.actor.kind,
|
|
110
|
-
event.actor.namespace,
|
|
111
|
-
event.actor.id,
|
|
112
|
-
]))
|
|
113
|
-
: undefined;
|
|
114
|
-
// Never trust or echo a `label` supplied by storage. The persisted event
|
|
115
|
-
// schema has no label; only this authenticated read path may add one.
|
|
116
|
-
const actor = {
|
|
117
|
-
kind: event.actor.kind,
|
|
118
|
-
...(event.actor.id ? { id: event.actor.id } : {}),
|
|
119
|
-
...(event.actor.namespace
|
|
120
|
-
? { namespace: event.actor.namespace }
|
|
121
|
-
: {}),
|
|
122
|
-
};
|
|
123
|
-
return {
|
|
124
|
-
...event,
|
|
125
|
-
actor: resolved ? { ...actor, label: resolved } : actor,
|
|
126
|
-
};
|
|
127
|
-
}),
|
|
128
|
-
};
|
|
129
82
|
}
|
|
130
83
|
export async function routeActivity(context) {
|
|
131
|
-
const { path, request
|
|
84
|
+
const { path, request } = context;
|
|
132
85
|
if (path !== "/ui/activity")
|
|
133
86
|
return null;
|
|
134
87
|
if (request.method !== "GET") {
|
|
135
88
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
136
89
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return authz.response;
|
|
140
|
-
if (!authz.operator) {
|
|
141
|
-
return privateJson({ error: "operator access required" }, { status: 403 });
|
|
142
|
-
}
|
|
143
|
-
if (opts.activityReadGate &&
|
|
144
|
-
!(await opts.activityReadGate(authz.actor))) {
|
|
145
|
-
return privateJson({ error: "forbidden" }, { status: 403 });
|
|
146
|
-
}
|
|
147
|
-
if (!opts.activity?.list) {
|
|
148
|
-
return privateJson({ error: "activity history is not configured" }, { status: 404 });
|
|
149
|
-
}
|
|
150
|
-
const cursor = url.searchParams.get("cursor") ?? undefined;
|
|
151
|
-
if (cursor && cursor.length > 500) {
|
|
152
|
-
return privateJson({ error: "invalid cursor" }, { status: 400 });
|
|
153
|
-
}
|
|
154
|
-
const requestedLimit = Number(url.searchParams.get("limit") ?? "50");
|
|
155
|
-
const limit = Number.isFinite(requestedLimit)
|
|
156
|
-
? Math.min(100, Math.max(1, Math.trunc(requestedLimit)))
|
|
157
|
-
: 50;
|
|
158
|
-
try {
|
|
159
|
-
const page = await opts.activity.list({
|
|
160
|
-
...(cursor !== undefined ? { cursor } : {}),
|
|
161
|
-
limit,
|
|
162
|
-
});
|
|
163
|
-
return privateJson(await enrichActivityActorLabels(page, opts.auth));
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
if (error instanceof InvalidActivityCursorError) {
|
|
167
|
-
return privateJson({ error: error.message }, { status: 400 });
|
|
168
|
-
}
|
|
169
|
-
opts.logger.error("[connecta] activity read failed", error);
|
|
170
|
-
return privateJson({ error: "activity history is temporarily unavailable" }, { status: 503 });
|
|
171
|
-
}
|
|
90
|
+
// A read: a reader who leaves stops the label lookups still queued.
|
|
91
|
+
return serveOperator(activityRead(context), request.signal);
|
|
172
92
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Display labels for stored actors, shared by the activity page and the
|
|
2
|
+
// artifact library. An actor is stored as a stable id; a label is resolved at
|
|
3
|
+
// read time from the one auth provider that owns the actor's directory, and
|
|
4
|
+
// only on an authorized read path. Nothing here is persisted, and a label a
|
|
5
|
+
// store happens to carry is never trusted or echoed.
|
|
6
|
+
import { activityActorNamespace } from "./shared.js";
|
|
7
|
+
export const ACTOR_LABEL_CONCURRENCY = 8;
|
|
8
|
+
export const ACTOR_LABEL_BUDGET_MS = 1_500;
|
|
9
|
+
const ACTOR_LABEL_MAX_LENGTH = 160;
|
|
10
|
+
export function cleanActorLabel(value) {
|
|
11
|
+
if (typeof value !== "string")
|
|
12
|
+
return undefined;
|
|
13
|
+
const compact = value.replace(/\s+/gu, " ").trim();
|
|
14
|
+
if (!compact)
|
|
15
|
+
return undefined;
|
|
16
|
+
return Array.from(compact).slice(0, ACTOR_LABEL_MAX_LENGTH).join("");
|
|
17
|
+
}
|
|
18
|
+
export function actorKey(actor) {
|
|
19
|
+
return JSON.stringify([actor.kind, actor.namespace, actor.id]);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One lookup per distinct actor, each against the single provider that owns
|
|
23
|
+
* the actor's directory. An actor no provider can claim unambiguously gets no
|
|
24
|
+
* lookup at all.
|
|
25
|
+
*/
|
|
26
|
+
export function labelLookups(actors, auth) {
|
|
27
|
+
const identities = new Map();
|
|
28
|
+
for (const actor of actors) {
|
|
29
|
+
if (!actor.id)
|
|
30
|
+
continue;
|
|
31
|
+
identities.set(actorKey(actor), {
|
|
32
|
+
kind: actor.kind,
|
|
33
|
+
id: actor.id,
|
|
34
|
+
...(actor.namespace ? { namespace: actor.namespace } : {}),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const lookups = [];
|
|
38
|
+
for (const [key, identity] of identities) {
|
|
39
|
+
const sameKindProviders = auth
|
|
40
|
+
.map((provider, index) => ({ provider, index }))
|
|
41
|
+
.filter(({ provider }) => provider.kind === identity.kind);
|
|
42
|
+
const candidates = sameKindProviders.filter(({ provider }) => Boolean(provider.activityActorLabel));
|
|
43
|
+
const eligible = identity.namespace
|
|
44
|
+
? candidates.filter(({ provider }) => activityActorNamespace(provider) === identity.namespace)
|
|
45
|
+
: (() => {
|
|
46
|
+
const directoryKey = ({ provider, index, }) => {
|
|
47
|
+
const namespace = activityActorNamespace(provider);
|
|
48
|
+
return namespace === undefined
|
|
49
|
+
? `provider:${index}`
|
|
50
|
+
: `namespace:${namespace}`;
|
|
51
|
+
};
|
|
52
|
+
// Every same-kind provider participates in the ambiguity check,
|
|
53
|
+
// even if it cannot resolve labels. Otherwise a legacy ID owned
|
|
54
|
+
// by a provider without a resolver could be disclosed to a
|
|
55
|
+
// different provider that happens to have one.
|
|
56
|
+
const directories = new Set(sameKindProviders.map(directoryKey));
|
|
57
|
+
if (directories.size !== 1)
|
|
58
|
+
return [];
|
|
59
|
+
const [directory] = directories;
|
|
60
|
+
return candidates.filter((candidate) => directoryKey(candidate) === directory);
|
|
61
|
+
})();
|
|
62
|
+
// One namespace is one directory. Use its first configured resolver so
|
|
63
|
+
// duplicate gate adapters over the same Clerk instance do not multiply
|
|
64
|
+
// the provider-level concurrency cap.
|
|
65
|
+
const provider = eligible[0]?.provider;
|
|
66
|
+
if (provider)
|
|
67
|
+
lookups.push({ key, id: identity.id, provider });
|
|
68
|
+
}
|
|
69
|
+
return lookups;
|
|
70
|
+
}
|
|
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
|
+
}
|