@zackbart/connecta 0.24.4 → 0.25.0

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