@zackbart/connecta 0.26.0 → 0.26.2

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 (47) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/README.md +11 -0
  3. package/dist/access-tokens.d.ts +38 -0
  4. package/dist/access-tokens.js +294 -0
  5. package/dist/artifacts/document.js +6 -2
  6. package/dist/artifacts/markdown.js +43 -14
  7. package/dist/artifacts/routes.js +27 -4
  8. package/dist/artifacts/validate.js +1 -0
  9. package/dist/auth/downstream-oauth.js +397 -48
  10. package/dist/auth/oauth-start-reset.js +20 -0
  11. package/dist/branding.d.ts +87 -0
  12. package/dist/branding.js +170 -1
  13. package/dist/connectors/remote-mcp.js +32 -7
  14. package/dist/execute.js +7 -8
  15. package/dist/identity.js +2 -0
  16. package/dist/index.d.ts +6 -2
  17. package/dist/index.js +13 -2
  18. package/dist/module-contracts.d.ts +5 -0
  19. package/dist/oauth-callback-outcome.js +43 -18
  20. package/dist/operator-ui/generated.js +2 -2
  21. package/dist/operator-ui/model.d.ts +1 -0
  22. package/dist/operator-ui/view.js +304 -33
  23. package/dist/page-styles.js +7 -0
  24. package/dist/registry.js +16 -13
  25. package/dist/routes/access-tokens.js +110 -0
  26. package/dist/routes/mcp.js +5 -1
  27. package/dist/routes/oauth-management.js +66 -12
  28. package/dist/routes/oauth.js +54 -87
  29. package/dist/routes/shared.d.ts +3 -1
  30. package/dist/routes/shared.js +6 -1
  31. package/dist/routes/ui.js +9 -1
  32. package/dist/server.js +8 -5
  33. package/dist/skills.js +6 -6
  34. package/dist/types.d.ts +5 -0
  35. package/dist/ui.d.ts +6 -1
  36. package/dist/ui.js +27 -64
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/documentation/architecture.md +19 -9
  40. package/documentation/auth.md +166 -2
  41. package/documentation/code-mode.md +15 -15
  42. package/documentation/meta-tools.md +12 -5
  43. package/ethos.md +1 -1
  44. package/package.json +5 -1
  45. package/templates/node/README.md +11 -0
  46. package/templates/node/package.json +1 -1
  47. package/templates/node/src/index.ts +5 -1
@@ -108,6 +108,7 @@ export interface UiConnector {
108
108
  }
109
109
  export type CredentialManagementCapability = "available" | "requires_operator" | "vault_not_configured" | "no_slots";
