@zackbart/connecta 0.33.0 → 0.34.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 (86) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +99 -128
  3. package/SECURITY.md +1 -1
  4. package/bin/version.mjs +1 -1
  5. package/dist/activity.d.ts +3 -0
  6. package/dist/branding.d.ts +7 -0
  7. package/dist/branding.js +76 -9
  8. package/dist/catalog-service.js +15 -1
  9. package/dist/config-value-sources.js +6 -1
  10. package/dist/config.d.ts +13 -2
  11. package/dist/config.js +36 -2
  12. package/dist/connector-caller.js +2 -1
  13. package/dist/describe-config.d.ts +3 -1
  14. package/dist/describe-config.js +1 -0
  15. package/dist/execute.js +1 -11
  16. package/dist/fix-prompt.js +2 -2
  17. package/dist/index.js +1 -0
  18. package/dist/membership.d.ts +2 -0
  19. package/dist/oauth-callback-outcome.js +2 -2
  20. package/dist/operator-ui/agents.d.ts +1 -1
  21. package/dist/operator-ui/agents.js +16 -16
  22. package/dist/operator-ui/config-summary.js +562 -0
  23. package/dist/operator-ui/connection-map.js +375 -0
  24. package/dist/operator-ui/endpoints.js +44 -15
  25. package/dist/operator-ui/fix-prompts.js +1 -1
  26. package/dist/operator-ui/generated.js +4 -4
  27. package/dist/operator-ui/member-page.js +13 -28
  28. package/dist/operator-ui/overview.js +134 -0
  29. package/dist/operator-ui/view.d.ts +27 -22
  30. package/dist/operator-ui/view.js +104 -88
  31. package/dist/page-styles.js +5 -3
  32. package/dist/providers/_shared/google/workspace.d.ts +6 -4
  33. package/dist/providers/_shared/google/workspace.js +6 -2
  34. package/dist/providers/ccb/index.js +1 -1
  35. package/dist/providers/docs/index.js +1 -1
  36. package/dist/providers/docs/skill.generated.js +2 -2
  37. package/dist/providers/drive/index.js +1 -1
  38. package/dist/providers/drive/skill.generated.js +2 -2
  39. package/dist/providers/forms/index.js +1 -1
  40. package/dist/providers/forms/skill.generated.js +2 -2
  41. package/dist/providers/gmail/index.js +1 -1
  42. package/dist/providers/gmail/skill.generated.js +2 -2
  43. package/dist/providers/mixpanel/skill.generated.js +3 -3
  44. package/dist/providers/notion/skill.generated.js +1 -1
  45. package/dist/providers/overflow/index.js +1 -1
  46. package/dist/providers/overflow/skill.generated.js +2 -2
  47. package/dist/providers/planning-center/index.js +1 -1
  48. package/dist/providers/planning-center/skill.generated.js +4 -4
  49. package/dist/providers/revenuecat/skill.generated.js +2 -2
  50. package/dist/providers/sheets/index.js +1 -1
  51. package/dist/providers/sheets/skill.generated.js +2 -2
  52. package/dist/providers/slides/index.js +1 -1
  53. package/dist/providers/slides/skill.generated.js +2 -2
  54. package/dist/providers/tithely/index.js +1 -1
  55. package/dist/providers/vercel/index.js +1 -1
  56. package/dist/providers/vercel/skill.generated.js +1 -1
  57. package/dist/registry.js +15 -0
  58. package/dist/routes/mcp.js +20 -13
  59. package/dist/routes/oauth.js +2 -2
  60. package/dist/skills.js +10 -0
  61. package/dist/storage/sql.js +5 -1
  62. package/dist/types.d.ts +18 -7
  63. package/dist/ui.js +9 -4
  64. package/dist/usage-guide.js +1 -1
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/documentation/README.md +2 -2
  68. package/documentation/architecture.md +18 -13
  69. package/documentation/auth.md +183 -67
  70. package/documentation/code-mode.md +23 -5
  71. package/documentation/deploying.md +5 -1
  72. package/documentation/integrating.md +9 -6
  73. package/documentation/meta-tools.md +36 -22
  74. package/documentation/migration-0.33.md +21 -13
  75. package/documentation/migration-0.34.md +67 -0
  76. package/documentation/operator-tests.md +46 -20
  77. package/documentation/operator-ui.md +294 -104
  78. package/examples/worker/AGENTS.md +15 -9
  79. package/examples/worker/README.md +41 -14
  80. package/examples/worker/src/connecta.config.ts +5 -4
  81. package/examples/worker/wrangler.jsonc +1 -1
  82. package/package.json +19 -19
  83. package/templates/node/AGENTS.md +13 -7
  84. package/templates/node/README.md +14 -11
  85. package/templates/node/package.json +1 -1
  86. package/templates/node/src/connecta.config.ts +2 -2
