@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,4 +1,7 @@
1
+ import { Deferred, Duration, Effect } from "effect";
1
2
  import { ConnectorCallError } from "./errors.js";
3
+ import { provideCallAdmissionProgram, startCallAdmission, } from "./runtime/call-admission.js";
4
+ import { fromSignal, runEdge } from "./runtime/run.js";
2
5
  const DEFAULT_MAX_QUEUE_SIZE = 32;
3
6
  const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
4
7
  const DEFAULT_RETRY_AFTER_MS = 1_000;
@@ -117,103 +120,10 @@ export class ConnectorCallAdmissionController {
117
120
  this.partitionKey = rule.partitionKey;
118
121
  }
119
122
  acquire(input) {
120
- // Copy the signal out before constructing any waiter closure. Referencing
121
- // `input` from a queued callback would retain its `args`, defeating the
122
- // limiter's payload-free state contract.
123
- const signal = input.signal;
124
- if (this.closed) {
125
- return Promise.reject(new CallAdmissionError("closed", "unavailable", `Connector "${this.connectorId}" call admission is closed.`));
126
- }
127
- if (signal?.aborted) {
128
- this.cancelledTotal++;
129
- return Promise.reject(this.cancelled(signal));
130
- }
131
- let key;
132
- try {
133
- key = this.partitionKey
134
- ? this.partitionKey({
135
- toolName: input.toolName,
136
- args: input.args,
137
- })
138
- : DEFAULT_PARTITION_KEY;
139
- }
140
- catch (cause) {
141
- this.rejectedTotal++;
142
- return Promise.reject(new CallAdmissionError("partition", "connector_call_failed", `Connector "${this.connectorId}" call-admission partitionKey threw.`, { cause }));
143
- }
144
- if (typeof key !== "string" ||
145
- enc.encode(key).length > MAX_PARTITION_KEY_BYTES) {
146
- this.rejectedTotal++;
147
- return Promise.reject(new CallAdmissionError("partition", "connector_call_failed", `Connector "${this.connectorId}" call-admission partitionKey must return a string of at most ${MAX_PARTITION_KEY_BYTES} UTF-8 bytes.`));
148
- }
149
- // A partition callback is operator code and may synchronously abort the
150
- // caller. Recheck after it returns so that cancellation cannot consume a
151
- // budget entry or concurrency slot.
152
- if (signal?.aborted) {
153
- this.cancelledTotal++;
154
- return Promise.reject(this.cancelled(signal));
155
- }
156
- const now = Date.now();
157
- let state = this.partitions.get(key);
158
- if (!state) {
159
- this.evictIdlePartitions(now);
160
- if (this.partitions.size >= this.maxPartitions) {
161
- this.rejectedTotal++;
162
- return Promise.reject(new CallAdmissionError("partition", "rate_limited", `Connector "${this.connectorId}" call-admission partition capacity is exhausted.`, { retryAfterMs: this.retryAfterMs }));
163
- }
164
- state = {
165
- active: 0,
166
- waiters: [],
167
- admittedAt: [],
168
- };
169
- this.partitions.set(key, state);
170
- }
171
- this.pruneBudget(state, now);
172
- const budgetRetryAfterMs = this.budgetRetryAfterMs(state, now);
173
- if (budgetRetryAfterMs !== undefined) {
174
- this.rateLimitedTotal++;
175
- return Promise.reject(this.budgetLimited(budgetRetryAfterMs));
176
- }
177
- if (this.maxConcurrency === undefined ||
178
- state.active < this.maxConcurrency) {
179
- return Promise.resolve(this.admit(state, now, 0));
180
- }
181
- if (state.waiters.length >= this.maxQueueSize) {
182
- this.rejectedTotal++;
183
- return Promise.reject(this.concurrencyLimited("queue is full"));
184
- }
185
- return new Promise((resolve, reject) => {
186
- const waiter = {
187
- queuedAt: now,
188
- resolve,
189
- reject,
190
- ...(signal ? { signal } : {}),
191
- };
192
- waiter.onAbort = () => {
193
- if (!this.removeWaiter(state, waiter))
194
- return;
195
- this.cleanupWaiter(waiter);
196
- this.cancelledTotal++;
197
- reject(this.cancelled(signal));
198
- this.maybeDeletePartition(key, state, Date.now());
199
- };
200
- waiter.timer = setTimeout(() => {
201
- if (!this.removeWaiter(state, waiter))
202
- return;
203
- this.cleanupWaiter(waiter);
204
- this.rejectedTotal++;
205
- reject(this.concurrencyLimited(`queue wait exceeded ${this.queueTimeoutMs}ms`));
206
- this.maybeDeletePartition(key, state, Date.now());
207
- }, this.queueTimeoutMs);
208
- state.waiters.push(waiter);
209
- this.queuedTotal++;
210
- signal?.addEventListener("abort", waiter.onAbort, { once: true });
211
- // Close the check-to-listener race: an abort before registration is not
212
- // replayed by AbortSignal, so inspect it once after the waiter is fully
213
- // removable and its timer is installed.
214
- if (signal?.aborted)
215
- waiter.onAbort();
216
- });
123
+ // The checks run now and the returned effect only waits. It closes over
124
+ // the partition key, not `input`, so a queued call does not retain its
125
+ // `args` — the limiter's payload-free state contract.
126
+ return runEdge(startCallAdmission(this, input));
217
127
  }