110
110
  export interface UiData {
111
+ accessTokenManagement?: "available" | "requires_operator";
111
112
  serverInfo: {
112
113
  name: string;
113
114
  version: string;
@@ -1,15 +1,39 @@
1
1
  /** Pages the nav lists. A single artifact is reached from the library, not the nav. */
2
2
  export const OPERATOR_PAGES = [
3
3
  "connections",
4
+ "tokens",
4
5
  "activity",
5
6
  "artifacts",
6
7
  ];
7
8
  export const PAGE_META = {
9
+ tokens: { path: "/tokens", label: "Access tokens" },
8
10
  connections: { path: "/", label: "Connections" },
9
11
  activity: { path: "/activity", label: "Activity" },
10
12
  artifacts: { path: "/artifacts", label: "Artifacts" },
11
13
  artifact: { path: "/artifacts", label: "Artifact" },
12
14
  };
15
+ /**
16
+ * What each page is for, in one line under its heading — on the gate as well
17
+ * as once signed in, so the page an operator lands on reads the same before
18
+ * and after the session is checked.
19
+ */
20
+ export function pageDescription(page, productDescription) {
21
+ if (page === "activity") {
22
+ return "Every connector tool call, by who made it and how it ended. Arguments and results are never stored.";
23
+ }
24
+ if (isArtifactPage(page)) {
25
+ return "Pages agents published for the team. Each one keeps every version.";
26
+ }
27
+ return productDescription;
28
+ }
29
+ /** What the gate says while the session is checked, page by page. */
30
+ export function checkingCopy(page) {
31
+ if (page === "artifact")
32
+ return "Loading artifact…";
33
+ if (page === "artifacts")
34
+ return "Loading artifacts…";
35
+ return "Checking your session…";
36
+ }
13
37
  export function pageForPath(path) {
14
38
  if (path.startsWith("/artifacts/"))
15
39
  return "artifact";
@@ -44,24 +68,29 @@ export function failure(message, fix) {
44
68
  return fix ? { message, tone: "error", fix } : { message, tone: "error" };
45
69
  }
46
70
  const REFUSED_COPY = {
47
- oauth_disconnect: "OAuth disconnect failed. If the downstream refused it, the deployment's log has its reply.",
48
- oauth_reconnect: "OAuth authorization could not restart. If the downstream refused it, the deployment's log has its reply.",
49
- credential_test: "The credential test could not run.",
71
+ oauth_disconnect: "Disconnect didn't finish. If the service refused it, the deployment's log has its reply.",
72
+ oauth_reconnect: "Authorization couldn't start. If the service refused it, the deployment's log has its reply.",
73
+ credential_test: "The credential test couldn't run.",
50
74
  };
51
- /** The route's answer to a finished OAuth action. Its state picks the sentence. */
52
- export function oauthDoneNotice(action, answer) {
75
+ /**
76
+ * The route's answer to a finished OAuth action. Its state picks the sentence;
77
+ * `opened` says whether the authorization page is already in a new tab.
78
+ */
79
+ export function oauthDoneNotice(action, answer, opened = true) {
53
80
  if (action === "oauth_disconnect") {
54
- return info("OAuth disconnected. Restart authorization when you are ready to reconnect.");
81
+ return info("Disconnected. Connect again whenever you are ready.");
55
82
  }
56
- return info(answer?.state === "ok"
57
- ? "OAuth reconnected."
58
- : "Authorization restarted. Open the authorization link to reconnect.");
83
+ if (answer?.state === "ok")
84
+ return info("Connected.");
85
+ return info(opened
86
+ ? "Finish authorizing in the new tab. This page updates when you come back."
87
+ : "Your browser blocked the new tab. Open the authorization page from the link here.");
59
88
  }
60
89
  /** A credential Test's result. `ok` is the only part of the answer it reads. */
61
90
  export function credentialTestNotice(connectorId, answer) {
62
91
  return answer?.ok === true
63
92
  ? info("Credential is valid.")
64
- : failure("Credential test failed: the downstream rejected the stored credential, or the test could not reach it. The deployment's log has the downstream's reply.", { kind: "credential_test_failed", connectorId });
93
+ : failure("Credential test failed: the service rejected the stored credential, or the test couldn't reach it. The deployment's log has the service's reply.", { kind: "credential_test_failed", connectorId });
65
94
  }
66
95
  /**
67
96
  * A route that refused one of these actions. A credential Test that found
@@ -80,6 +109,102 @@ export function refusedNotice(action, connectorId, problem) {
80
109
  connectorId,
81
110
  });
82
111
  }
112
+ /**
113
+ * A credential save or removal the route refused, by status. The route's
114
+ * `error` can name a vault failure or echo validation detail, so none of it
115
+ * is shown; the status is enough to say what to do next.
116
+ */
117
+ function credentialRefusedCopy(action, status, problem) {
118
+ if (problem === "credential_mismatch")
119
+ return PROBLEM_COPY.credential_mismatch;
120
+ const verb = action === "credential_save" ? "saved" : "removed";
121
+ if (status === 400 || status === 413 || status === 415) {
122
+ return action === "credential_save"
123
+ ? "The credential wasn't saved: a value is empty or not in the expected format. Check it and save again."
124
+ : "The credential wasn't removed. Refresh the page and try again.";
125
+ }
126
+ if (status === 404) {
127
+ return "This connector no longer has a credential slot. Refresh the page to see its current setup.";
128
+ }
129
+ if (status === 503) {
130
+ return `Credential storage isn't configured on this deployment, so nothing was ${verb}.`;
131
+ }
132
+ return `The credential wasn't ${verb}. Try again; if it keeps failing, the deployment's log has the reason.`;
133
+ }
134
+ /**
135
+ * Every row action's failure, in one voice. The page's own failures — a
136
+ * lapsed session, a denied identity, a dropped connection — read the same for
137
+ * every action; a route's refusal picks the action's fixed sentence.
138
+ */
139
+ export function actionFailedNotice(action, connectorId, facts, productName) {
140
+ if (facts.kind === "session") {
141
+ return failure("Your session has ended. Sign in again, then retry.");
142
+ }
143
+ if (facts.kind === "forbidden") {
144
+ return failure("You don't have permission to change this connection's authentication.");
145
+ }
146
+ if (facts.kind === "network") {
147
+ return failure(`Couldn't reach ${productName}. Check your connection and try again.`);
148
+ }
149
+ if (action === "credential_save" || action === "credential_remove") {
150
+ return failure(credentialRefusedCopy(action, facts.status, facts.problem));
151
+ }
152
+ return refusedNotice(action, connectorId, facts.problem);
153
+ }
154
+ export function connectorLoadFailureCopy(failure, productName) {
155
+ return failure === "session"
156
+ ? "Your session wasn't accepted while loading this connector. Sign in again to see its status."
157
+ : `Couldn't reach ${productName} to load this connector. Check your connection, then refresh it.`;
158
+ }
159
+ /** The signed-in page's answer when `/ui/data` could not be read. */
160
+ export function loadFailureCopy(failure, productName) {
161
+ return {
162
+ title: `Couldn't reach ${productName}`,
163
+ body: failure === "network"
164
+ ? "Your browser couldn't connect. Check your connection, then retry."
165
+ : "The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.",
166
+ };
167
+ }
168
+ /** A collection page's load failure: fixed words, whatever the route said. */
169
+ export function collectionFailureCopy(collection, facts, productName) {
170
+ if (facts.kind === "network") {
171
+ return `Couldn't reach ${productName}. Check your connection, then retry.`;
172
+ }
173
+ if (facts.kind === "session")
174
+ return "Your session has ended. Sign in again to see this page.";
175
+ if (facts.kind === "forbidden" || facts.status === 404) {
176
+ if (collection === "artifact")
177
+ return "There is no artifact here, or this identity can't open it.";
178
+ return collection === "activity"
179
+ ? "Activity isn't available to this identity."
180
+ : "Artifacts aren't available to this identity.";
181
+ }
182
+ return "The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.";
183
+ }
184
+ /** The confirm's question, naming the connector by its title. */
185
+ export function confirmCopy(action, name) {
186
+ if (action === "oauth_disconnect") {
187
+ return {
188
+ question: `Disconnect ${name}? Its stored grant and any pending authorization are removed, and its tools stop working until it is connected again.`,
189
+ confirm: "Disconnect",
190
+ };
191
+ }
192
+ if (action === "oauth_restart") {
193
+ return {
194
+ question: `Reconnect ${name}? Its current grant stops working until you finish authorizing in the new tab.`,
195
+ confirm: "Reconnect",
196
+ };
197
+ }
198
+ return {
199
+ question: `Remove ${name}'s credential? The connector stops authenticating until a replacement is added.`,
200
+ confirm: "Remove",
201
+ };
202
+ }
203
+ export function accessTokenUnavailableCopy(capability) {
204
+ return capability === undefined
205
+ ? "Access tokens are not configured for this deployment."
206
+ : "Token management requires an interactive sign-in and explicit permission.";
207
+ }
83
208
  export function initialState(page) {
84
209
  return {
85
210
  page,
@@ -87,7 +212,9 @@ export function initialState(page) {
87
212
  session: "loading",
88
213
  gate: null,
89
214
  refreshing: false,
215
+ loadFailure: null,
90
216
  pendingFocus: null,
217
+ focusIfLost: null,
91
218
  ...identityScopedState(),
92
219
  };
93
220
  }
@@ -101,10 +228,21 @@ function identityScopedState() {
101
228
  data: null,
102
229
  connectorFilter: "",
103
230
  oauthNotice: null,
231
+ oauthNoticeFor: null,
104
232
  oauthBusy: null,
233
+ oauthBlocked: null,
234
+ confirming: null,
235
+ connectorFailures: {},
105
236
  credentialNotice: null,
237
+ credentialNoticeFor: null,
106
238
  credentialEditing: null,
107
239
  credentialBusy: null,
240
+ tokenPhase: "idle",
241
+ tokenNotice: null,
242
+ tokens: [],
243
+ createdToken: null,
244
+ tokenRenaming: null,
245
+ tokenBusy: false,
108
246
  activityPhase: "idle",
109
247
  activityNotice: null,
110
248
  activityEvents: [],
@@ -131,7 +269,9 @@ export function resetIdentity(state, gate = null) {
131
269
  session: "gated",
132
270
  gate,
133
271
  refreshing: false,
272
+ loadFailure: null,
134
273
  pendingFocus: null,
274
+ focusIfLost: null,
135
275
  ...identityScopedState(),
136
276
  };
137
277
  }
@@ -143,8 +283,13 @@ export function withPage(state, page) {
143
283
  return {
144
284
  ...state,
145
285
  page,
286
+ createdToken: null,
287
+ tokenRenaming: null,
288
+ tokenNotice: null,
146
289
  credentialEditing: null,
147
290
  credentialNotice: null,
291
+ credentialNoticeFor: null,
292
+ confirming: null,
148
293
  };
149
294
  }
150
295
  export function credentialUnavailableCopy(capability) {
@@ -156,13 +301,18 @@ export function credentialUnavailableCopy(capability) {
156
301
  }
157
302
  return "Credential management requires an interactive user. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
158
303
  }
159
- export function connectorStatusLabel(status) {
304
+ /**
305
+ * The status badge. An auth-needed connector is named by what it needs, so a
306
+ * row whose fix is "Add credential" does not say "Authorization needed".
307
+ */
308
+ export function connectorStatusLabel(status, problem) {
160
309
  if (status === "loading")
161
310
  return "Loading details";
162
311
  if (status === "ok")
163
312
  return "Connected";
164
- if (status === "auth_required")
165
- return "Authorization needed";
313
+ if (status === "auth_required") {
314
+ return problem === "credential_required" ? "Credential needed" : "Authorization needed";
315
+ }
166
316
  return "Unavailable";
167
317
  }
168
318
  export function toolCountLabel(count) {
@@ -182,16 +332,24 @@ export function summarizeConnectors(connectors) {
182
332
  total: connectors.length,
183
333
  connected: 0,
184
334
  attention: 0,
335
+ credentials: 0,
185
336
  unavailable: 0,
337
+ loading: 0,
186
338
  tools: 0,
187
339
  drifting: 0,
188
340
  };
189
341
  for (const connector of connectors) {
190
342
  if (connector.status === "ok")
191
343
  summary.connected += 1;
192
- else if (connector.status === "auth_required")
193
- summary.attention += 1;
194
- else if (connector.status !== "loading")
344
+ else if (connector.status === "auth_required") {
345
+ if (connector.problem === "credential_required")
346
+ summary.credentials += 1;
347
+ else
348
+ summary.attention += 1;
349
+ }
350
+ else if (connector.status === "loading")
351
+ summary.loading += 1;
352
+ else
195
353
  summary.unavailable += 1;
196
354
  summary.tools += connector.toolCount || 0;
197
355
  if (driftState(connector.catalogDrift) === "warning")
@@ -205,6 +363,16 @@ export function summarizeConnectors(connectors) {
205
363
  * zero, so a healthy deployment stays short.
206
364
  */
207
365
  export function connectorSummaryParts(summary) {
366
+ // Nothing has answered yet: "0 connected · 0 tools" would be a claim, and
367
+ // a false one.
368
+ if (summary.total > 0 && summary.loading === summary.total) {
369
+ return [
370
+ {
371
+ text: `Checking ${summary.total} connector${summary.total === 1 ? "" : "s"}…`,
372
+ tone: "neutral",
373
+ },
374
+ ];
375
+ }
208
376
  return [
209
377
  { text: `${summary.connected} connected`, tone: "neutral" },
210
378
  ...(summary.attention
@@ -215,10 +383,21 @@ export function connectorSummaryParts(summary) {
215
383
  },
216
384
  ]
217
385
  : []),
386
+ ...(summary.credentials
387
+ ? [
388
+ {
389
+ text: `${summary.credentials} need${summary.credentials === 1 ? "s" : ""} a credential`,
390
+ tone: "warn",
391
+ },
392
+ ]
393
+ : []),
218
394
  ...(summary.unavailable
219
395
  ? [{ text: `${summary.unavailable} unavailable`, tone: "danger" }]
220
396
  : []),
221
397
  { text: toolCountLabel(summary.tools), tone: "neutral" },
398
+ ...(summary.loading
399
+ ? [{ text: `${summary.loading} still loading`, tone: "neutral" }]
400
+ : []),
222
401
  ];
223
402
  }
224
403
  /**
@@ -262,6 +441,18 @@ const PROBLEM_COPY = {
262
441
  export function problemCopy(problem) {
263
442
  return problem ? PROBLEM_COPY[problem] ?? null : null;
264
443
  }
444
+ /**
445
+ * A problem someone can fix by authorizing is a warning, matching the amber
446
+ * "Authorization needed" badge above it; one where something broke is an
447
+ * error. The two used to share the red box, so the row contradicted itself.
448
+ */
449
+ export function problemTone(problem) {
450
+ return problem === "oauth_required" ||
451
+ problem === "credential_required" ||
452
+ problem === "auth_required"
453
+ ? "warn"
454
+ : "danger";
455
+ }
265
456
  /** Who owns this connector's downstream credentials, in two words. */
266
457
  export function authScopeLabel(scope) {
267
458
  return scope === "personal" ? "personal auth" : "shared auth";
@@ -348,14 +539,31 @@ export function formatDate(value) {
348
539
  if (!value)
349
540
  return "";
350
541
  const date = new Date(value);
351
- return Number.isNaN(date.valueOf()) ? "" : date.toLocaleString();
542
+ // Minutes, not seconds: nothing on these pages is read to the second, and
543
+ // the seconds were most of the noise in every row that carried a date.
544
+ return Number.isNaN(date.valueOf())
545
+ ? ""
546
+ : date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
547
+ }
548
+ const ACTOR_KINDS = {
549
+ clerk: "Clerk",
550
+ bearer: "Bearer token",
551
+ "cloudflare-access": "Cloudflare Access",
552
+ anonymous: "Anonymous",
553
+ };
554
+ /** How a caller signed in, as a person reads it. An unknown kind is shown as sent. */
555
+ function actorKindLabel(kind) {
556
+ if (!kind)
557
+ return "Unknown caller";
558
+ return ACTOR_KINDS[kind] ?? kind;
352
559
  }
560
+ /** Who made a call: their name when there is one, and how they signed in. */
353
561
  export function actorLabel(actor) {
354
562
  if (!actor?.kind)
355
- return "unknown";
356
- if (actor.label)
357
- return `${actor.kind} · ${actor.label}`;
358
- return actor.id ? `${actor.kind} · ${actor.id}` : actor.kind;
563
+ return "Unknown caller";
564
+ const who = actor.label || actor.id;
565
+ const kind = actorKindLabel(actor.kind);
566
+ return who ? `${who} (${kind})` : kind;
359
567
  }
360
568
  /**
361
569
  * The stable id behind a friendly label, shown only when a label or namespace
@@ -384,6 +592,9 @@ function activityMatches(event, query) {
384
592
  event.actor?.id,
385
593
  event.actor?.namespace,
386
594
  event.actor?.label,
595
+ activityOutcomeBadge(event.outcome).label,
596
+ activityDetail(event),
597
+ actorKindLabel(event.actor?.kind),
387
598
  ].some((value) => String(value ?? "").toLowerCase().includes(q));
388
599
  }
389
600
  export function filterActivity(events, query) {
@@ -392,9 +603,9 @@ export function filterActivity(events, query) {
392
603
  /** Counts only. What the deployment ran, never what it sent or received. */
393
604
  export function activitySummary(events) {
394
605
  if (events.length === 0)
395
- return "Arguments and results are never stored.";
606
+ return "";
396
607
  const tools = new Set(events.map((event) => event.address)).size;
397
- return `${events.length} loaded call${events.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"} · no arguments or results stored`;
608
+ return `${events.length} loaded call${events.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"}`;
398
609
  }
399
610
  // A pause and an approval are neither success nor failure: each gets its own
400
611
  // class, and the stylesheet paints neither as an error.
@@ -409,25 +620,85 @@ const ACTIVITY_OUTCOMES = [
409
620
  export function activityOutcomeClass(outcome) {
410
621
  return ACTIVITY_OUTCOMES.includes(outcome) ? outcome : "error";
411
622
  }
412
- /** The one-line detail under an address: source, retries, and friction. */
623
+ /** Each outcome as a badge: a word a person reads, and a tone that is never alone. */
624
+ const OUTCOME_BADGE = {
625
+ success: { label: "Succeeded", tone: "ok" },
626
+ error: { label: "Failed", tone: "danger" },
627
+ timeout: { label: "Timed out", tone: "danger" },
628
+ cancelled: { label: "Cancelled", tone: "neutral" },
629
+ paused: { label: "Waiting for approval", tone: "warn" },
630
+ approved: { label: "Approved", tone: "ok" },
631
+ };
632
+ export function activityOutcomeBadge(outcome) {
633
+ return OUTCOME_BADGE[outcome] ?? { label: "Failed", tone: "danger" };
634
+ }
635
+ const SOURCE_LABELS = {
636
+ execute_code: "In a program",
637
+ call_tool: "Direct call",
638
+ call_destructive_tool: "Direct call, approved by the host",
639
+ resume_execution: "Resumed program",
640
+ batch_call: "Batch call",
641
+ };
642
+ /**
643
+ * Friction classes and the error codes an operator meets most, in words. An
644
+ * unknown code falls back to itself with the underscores taken out, which is
645
+ * still more readable than the wire form and never a guess at its meaning.
646
+ */
647
+ const REASON_LABELS = {
648
+ tool_not_found: "tool not found",
649
+ unknown_address: "tool not found",
650
+ unknown_tool: "tool not found",
651
+ ambiguous_tool_alias: "ambiguous tool name",
652
+ schema_retry: "arguments didn't match the schema",
653
+ invalid_args: "arguments didn't match the schema",
654
+ destructive_reroute: "needed host approval",
655
+ destructive_tool_requires_approval: "needed host approval",
656
+ approval_required: "needed approval",
657
+ auth_required: "needed authorization",
658
+ result_too_large: "result too large to return inline",
659
+ timeout: "timed out",
660
+ };
661
+ function reasonLabel(code) {
662
+ return REASON_LABELS[code] ?? code.replaceAll("_", " ");
663
+ }
664
+ /** The one-line detail under an address: where it ran, retries, and why it stalled. */
413
665
  export function activityDetail(event) {
414
- const parts = [event.source];
666
+ const parts = [SOURCE_LABELS[event.source] ?? event.source];
415
667
  if (event.approval === "tool")
416
668
  parts.push("approved for the rest of the run");
417
669
  if (event.approval === "call")
418
670
  parts.push("approved for this call");
419
671
  if (event.attempts > 1)
420
672
  parts.push(`${event.attempts} attempts`);
421
- if (event.friction)
422
- parts.push(event.friction);
423
- // The friction class and the code coincide for auth_required and
424
- // result_too_large. Printing "· auth_required · auth_required" says nothing
425
- // twice, so the coarse class stands in for both when they agree.
426
- if (event.errorCode && event.errorCode !== event.friction) {
427
- parts.push(event.errorCode);
428
- }
673
+ const reasons = [event.friction, event.errorCode]
674
+ .filter((code) => Boolean(code))
675
+ .map(reasonLabel);
676
+ // The friction class and the code often say the same thing in two
677
+ // vocabularies ("auth_required" twice); once is enough.
678
+ for (const reason of new Set(reasons))
679
+ parts.push(reason);
429
680
  return parts.join(" · ");
430
681
  }
682
+ /**
683
+ * An artifact's last refresh, when it is worth a badge. Only a failure is: a
684
+ * success is what "Current data" already says, and a superseded run is
685
+ * nobody's concern.
686
+ */
687
+ export function artifactRefreshBadge(last) {
688
+ return last?.status === "failed" ? { label: "Refresh failed", tone: "danger" } : null;
689
+ }
690
+ /**
691
+ * A stored credential that cannot be used, by cause. The payload's `error`
692
+ * beside it is not shown: it can carry a vault's own words.
693
+ */
694
+ export function credentialProblemCopy(problem) {
695
+ if (problem === "credential_mismatch")
696
+ return PROBLEM_COPY.credential_mismatch;
697
+ if (problem === "credential_unreadable") {
698
+ return "The stored credential can't be read, so it can't be used. Replace it, or remove it and add a new one.";
699
+ }
700
+ return "The stored credential can't be used. Replace it, or remove it and add a new one.";
701
+ }
431
702
  export function credentialStateLabel(credential) {
432
703
  if (!credential.configured)
433
704
  return "not configured";
@@ -0,0 +1,7 @@
1
+ // Generated by scripts/build-operator-ui.mjs. Do not edit.
2
+ // Source: src/operator-ui/tokens.css and src/operator-ui/page.css, which the
3
+ // operator UI's browser.css imports too, so every page reads one token layer.
4
+ /** Custom properties and both palettes; styles no element. */
5
+ export const TOKENS_CSS = ":root{color-scheme:light;--accent: #2f5fe0;--radius: 10px;--sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;--mono: ui-monospace, \"SF Mono\", Menlo, Monaco, \"Cascadia Code\", Consolas, monospace;--bg: #f6f7f9;--surface: #ffffff;--surface-2: #f1f3f6;--border: #e2e5ea;--border-strong: #cdd2da;--text: #131820;--muted: #5b6472;--ok: #12734a;--warn: #9a5b06;--danger: #bb3226;--on-accent: #ffffff;--link: var(--accent);--tint: color-mix(in srgb, var(--accent) 8%, var(--surface));--shell: 68rem;--pad: 1.25rem;--gap: 1rem}@media(prefers-color-scheme:dark){html:not([data-scheme=light]){color-scheme:dark}}html[data-scheme=dark]{color-scheme:dark}@media(prefers-color-scheme:dark){html:not([data-scheme=light]){--bg: #0d1016;--surface: #151a21;--surface-2: #1c222c;--border: #262d39;--border-strong: #38414f;--text: #e6eaf1;--muted: #97a1b2;--ok: #4cc48c;--warn: #e2a33f;--danger: #f4776c;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}}html[data-scheme=dark]{--bg: #0d1016;--surface: #151a21;--surface-2: #1c222c;--border: #262d39;--border-strong: #38414f;--text: #e6eaf1;--muted: #97a1b2;--ok: #4cc48c;--warn: #e2a33f;--danger: #f4776c;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}";
6
+ /** Base typography, the shell, masthead, buttons, badges, and messages. */
7
+ export const PAGE_CSS = "*{box-sizing:border-box}html{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:16px;line-height:1.5;-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh}:is(h1,h2,h3,p,ul,ol){margin:0;padding:0}:is(h1,h2,h3){font-weight:600;line-height:1.3}h1{font-size:1.5rem;letter-spacing:-.01em}h2{font-size:1rem}:is(ul,ol){list-style:none}a{color:var(--link);text-decoration-thickness:1px;text-underline-offset:2px}button,input{font:inherit}:is(a,button,input,summary):focus-visible{border-radius:calc(var(--radius) / 2);outline:2px solid var(--link);outline-offset:2px}.skip-link{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);left:var(--pad);padding:.5rem .75rem;position:fixed;top:-4rem;z-index:10}.skip-link:focus{top:var(--pad)}.shell{margin:0 auto;max-width:var(--shell);padding-left:var(--pad);padding-right:var(--pad)}.cap,.meta{color:var(--muted);font-size:.875rem}.mono{font-family:var(--mono);font-size:.8125rem}.visually-hidden{clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.masthead{align-items:center;background:var(--surface);border-bottom:1px solid var(--border);display:flex;gap:var(--gap);justify-content:space-between;max-width:none;padding-bottom:.75rem;padding-top:.75rem;position:sticky;top:0;z-index:5}.masthead-inner{align-items:center;display:flex;gap:var(--gap);justify-content:space-between;margin:0 auto;max-width:var(--shell);width:100%}.mast-nav,.mast-actions,.page-nav,.session-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.mast-nav,.mast-actions{gap:var(--gap)}.navlink{background:none;border:0;border-radius:calc(var(--radius) - 2px);color:var(--muted);cursor:pointer;font-size:.9375rem;padding:.35rem .6rem;text-decoration:none}.navlink:hover{background:var(--surface-2);color:var(--text)}.navlink[aria-current=page]{background:var(--tint);color:var(--link);font-weight:500}.brand,.product{padding-left:0;padding-right:0;text-decoration:none}.brand{color:var(--text);font-weight:600}.brand:hover,.product:hover{background:none}.product{color:var(--muted)}.page{padding-bottom:4rem;padding-top:2rem}.lead{margin-bottom:1.5rem}.lead-copy{color:var(--muted);display:grid;gap:.5rem;margin-top:.35rem}.lead-copy p{max-width:60ch}.btn{background:var(--surface);border:1px solid var(--border-strong);border-radius:calc(var(--radius) - 2px);color:var(--text);cursor:pointer;display:inline-flex;align-items:center;font-size:.875rem;gap:.35rem;padding:.35rem .7rem;text-decoration:none;white-space:nowrap}.btn:hover{background:var(--surface-2)}.btn:disabled{cursor:not-allowed;opacity:.55}.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}.btn.primary:hover{background:color-mix(in srgb,var(--accent) 88%,black)}.btn.danger{color:var(--danger)}.btn.danger:hover{background:color-mix(in srgb,var(--danger) 10%,var(--surface))}.btn.quiet{background:none;border-color:var(--border);color:var(--muted)}.btn.quiet:hover{background:var(--surface-2);color:var(--text)}.badge{align-items:center;background:var(--surface-2);border-radius:999px;color:var(--muted);display:inline-flex;font-size:.75rem;gap:.3rem;line-height:1.6;padding:.1rem .5rem;white-space:nowrap}.badge.ok{background:color-mix(in srgb,var(--ok) 12%,var(--surface));color:var(--ok)}.badge.warn{background:color-mix(in srgb,var(--warn) 14%,var(--surface));color:var(--warn)}.badge.danger{background:color-mix(in srgb,var(--danger) 12%,var(--surface));color:var(--danger)}.badge.accent{background:var(--tint);color:var(--link)}.msg{background:color-mix(in srgb,var(--danger) 8%,var(--surface));border:1px solid color-mix(in srgb,var(--danger) 28%,var(--surface));border-radius:calc(var(--radius) - 2px);color:var(--danger);font-size:.875rem;padding:.5rem .75rem}.status-page{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);display:grid;gap:.75rem;margin:1.5rem auto 0;max-width:34rem;padding:1.5rem}.status-head{align-items:center;display:flex;gap:.6rem}.status-mark{align-items:center;background:var(--surface-2);border-radius:50%;color:var(--muted);display:inline-flex;flex:none;height:2rem;justify-content:center;width:2rem}.status-mark svg{height:1.25rem;width:1.25rem}.status-mark.ok{background:color-mix(in srgb,var(--ok) 14%,var(--surface));color:var(--ok)}.status-mark.danger{background:color-mix(in srgb,var(--danger) 12%,var(--surface));color:var(--danger)}.status-label{color:var(--muted);font-size:.875rem;font-weight:600}.status-label.ok{color:var(--ok)}.status-label.danger{color:var(--danger)}.status-page h1{overflow-wrap:anywhere}.status-copy{color:var(--muted)}.status-actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.25rem}.status-details{border-top:1px solid var(--border);color:var(--muted);font-size:.875rem;margin-top:.25rem;padding-top:.75rem}.status-details>summary{cursor:pointer;width:fit-content}.status-details>p{margin-top:.5rem}.status-details pre{background:var(--surface-2);border:1px solid var(--border);border-radius:calc(var(--radius) / 2);color:var(--text);font-family:var(--mono);font-size:.75rem;line-height:1.55;margin:.5rem 0 0;max-height:18rem;overflow:auto;overflow-wrap:anywhere;padding:.5rem .6rem;white-space:pre-wrap}@media(max-width:40rem){:root{--pad: 1rem}.status-page{margin-top:0;padding:1.25rem var(--pad)}}";
package/dist/registry.js CHANGED
@@ -767,9 +767,10 @@ export class Registry {
767
767
  });
768
768
  });
769
769
  }
770
- // Remove the persisted catalog: the manifest always, then the chunks it
771
- // names. Every delete is attempted; the effect fails with the manifest
772
- // read's error first, then the first chunk delete's.
770
+ // Remove the persisted catalog: the manifest and every chunk it names, all
771
+ // at once under the chunk I/O bound. Every delete is attempted; the effect
772
+ // fails with the manifest read's error first, then the manifest delete's,
773
+ // then the first chunk delete's by index.
773
774
  deleteCatalog(id) {
774
775
  return Effect.gen({ self: this }, function* () {
775
776
  const read = yield* Effect.result(storageGet(this.catalogKey(id)));
@@ -777,18 +778,20 @@ export class Registry {
777
778
  ? this.parseCatalogManifest(read.success)
778
779
  : null;
779
780
  // The root is authoritative, so attempt its deletion even when the
780
- // best-effort read needed for physical chunk cleanup failed.
781
- yield* storageDelete(this.catalogKey(id));
782
- let firstError;
783
- for (let index = 0; manifest && index < manifest.chunkCount; index++) {
784
- const deleted = yield* Effect.result(storageDelete(this.catalogChunkKey(id, manifest.revision, index)));
785
- if (Result.isFailure(deleted))
786
- firstError ??= deleted;
787
- }
781
+ // best-effort read needed for physical chunk cleanup failed. It need
782
+ // not land first: a reader that finds the root but not a chunk has an
783
+ // incomplete catalog, which is never served, and an eventually
784
+ // consistent store could show it that way whatever the order.
785
+ const keys = [
786
+ this.catalogKey(id),
787
+ ...Array.from({ length: manifest?.chunkCount ?? 0 }, (_, index) => this.catalogChunkKey(id, manifest.revision, index)),
788
+ ];
789
+ const deleted = yield* forEachChunk(keys, (key) => storageDelete(key));
788
790
  if (Result.isFailure(read))
789
791
  return yield* Effect.fail(read.failure);
790
- if (firstError)
791
- return yield* Effect.fail(firstError.failure);
792
+ const failed = deleted.find(Result.isFailure);
793
+ if (failed)
794
+ return yield* Effect.fail(failed.failure);
792
795
  });
793
796
  }
794
797
  catalogGeneration(id) {