@zackbart/connecta 0.5.0 → 0.6.1

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 (76) hide show
  1. package/CHANGELOG.md +505 -0
  2. package/README.md +159 -267
  3. package/dist/auth/bearer.d.ts +10 -3
  4. package/dist/auth/bearer.d.ts.map +1 -1
  5. package/dist/auth/bearer.js +21 -0
  6. package/dist/auth/bearer.js.map +1 -1
  7. package/dist/auth/clerk.d.ts +28 -3
  8. package/dist/auth/clerk.d.ts.map +1 -1
  9. package/dist/auth/clerk.js +161 -4
  10. package/dist/auth/clerk.js.map +1 -1
  11. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  12. package/dist/connectors/remote-mcp.js +8 -0
  13. package/dist/connectors/remote-mcp.js.map +1 -1
  14. package/dist/credential-health.d.ts +220 -0
  15. package/dist/credential-health.d.ts.map +1 -0
  16. package/dist/credential-health.js +551 -0
  17. package/dist/credential-health.js.map +1 -0
  18. package/dist/credentials.d.ts +35 -1
  19. package/dist/credentials.d.ts.map +1 -1
  20. package/dist/credentials.js +42 -0
  21. package/dist/credentials.js.map +1 -1
  22. package/dist/execute.d.ts.map +1 -1
  23. package/dist/execute.js +16 -4
  24. package/dist/execute.js.map +1 -1
  25. package/dist/index.d.ts +46 -5
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +118 -15
  28. package/dist/index.js.map +1 -1
  29. package/dist/meta-tools.d.ts +56 -5
  30. package/dist/meta-tools.d.ts.map +1 -1
  31. package/dist/meta-tools.js +249 -92
  32. package/dist/meta-tools.js.map +1 -1
  33. package/dist/registry.d.ts +62 -0
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +85 -1
  36. package/dist/registry.js.map +1 -1
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +305 -40
  39. package/dist/server.js.map +1 -1
  40. package/dist/skills.d.ts +1 -1
  41. package/dist/skills.d.ts.map +1 -1
  42. package/dist/skills.js +3 -3
  43. package/dist/skills.js.map +1 -1
  44. package/dist/timeout.d.ts +16 -0
  45. package/dist/timeout.d.ts.map +1 -0
  46. package/dist/timeout.js +38 -0
  47. package/dist/timeout.js.map +1 -0
  48. package/dist/toolkits.d.ts +95 -1
  49. package/dist/toolkits.d.ts.map +1 -1
  50. package/dist/toolkits.js +190 -5
  51. package/dist/toolkits.js.map +1 -1
  52. package/dist/types.d.ts +81 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/ui.d.ts +52 -0
  55. package/dist/ui.d.ts.map +1 -1
  56. package/dist/ui.js +144 -13
  57. package/dist/ui.js.map +1 -1
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +1 -1
  61. package/src/auth/bearer.ts +35 -1
  62. package/src/auth/clerk.ts +204 -7
  63. package/src/connectors/remote-mcp.ts +9 -0
  64. package/src/credential-health.ts +753 -0
  65. package/src/credentials.ts +71 -1
  66. package/src/execute.ts +28 -4
  67. package/src/index.ts +204 -22
  68. package/src/meta-tools.ts +286 -109
  69. package/src/registry.ts +125 -1
  70. package/src/server.ts +366 -38
  71. package/src/skills.ts +3 -3
  72. package/src/timeout.ts +49 -0
  73. package/src/toolkits.ts +241 -6
  74. package/src/types.ts +87 -1
  75. package/src/ui.ts +156 -14
  76. package/src/version.ts +1 -1