@@ -14,21 +14,18 @@ const REGISTRATION_LABELS = {
14
14
  export function registrationPathLabel(path) {
15
15
  return REGISTRATION_LABELS[path];
16
16
  }
17
- /**
18
- * A connector's source column: the maintained provider with the
19
- * implementation it uses, since a dual provider's `auth` selects either the
20
- * vendor's hosted MCP server or Connecta's REST connector.
21
- */
22
- export function connectorSourceLabel(source) {
23
- if (!source.provider)
24
- return source.kind;
25
- const implementation = source.kind === "remote-mcp" ? "hosted MCP" : source.kind === "api" ? "REST" : source.kind;
26
- return `${source.provider} · ${implementation}`;
27
- }
28
17
  /** Pages the nav lists, in dashboard order. */
29
- export const OPERATOR_PAGES = ["endpoints", "connections", "activity", "tokens", "settings"];
18
+ export const OPERATOR_PAGES = [
19
+ "overview",
20
+ "endpoints",
21
+ "connections",
22
+ "activity",
23
+ "tokens",
24
+ "settings",
25
+ ];
30
26
  export const PAGE_META = {
31
- endpoints: { path: "/", label: "Endpoints" },
27
+ overview: { path: "/", label: "Overview" },
28
+ endpoints: { path: "/endpoints", label: "Endpoints" },
32
29
  connector: { path: "/connectors", label: "Connector" },
33
30
  settings: { path: "/settings", label: "Settings" },
34
31
  tokens: { path: "/tokens", label: "Tokens" },
@@ -37,7 +34,8 @@ export const PAGE_META = {
37
34
  };
38
35
  /** Legacy links remain valid, with the browser replacing their URL. */
39
36
  export function canonicalOperatorPath(path) {
40
- return { "/access": "/", "/tools": "/connectors", "/config": "/settings" }[path] ?? path;
37
+ return ({ "/access": "/endpoints", "/tools": "/connectors", "/config": "/settings" }[path] ??
38
+ path);
41
39
  }
42
40
  /**
43
41
  * What each page is for, in one line under its heading — on the gate as well
@@ -46,7 +44,7 @@ export function canonicalOperatorPath(path) {
46
44
  */
47
45
  export function pageDescription(page, productDescription) {
48
46
  if (page === "activity") {
49
- return "Every connector tool call, by who made it and how it ended. Arguments and results are never stored.";
47
+ return "Tool calls by caller and result. Arguments and results are not stored.";
50
48
  }
51
49
  return productDescription;
52
50
  }
@@ -55,12 +53,8 @@ export function pageDescription(page, productDescription) {
55
53
  * ask, and that this is not an error to retry.
56
54
  */
57
55
  export function noAccessCopy(kind, productName) {
58
- const who = kind === "clerk"
59
- ? "This account is signed in"
60
- : kind === "cloudflare-access"
61
- ? "Cloudflare Access signed you in"
62
- : "This token is valid";
63
- return `${who}, but it isn't a maintainer or a member of any endpoint on ${productName}. Ask a maintainer of this deployment to add you to an endpoint, then reload.`;
56
+ const who = kind === "clerk" ? "This account" : kind === "cloudflare-access" ? "This Cloudflare Access identity" : "This token";
57
+ return `${who} has no access to ${productName}. Ask a maintainer to add you to an endpoint, then reload.`;
64
58
  }
65
59
  /** What the gate says while the session is checked, page by page. */
66
60
  export function checkingCopy() {
@@ -81,8 +75,8 @@ export function failure(message, fix) {
81
75
  return fix ? { message, tone: "error", fix } : { message, tone: "error" };
82
76
  }
83
77
  const REFUSED_COPY = {
84
- oauth_disconnect: "Disconnect didn't finish. If the service refused it, the deployment's log has its reply.",
85
- oauth_reconnect: "Authorization couldn't start. If the service refused it, the deployment's log has its reply.",
78
+ oauth_disconnect: "Disconnect failed. The deployment log has the reason.",
79
+ oauth_reconnect: "Authorization couldn't start. The deployment log has the reason.",
86
80
  credential_test: "The credential test couldn't run.",
87
81
  };
88
82
  /**
@@ -92,20 +86,21 @@ const REFUSED_COPY = {
92
86
  export function oauthDoneNotice(action, answer, opened = true) {
93
87
  if (action === "oauth_disconnect") {
94
88
  if (answer?.code === "oauth_revocation_failed")
95
- return info("Disconnected locally. Provider revocation could not be confirmed. Revoke the grant in the provider's console.");
96
- return info("Disconnected. Connect again whenever you are ready.");
89
+ return info("Disconnected here. Revoke the grant in the provider's console too.");
90
+ return info("Disconnected.");
97
91
  }
98
92
  if (answer?.state === "ok")
99
93
  return info("Connected.");
100
- return info(opened
101
- ? "Finish authorizing in the new tab. This page updates when you come back."
102
- : "Your browser blocked the new tab. Open the authorization page from the link here.");
94
+ return info(opened ? "Finish in the new tab." : "The new tab was blocked. Open the link here.");
103
95
  }
104
96
  /** A credential Test's result. `ok` is the only part of the answer it reads. */
105
97
  export function credentialTestNotice(connectorId, answer) {
106
98
  return answer?.ok === true
107
99
  ? info("Credential is valid.")
108
- : 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 });
100
+ : failure("Credential test failed. The deployment log has the reason.", {
101
+ kind: "credential_test_failed",
102
+ connectorId,
103
+ });
109
104
  }
110
105
  /**
111
106
  * A route that refused one of these actions. A credential Test that found
@@ -134,19 +129,19 @@ function credentialRefusedCopy(action, status, problem) {
134
129
  const verb = action === "credential_save" ? "saved" : "removed";
135
130
  if (status === 400 || status === 413 || status === 415) {
136
131
  return action === "credential_save"
137
- ? "The credential wasn't saved: a value is empty or not in the expected format. Check it and save again."
138
- : "The credential wasn't removed. Refresh the page and try again.";
132
+ ? "Not saved. A value is empty or in the wrong format."
133
+ : "Not removed. Refresh and try again.";
139
134
  }
140
135
  if (status === 404) {
141
- return "This connector no longer has a credential slot. Refresh the page to see its current setup.";
136
+ return "This connector no longer has a credential slot. Refresh the page.";
142
137
  }
143
138
  if (status === 503) {
144
- return `Credential storage isn't configured on this deployment, so nothing was ${verb}.`;
139
+ return `No credential storage is configured, so nothing was ${verb}.`;
145
140
  }
146
- return `The credential wasn't ${verb}. Try again; if it keeps failing, the deployment's log has the reason.`;
141
+ return `Not ${verb}. Try again, or check the deployment log.`;
147
142
  }
148
143
  /** An action whose permission or target changed before it was sent. */
149
- export const STALE_COPY = "This changed before the request was sent, so nothing was sent. Check it and try again.";
144
+ export const STALE_COPY = "This changed before sending, so nothing was sent. Try again.";
150
145
  /**
151
146
  * Every row action's failure, in one voice. The page's own failures — a
152
147
  * lapsed session, a denied identity, a dropped connection — read the same for
@@ -154,13 +149,13 @@ export const STALE_COPY = "This changed before the request was sent, so nothing
154
149
  */
155
150
  export function actionFailedNotice(action, connectorId, facts, productName) {
156
151
  if (facts.kind === "session") {
157
- return failure("Your session has ended. Sign in again, then retry.");
152
+ return failure("Session ended. Sign in again.");
158
153
  }
159
154
  if (facts.kind === "forbidden") {
160
- return failure("You don't have permission to change this connection's authentication.");
155
+ return failure("You can't change this connection's authentication.");
161
156
  }
162
157
  if (facts.kind === "network") {
163
- return failure(`Couldn't reach ${productName}. Check your connection and try again.`);
158
+ return failure(`Couldn't reach ${productName}. Try again.`);
164
159
  }
165
160
  if (facts.kind === "stale")
166
161
  return failure(STALE_COPY);
@@ -171,55 +166,55 @@ export function actionFailedNotice(action, connectorId, facts, productName) {
171
166
  }
172
167
  export function connectorLoadFailureCopy(failure, productName) {
173
168
  if (failure === "unavailable")
174
- return "Session identity is temporarily unavailable. Retry this connection.";
169
+ return "Session identity is unavailable. Retry.";
175
170
  return failure === "session"
176
- ? "Your session wasn't accepted while loading this connector. Retry to check again. Sign in again if the session keeps failing."
177
- : `Couldn't reach ${productName} to load this connector. Check your connection, then refresh it.`;
171
+ ? "Session not accepted. Retry, or sign in again."
172
+ : `Couldn't reach ${productName}. Refresh to retry.`;
178
173
  }
179
174
  /** The signed-in page's answer when `/ui/data` could not be read. */
180
175
  export function loadFailureCopy(failure, productName) {
181
176
  return {
182
177
  title: `Couldn't reach ${productName}`,
183
178
  body: failure === "network"
184
- ? "Your browser couldn't connect. Check your connection, then retry."
185
- : "The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.",
179
+ ? "Your browser couldn't connect. Retry."
180
+ : "The deployment returned an error. Retry, or check its log.",
186
181
  };
187
182
  }
188
183
  /** A collection page's load failure: fixed words, whatever the route said. */
189
184
  export function collectionFailureCopy(_collection, facts, productName) {
190
185
  if (facts.kind === "network") {
191
- return `Couldn't reach ${productName}. Check your connection, then retry.`;
186
+ return `Couldn't reach ${productName}. Retry.`;
192
187
  }
193
188
  if (facts.kind === "session")
194
- return "Your session has ended. Sign in again to see this page.";
189
+ return "Session ended. Sign in again.";
195
190
  if (facts.kind === "forbidden" || facts.status === 404) {
196
- return "Activity isn't available to this identity.";
191
+ return "Activity isn't available to you.";
197
192
  }
198
- return "The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.";
193
+ return "The deployment returned an error. Retry, or check its log.";
199
194
  }
200
195
  /** The confirm's question, naming the connector by its title. */
201
196
  export function confirmCopy(action, name) {
202
197
  if (action === "oauth_disconnect") {
203
198
  return {
204
- question: `Disconnect ${name}? Its stored grant and any pending authorization are removed, and its tools stop working until it is connected again.`,
199
+ question: `Disconnect ${name}? Its tools stop working until you connect again.`,
205
200
  confirm: "Disconnect",
206
201
  };
207
202
  }
208
203
  if (action === "oauth_restart") {
209
204
  return {
210
- question: `Reconnect ${name}? Its current grant stops working until you finish authorizing in the new tab.`,
205
+ question: `Reconnect ${name}? It stops working until you finish in the new tab.`,
211
206
  confirm: "Reconnect",
212
207
  };
213
208
  }
214
209
  return {
215
- question: `Remove ${name}'s credential? The connector stops authenticating until a replacement is added.`,
210
+ question: `Remove ${name}'s credential? Its tools stop working until you add a new one.`,
216
211
  confirm: "Remove",
217
212
  };
218
213
  }
219
214
  export function accessTokenUnavailableCopy(capability) {
220
215
  return capability === undefined
221
- ? "Access tokens are not configured for this deployment."
222
- : "Token management requires an interactive sign-in and explicit permission.";
216
+ ? "Agent tokens aren't configured."
217
+ : "Managing tokens needs an interactive sign-in with permission.";
223
218
  }
224
219
  /** Who the contract says this viewer is, or null before it loads. */
225
220
  export function operatorRole(state) {
@@ -352,12 +347,12 @@ export function withPage(state, page) {
352
347
  }
353
348
  export function credentialUnavailableCopy(capability) {
354
349
  if (capability === "no_slots") {
355
- return "No connectors declare operator-managed credential slots. Connector credentials remain configuration-as-code until a slot is declared.";
350
+ return "No connector declares a credential slot. Credentials stay in code.";
356
351
  }
357
352
  if (capability === "vault_not_configured") {
358
- return "Credential storage is not configured. Configure a vault before managing connector credentials here.";
353
+ return "No credential vault is configured.";
359
354
  }
360
- return "Credential management requires an interactive user. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
355
+ return "Managing credentials needs an interactive sign-in. A bearer token can only view them.";
361
356
  }
362
357
  /**
363
358
  * The status badge. An auth-needed connector is named by what it needs, so a
@@ -394,12 +389,12 @@ export function connectorStatusTone(status) {
394
389
  * the same kind.
395
390
  */
396
391
  const PROBLEM_COPY = {
397
- connector_unavailable: "Unavailable: its status check or catalog load failed, or did not finish in time. The deployment's log has the downstream error.",
398
- oauth_required: "Needs OAuth authorization: no grant is stored, or the stored grant expired or was revoked.",
399
- credential_required: "Needs a credential: nothing usable is stored in its credential slot.",
400
- auth_required: "Needs authorization. Its secret lives in deployment configuration, not on this page.",
401
- credential_mismatch: "The stored credential does not match the fields this connector declares, so it cannot be used.",
402
- catalog_failed: "Connected, but its tool catalog could not be loaded, so none of its tools are served.",
392
+ connector_unavailable: "Unavailable. Its status check or catalog load failed or timed out. The deployment log has the error.",
393
+ oauth_required: "Needs OAuth authorization. No grant is stored, or it expired or was revoked.",
394
+ credential_required: "Needs a credential. Nothing usable is stored.",
395
+ auth_required: "Needs authorization. Its secret is set in deployment config, not here.",
396
+ credential_mismatch: "The stored credential doesn't match this connector's fields.",
397
+ catalog_failed: "Connected, but its catalog failed to load, so no tools are served.",
403
398
  };
404
399
  export function problemCopy(problem) {
405
400
  return problem ? (PROBLEM_COPY[problem] ?? null) : null;
@@ -414,19 +409,43 @@ export function problemTone(problem) {
414
409
  ? "warn"
415
410
  : "danger";
416
411
  }
412
+ const PROBLEM_LABEL = {
413
+ connector_unavailable: "Unavailable",
414
+ oauth_required: "Authorization needed",
415
+ credential_required: "Credential needed",
416
+ auth_required: "Authorization needed",
417
+ credential_mismatch: "Credential mismatch",
418
+ catalog_failed: "Catalog failed",
419
+ };
420
+ function problemAction(problem) {
421
+ if (problem === "credential_mismatch")
422
+ return "Replace key";
423
+ return problemTone(problem) === "warn" ? "Connect" : "Inspect";
424
+ }
417
425
  /**
418
- * What this identity may do with a connector, in one line. Being able to use it
419
- * is implied by seeing it at all, so the sentence covers authentication, which
420
- * is the part that differs between operators.
426
+ * The verdict the Connectors list, the Overview, Endpoints and a connector's
427
+ * page all show. The server's problem code comes first, then a failed detail
428
+ * read on this page, then a non-ok status, then catalog drift. A problem
429
+ * outranks the status, so a connector whose catalog failed is never
430
+ * "Connected". A failed read marks its connector `connector_unavailable` in
431
+ * the store, so that one code beside a failed read is the page's own and
432
+ * reads "Couldn't load" instead of blaming the downstream.
421
433
  */
422
- export function permissionLabel(connector) {
423
- if (connector.permissions?.manageSharedAuth) {
424
- return "You can manage shared authentication for this connection.";
434
+ export function connectorVerdict(live, signals = {}) {
435
+ if (live.problem && !(signals.failed && live.problem === "connector_unavailable")) {
436
+ return { tone: problemTone(live.problem), label: PROBLEM_LABEL[live.problem], action: problemAction(live.problem) };
425
437
  }
426
- if (connector.permissions?.connectPersonal) {
427
- return "You can connect your own account to this connection.";
438
+ if (signals.failed)
439
+ return { tone: "warn", label: "Couldn't load", action: "Inspect" };
440
+ if (live.status === "loading")
441
+ return { tone: "neutral", label: connectorStatusLabel(live.status), action: null };
442
+ if (live.status !== "ok") {
443
+ const tone = connectorStatusTone(live.status) === "warn" ? "warn" : "danger";
444
+ return { tone, label: connectorStatusLabel(live.status), action: tone === "warn" ? "Connect" : "Inspect" };
428
445
  }
429
- return "Authentication for this connection is managed by your deployment.";
446
+ if (signals.drift)
447
+ return { tone: "warn", label: "Catalog drift", action: "Review" };
448
+ return { tone: "ok", label: "Connected", action: null };
430
449
  }
431
450
  /**
432
451
  * The categories, in the order an operator should read them: what the
@@ -466,7 +485,7 @@ export function driftCounts(drift) {
466
485
  export function driftSummary(drift) {
467
486
  const state = driftState(drift);
468
487
  if (state === "unavailable") {
469
- return "No catalog refresh observed yet in this runtime.";
488
+ return "No catalog refresh yet.";
470
489
  }
471
490
  const observed = formatDate(drift?.observedAt);
472
491
  const when = observed ? ` · observed ${observed}` : "";
@@ -508,6 +527,7 @@ const ACTOR_KINDS = {
508
527
  bearer: "Bearer token",
509
528
  "cloudflare-access": "Cloudflare Access",
510
529
  anonymous: "Anonymous",
530
+ access_token: "Agent token",
511
531
  };
512
532
  /** How a caller signed in, as a person reads it. An unknown kind is shown as sent. */
513
533
  function actorKindLabel(kind) {
@@ -565,9 +585,10 @@ export function activitySummary(events) {
565
585
  if (events.length === 0)
566
586
  return "";
567
587
  const calls = events.filter((event) => event.kind !== "catalog_drift");
588
+ const failed = calls.filter((event) => ["error", "timeout"].includes(activityOutcomeClass(event.outcome))).length;
568
589
  const changes = events.length - calls.length;
569
590
  const tools = new Set(calls.map((event) => event.address)).size;
570
- return `${calls.length} loaded call${calls.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"}${changes ? ` · ${changes} catalog change${changes === 1 ? "" : "s"}` : ""}`;
591
+ return `${calls.length} loaded call${calls.length === 1 ? "" : "s"} · ${failed} failed · ${tools} tool${tools === 1 ? "" : "s"}${changes ? ` · ${changes} catalog change${changes === 1 ? "" : "s"}` : ""}`;
571
592
  }
572
593
  // A pause and an approval are neither success nor failure: each gets its own
573
594
  // class, and the stylesheet paints neither as an error. Nothing has emitted
@@ -613,12 +634,17 @@ const REASON_LABELS = {
613
634
  read_only_endpoint: "endpoint is read-only",
614
635
  approval_required: "needed approval",
615
636
  auth_required: "needed authorization",
616
- result_too_large: "result too large to return inline",
637
+ result_too_large: "result too large",
617
638
  timeout: "timed out",
618
639
  };
619
640
  function reasonLabel(code) {
620
641
  return REASON_LABELS[code] ?? code.replaceAll("_", " ");
621
642
  }
643
+ /** Why a call stalled or failed, in words; "" when nothing was recorded. */
644
+ export function activityReason(event) {
645
+ const reasons = [event.friction, event.errorCode].filter((code) => Boolean(code)).map(reasonLabel);
646
+ return [...new Set(reasons)].join(" · ");
647
+ }
622
648
  /** The one-line detail under an address: where it ran, retries, and why it stalled. */
623
649
  export function activityDetail(event) {
624
650
  if (event.kind === "catalog_drift")
@@ -645,27 +671,17 @@ export function credentialProblemCopy(problem) {
645
671
  if (problem === "credential_mismatch")
646
672
  return PROBLEM_COPY.credential_mismatch;
647
673
  if (problem === "credential_unreadable") {
648
- return "Credential status is unavailable. Retry the status check before replacing or removing the credential.";
674
+ return "Credential status is unavailable. Retry before replacing or removing it.";
649
675
  }
650
- return "The stored credential can't be used. Replace it, or remove it and add a new one.";
651
- }
652
- export function credentialStateLabel(credential) {
653
- if (credential.configured === null || credential.problem === "credential_unreadable")
654
- return "unavailable";
655
- if (!credential.configured)
656
- return "not configured";
657
- const masked = "configured";
658
- return credential.updatedAt ? `${masked} · updated ${formatDate(credential.updatedAt)}` : masked;
676
+ return "The stored credential can't be used. Replace it.";
659
677
  }
660
678
  /** Gate copy for each browser-auth shape, so the sign-in state is never a blank page. */
661
679
  export function gateCopy(kind, signedIn) {
662
680
  if (kind === "cloudflare-access") {
663
- return "Checking Cloudflare Access identity failed. Retry to check whether this browser can open operator pages.";
681
+ return "Couldn't check your Cloudflare Access identity. Retry.";
664
682
  }
665
683
  if (kind !== "clerk") {
666
- return "Paste an operator bearer token to open this page. Nothing is requested until you do.";
684
+ return "Paste an operator token.";
667
685
  }
668
- return signedIn
669
- ? "Signed in with Clerk, but this account cannot open deployment-wide operator pages."
670
- : "Sign in with Clerk to open this operator page.";
686
+ return signedIn ? "This Clerk account can't open operator pages." : "Sign in with Clerk.";
671
687
  }
@@ -1,6 +1,8 @@
1
1
  // Generated by scripts/build-operator-ui.mjs. Do not edit.
2
- // Source: src/operator-ui/tokens.css and src/operator-ui/page.css.
2
+ // Source: src/operator-ui/tokens.css, src/operator-ui/page.css, and the hashed font assets.
3
3
  /** Custom properties and both palettes; styles no element. */
4
- export const TOKENS_CSS = ":root{color-scheme:light;--default-accent: #171717;--accent: var(--default-accent);--radius: 8px;--sans: \"Geist Sans\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;--mono: \"Geist Mono\", ui-monospace, \"SF Mono\", Menlo, Monaco, \"Cascadia Code\", Consolas, monospace;--bg: #fafafa;--surface: #ffffff;--surface-2: #f5f5f5;--border: #eaeaea;--border-strong: #d4d4d4;--text: #171717;--muted: #666666;--ok: #0a7d33;--warn: #a35200;--danger: #d8242c;--on-accent: #ffffff;--link: var(--accent);--blue: #0060df;--tint: color-mix(in srgb, var(--accent) 8%, var(--surface));--shell: 68rem;--pad: 1.25rem;--gap: 1rem;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-6: 1.5rem;--space-8: 2rem;--text-xs: .75rem;--text-sm: .8125rem;--text-base: .875rem}@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]){--default-accent: #ededed;--on-accent: #171717;--blue: #70b3ff;--bg: #0a0a0a;--surface: #111111;--surface-2: #1a1a1a;--border: #2e2e2e;--border-strong: #454545;--text: #ededed;--muted: #a1a1a1;--ok: #4cce83;--warn: #f5b85a;--danger: #ff777f;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}}html[data-scheme=dark]{--default-accent: #ededed;--on-accent: #171717;--blue: #70b3ff;--bg: #0a0a0a;--surface: #111111;--surface-2: #1a1a1a;--border: #2e2e2e;--border-strong: #454545;--text: #ededed;--muted: #a1a1a1;--ok: #4cce83;--warn: #f5b85a;--danger: #ff777f;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}";
4
+ export const TOKENS_CSS = ":root{color-scheme:light;--sans: \"Hanken Grotesk\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;--mono: \"JetBrains Mono\", ui-monospace, \"SF Mono\", Menlo, Monaco, \"Cascadia Code\", Consolas, monospace;--text-2xs: .6875rem;--text-xs: .78125rem;--text-sm: .84375rem;--text-base: .9375rem;--text-md: 1.0625rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.75rem;--text-3xl: 2.125rem;--weight-medium: 500;--weight-semibold: 620;--weight-bold: 700;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.25rem;--space-6: 1.5rem;--space-8: 2rem;--space-10: 2.5rem;--shell: 73.75rem;--pad: 1.5rem;--gap: 1rem;--topbar: 3.875rem;--radius: 14px;--radius-md: min(var(--radius), max(calc(var(--radius) - 4px), 2px));--radius-sm: min(var(--radius), max(calc(var(--radius) - 7px), 2px));--radius-pill: 999px;--bg: #f6f4ef;--surface: #ffffff;--surface-2: #fbfaf7;--sunk: #f0ede6;--hover: #f5f2ec;--line: #e9e4da;--line-strong: #dcd5c8;--line-field: #938b7d;--text: #1d1a16;--text-2: #5c564c;--text-3: #756e61;--scrim: rgb(20 16 8 / .38);--avatar-fill: var(--text);--avatar-ink: var(--bg);--default-accent: #4c44e0;--accent: var(--default-accent);--accent-strong: color-mix(in srgb, var(--accent) 84%, #000000);--accent-soft: color-mix(in srgb, var(--accent) 9%, var(--surface));--accent-line: color-mix(in srgb, var(--accent) 26%, var(--surface));--on-accent: var(--custom-on-accent, #ffffff);--link: var(--accent);--ok: #16774b;--ok-soft: #e4f3ea;--ok-line: #bfe2cd;--warn: #9a5800;--warn-soft: #fcf0da;--warn-line: #f1d7a6;--danger: #bc3a27;--danger-soft: #fbe8e4;--danger-line: #f2c5bc;--shadow-1: 0 1px 2px rgb(40 30 10 / .05), 0 1px 1px rgb(40 30 10 / .03);--shadow-2: 0 1px 2px rgb(40 30 10 / .04), 0 6px 20px -6px rgb(40 30 10 / .1);--shadow-3: 0 24px 60px -16px rgb(30 20 0 / .28), 0 2px 6px rgb(30 20 0 / .06)}@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: #12110f;--surface: #1b1a17;--surface-2: #1f1d1a;--sunk: #16150f;--hover: #24221e;--line: #2c2925;--line-strong: #3a3631;--line-field: #757064;--text: #f3efe7;--text-2: #b5aea2;--text-3: #8a8378;--scrim: rgb(0 0 0 / .6);--avatar-fill: #34312c;--avatar-ink: #e9e4da;--default-accent: #8c86ff;--accent-strong: color-mix(in srgb, var(--accent) 80%, #ffffff);--accent-soft: color-mix(in srgb, var(--accent) 17%, var(--surface));--accent-line: color-mix(in srgb, var(--accent) 38%, var(--surface));--on-accent: var(--custom-on-accent, #0f0e1f);--link: var(--custom-accent-text, color-mix(in srgb, var(--accent) 80%, #ffffff));--ok: #5bd08f;--ok-soft: #173326;--ok-line: #24513a;--warn: #f0b354;--warn-soft: #352812;--warn-line: #5a4319;--danger: #ff8a75;--danger-soft: #3a1c16;--danger-line: #5e2c22;--shadow-1: 0 1px 2px rgb(0 0 0 / .4);--shadow-2: 0 1px 2px rgb(0 0 0 / .3), 0 8px 24px -8px rgb(0 0 0 / .5);--shadow-3: 0 30px 80px -20px rgb(0 0 0 / .7)}}html[data-scheme=dark]{--bg: #12110f;--surface: #1b1a17;--surface-2: #1f1d1a;--sunk: #16150f;--hover: #24221e;--line: #2c2925;--line-strong: #3a3631;--line-field: #757064;--text: #f3efe7;--text-2: #b5aea2;--text-3: #8a8378;--scrim: rgb(0 0 0 / .6);--avatar-fill: #34312c;--avatar-ink: #e9e4da;--default-accent: #8c86ff;--accent-strong: color-mix(in srgb, var(--accent) 80%, #ffffff);--accent-soft: color-mix(in srgb, var(--accent) 17%, var(--surface));--accent-line: color-mix(in srgb, var(--accent) 38%, var(--surface));--on-accent: var(--custom-on-accent, #0f0e1f);--link: var(--custom-accent-text, color-mix(in srgb, var(--accent) 80%, #ffffff));--ok: #5bd08f;--ok-soft: #173326;--ok-line: #24513a;--warn: #f0b354;--warn-soft: #352812;--warn-line: #5a4319;--danger: #ff8a75;--danger-soft: #3a1c16;--danger-line: #5e2c22;--shadow-1: 0 1px 2px rgb(0 0 0 / .4);--shadow-2: 0 1px 2px rgb(0 0 0 / .3), 0 8px 24px -8px rgb(0 0 0 / .5);--shadow-3: 0 30px 80px -20px rgb(0 0 0 / .7)}";
5
5
  /** Base typography, the shell, masthead, buttons, badges, and messages. */
6
- 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)}}";
6
+ 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.55;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility}body{background:var(--bg);font-size:var(--text-base);letter-spacing:-.005em;margin:0;min-height:100vh}:is(h1,h2,h3,p,ul,ol){margin:0;padding:0}:is(h1,h2,h3){font-weight:var(--weight-semibold);line-height:1.25}h1{font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:-.03em;line-height:1.12}h2{font-size:var(--text-lg);letter-spacing:-.015em}h3{font-size:var(--text-base);letter-spacing:-.01em}:is(ul,ol){list-style:none}a{color:var(--link);text-decoration-thickness:1px;text-underline-offset:3px}button,input,select,textarea{color:inherit;font:inherit;letter-spacing:inherit}code,pre,kbd{font-family:var(--mono);letter-spacing:0}::selection{background:var(--accent-soft)}:is(a,button,input,select,textarea,summary,[tabindex]):focus-visible{border-radius:var(--radius-sm);outline:2px solid var(--link);outline-offset:2px}.skip-link{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-md);box-shadow:var(--shadow-2);color:var(--text);font-weight:var(--weight-semibold);left:var(--pad);padding:.5rem .875rem;position:fixed;text-decoration:none;top:-4rem;z-index:60}.skip-link:focus{top:.75rem}.shell{margin:0 auto;max-width:var(--shell);padding-left:var(--pad);padding-right:var(--pad)}.cap,.meta{color:var(--text-2);font-size:var(--text-sm)}.cap{color:var(--text-3);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:.08em;text-transform:uppercase}.mono{font-family:var(--mono);font-size:.86em}.visually-hidden{clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.masthead{backdrop-filter:saturate(1.4) blur(14px);background:color-mix(in srgb,var(--bg) 84%,transparent);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:30}.masthead.shell{max-width:none}.masthead-inner{align-items:center;display:flex;gap:var(--space-4);margin:0 auto;max-width:calc(var(--shell) - 2 * var(--pad));min-height:var(--topbar);width:100%}.mast-nav{align-items:center;display:flex;gap:var(--space-2);min-width:0}.brand-mark{border-radius:var(--radius-sm);display:block;flex:none;height:1.625rem;margin-right:.125rem;object-fit:contain;width:1.625rem}span.brand-mark{background:conic-gradient(from 45deg,var(--text) 0 90deg,transparent 0),radial-gradient(circle closest-side,transparent 0 34%,var(--bg) 38% 58.5%,transparent 62.5%),var(--text)}.navlink{background:none;border:0;border-radius:var(--radius-md);color:var(--text-2);cursor:pointer;font-size:var(--text-base);font-weight:var(--weight-medium);padding:.4375rem .75rem;text-decoration:none}.navlink:hover{background:var(--hover);color:var(--text)}.brand,.product{font-size:var(--text-base);overflow-wrap:anywhere;padding-left:0;padding-right:0;text-decoration:none}.brand{color:var(--text);font-size:1rem;font-weight:var(--weight-bold);letter-spacing:-.02em}.brand:hover,.product:hover{background:none}.product{color:var(--text-2);font-weight:var(--weight-medium)}.product:before{color:var(--line-strong);content:\"/\";font-weight:400;margin-right:.5rem}.page{padding-bottom:6rem;padding-top:var(--space-10)}.lead{margin-bottom:var(--space-8)}.lead-copy{color:var(--text-2);display:grid;font-size:var(--text-md);gap:var(--space-2);margin-top:var(--space-2)}.lead-copy p{max-width:60ch}.btn{align-items:center;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-1);color:var(--text);cursor:pointer;display:inline-flex;font-size:var(--text-sm);font-weight:var(--weight-semibold);gap:.4375rem;justify-content:center;line-height:1.2;min-height:2.25rem;padding:.375rem .875rem;text-decoration:none;transition:background-color .15s,border-color .15s,color .15s;white-space:nowrap}.btn:hover{background:var(--hover)}.btn:active{translate:0 1px}.btn:disabled{cursor:not-allowed;opacity:.5;translate:none}.btn.primary{background:var(--accent);border-color:var(--accent);box-shadow:0 1px 2px color-mix(in srgb,var(--accent) 30%,transparent),inset 0 1px #ffffff24;color:var(--on-accent)}.btn.primary:hover{background:var(--accent-strong);border-color:var(--accent-strong)}.btn.danger{border-color:var(--danger-line);color:var(--danger)}.btn.danger:hover{background:var(--danger-soft)}.btn.quiet{background:transparent;border-color:transparent;box-shadow:none;color:var(--text-2)}.btn.quiet:hover{background:var(--hover);color:var(--text)}.badge{align-items:center;background:var(--sunk);border-radius:var(--radius-pill);color:var(--text-2);display:inline-flex;font-size:var(--text-xs);font-weight:var(--weight-semibold);gap:.375rem;line-height:1.2;min-height:1.5rem;padding:.125rem .5625rem;white-space:nowrap}.badge.ok{background:var(--ok-soft);color:var(--ok)}.badge.warn{background:var(--warn-soft);color:var(--warn)}.badge.danger{background:var(--danger-soft);color:var(--danger)}.badge.accent{background:var(--accent-soft);color:var(--link)}.msg{background:var(--danger-soft);border:1px solid var(--danger-line);border-radius:var(--radius-md);color:var(--text);font-size:var(--text-sm);padding:.75rem 1rem}.msg.warn{background:var(--warn-soft);border-color:var(--warn-line)}.status-page{background:var(--surface);border:1px solid var(--line);border-radius:min(calc(var(--radius) + 4px),calc(var(--radius) * 2));box-shadow:var(--shadow-2);display:grid;gap:var(--space-3);margin:8vh auto 0;max-width:30rem;padding:var(--space-8)}.status-head{align-items:center;display:flex;gap:.625rem}.status-mark{align-items:center;background:var(--sunk);border-radius:var(--radius-md);color:var(--text-2);display:inline-flex;flex:none;height:2.25rem;justify-content:center;width:2.25rem}.status-mark svg{height:1.25rem;width:1.25rem}.status-mark.ok{background:var(--ok-soft);color:var(--ok)}.status-mark.danger{background:var(--danger-soft);color:var(--danger)}.status-label{color:var(--text-3);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:.08em;text-transform:uppercase}.status-label.ok{color:var(--ok)}.status-label.danger{color:var(--danger)}.status-page h1{font-size:var(--text-2xl);overflow-wrap:anywhere}.status-page h1+p,.status-copy{color:var(--text-2)}.status-actions{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-2)}.status-details{border-top:1px solid var(--line);color:var(--text-2);font-size:var(--text-sm);margin-top:var(--space-2);padding-top:var(--space-4)}.status-details>summary{align-items:center;cursor:pointer;display:flex;font-weight:var(--weight-semibold);gap:.5rem;list-style:none;width:fit-content}.status-details>summary::-webkit-details-marker{display:none}.status-details>summary:before{border-bottom:1.5px solid currentColor;border-right:1.5px solid currentColor;content:\"\";height:.375rem;rotate:-45deg;transition:rotate .15s;width:.375rem}.status-details[open]>summary:before{rotate:45deg}.status-details>p{margin-top:var(--space-2)}.status-details pre{background:var(--sunk);border:1px solid var(--line);border-radius:var(--radius-md);color:var(--text-2);font-size:var(--text-xs);line-height:1.6;margin:var(--space-2) 0 0;max-height:18rem;overflow:auto;overflow-wrap:anywhere;padding:.75rem .875rem;white-space:pre-wrap}@media(max-width:51.25rem){:root{--pad: 1rem}h1{font-size:var(--text-2xl)}.page{padding-top:var(--space-8)}.status-page{margin-top:var(--space-4);padding:var(--space-6) var(--space-5)}}";
7
+ /** The operator fonts' hashed paths, which only a deployment with the UI serves. */
8
+ export const PAGE_FONTS = [{ "family": "Hanken Grotesk", "weight": "100 900", "path": "/ui/assets/hanken-grotesk-latin-wght-normal-ZZ3K7K5Y.woff2" }, { "family": "JetBrains Mono", "weight": "100 800", "path": "/ui/assets/jetbrains-mono-latin-wght-normal-VBS632QH.woff2" }];
@@ -12,8 +12,10 @@
12
12
  * Workspace address, and the provider reads the identity core attached beside
13
13
  * the context. An agent, which has no Workspace account, acts instead as the
14
14
  * `owner` of the pool it came through, which core copies from that pool's
15
- * config beside the identity; the mapping never sees an agent, and an agent
16
- * in a pool without an owner fails `auth_required` (decisions/0006). A fixed
15
+ * config beside the identity; the mapping never sees an agent. In a pool
16
+ * without an owner the connector is absent for agents (core's scoped
17
+ * registry view hides it), and this provider still fails `auth_required` if
18
+ * one reaches it anyway (decisions/0006). A fixed
17
19
  * `subject` address is every caller's, owners aside. No tool argument,
18
20
  * header, or program can name the subject — every product's base URL is
19
21
  * confined beneath `users/me`, or its product's equivalent, so the token's
@@ -61,7 +63,7 @@ export type { GoogleServiceAccount } from "./service-account.js";
61
63
  * for a caller who has none — that call then fails closed. It is never called
62
64
  * for an open deployment's anonymous requests, for a call no request
63
65
  * admitted, or for an agent: an agent acts as the `owner` of the pool it
64
- * came through, and fails closed in a pool without one. It may be async, for
66
+ * came through, and never sees the connector in a pool without one. It may be async, for
65
67
  * a directory lookup (a Clerk user id to an email, say), and receives the
66
68
  * call's cancellation signal for that; it runs once per tool call and should
67
69
  * cache what it fetches. A string is one fixed account for every caller who
@@ -144,8 +146,8 @@ export declare const GOOGLE_WORKSPACE_OPTIONS: import("../../../config-schema.js
144
146
  title: import("../../../config-schema.js").Field<unknown, unknown, true>;
145
147
  maxResultBytes: import("../../../config-schema.js").Field<unknown, unknown, true>;
146
148
  purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
147
- baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
148
149
  subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
150
+ baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
149
151
  };
150
152
  };
151
153
  /** The checked, construction-time half of {@link GoogleWorkspaceOptions}. */
@@ -12,8 +12,10 @@
12
12
  * Workspace address, and the provider reads the identity core attached beside
13
13
  * the context. An agent, which has no Workspace account, acts instead as the
14
14
  * `owner` of the pool it came through, which core copies from that pool's
15
- * config beside the identity; the mapping never sees an agent, and an agent
16
- * in a pool without an owner fails `auth_required` (decisions/0006). A fixed
15
+ * config beside the identity; the mapping never sees an agent. In a pool
16
+ * without an owner the connector is absent for agents (core's scoped
17
+ * registry view hides it), and this provider still fails `auth_required` if
18
+ * one reaches it anyway (decisions/0006). A fixed
17
19
  * `subject` address is every caller's, owners aside. No tool argument,
18
20
  * header, or program can name the subject — every product's base URL is
19
21
  * confined beneath `users/me`, or its product's equivalent, so the token's
@@ -109,6 +111,8 @@ async function subjectFor(provider, subject, ctx) {
109
111
  // An agent has no Workspace account of its own. It acts as the owner of the
110
112
  // pool it came through, which core copied from that pool's config, and the
111
113
  // mapping, which answers for people, is never asked about it (decisions/0006).
114
+ // Core's scoped view already hides the connector from an agent without an
115
+ // owner; this refusal is the fallback for any context built without it.
112
116
  if (caller.identity.agent !== undefined) {
113
117
  if (caller.pool !== undefined && isWorkspaceAddress(caller.poolOwner))
114
118
  return caller.poolOwner;
@@ -25,7 +25,7 @@ import { skill } from "./skill.generated.js";
25
25
  *
26
26
  * Drift is handled the Vercel way: `src/providers/ccb/drift.json` records
27
27
  * every endpoint a named tool touches, and
28
- * `npm run providers:check -- --provider ccb` compares those rows with the
28
+ * `pnpm run providers:check --provider ccb` compares those rows with the
29
29
  * published, credential-free OpenAPI document at
30
30
  * https://docs.pushpay.io/chms-v2/openapi/chms-api-specs.json.
31
31
  */
@@ -21,7 +21,7 @@ import { skill } from "./skill.generated.js";
21
21
  *
22
22
  * Drift. Google publishes a credential-free Discovery document per API.
23
23
  * `src/providers/docs/drift.json` records the three methods the tools
24
- * call, and `npm run providers:check -- --provider docs` reports a touched
24
+ * call, and `pnpm run providers:check --provider docs` reports a touched
25
25
  * contract that moved or a method that stopped accepting the scope below.
26
26
  */
27
27
  import { apiConnector as api, defined } from "../../connectors/api-connector.js";
@@ -1,9 +1,9 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "# Google Docs usage\n\nReads and edits Google Docs as the signed-in person through Workspace delegation; it cannot search or list documents.\n\nConnection purpose: \n\n## Whose documents\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the documents Drive shares with that person; an\nagent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument names an\naccount. A call with none mapped, or from an agent whose pool has no owner,\nfails `auth_required`; only an operator can change either.\n\n## Finding a document\n\nThere is no search or list tool. Take the id from a\ndocs.google.com URL (the part after `/d/`), from the person, or from a\nGoogle Drive connection's file search. A failure on an id may mean it does\nnot exist or is not shared with this person; Google does not say which.\n\n## Reading\n\n- `get_document` renders every tab as markdown-ish text: headings, lists,\n links, tables as pipe rows, footnotes after the body, images and other\n embeds as bracketed markers. Headers, footers, floating images, and\n comments are not rendered; a tab's `notRendered` names any it holds, and\n `suggestion_marks` there means the text mixes unmarked suggested\n insertions and deletions in (read with `suggestions` to preview). Text past `maxChars` (shared across tabs) ends with a marker;\n pass `tabId` to read one tab.\n- Before an index-based edit, read with `withIndexes: true` and target an\n element's `startIndex`/`endIndex`. A long document's rows page with\n `page.nextCursor`; send `maxChars: 0` on later pages, and start over if\n `revisionId` changed between them. Indexes are UTF-16 code units and\n shift after every edit. Keep `revisionId` and pass it as\n `requiredRevisionId`, so an edit against a document someone else has\n changed fails `conflict` with nothing applied; re-read and recompute. Indexes\n from a suggestions preview (`accepted`/`rejected`) are not valid for\n edits.\n- `raw: true` returns Google's document resource for the tabs instead of\n the rendering — styles, headers, footers, named ranges, list and segment ids\n that `batch_update_document` requests may need. It is large: pass\n `tabId`, and call it directly rather than inside `execute_code`,\n which carries at most 256 KiB per result.\n- The same holds for a `maxChars` above the default: a long rendering\n can pass here and still be refused inside `execute_code`, even when the\n program would only measure it. Read it with a direct `call_tool`, paged\n with `connecta.result`, or read one tab at a time.\n\n## Editing\n\n- `create_document` makes a new document in My Drive's root. Never create\n again after a failure that may have left one behind; the message says\n which. When the document exists but its starting text failed, the message\n names its id: append the text with `append_text` straight away only when\n Google refused it; otherwise read the document first and append only what\n is missing.\n- An edit sent with no answer back, answered with a server error, or\n redirected says its outcome is unknown: re-read before repeating it, or\n repeat it with the same `requiredRevisionId`, which Google refuses if the\n first attempt landed. One Google answered 2xx whose reply was unreadable\n says it was applied: do not repeat it.\n- `append_text` joins the last paragraph; begin with `\\n` for a new one.\n- `replace_all_text` changes every match at once; read first and make\n `find` specific.\n- `batch_update_document` takes raw Docs API requests (`deleteContentRange`,\n `updateTextStyle`, `insertTable`, `createParagraphBullets`, …) for what\n the named tools cannot do. It is always destructive, and requires the\n `requiredRevisionId` from the read it was planned against: up to 100\n requests, each exactly one generally available kind (the Developer Preview\n comment and suggestion kinds are refused). Several requests in one\n call apply atomically, and each sees the index shifts of those before it;\n order deletions from the end of the document backwards.\n",
3
+ "content": "# Google Docs usage\n\nReads and edits Google Docs as the signed-in person through Workspace delegation; it cannot search or list documents.\n\nConnection purpose: \n\n## Whose documents\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the documents Drive shares with that person; an\nagent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument names an\naccount. A call with none mapped fails `auth_required`; only an operator can change\nthat.\n\n## Finding a document\n\nThere is no search or list tool. Take the id from a\ndocs.google.com URL (the part after `/d/`), from the person, or from a\nGoogle Drive connection's file search. A failure on an id may mean it does\nnot exist or is not shared with this person; Google does not say which.\n\n## Reading\n\n- `get_document` renders every tab as markdown-ish text: headings, lists,\n links, tables as pipe rows, footnotes after the body, images and other\n embeds as bracketed markers. Headers, footers, floating images, and\n comments are not rendered; a tab's `notRendered` names any it holds, and\n `suggestion_marks` there means the text mixes unmarked suggested\n insertions and deletions in (read with `suggestions` to preview). Text past `maxChars` (shared across tabs) ends with a marker;\n pass `tabId` to read one tab.\n- Before an index-based edit, read with `withIndexes: true` and target an\n element's `startIndex`/`endIndex`. A long document's rows page with\n `page.nextCursor`; send `maxChars: 0` on later pages, and start over if\n `revisionId` changed between them. Indexes are UTF-16 code units and\n shift after every edit. Keep `revisionId` and pass it as\n `requiredRevisionId`, so an edit against a document someone else has\n changed fails `conflict` with nothing applied; re-read and recompute. Indexes\n from a suggestions preview (`accepted`/`rejected`) are not valid for\n edits.\n- `raw: true` returns Google's document resource for the tabs instead of\n the rendering — styles, headers, footers, named ranges, list and segment ids\n that `batch_update_document` requests may need. It is large: pass\n `tabId`, and call it directly rather than inside `execute_code`,\n which carries at most 256 KiB per result.\n- The same holds for a `maxChars` above the default: a long rendering\n can pass here and still be refused inside `execute_code`, even when the\n program would only measure it. Read it with a direct `call_tool`, paged\n with `connecta.result`, or read one tab at a time.\n\n## Editing\n\n- `create_document` makes a new document in My Drive's root. Never create\n again after a failure that may have left one behind; the message says\n which. When the document exists but its starting text failed, the message\n names its id: append the text with `append_text` straight away only when\n Google refused it; otherwise read the document first and append only what\n is missing.\n- An edit sent with no answer back, answered with a server error, or\n redirected says its outcome is unknown: re-read before repeating it, or\n repeat it with the same `requiredRevisionId`, which Google refuses if the\n first attempt landed. One Google answered 2xx whose reply was unreadable\n says it was applied: do not repeat it.\n- `append_text` joins the last paragraph; begin with `\\n` for a new one.\n- `replace_all_text` changes every match at once; read first and make\n `find` specific.\n- `batch_update_document` takes raw Docs API requests (`deleteContentRange`,\n `updateTextStyle`, `insertTable`, `createParagraphBullets`, …) for what\n the named tools cannot do. It is always destructive, and requires the\n `requiredRevisionId` from the read it was planned against: up to 100\n requests, each exactly one generally available kind (the Developer Preview\n comment and suggestion kinds are refused). Several requests in one\n call apply atomically, and each sees the index shifts of those before it;\n order deletions from the end of the document backwards.\n",
4
4
  "instructionsHeading": "Connection instructions",
5
5
  "fragments": {
6
6
  "guide_0": "# Google Docs usage\n\nReads and edits Google Docs as the signed-in person through Workspace delegation; it cannot search or list documents.\n\nConnection purpose: ",
7
- "guide_1": "\n\n## Whose documents\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the documents Drive shares with that person; an\nagent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument names an\naccount. A call with none mapped, or from an agent whose pool has no owner,\nfails `auth_required`; only an operator can change either.\n\n## Finding a document\n\nThere is no search or list tool. Take the id from a\ndocs.google.com URL (the part after `/d/`), from the person, or from a\nGoogle Drive connection's file search. A failure on an id may mean it does\nnot exist or is not shared with this person; Google does not say which.\n\n## Reading\n\n- `get_document` renders every tab as markdown-ish text: headings, lists,\n links, tables as pipe rows, footnotes after the body, images and other\n embeds as bracketed markers. Headers, footers, floating images, and\n comments are not rendered; a tab's `notRendered` names any it holds, and\n `suggestion_marks` there means the text mixes unmarked suggested\n insertions and deletions in (read with `suggestions` to preview). Text past `maxChars` (shared across tabs) ends with a marker;\n pass `tabId` to read one tab.\n- Before an index-based edit, read with `withIndexes: true` and target an\n element's `startIndex`/`endIndex`. A long document's rows page with\n `page.nextCursor`; send `maxChars: 0` on later pages, and start over if\n `revisionId` changed between them. Indexes are UTF-16 code units and\n shift after every edit. Keep `revisionId` and pass it as\n `requiredRevisionId`, so an edit against a document someone else has\n changed fails `conflict` with nothing applied; re-read and recompute. Indexes\n from a suggestions preview (`accepted`/`rejected`) are not valid for\n edits.\n- `raw: true` returns Google's document resource for the tabs instead of\n the rendering — styles, headers, footers, named ranges, list and segment ids\n that `batch_update_document` requests may need. It is large: pass\n `tabId`, and call it directly rather than inside `execute_code`,\n which carries at most 256 KiB per result.\n- The same holds for a `maxChars` above the default: a long rendering\n can pass here and still be refused inside `execute_code`, even when the\n program would only measure it. Read it with a direct `call_tool`, paged\n with `connecta.result`, or read one tab at a time.\n\n## Editing\n\n- `create_document` makes a new document in My Drive's root. Never create\n again after a failure that may have left one behind; the message says\n which. When the document exists but its starting text failed, the message\n names its id: append the text with `append_text` straight away only when\n Google refused it; otherwise read the document first and append only what\n is missing.\n- An edit sent with no answer back, answered with a server error, or\n redirected says its outcome is unknown: re-read before repeating it, or\n repeat it with the same `requiredRevisionId`, which Google refuses if the\n first attempt landed. One Google answered 2xx whose reply was unreadable\n says it was applied: do not repeat it.\n- `append_text` joins the last paragraph; begin with `\\n` for a new one.\n- `replace_all_text` changes every match at once; read first and make\n `find` specific.\n- `batch_update_document` takes raw Docs API requests (`deleteContentRange`,\n `updateTextStyle`, `insertTable`, `createParagraphBullets`, …) for what\n the named tools cannot do. It is always destructive, and requires the\n `requiredRevisionId` from the read it was planned against: up to 100\n requests, each exactly one generally available kind (the Developer Preview\n comment and suggestion kinds are refused). Several requests in one\n call apply atomically, and each sees the index shifts of those before it;\n order deletions from the end of the document backwards.\n"
7
+ "guide_1": "\n\n## Whose documents\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the documents Drive shares with that person; an\nagent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument names an\naccount. A call with none mapped fails `auth_required`; only an operator can change\nthat.\n\n## Finding a document\n\nThere is no search or list tool. Take the id from a\ndocs.google.com URL (the part after `/d/`), from the person, or from a\nGoogle Drive connection's file search. A failure on an id may mean it does\nnot exist or is not shared with this person; Google does not say which.\n\n## Reading\n\n- `get_document` renders every tab as markdown-ish text: headings, lists,\n links, tables as pipe rows, footnotes after the body, images and other\n embeds as bracketed markers. Headers, footers, floating images, and\n comments are not rendered; a tab's `notRendered` names any it holds, and\n `suggestion_marks` there means the text mixes unmarked suggested\n insertions and deletions in (read with `suggestions` to preview). Text past `maxChars` (shared across tabs) ends with a marker;\n pass `tabId` to read one tab.\n- Before an index-based edit, read with `withIndexes: true` and target an\n element's `startIndex`/`endIndex`. A long document's rows page with\n `page.nextCursor`; send `maxChars: 0` on later pages, and start over if\n `revisionId` changed between them. Indexes are UTF-16 code units and\n shift after every edit. Keep `revisionId` and pass it as\n `requiredRevisionId`, so an edit against a document someone else has\n changed fails `conflict` with nothing applied; re-read and recompute. Indexes\n from a suggestions preview (`accepted`/`rejected`) are not valid for\n edits.\n- `raw: true` returns Google's document resource for the tabs instead of\n the rendering — styles, headers, footers, named ranges, list and segment ids\n that `batch_update_document` requests may need. It is large: pass\n `tabId`, and call it directly rather than inside `execute_code`,\n which carries at most 256 KiB per result.\n- The same holds for a `maxChars` above the default: a long rendering\n can pass here and still be refused inside `execute_code`, even when the\n program would only measure it. Read it with a direct `call_tool`, paged\n with `connecta.result`, or read one tab at a time.\n\n## Editing\n\n- `create_document` makes a new document in My Drive's root. Never create\n again after a failure that may have left one behind; the message says\n which. When the document exists but its starting text failed, the message\n names its id: append the text with `append_text` straight away only when\n Google refused it; otherwise read the document first and append only what\n is missing.\n- An edit sent with no answer back, answered with a server error, or\n redirected says its outcome is unknown: re-read before repeating it, or\n repeat it with the same `requiredRevisionId`, which Google refuses if the\n first attempt landed. One Google answered 2xx whose reply was unreadable\n says it was applied: do not repeat it.\n- `append_text` joins the last paragraph; begin with `\\n` for a new one.\n- `replace_all_text` changes every match at once; read first and make\n `find` specific.\n- `batch_update_document` takes raw Docs API requests (`deleteContentRange`,\n `updateTextStyle`, `insertTable`, `createParagraphBullets`, …) for what\n the named tools cannot do. It is always destructive, and requires the\n `requiredRevisionId` from the read it was planned against: up to 100\n requests, each exactly one generally available kind (the Developer Preview\n comment and suggestion kinds are refused). Several requests in one\n call apply atomically, and each sees the index shifts of those before it;\n order deletions from the end of the document backwards.\n"
8
8
  }
9
9
  };
@@ -26,7 +26,7 @@ import { skill } from "./skill.generated.js";
26
26
  * changes sharing is destructive and crosses the host's approval prompt.
27
27
  *
28
28
  * Drift. `src/providers/drive/drift.json` records the eleven methods the
29
- * tools call, and `npm run providers:check -- --provider drive` reports a
29
+ * tools call, and `pnpm run providers:check --provider drive` reports a
30
30
  * touched contract that moved or a method that stopped accepting the scope
31
31
  * below.
32
32
  */