@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.
Files changed (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
@@ -1,172 +1,92 @@
1
+ import { Duration, Effect } from "effect";
1
2
  import { InvalidActivityCursorError, } from "../activity.js";
2
- import { activityActorNamespace, authorize, privateJson, } from "./shared.js";
3
- const ACTIVITY_LABEL_CONCURRENCY = 8;
4
- const ACTIVITY_LABEL_PAGE_BUDGET_MS = 1_500;
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
- async function enrichActivityActorLabels(page, auth) {
40
- const identities = new Map();
41
- for (const event of page.events) {
42
- if (!event.actor.id)
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, url, baseUrl, opts, runtimeContext } = context;
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
- 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
- }
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
+ }