218
128
  /** A principal registry may be evicted only after calls and budgets drain. */
219
129
  isIdle() {
@@ -253,8 +163,7 @@ export class ConnectorCallAdmissionController {
253
163
  this.closed = true;
254
164
  for (const state of this.partitions.values()) {
255
165
  for (const waiter of state.waiters.splice(0)) {
256
- this.cleanupWaiter(waiter);
257
- waiter.reject(new CallAdmissionError("closed", "unavailable", `Connector "${this.connectorId}" call admission is closed.`));
166
+ Deferred.doneUnsafe(waiter.outcome, Effect.fail(new CallAdmissionError("closed", "unavailable", `Connector "${this.connectorId}" call admission is closed.`)));
258
167
  }
259
168
  }
260
169
  }
@@ -287,22 +196,24 @@ export class ConnectorCallAdmissionController {
287
196
  while (state.active < this.maxConcurrency &&
288
197
  state.waiters.length > 0) {
289
198
  const waiter = state.waiters.shift();
290
- this.cleanupWaiter(waiter);
291
- if (waiter.signal?.aborted) {
292
- this.cancelledTotal++;
293
- waiter.reject(this.cancelled(waiter.signal));
294
- continue;
295
- }
296
- const now = Date.now();
199
+ // The waiter's signal is not read here. This runs in whichever request
200
+ // released a slot, and on Workers reading an AbortSignal that another
201
+ // request created throws, failing the releaser and stranding the
202
+ // waiter. Nor does it need reading: a waiter whose signal aborted took
203
+ // itself out of the queue when it did, from its own abort listener.
204
+ //
205
+ // The waiter's own clock: the fiber's Clock for an Effect caller, the
206
+ // live `Date.now` for a Promise one.
207
+ const now = waiter.now();
297
208
  this.pruneBudget(state, now);
298
209
  const retryAfterMs = this.budgetRetryAfterMs(state, now);
299
210
  if (retryAfterMs !== undefined) {
300
211
  this.rateLimitedTotal++;
301
- waiter.reject(this.budgetLimited(retryAfterMs));
212
+ Deferred.doneUnsafe(waiter.outcome, Effect.fail(this.budgetLimited(retryAfterMs)));
302
213
  continue;
303
214
  }
304
215
  const waitMs = Math.max(0, now - waiter.queuedAt);
305
- waiter.resolve(this.admit(state, now, waitMs));
216
+ Deferred.doneUnsafe(waiter.outcome, Effect.succeed(this.admit(state, now, waitMs)));
306
217
  }
307
218
  }
308
219
  pruneBudget(state, now) {
@@ -350,12 +261,20 @@ export class ConnectorCallAdmissionController {
350
261
  state.waiters.splice(index, 1);
351
262
  return true;
352
263
  }
353
- cleanupWaiter(waiter) {
354
- if (waiter.timer !== undefined)
355
- clearTimeout(waiter.timer);
356
- if (waiter.onAbort) {
357
- waiter.signal?.removeEventListener("abort", waiter.onAbort);
264
+ // The waiting fiber was interrupted — an Effect caller's scope closed, not a
265
+ // signal. Withdraw the waiter if it is still queued; if the pump handed it a
266
+ // permit in the same breath, give the slot back, because nobody is left to
267
+ // receive it. (A line comment, not JSDoc: TypeScript copies a private
268
+ // member's JSDoc into the published declaration.)
269
+ cleanupWaiter(key, state, waiter) {
270
+ if (this.removeWaiter(state, waiter)) {
271
+ this.cancelledTotal++;
272
+ this.maybeDeletePartition(key, state, waiter.now());
273
+ return Effect.void;
358
274
  }
275
+ if (!Deferred.isDoneUnsafe(waiter.outcome))
276
+ return Effect.void;
277
+ return Deferred.await(waiter.outcome).pipe(Effect.tap((permit) => Effect.sync(() => permit.release())), Effect.ignore);
359
278
  }
360
279
  concurrencyLimited(reason) {
361
280
  return new CallAdmissionError("concurrency", "rate_limited", `Connector "${this.connectorId}" call concurrency ${reason}.`, { retryAfterMs: this.retryAfterMs });
@@ -366,4 +285,111 @@ export class ConnectorCallAdmissionController {
366
285
  cancelled(signal) {
367
286
  return new CallAdmissionError("cancelled", "timeout", `Connector "${this.connectorId}" call was cancelled before admission.`, { cause: signal.reason });
368
287
  }
288
+ // The admission program, handed to src/runtime/call-admission.ts. It lives
289
+ // in a static block because the class declaration ships, private member
290
+ // names and all: a new method or field would change the published `.d.ts`,
291
+ // and a static block is the one place outside an instance method that may
292
+ // read this bookkeeping while emitting nothing there.
293
+ //
294
+ // Calling the program decides: everything up to a grant, a refusal, or a
295
+ // place in the queue happens synchronously, and only the queued wait is left
296
+ // for a fiber. Semaphore does not fit that wait — permits here are
297
+ // per-partition, the queue is bounded and counted, and each waiter carries
298
+ // its own timeout and budget recheck — so the queue stays explicit, as in
299
+ // AdmissionController, with a Deferred per waiter.
300
+ static {
301
+ provideCallAdmissionProgram((controller, input, now) => {
302
+ // Copy the signal out before building the wait: the effect returned
303
+ // below must not reach `input`, or a queued call would retain `args`.
304
+ const signal = input.signal;
305
+ if (controller.closed) {
306
+ return Effect.fail(new CallAdmissionError("closed", "unavailable", `Connector "${controller.connectorId}" call admission is closed.`));
307
+ }
308
+ if (signal?.aborted) {
309
+ controller.cancelledTotal++;
310
+ return Effect.fail(controller.cancelled(signal));
311
+ }
312
+ let key;
313
+ try {
314
+ key = controller.partitionKey
315
+ ? controller.partitionKey({
316
+ toolName: input.toolName,
317
+ args: input.args,
318
+ })
319
+ : DEFAULT_PARTITION_KEY;
320
+ }
321
+ catch (cause) {
322
+ controller.rejectedTotal++;
323
+ return Effect.fail(new CallAdmissionError("partition", "connector_call_failed", `Connector "${controller.connectorId}" call-admission partitionKey threw.`, { cause }));
324
+ }
325
+ if (typeof key !== "string" ||
326
+ enc.encode(key).length > MAX_PARTITION_KEY_BYTES) {
327
+ controller.rejectedTotal++;
328
+ return Effect.fail(new CallAdmissionError("partition", "connector_call_failed", `Connector "${controller.connectorId}" call-admission partitionKey must return a string of at most ${MAX_PARTITION_KEY_BYTES} UTF-8 bytes.`));
329
+ }
330
+ // A partition callback is operator code and may synchronously abort the
331
+ // caller. Recheck after it returns so that cancellation cannot consume a
332
+ // budget entry or concurrency slot.
333
+ if (signal?.aborted) {
334
+ controller.cancelledTotal++;
335
+ return Effect.fail(controller.cancelled(signal));
336
+ }
337
+ const queuedAt = now();
338
+ let found = controller.partitions.get(key);
339
+ if (!found) {
340
+ controller.evictIdlePartitions(queuedAt);
341
+ if (controller.partitions.size >= controller.maxPartitions) {
342
+ controller.rejectedTotal++;
343
+ return Effect.fail(new CallAdmissionError("partition", "rate_limited", `Connector "${controller.connectorId}" call-admission partition capacity is exhausted.`, { retryAfterMs: controller.retryAfterMs }));
344
+ }
345
+ found = { active: 0, waiters: [], admittedAt: [] };
346
+ controller.partitions.set(key, found);
347
+ }
348
+ const state = found;
349
+ controller.pruneBudget(state, queuedAt);
350
+ const budgetRetryAfterMs = controller.budgetRetryAfterMs(state, queuedAt);
351
+ if (budgetRetryAfterMs !== undefined) {
352
+ controller.rateLimitedTotal++;
353
+ return Effect.fail(controller.budgetLimited(budgetRetryAfterMs));
354
+ }
355
+ if (controller.maxConcurrency === undefined ||
356
+ state.active < controller.maxConcurrency) {
357
+ return Effect.succeed(controller.admit(state, queuedAt, 0));
358
+ }
359
+ if (state.waiters.length >= controller.maxQueueSize) {
360
+ controller.rejectedTotal++;
361
+ return Effect.fail(controller.concurrencyLimited("queue is full"));
362
+ }
363
+ const waiter = {
364
+ queuedAt,
365
+ now,
366
+ outcome: Deferred.makeUnsafe(),
367
+ };
368
+ state.waiters.push(waiter);
369
+ controller.queuedTotal++;
370
+ // A timeout or abort settles the waiter only if it takes it out of the
371
+ // queue first. Otherwise the pump or close() got there in the same tick,
372
+ // and the outcome they recorded is the answer.
373
+ const giveUp = (count, error) => Effect.suspend(() => {
374
+ if (!controller.removeWaiter(state, waiter)) {
375
+ return Deferred.await(waiter.outcome);
376
+ }
377
+ count();
378
+ const refusal = error();
379
+ controller.maybeDeletePartition(key, state, now());
380
+ return Effect.fail(refusal);
381
+ });
382
+ // One flat race; each contender is a forked fiber. The abort contender
383
+ // checks the signal as it starts, which closes the check-to-listener
384
+ // race an AbortSignal would not replay.
385
+ const contenders = [
386
+ Deferred.await(waiter.outcome),
387
+ Effect.sleep(Duration.millis(controller.queueTimeoutMs)).pipe(Effect.andThen(giveUp(() => controller.rejectedTotal++, () => controller.concurrencyLimited(`queue wait exceeded ${controller.queueTimeoutMs}ms`)))),
388
+ ];
389
+ if (signal) {
390
+ contenders.push(fromSignal(signal).pipe(Effect.catch(() => giveUp(() => controller.cancelledTotal++, () => controller.cancelled(signal)))));
391
+ }
392
+ return Effect.raceAllFirst(contenders).pipe(Effect.onInterrupt(() => controller.cleanupWaiter(key, state, waiter)));
393
+ });
394
+ }
369
395
  }