@@ -0,0 +1,551 @@
1
+ // Proactive liveness checks for the credentials connecta itself stores —
2
+ // downstream-OAuth tokens and operator-managed vault credentials (issue #24).
3
+ //
4
+ // The problem this solves: a connector's auth state used to flip only when
5
+ // something *observed* a failure, so an expired or revoked token surfaced
6
+ // mid-task as a failed agent call. A liveness check asks the connector whether
7
+ // the credential it holds still works, records the verdict, and lets the cached
8
+ // status surfaces (`list_connectors({ probe: false })`, `/ui`) report
9
+ // `auth_required` BEFORE a real call discovers it.
10
+ //
11
+ // Runtime-agnostic on purpose: nothing here schedules itself. The core exposes a
12
+ // due-gated sweep (piggybacked on inbound traffic by the server) and an awaited
13
+ // entry point (`Connecta.checkCredentials()`) an operator wires to whatever
14
+ // scheduler their runtime has — a Worker cron trigger, a Node `setInterval`.
15
+ // There is no background daemon and no long-lived timer, so Workers and Node run
16
+ // the same code.
17
+ import { credentialTestRule } from "./credentials.js";
18
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withTimeout } from "./timeout.js";
19
+ export const DEFAULT_CREDENTIAL_CHECK_INTERVAL_SECONDS = 900;
20
+ export const DEFAULT_CREDENTIAL_CHECK_CONCURRENCY = 4;
21
+ /**
22
+ * How long a read of one connector's record is served from memory before going
23
+ * back to storage. Short: it exists so a burst of `list_connectors` calls costs
24
+ * one storage read rather than one per call, not to cache a verdict.
25
+ */
26
+ const MIRROR_TTL_MS = 5_000;
27
+ /**
28
+ * Minimum gap between storage WRITES of an unchanged verdict. A liveness
29
+ * observation arrives from more than one place (the sweep, and every
30
+ * `list_connectors({ probe: true })`), and re-persisting "still ok" on each one
31
+ * would spend a KV write per probe for no new information. A verdict whose state
32
+ * or message CHANGED is always written immediately.
33
+ */
34
+ const MIN_WRITE_GAP_MS = 60_000;
35
+ function msg(err) {
36
+ return err instanceof Error ? err.message : String(err);
37
+ }
38
+ function storageKey(connectorId) {
39
+ return `credhealth:${connectorId}`;
40
+ }
41
+ /**
42
+ * Generation counter key. Connector ids are `[a-z0-9_-]+`, so the extra colon
43
+ * puts this outside the space `storageKey` can produce — no id can collide with
44
+ * another id's counter.
45
+ */
46
+ function generationKey(connectorId) {
47
+ return `credhealth:gen:${connectorId}`;
48
+ }
49
+ function validRecord(raw) {
50
+ if (!raw)
51
+ return null;
52
+ try {
53
+ const value = JSON.parse(raw);
54
+ if ((value.state !== "ok" &&
55
+ value.state !== "auth_required" &&
56
+ value.state !== "error") ||
57
+ typeof value.checkedAt !== "string" ||
58
+ Number.isNaN(Date.parse(value.checkedAt))) {
59
+ return null;
60
+ }
61
+ const stamped = Date.parse(value.checkedAt);
62
+ const now = Date.now();
63
+ return {
64
+ state: value.state,
65
+ // A verdict from the future is a clock-skewed isolate, and left alone it
66
+ // would be permanently fresh (never re-checked) AND permanently newer than
67
+ // any real-call success (never retired) — a wrong answer that cannot age
68
+ // out. Clamping to now costs at most one early re-check.
69
+ checkedAt: stamped > now ? new Date(now).toISOString() : value.checkedAt,
70
+ ...(typeof value.message === "string" ? { message: value.message } : {}),
71
+ ...(typeof value.authorizationUrl === "string"
72
+ ? { authorizationUrl: value.authorizationUrl }
73
+ : {}),
74
+ };
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * The stored verdicts, in the deployment's own KVStorage under
82
+ * `credhealth:<connectorId>`.
83
+ *
84
+ * Persisted rather than held in memory because the two runtimes disagree about
85
+ * what "in memory" means: a Cloudflare cron trigger runs in a different isolate
86
+ * from the fetch handlers, so a verdict only reaches `list_connectors` if it
87
+ * goes through storage. The in-memory mirror is a read cache over that, not the
88
+ * source of truth.
89
+ *
90
+ * Never throws: a check that cannot be persisted (or read back) must degrade to
91
+ * "no verdict" rather than break the status surface that reads it.
92
+ */
93
+ class CredentialHealthStore {
94
+ storage;
95
+ logger;
96
+ mirror = new Map();
97
+ constructor(storage, logger) {
98
+ this.storage = storage;
99
+ this.logger = logger;
100
+ }
101
+ async get(connectorId) {
102
+ const cached = this.mirror.get(connectorId);
103
+ if (cached && Date.now() - cached.readAt < MIRROR_TTL_MS) {
104
+ return cached.record ?? undefined;
105
+ }
106
+ let record = null;
107
+ try {
108
+ record = validRecord(await this.storage.get(storageKey(connectorId)));
109
+ }
110
+ catch (err) {
111
+ this.logger.warn(`[connecta] connector "${connectorId}" credential-health read failed: ${msg(err)}`);
112
+ return cached?.record ?? undefined;
113
+ }
114
+ this.mirror.set(connectorId, { record, readAt: Date.now() });
115
+ return record ?? undefined;
116
+ }
117
+ /**
118
+ * Monotonic per-connector counter, advanced by {@link clear}. Read straight
119
+ * from storage, never from the mirror: its whole job is to notice a change
120
+ * another isolate made, which a read cache would hide.
121
+ *
122
+ * A read failure answers 0. Paired with the fence in `put`, that fails
123
+ * *closed* — a mismatched generation drops the verdict — because losing one
124
+ * verdict costs a re-check, while resurrecting one costs an operator a
125
+ * connector that reports dead after they just fixed it.
126
+ */
127
+ async generation(connectorId) {
128
+ try {
129
+ const raw = await this.storage.get(generationKey(connectorId));
130
+ const value = raw ? Number(raw) : 0;
131
+ return Number.isFinite(value) ? value : 0;
132
+ }
133
+ catch {
134
+ return 0;
135
+ }
136
+ }
137
+ /**
138
+ * Write a verdict. `expectedGeneration` fences the write against a `clear`
139
+ * that landed while the check was in flight: pass the generation captured
140
+ * before the check started, and the write is dropped if it has since advanced.
141
+ * Omit it for a verdict observed synchronously (a live probe, an operator's
142
+ * Test), where there is no window to race.
143
+ *
144
+ * Returns whether the verdict was actually stored.
145
+ */
146
+ async put(connectorId, record, expectedGeneration) {
147
+ if (expectedGeneration !== undefined &&
148
+ (await this.generation(connectorId)) !== expectedGeneration) {
149
+ // The credential this verdict judged was replaced or removed mid-check.
150
+ // Drop the mirror too: this isolate's idea of the verdict is as stale as
151
+ // the write it just declined to make.
152
+ this.mirror.delete(connectorId);
153
+ return false;
154
+ }
155
+ const current = await this.get(connectorId);
156
+ const unchanged = current !== undefined &&
157
+ current.state === record.state &&
158
+ current.message === record.message &&
159
+ current.authorizationUrl === record.authorizationUrl;
160
+ if (unchanged &&
161
+ Date.parse(record.checkedAt) - Date.parse(current.checkedAt) <
162
+ MIN_WRITE_GAP_MS) {
163
+ return true;
164
+ }
165
+ this.mirror.set(connectorId, { record, readAt: Date.now() });
166
+ try {
167
+ await this.storage.set(storageKey(connectorId), JSON.stringify(record));
168
+ }
169
+ catch (err) {
170
+ this.logger.warn(`[connecta] connector "${connectorId}" credential-health persistence failed: ${msg(err)}`);
171
+ }
172
+ return true;
173
+ }
174
+ /**
175
+ * Forget a connector's verdict, and advance its generation so a check already
176
+ * in flight — in this isolate or any other — cannot write the verdict it
177
+ * formed about the credential that was just replaced.
178
+ *
179
+ * Bump BEFORE the delete, the same ordering the OAuth force path uses: a
180
+ * racing writer must see the advance rather than land between the two writes.
181
+ */
182
+ async clear(connectorId) {
183
+ this.mirror.delete(connectorId);
184
+ try {
185
+ const next = (await this.generation(connectorId)) + 1;
186
+ await this.storage.set(generationKey(connectorId), String(next));
187
+ await this.storage.delete(storageKey(connectorId));
188
+ }
189
+ catch (err) {
190
+ this.logger.warn(`[connecta] connector "${connectorId}" credential-health reset failed: ${msg(err)}`);
191
+ }
192
+ }
193
+ }
194
+ /**
195
+ * Whether a connector holds a credential connecta stores AND exposes a way to
196
+ * ask whether it still works.
197
+ *
198
+ * Deliberately narrow. `listTools`/`callTool` are NOT liveness probes here: a
199
+ * tool call may mutate downstream state, and the catalog path is already covered
200
+ * by the existing probe. So a connector is checkable only through the two hooks
201
+ * that exist to answer exactly this question — `testCredential(s)` (what /ui's
202
+ * Test button runs) and `status()` — and only when it has a credential of ours
203
+ * to be asked about: an operator-managed `credential`, or a stored downstream
204
+ * grant it reports via `hasStoredCredential`. A static-token connector stores
205
+ * nothing here and is never probed on a timer.
206
+ *
207
+ * Whether a test hook counts is `credentialTestRule`'s call, not this function's
208
+ * — the same rule /ui's Test button and the credential API read (issue #55), so
209
+ * a credential the operator cannot test by hand is not one a sweep tests behind
210
+ * their back. A connector whose only hook cannot test its declared shape is
211
+ * checkable only if it also implements `status()`.
212
+ */
213
+ export function isCheckableConnector(connector) {
214
+ const hasCredentialStore = Boolean(connector.credential || connector.hasStoredCredential);
215
+ const canAsk = Boolean(credentialTestRule(connector).mode !== null || connector.status);
216
+ return hasCredentialStore && canAsk;
217
+ }
218
+ /**
219
+ * The credential test the connector's DECLARED shape selects, bound to what is
220
+ * actually stored — or undefined when there is no honest question to put.
221
+ *
222
+ * `isCheckableConnector` answers the static question ("could this connector be
223
+ * asked at all"); this answers it against the vault. The hook itself is picked
224
+ * by `credentialTestRule` (src/credentials.ts, issue #55), the one rule /ui's
225
+ * `testable` flag and `POST /ui/credentials/<id>/test` also read: named
226
+ * `credential.fields` are tested as a set by `testCredentials`, a single-value
227
+ * `credential` by `testCredential` on the vault's reserved `value` field, and
228
+ * the other hook is never substituted. Substituting it is what a sweep must not
229
+ * do quietly — handing `testCredential` a `values.value` that named fields never
230
+ * wrote would test the empty string and record a confident `auth_required` about
231
+ * a credential nothing examined, and handing `testCredentials` the reserved
232
+ * `{ value }` map would call a hook with a shape its connector never declared.
233
+ * Either way the connector is skipped (`not_checkable`) rather than given an
234
+ * invented verdict, and `createConnecta` already warned about the mismatch at
235
+ * construction.
236
+ */
237
+ function testHookFor(connector, values) {
238
+ if (!values)
239
+ return undefined;
240
+ const { mode } = credentialTestRule(connector);
241
+ if (mode === "multiple") {
242
+ return (ctx) => connector.testCredentials(values, ctx);
243
+ }
244
+ // A single-value shape with nothing under the reserved field is still nothing
245
+ // to test, so the stored value gets the last word even when the rule fits.
246
+ if (mode === "single" && typeof values.value === "string") {
247
+ return (ctx) => connector.testCredential(values.value, ctx);
248
+ }
249
+ return undefined;
250
+ }
251
+ /** Run `fn` over `items` with at most `limit` in flight, preserving order. */
252
+ async function mapWithConcurrency(items, limit, fn) {
253
+ const out = new Array(items.length);
254
+ let next = 0;
255
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
256
+ while (next < items.length) {
257
+ const index = next++;
258
+ out[index] = await fn(items[index]);
259
+ }
260
+ });
261
+ await Promise.all(workers);
262
+ return out;
263
+ }
264
+ /**
265
+ * Runs and caches credential liveness checks. One instance per `Registry`.
266
+ *
267
+ * Cost is bounded four ways, because a status surface an agent polls must never
268
+ * become a way to hammer a downstream auth endpoint:
269
+ *
270
+ * 1. **Eligibility** — only connectors holding a credential of ours are probed
271
+ * at all (`isCheckableConnector`), and only when something is actually stored.
272
+ * 2. **Freshness (cross-isolate)** — a persisted verdict younger than
273
+ * `intervalSeconds` short-circuits the check, so every isolate and every
274
+ * trigger share one budget.
275
+ * 3. **Sweep gate (per isolate)** — `sweepIfDue` runs at most one traffic-
276
+ * triggered sweep per interval per isolate, and never two at once, so a burst
277
+ * of requests costs one sweep, not one per request.
278
+ * 4. **Deadline + fan-out bound** — each check is bounded by `timeoutMs` and at
279
+ * most `concurrency` run together (the same shape as the `probeTimeoutMs`
280
+ * bound on the discovery fan-out, issue #19).
281
+ */
282
+ export class CredentialHealthChecker {
283
+ deps;
284
+ store;
285
+ intervalMs;
286
+ concurrency;
287
+ timeoutMs;
288
+ onRequest;
289
+ /** Per-connector checks in flight in THIS isolate. */
290
+ inFlight = new Map();
291
+ /** Earliest a traffic-triggered sweep may run again in this isolate. */
292
+ nextSweepAt = 0;
293
+ sweeping;
294
+ constructor(deps, config = {}) {
295
+ this.deps = deps;
296
+ this.store = new CredentialHealthStore(deps.storage, deps.logger);
297
+ // Out-of-range tuning falls back to the default rather than being coerced:
298
+ // a zero or negative interval would turn the rate limit off, which is the
299
+ // one thing this class is for.
300
+ const seconds = config.intervalSeconds;
301
+ this.intervalMs =
302
+ seconds !== undefined && Number.isFinite(seconds) && seconds > 0
303
+ ? seconds * 1000
304
+ : DEFAULT_CREDENTIAL_CHECK_INTERVAL_SECONDS * 1000;
305
+ this.concurrency =
306
+ config.concurrency !== undefined &&
307
+ Number.isInteger(config.concurrency) &&
308
+ config.concurrency > 0
309
+ ? config.concurrency
310
+ : DEFAULT_CREDENTIAL_CHECK_CONCURRENCY;
311
+ this.timeoutMs =
312
+ normalizeTimeoutMs(config.timeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
313
+ this.onRequest = config.onRequest ?? true;
314
+ }
315
+ /**
316
+ * The stored verdict, if any. No downstream I/O — and no storage read at all
317
+ * for a connector that stores no credential of ours, since only a checkable
318
+ * connector can ever have had a verdict written. That keeps
319
+ * `list_connectors({ probe: false })` exactly as cheap as it was for the
320
+ * deployments this feature does not apply to.
321
+ */
322
+ healthFor(connectorId) {
323
+ const connector = this.deps.getConnector(connectorId);
324
+ if (!connector || !isCheckableConnector(connector)) {
325
+ return Promise.resolve(undefined);
326
+ }
327
+ return this.store.get(connectorId);
328
+ }
329
+ /**
330
+ * Record a liveness verdict observed elsewhere — today, the live status a
331
+ * `list_connectors({ probe: true })` just performed. Filtered by the same
332
+ * eligibility rule as a check, so this stays a record of *credential* health
333
+ * rather than a general status cache, and so it also counts against the
334
+ * freshness budget: an operator who just probed live does not get swept again
335
+ * moments later.
336
+ */
337
+ async record(connectorId, record) {
338
+ const connector = this.deps.getConnector(connectorId);
339
+ if (!connector || !isCheckableConnector(connector))
340
+ return;
341
+ await this.store.put(connectorId, record);
342
+ }
343
+ /** Forget a connector's verdict — its credential just changed under us. */
344
+ clear(connectorId) {
345
+ return this.store.clear(connectorId);
346
+ }
347
+ /** Whether any connector in this deployment could be checked at all. */
348
+ hasCheckableConnectors() {
349
+ return this.deps.listConnectors().some(isCheckableConnector);
350
+ }
351
+ /**
352
+ * Check every (or the named) connector's stored credential and return one
353
+ * outcome per connector considered. Never rejects: a connector that throws,
354
+ * hangs past `timeoutMs`, or cannot be persisted becomes an `error` verdict.
355
+ */
356
+ async check(baseUrl, opts = {}) {
357
+ // An id naming no connector is reported, not dropped: a typo in a scheduled
358
+ // check would otherwise return an empty list that looks exactly like a
359
+ // deployment with nothing to check.
360
+ const targets = opts.ids
361
+ ? opts.ids.map((id) => this.deps.getConnector(id) ?? id)
362
+ : this.deps.listConnectors();
363
+ return mapWithConcurrency(targets, this.concurrency, (target) => typeof target === "string"
364
+ ? Promise.resolve({ connectorId: target, skipped: "not_found" })
365
+ : this.checkOne(target, baseUrl, opts));
366
+ }
367
+ /**
368
+ * The traffic-triggered sweep: a promise to hand to `ctx.waitUntil`, or
369
+ * `undefined` when nothing is due (the common case, and free — no I/O). The
370
+ * gate is armed BEFORE the sweep starts, so a burst of concurrent requests
371
+ * produces one sweep.
372
+ */
373
+ sweepIfDue(baseUrl) {
374
+ if (!this.onRequest || this.sweeping)
375
+ return undefined;
376
+ const now = Date.now();
377
+ if (now < this.nextSweepAt)
378
+ return undefined;
379
+ if (!this.hasCheckableConnectors())
380
+ return undefined;
381
+ this.nextSweepAt = now + this.intervalMs;
382
+ const sweep = this.check(baseUrl).finally(() => {
383
+ this.sweeping = undefined;
384
+ });
385
+ this.sweeping = sweep;
386
+ return sweep;
387
+ }
388
+ async checkOne(connector, baseUrl, opts) {
389
+ const connectorId = connector.id;
390
+ if (!isCheckableConnector(connector)) {
391
+ return { connectorId, skipped: "not_checkable" };
392
+ }
393
+ if (this.inFlight.has(connectorId)) {
394
+ // Report rather than join: the caller wants to know a check happened, not
395
+ // to be blocked behind one someone else already pays for.
396
+ return {
397
+ connectorId,
398
+ skipped: "in_flight",
399
+ ...(await this.recordOrNothing(connectorId)),
400
+ };
401
+ }
402
+ if (!opts.force) {
403
+ const current = await this.store.get(connectorId);
404
+ if (current &&
405
+ Date.now() - Date.parse(current.checkedAt) < this.intervalMs) {
406
+ return { connectorId, skipped: "fresh", record: current };
407
+ }
408
+ }
409
+ const run = this.runCheck(connector, baseUrl, opts.requestScope);
410
+ this.inFlight.set(connectorId, run);
411
+ try {
412
+ return await run;
413
+ }
414
+ finally {
415
+ this.inFlight.delete(connectorId);
416
+ }
417
+ }
418
+ async recordOrNothing(connectorId) {
419
+ const record = await this.store.get(connectorId);
420
+ return record ? { record } : {};
421
+ }
422
+ async runCheck(connector, baseUrl, requestScope) {
423
+ const connectorId = connector.id;
424
+ const started = Date.now();
425
+ // Captured BEFORE anything downstream happens: everything after this point
426
+ // is a window in which the operator may replace the very credential being
427
+ // judged, and `settle` fences the write against exactly that.
428
+ const generation = await this.store.generation(connectorId);
429
+ const ctx = this.deps.contextFor(connectorId, baseUrl, requestScope);
430
+ let values = null;
431
+ if (connector.credential && this.deps.credentialVault) {
432
+ try {
433
+ values = await this.deps.credentialVault.getAll(connectorId);
434
+ }
435
+ catch (err) {
436
+ // A stored credential that cannot be decrypted (rotated key, corrupt
437
+ // envelope) is exactly the kind of dead credential this feature exists
438
+ // to surface early, so it is a verdict rather than a skip.
439
+ return this.settle(connectorId, started, generation, {
440
+ state: "auth_required",
441
+ checkedAt: new Date().toISOString(),
442
+ message: msg(err),
443
+ });
444
+ }
445
+ }
446
+ const stored = connector.hasStoredCredential
447
+ ? await connector
448
+ .hasStoredCredential(ctx)
449
+ .catch(() => values !== null)
450
+ : values !== null;
451
+ if (!stored)
452
+ return { connectorId, skipped: "no_credential" };
453
+ if (!this.canAsk(connector, values)) {
454
+ return { connectorId, skipped: "not_checkable" };
455
+ }
456
+ try {
457
+ const verdict = await withTimeout(this.probe(connector, ctx, values), this.timeoutMs, `credential check of "${connectorId}"`);
458
+ return await this.settle(connectorId, started, generation, {
459
+ ...verdict,
460
+ checkedAt: new Date().toISOString(),
461
+ });
462
+ }
463
+ catch (err) {
464
+ return await this.settle(connectorId, started, generation, {
465
+ state: "error",
466
+ checkedAt: new Date().toISOString(),
467
+ message: msg(err),
468
+ });
469
+ }
470
+ }
471
+ /**
472
+ * `isCheckableConnector` re-asked against what is actually stored: the hook
473
+ * the declared shape selects, bound to a value that fits it (see
474
+ * {@link testHookFor}), or a `status()` to fall back on. Neither ⇒ there is no
475
+ * honest question to put to this connector.
476
+ */
477
+ canAsk(connector, values) {
478
+ return Boolean(testHookFor(connector, values) || connector.status);
479
+ }
480
+ /**
481
+ * Ask the connector whether the credential it holds still works — with no
482
+ * downstream mutation and no tool call. A credential test is preferred for a
483
+ * vault credential because it validates the stored value itself; `status()` is
484
+ * the downstream-OAuth answer (it refreshes the grant, which is the liveness
485
+ * question for a token).
486
+ */
487
+ async probe(connector, ctx, values) {
488
+ const test = testHookFor(connector, values);
489
+ if (test) {
490
+ const result = await test(ctx);
491
+ if (result.ok) {
492
+ return { state: "ok", ...(result.message ? { message: result.message } : {}) };
493
+ }
494
+ // A rejected stored credential needs an operator, not a retry — the same
495
+ // actionable state a revoked OAuth grant reports. There is no consent URL
496
+ // for a vault credential; /ui's credential form is where it is replaced.
497
+ return {
498
+ state: "auth_required",
499
+ message: result.message ??
500
+ "Stored credential was rejected by the connector — replace it in /ui.",
501
+ };
502
+ }
503
+ const status = await connector.status(ctx);
504
+ return {
505
+ state: status.state,
506
+ ...(status.message ? { message: status.message } : {}),
507
+ ...(status.authorizationUrl
508
+ ? { authorizationUrl: status.authorizationUrl }
509
+ : {}),
510
+ };
511
+ }
512
+ async settle(connectorId, started, generation, record) {
513
+ const stored = await this.store.put(connectorId, record, generation);
514
+ return {
515
+ connectorId,
516
+ record,
517
+ ...(stored ? {} : { discarded: true }),
518
+ latencyMs: Date.now() - started,
519
+ };
520
+ }
521
+ }
522
+ /**
523
+ * Whether a liveness verdict may DECIDE a connector's cached status.
524
+ *
525
+ * Only `auth_required` ever does, and only while nothing better has happened
526
+ * since. Two separate judgements:
527
+ *
528
+ * 1. **`error` is not credential evidence.** A check that timed out, threw, or
529
+ * got a 502 from the provider's status endpoint failed to *complete* — it
530
+ * learned nothing about the credential. Letting it set the status would flip
531
+ * a connector whose calls are fine to `error` for a whole interval on a DNS
532
+ * blip. Error verdicts stay visible in `credentialCheck` (an operator wants
533
+ * to know checks are failing) but the status keeps coming from observed real
534
+ * calls, which is evidence.
535
+ * 2. **A successful real call retires the verdict.** Traffic beats a background
536
+ * probe, so a `lastSuccessAt` at or after `checkedAt` means the credential
537
+ * demonstrably works whatever the check concluded. The next check re-decides.
538
+ *
539
+ * `auth_required` deliberately outranks an observed real-call *failure*: both
540
+ * say something is wrong, and only one of them carries the URL that fixes it.
541
+ * The failure stays visible as `lastError`.
542
+ */
543
+ export function credentialVerdictApplies(record, lastSuccessAt) {
544
+ if (!record || record.state !== "auth_required")
545
+ return false;
546
+ if (!lastSuccessAt)
547
+ return true;
548
+ const success = Date.parse(lastSuccessAt);
549
+ return Number.isNaN(success) || success < Date.parse(record.checkedAt);
550
+ }
551
+ //# sourceMappingURL=credential-health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-health.js","sourceRoot":"","sources":["../src/credential-health.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { ConnectorCredentialValues, KVStorage } from "./types.js";
1
+ import type { Connector, ConnectorCredentialValues, KVStorage } from "./types.js";
2
2
  export interface CredentialFieldMetadata {
3
3
  configured: true;
4
4
  /** Only emitted when the value is long enough that four chars don't leak much. */
@@ -13,6 +13,40 @@ export interface CredentialMetadata {
13
13
  /** Per-field masked metadata for named multi-value credentials. */
14
14
  fields?: Record<string, CredentialFieldMetadata>;
15
15
  }
16
+ /** Which hook a testable credential is checked with. */
17
+ export type CredentialTestMode = "single" | "multiple";
18
+ /** A declared credential shape whose only test hook cannot test it. */
19
+ export interface CredentialTestMismatch {
20
+ /** The shape the connector declared. */
21
+ shape: CredentialTestMode;
22
+ /** The hook it implements, which that shape cannot use. */
23
+ hook: "testCredential" | "testCredentials";
24
+ }
25
+ export interface CredentialTestRule {
26
+ /** The hook to call, or null when this credential cannot be tested at all. */
27
+ mode: CredentialTestMode | null;
28
+ /** Set only when the sole implemented hook is the one the shape cannot use. */
29
+ mismatch?: CredentialTestMismatch;
30
+ }
31
+ /**
32
+ * The one rule deciding whether a connector's credential can be tested — read
33
+ * by /ui's `testable` flag, by the `POST /ui/credentials/<id>/test` route when
34
+ * it picks a hook, and by the construction-time mismatch warning, so those
35
+ * three cannot drift apart.
36
+ *
37
+ * The declared credential *shape* selects the hook: named `credential.fields`
38
+ * are tested as a set by `testCredentials`, a single-value `credential` by
39
+ * `testCredential` on the vault's reserved `value` field. The other hook is
40
+ * never substituted — it would be handed a shape the connector never declared —
41
+ * so a connector implementing only the mismatched hook is not testable, and
42
+ * says so at construction rather than under an operator's click.
43
+ */
44
+ export declare function credentialTestRule(connector: Pick<Connector, "credential" | "testCredential" | "testCredentials">): CredentialTestRule;
45
+ /**
46
+ * One clause naming a mismatch, shared by the startup warning and the test
47
+ * route's 400 so an operator reads the same explanation in both places.
48
+ */
49
+ export declare function describeCredentialTestMismatch(mismatch: CredentialTestMismatch): string;
16
50
  /**
17
51
  * Encrypted, connector-scoped credential vault over the deployment's existing
18
52
  * KVStorage. Only ciphertext enters KV; the AES-GCM key remains an environment
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAqBvE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,IAAI,CAAC;IACjB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,IAAI,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CAClD;AAkGD;;;;GAIG;AACH,qBAAa,eAAe;IAIxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;gBAGtB,OAAO,EAAE,SAAS,EACnC,aAAa,EAAE,MAAM;IAiBvB,OAAO,CAAC,cAAc;YAIR,IAAI;IAoBZ,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,SAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIjE,MAAM,CACV,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;IAKtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAwBjE,GAAG,CACP,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,CAAC;IAOxB,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,yBAAyB,EACjC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,CAAC;IA2BxB,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjD"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,yBAAyB,EACzB,SAAS,EACV,MAAM,YAAY,CAAC;AAqBpB,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,IAAI,CAAC;IACjB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,IAAI,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CAClD;AAED,wDAAwD;AACxD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEvD,uEAAuE;AACvE,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,KAAK,EAAE,kBAAkB,CAAC;IAC1B,2DAA2D;IAC3D,IAAI,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,IAAI,CACb,SAAS,EACT,YAAY,GAAG,gBAAgB,GAAG,iBAAiB,CACpD,GACA,kBAAkB,CAYpB;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,sBAAsB,GAC/B,MAAM,CAQR;AAkGD;;;;GAIG;AACH,qBAAa,eAAe;IAIxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAqB;gBAGtB,OAAO,EAAE,SAAS,EACnC,aAAa,EAAE,MAAM;IAiBvB,OAAO,CAAC,cAAc;YAIR,IAAI;IAoBZ,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,SAAU,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIjE,MAAM,CACV,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;IAKtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAwBjE,GAAG,CACP,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,CAAC;IAOxB,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,yBAAyB,EACjC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,CAAC;IA2BxB,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjD"}