@zackbart/connecta 0.32.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 (158) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +95 -116
  3. package/SECURITY.md +1 -1
  4. package/bin/connecta.mjs +24 -6
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +167 -23
  7. package/dist/access-tokens.js +676 -140
  8. package/dist/activity.d.ts +3 -0
  9. package/dist/agent-identity.js +47 -0
  10. package/dist/auth/cloudflare-access.js +13 -5
  11. package/dist/branding.d.ts +7 -0
  12. package/dist/branding.js +76 -9
  13. package/dist/call-admission-context.js +15 -0
  14. package/dist/call-admission.js +30 -43
  15. package/dist/call-recovery.js +6 -2
  16. package/dist/catalog-service.js +15 -1
  17. package/dist/config-value-sources.js +16 -9
  18. package/dist/config.d.ts +48 -75
  19. package/dist/config.js +144 -36
  20. package/dist/connector-access.d.ts +4 -34
  21. package/dist/connector-access.js +15 -99
  22. package/dist/connector-caller.d.ts +7 -1
  23. package/dist/connector-caller.js +4 -2
  24. package/dist/connectors/api-connector.d.ts +27 -4
  25. package/dist/connectors/api-connector.js +2 -0
  26. package/dist/connectors/remote-mcp.js +2 -0
  27. package/dist/describe-config.d.ts +31 -7
  28. package/dist/describe-config.js +8 -17
  29. package/dist/execute.js +1 -11
  30. package/dist/fix-prompt.js +2 -2
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +5 -3
  34. package/dist/membership.d.ts +53 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/module-contracts.d.ts +23 -6
  37. package/dist/oauth-callback-outcome.js +2 -2
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -4
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/config-summary.js +562 -0
  43. package/dist/operator-ui/connection-map.js +375 -0
  44. package/dist/operator-ui/contract.d.ts +149 -28
  45. package/dist/operator-ui/endpoints.js +221 -0
  46. package/dist/operator-ui/fix-prompts.js +5 -5
  47. package/dist/operator-ui/generated.js +5 -4
  48. package/dist/operator-ui/member-page.js +97 -0
  49. package/dist/operator-ui/model.d.ts +14 -6
  50. package/dist/operator-ui/overview.js +134 -0
  51. package/dist/operator-ui/view.d.ts +107 -37
  52. package/dist/operator-ui/view.js +163 -86
  53. package/dist/page-styles.js +5 -3
  54. package/dist/provider.d.ts +61 -8
  55. package/dist/provider.js +46 -7
  56. package/dist/providers/_shared/google/workspace.d.ts +56 -17
  57. package/dist/providers/_shared/google/workspace.js +34 -8
  58. package/dist/providers/_shared/rest/tools.js +2 -2
  59. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  60. package/dist/providers/_shared/rest/value-safety.js +699 -7
  61. package/dist/providers/ccb/index.js +2 -2
  62. package/dist/providers/cloudflare/index.js +3 -3
  63. package/dist/providers/cloudflare/rest.js +25 -3
  64. package/dist/providers/cloudflare/skill.generated.js +1 -1
  65. package/dist/providers/cloudflare/value-safety.js +32 -0
  66. package/dist/providers/docs/index.js +4 -4
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.js +4 -4
  69. package/dist/providers/drive/skill.generated.js +2 -2
  70. package/dist/providers/forms/index.js +4 -4
  71. package/dist/providers/forms/skill.generated.js +2 -2
  72. package/dist/providers/gmail/index.js +11 -9
  73. package/dist/providers/gmail/skill.generated.js +2 -2
  74. package/dist/providers/mixpanel/index.js +1 -1
  75. package/dist/providers/mixpanel/skill.generated.js +3 -3
  76. package/dist/providers/notion/index.d.ts +7 -0
  77. package/dist/providers/notion/index.js +100 -11
  78. package/dist/providers/notion/rest.js +45 -5
  79. package/dist/providers/notion/skill.generated.js +2 -2
  80. package/dist/providers/notion/value-safety.js +30 -5
  81. package/dist/providers/overflow/index.js +2 -2
  82. package/dist/providers/overflow/skill.generated.js +2 -2
  83. package/dist/providers/planning-center/index.js +2 -2
  84. package/dist/providers/planning-center/skill.generated.js +4 -4
  85. package/dist/providers/revenuecat/skill.generated.js +2 -2
  86. package/dist/providers/sheets/index.d.ts +3 -0
  87. package/dist/providers/sheets/index.js +7 -4
  88. package/dist/providers/sheets/skill.generated.js +2 -2
  89. package/dist/providers/slides/index.js +4 -4
  90. package/dist/providers/slides/skill.generated.js +2 -2
  91. package/dist/providers/stripe/rest.js +9 -2
  92. package/dist/providers/stripe/skill.generated.js +1 -1
  93. package/dist/providers/stripe/value-safety.js +22 -0
  94. package/dist/providers/tithely/index.js +1 -1
  95. package/dist/providers/vercel/index.js +1 -1
  96. package/dist/providers/vercel/rest.js +20 -2
  97. package/dist/providers/vercel/skill.generated.js +2 -2
  98. package/dist/providers/vercel/value-safety.js +22 -0
  99. package/dist/registry.d.ts +2 -3
  100. package/dist/registry.js +23 -9
  101. package/dist/routes/access-tokens.js +82 -75
  102. package/dist/routes/activity-disclosure.js +17 -18
  103. package/dist/routes/activity.js +34 -54
  104. package/dist/routes/actor-labels.js +33 -4
  105. package/dist/routes/agent-path.js +43 -0
  106. package/dist/routes/connect.js +16 -12
  107. package/dist/routes/credentials.js +16 -40
  108. package/dist/routes/mcp.js +161 -104
  109. package/dist/routes/oauth-management.js +11 -5
  110. package/dist/routes/oauth.js +5 -11
  111. package/dist/routes/operator.js +24 -23
  112. package/dist/routes/shared.d.ts +60 -20
  113. package/dist/routes/shared.js +187 -96
  114. package/dist/routes/ui-config.js +437 -97
  115. package/dist/routes/ui-data.js +2 -2
  116. package/dist/routes/ui.js +9 -16
  117. package/dist/runtime/call-admission.js +9 -0
  118. package/dist/server.js +5 -0
  119. package/dist/skills.js +10 -0
  120. package/dist/storage/keys.js +9 -1
  121. package/dist/storage/sql.js +5 -1
  122. package/dist/types.d.ts +29 -7
  123. package/dist/ui.d.ts +1 -1
  124. package/dist/ui.js +23 -16
  125. package/dist/usage-guide.js +1 -1
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/dist/workspace-address.js +9 -0
  129. package/dist/workspace-subject.js +23 -0
  130. package/documentation/README.md +47 -23
  131. package/documentation/architecture.md +371 -61
  132. package/documentation/auth.md +897 -249
  133. package/documentation/code-mode.md +37 -16
  134. package/documentation/deploying.md +126 -25
  135. package/documentation/integrating.md +97 -23
  136. package/documentation/meta-tools.md +50 -31
  137. package/documentation/migration-0.33.md +352 -0
  138. package/documentation/migration-0.34.md +67 -0
  139. package/documentation/operating.md +3 -1
  140. package/documentation/operator-tests.md +147 -34
  141. package/documentation/operator-ui.md +519 -64
  142. package/documentation/provider-auth-migration.md +37 -23
  143. package/documentation/provider-migration-0.29.md +2 -2
  144. package/examples/worker/AGENTS.md +37 -9
  145. package/examples/worker/README.md +127 -61
  146. package/examples/worker/src/connecta.config.ts +40 -23
  147. package/examples/worker/wrangler.jsonc +4 -1
  148. package/package.json +19 -18
  149. package/templates/node/.env.example +3 -0
  150. package/templates/node/AGENTS.md +33 -6
  151. package/templates/node/Dockerfile +2 -2
  152. package/templates/node/README.md +80 -52
  153. package/templates/node/docker-compose.yml +3 -2
  154. package/templates/node/package.json +4 -3
  155. package/templates/node/src/adopt-token.ts +23 -0
  156. package/templates/node/src/connecta.config.ts +28 -12
  157. package/templates/node/src/rotate-token.ts +17 -0
  158. package/templates/node/src/provision-token.ts +0 -17
@@ -0,0 +1,97 @@
1
+ import { connectorStatusLabel, connectorStatusTone } from "./view.js";
2
+ /**
3
+ * Whether the member's own partition holds a credential, read from `actions`
4
+ * alone (P6b): `clear` is listed only when a value is stored. Connection
5
+ * state says whether it works, never whether it exists.
6
+ */
7
+ function credentialStored(manage) {
8
+ return manage.actions.includes("clear");
9
+ }
10
+ function controlsFor(manage, connection) {
11
+ const can = (action) => (manage.kind === "none" ? false : manage.actions.includes(action));
12
+ if (manage.kind === "oauth") {
13
+ const connected = connection.state === "ok";
14
+ const controls = [];
15
+ if (!connected && can("connect")) {
16
+ controls.push({ kind: "connect", label: connection.state === "auth_required" ? "Connect" : "Reconnect" });
17
+ }
18
+ if (connected && can("restart"))
19
+ controls.push({ kind: "restart", label: "Reconnect" });
20
+ if (connection.state !== "auth_required" && can("disconnect")) {
21
+ controls.push({ kind: "disconnect", label: "Disconnect" });
22
+ }
23
+ return controls;
24
+ }
25
+ if (manage.kind === "credential") {
26
+ // `save` without `clear`: nothing stored yet. `test` is listed only for a stored value.
27
+ const stored = credentialStored(manage);
28
+ return [
29
+ ...(can("save")
30
+ ? [{ kind: "save", label: stored ? "Replace" : "Add credential" }]
31
+ : []),
32
+ ...(can("test") ? [{ kind: "test", label: "Test" }] : []),
33
+ ...(can("clear") ? [{ kind: "clear", label: "Clear" }] : []),
34
+ ];
35
+ }
36
+ return [];
37
+ }
38
+ /**
39
+ * A few words under the account's name, only where the badge alone would
40
+ * leave something out: a saved credential that no longer fits, or one saved
41
+ * but not working.
42
+ */
43
+ function summaryFor(manage, connection) {
44
+ if (manage.kind !== "credential" || !credentialStored(manage))
45
+ return null;
46
+ if (connection.problem === "credential_mismatch")
47
+ return "Saved credential no longer fits";
48
+ return connection.state === "ok" ? null : "Saved, but not working";
49
+ }
50
+ const toolCount = (connector) => connector.tools.length;
51
+ export function memberPage(contract) {
52
+ if (contract?.role !== "member")
53
+ return { endpoints: [], accounts: [], shared: [] };
54
+ return {
55
+ endpoints: contract.pools.map((pool) => ({ pool: pool.name, path: pool.humanPath })),
56
+ accounts: contract.connectors
57
+ .filter((connector) => connector.authScope === "personal")
58
+ .map((connector) => {
59
+ const connection = connector.connection ?? { state: "auth_required" };
60
+ return {
61
+ id: connector.id,
62
+ title: connector.title || connector.id,
63
+ toolCount: toolCount(connector),
64
+ badge: connector.connection
65
+ ? {
66
+ label: connectorStatusLabel(connection.state, connection.problem),
67
+ tone: connectorStatusTone(connection.state),
68
+ }
69
+ : null,
70
+ summary: summaryFor(connector.manage, connection),
71
+ manage: connector.manage,
72
+ controls: controlsFor(connector.manage, connection),
73
+ };
74
+ }),
75
+ // Names and counts only: a shared connector's state is not the member's to see.
76
+ shared: contract.connectors
77
+ .filter((connector) => connector.authScope === "shared")
78
+ .map((connector) => ({
79
+ id: connector.id,
80
+ title: connector.title || connector.id,
81
+ toolCount: toolCount(connector),
82
+ })),
83
+ };
84
+ }
85
+ export function toolCountLabel(count) {
86
+ return `${count} ${count === 1 ? "tool" : "tools"}`;
87
+ }
88
+ /** The in-page question before a member's action that takes something away. */
89
+ export function memberConfirmCopy(action, name) {
90
+ if (action === "oauth_restart") {
91
+ return { question: `Reconnect ${name}? It stops working until you finish in the new tab.`, confirm: "Reconnect" };
92
+ }
93
+ if (action === "oauth_disconnect") {
94
+ return { question: `Disconnect your ${name} account? Its tools stop working for you.`, confirm: "Disconnect" };
95
+ }
96
+ return { question: `Clear your ${name} credential? Its tools stop working for you.`, confirm: "Clear" };
97
+ }
@@ -1,4 +1,5 @@
1
- import type { OperatorConnectorOverlay, OperatorTool, OperatorUiContract, OperatorViewer } from "./contract.js";
1
+ import type { ConnectaConfigDescription } from "../describe-config.js";
2
+ import type { OperatorConnectorOverlay, OperatorTool } from "./contract.js";
2
3
  import type { CatalogAccessObservation, CatalogDriftReport, ResourceTemplateRefusalCode } from "../types.js";
3
4
  /** Tool verdicts, independent of whether the selected pool is read-only. */
4
5
  export type UiToolSafety = "runs_in_programs" | "needs_approval";
@@ -28,7 +29,8 @@ interface UiCredentialField {
28
29
  description?: string;
29
30
  placeholder?: string;
30
31
  inputType: "email" | "password" | "text";
31
- configured: boolean;
32
+ /** null when metadata could not be read. */
33
+ configured: boolean | null;
32
34
  lastFour?: string;
33
35
  updatedAt?: string;
34
36
  }
@@ -37,7 +39,8 @@ interface UiCredential {
37
39
  description?: string;
38
40
  placeholder?: string;
39
41
  fields?: UiCredentialField[];
40
- configured: boolean;
42
+ /** null when metadata could not be read. */
43
+ configured: boolean | null;
41
44
  /** A stored value exists and may be deleted even if it cannot be decrypted. */
42
45
  removable?: boolean;
43
46
  lastFour?: string;
@@ -60,7 +63,12 @@ export interface UiConnector {
60
63
  title?: string;
61
64
  description?: string;
62
65
  status: "loading" | OperatorConnectorOverlay["status"];
63
- permissions?: Omit<OperatorViewer["permissions"]["connectors"][number], "id">;
66
+ /** What this maintainer may do with the connector. */
67
+ permissions?: {
68
+ use: boolean;
69
+ manageSharedAuth: boolean;
70
+ connectPersonal: boolean;
71
+ };
64
72
  /**
65
73
  * Why this connector is not usable, when it is not. See `UiProblem`. The
66
74
  * only failure detail the payload carries; the status message behind it is
@@ -91,9 +99,9 @@ export interface UiConnector {
91
99
  export type CredentialManagementCapability = "available" | "requires_operator" | "vault_not_configured" | "no_slots";
92
100
  export interface UiData {
93
101
  accessTokenManagement?: "available" | "requires_operator";
94
- serverInfo: Pick<OperatorUiContract["config"]["server"], "name" | "version">;
102
+ serverInfo: Pick<ConnectaConfigDescription["server"], "name" | "version">;
95
103
  /** Version of the installed @zackbart/connecta package. */
96
- connectaVersion: OperatorUiContract["config"]["connectaVersion"];
104
+ connectaVersion: ConnectaConfigDescription["connectaVersion"];
97
105
  connectors: UiConnector[];
98
106
  activityEnabled: boolean;
99
107
  credentialManagement: CredentialManagementCapability;
@@ -0,0 +1,134 @@
1
+ import { endpointRows } from "./endpoints.js";
2
+ import { joinList } from "./connection-map.js";
3
+ import { connectorVerdict } from "./view.js";
4
+ const PROBLEM_TITLE = {
5
+ connector_unavailable: "is unavailable",
6
+ oauth_required: "needs authorization",
7
+ credential_required: "needs a credential",
8
+ auth_required: "needs a secret in the deployment",
9
+ credential_mismatch: "can't use its saved credential",
10
+ catalog_failed: "couldn't load its tools",
11
+ };
12
+ function affects(paths) {
13
+ return paths.length ? `Affects ${joinList(paths)}` : "No endpoint serves it";
14
+ }
15
+ /**
16
+ * One row per connector that is not healthy, failing first, then one per
17
+ * agent that sits in an endpoint without a working token. Each row carries
18
+ * the fix the rest of the UI already offers for it.
19
+ */
20
+ export function attentionItems(contract, options = {}) {
21
+ if (contract.role !== "maintainer")
22
+ return [];
23
+ const rows = endpointRows(contract);
24
+ const connectors = [];
25
+ for (const config of contract.config?.connectors ?? []) {
26
+ const live = contract.live.connectors.find((c) => c.id === config.id);
27
+ if (!live)
28
+ continue;
29
+ const signals = options.signals?.[config.id] ?? {};
30
+ const verdict = connectorVerdict(live, signals);
31
+ if ((verdict.tone !== "warn" && verdict.tone !== "danger") || !verdict.action)
32
+ continue;
33
+ const title = config.title ?? config.id;
34
+ const id = encodeURIComponent(config.id);
35
+ const paths = rows.filter((row) => row.groups.some((group) => group.config.id === config.id)).map((r) => r.path);
36
+ const connection = `/connectors/${id}#connection`;
37
+ const base = {
38
+ key: `connector:${config.id}`,
39
+ kind: "connector",
40
+ tone: verdict.tone,
41
+ subject: title,
42
+ action: { label: verdict.action, href: connection, ariaLabel: `${verdict.action} ${title}` },
43
+ href: connection,
44
+ };
45
+ // The server's problem code outranks a failed read on this page: the row
46
+ // keeps its fix prompt and says the details didn't load. "Unavailable"
47
+ // beside a failed read is the page's own (see connectorVerdict).
48
+ const reported = live.problem ?? (live.status === "error" ? "connector_unavailable" : undefined);
49
+ const problem = signals.failed && reported === "connector_unavailable" ? undefined : reported;
50
+ if (problem) {
51
+ connectors.push({
52
+ ...base,
53
+ problem: PROBLEM_TITLE[problem],
54
+ body: signals.failed ? `Details didn't load · ${affects(paths)}` : affects(paths),
55
+ fix: { kind: problem, connectorId: config.id },
56
+ });
57
+ }
58
+ else if (signals.failed) {
59
+ connectors.push({
60
+ ...base,
61
+ problem: "didn't load on this page",
62
+ body: affects(paths),
63
+ fix: null,
64
+ });
65
+ }
66
+ else if (live.status !== "ok") {
67
+ connectors.push({
68
+ ...base,
69
+ problem: verdict.label === "Authorization needed" ? "needs authorization" : "isn't connected",
70
+ body: affects(paths),
71
+ fix: null,
72
+ });
73
+ }
74
+ else {
75
+ // Healthy but drifted: the live tool list differs from the reviewed one.
76
+ connectors.push({
77
+ ...base,
78
+ problem: "has a changed tool list",
79
+ body: affects(paths),
80
+ fix: { kind: "catalog_drift", connectorId: config.id },
81
+ });
82
+ }
83
+ }
84
+ const agents = contract.agents
85
+ .filter((agent) => agent.pools.length && agent.token.state !== "issued")
86
+ .map((agent) => {
87
+ // An agent connects only at a pool's agent URL.
88
+ const paths = agent.pools.map((name) => contract.pools.find((pool) => pool.name === name)?.agentPath ?? `/agent/${name}`);
89
+ const revoked = agent.token.state === "revoked";
90
+ return {
91
+ key: `agent:${agent.name}`,
92
+ kind: "agent",
93
+ tone: "warn",
94
+ subject: agent.name,
95
+ problem: revoked ? "has a revoked token" : "has no token",
96
+ body: `Can't connect at ${joinList(paths)}`,
97
+ action: options.tokensPage
98
+ ? { label: "Issue token", href: "/tokens", ariaLabel: `Issue a token for ${agent.name}` }
99
+ : null,
100
+ href: options.tokensPage ? "/tokens" : null,
101
+ fix: null,
102
+ };
103
+ });
104
+ return [
105
+ ...connectors.filter((item) => item.tone === "danger"),
106
+ ...connectors.filter((item) => item.tone === "warn"),
107
+ ...agents,
108
+ ];
109
+ }
110
+ /** The numbers above the list, counted from the same map the page draws. */
111
+ export function overviewHeadline(contract, map, items, signals = {}) {
112
+ const bar = (contract.config?.connectors ?? []).flatMap((config) => {
113
+ const live = contract.live.connectors.find((c) => c.id === config.id);
114
+ if (!live)
115
+ return [];
116
+ const health = connectorVerdict(live, signals[config.id]);
117
+ return [{ id: config.id, title: config.title ?? config.id, tone: health.tone, label: health.label }];
118
+ });
119
+ const healthy = bar.filter((entry) => entry.tone === "ok").length;
120
+ const warn = bar.filter((entry) => entry.tone === "warn").length;
121
+ const danger = bar.filter((entry) => entry.tone === "danger").length;
122
+ const count = (kind) => map.nodes.filter((node) => node.kind === kind).length;
123
+ return {
124
+ connectors: bar.length,
125
+ healthy,
126
+ warn,
127
+ danger,
128
+ bar,
129
+ endpoints: count("endpoint"),
130
+ readOnlyEndpoints: map.nodes.filter((node) => node.kind === "endpoint" && node.readOnly).length,
131
+ people: count("person"),
132
+ agents: contract.agents.length,
133
+ };
134
+ }
@@ -1,30 +1,29 @@
1
- import type { OperatorUiContract } from "./contract.js";
1
+ import type { OperatorRole, OperatorUiContractV2 } from "./contract.js";
2
2
  import type { CatalogDriftReport } from "../types.js";
3
3
  import type { CredentialManagementCapability, UiConnector, UiData, UiProblem } from "./model.js";
4
4
  import type { FixPromptKind } from "./fix-prompts.js";
5
+ import type { AgentSlot } from "./agents.js";
5
6
  export declare function registrationPathLabel(path: NonNullable<UiConnector["registrationPath"]>): string;
6
- /**
7
- * A connector's source column: the maintained provider with the
8
- * implementation it uses, since a dual provider's `auth` selects either the
9
- * vendor's hosted MCP server or Connecta's REST connector.
10
- */
11
- export declare function connectorSourceLabel(source: {
12
- kind: string;
13
- provider?: string;
14
- }): string;
15
- export type OperatorPage = "overview" | "tools" | "access" | "config" | "connector" | "tokens" | "connections" | "activity";
16
- /** Pages the nav lists. */
7
+ export type OperatorPage = "overview" | "endpoints" | "settings" | "connector" | "tokens" | "connections" | "activity";
8
+ /** Pages the nav lists, in dashboard order. */
17
9
  export declare const OPERATOR_PAGES: readonly OperatorPage[];
18
10
  export declare const PAGE_META: Readonly<Record<OperatorPage, {
19
11
  path: string;
20
12
  label: string;
21
13
  }>>;
14
+ /** Legacy links remain valid, with the browser replacing their URL. */
15
+ export declare function canonicalOperatorPath(path: string): string;
22
16
  /**
23
17
  * What each page is for, in one line under its heading — on the gate as well
24
18
  * as once signed in, so the page an operator lands on reads the same before
25
19
  * and after the session is checked.
26
20
  */
27
21
  export declare function pageDescription(page: OperatorPage, productDescription: string): string;
22
+ /**
23
+ * Signed in, but neither a maintainer nor a member of any endpoint: who to
24
+ * ask, and that this is not an error to retry.
25
+ */
26
+ export declare function noAccessCopy(kind: string, productName: string): string;
28
27
  /** What the gate says while the session is checked, page by page. */
29
28
  export declare function checkingCopy(): string;
30
29
  export declare function pageForPath(path: string): OperatorPage;
@@ -83,12 +82,16 @@ export declare function failure(message: string, fix?: NoticeFix): Notice;
83
82
  * mutation that is a permission, not an expired session, so it stays inline.
84
83
  * - `network` — the browser could not reach the deployment at all.
85
84
  * - `refused` — the route answered, and said no.
85
+ * - `stale` — never sent: what the action was checked against changed before
86
+ * dispatch (a refreshed slot list or member contract).
86
87
  */
87
- export type RequestFailureKind = "session" | "forbidden" | "network" | "refused";
88
+ export type RequestFailureKind = "session" | "forbidden" | "network" | "refused" | "stale";
88
89
  export interface RequestFailureFacts {
89
90
  kind: RequestFailureKind;
90
91
  status?: number | undefined;
91
92
  problem?: unknown;
93
+ /** A refusal's machine `code` from a closed set, such as `slot_changed`; checked, never shown. */
94
+ code?: unknown;
92
95
  }
93
96
  /**
94
97
  * What the notices for an OAuth action and a credential Test say. Each of
@@ -121,6 +124,8 @@ export declare function credentialTestNotice(connectorId: string, answer: {
121
124
  * checked, not trusted.
122
125
  */
123
126
  export declare function refusedNotice(action: DownstreamAction, connectorId: string, problem?: unknown): Notice;
127
+ /** An action whose permission or target changed before it was sent. */
128
+ export declare const STALE_COPY = "This changed before sending, so nothing was sent. Try again.";
124
129
  /**
125
130
  * Every row action's failure, in one voice. The page's own failures — a
126
131
  * lapsed session, a denied identity, a dropped connection — read the same for
@@ -133,7 +138,7 @@ export declare function actionFailedNotice(action: RowAction, connectorId: strin
133
138
  * server's `connector_unavailable` and keeps its own copy and fix prompt;
134
139
  * these two have nothing for a coding agent to fix.
135
140
  */
136
- export type ConnectorLoadFailure = "session" | "network";
141
+ export type ConnectorLoadFailure = "session" | "network" | "unavailable";
137
142
  export declare function connectorLoadFailureCopy(failure: ConnectorLoadFailure, productName: string): string;
138
143
  /** The signed-in page's answer when `/ui/data` could not be read. */
139
144
  export declare function loadFailureCopy(failure: "server" | "network", productName: string): {
@@ -158,20 +163,27 @@ export declare function confirmCopy(action: PendingConfirm["action"], name: stri
158
163
  * page fetch again after an identity change.
159
164
  */
160
165
  type LoadPhase = "idle" | "loading" | "ready" | "error";
161
- export interface UiAccessToken {
162
- id: string;
163
- name: string;
164
- tokenPrefix: string;
165
- createdAt: string;
166
- revokedAt?: string;
167
- }
168
166
  export declare function accessTokenUnavailableCopy(capability?: string): string;
169
167
  export interface OperatorState {
170
168
  tokenPhase: LoadPhase;
171
169
  tokenNotice: Notice | null;
172
- tokens: UiAccessToken[];
170
+ /** One slot per declared agent, from `/ui/access-tokens`. */
171
+ tokens: AgentSlot[];
172
+ /** A just-issued secret, shown once in its dialog and dropped when that closes. */
173
173
  createdToken: string | null;
174
- tokenRenaming: string | null;
174
+ /** The agent `createdToken` belongs to. */
175
+ createdTokenFor: string | null;
176
+ /** A rotation or revocation waiting on its in-row confirmation. */
177
+ tokenConfirm: {
178
+ name: string;
179
+ action: "rotate" | "revoke";
180
+ } | null;
181
+ /**
182
+ * Agents whose last token operation ended with an unknown outcome (503,
183
+ * 499, or a dropped connection). Their actions stay off until a slot list
184
+ * read that started after the failure succeeds.
185
+ */
186
+ unresolvedSlots: string[];
175
187
  tokenBusy: boolean;
176
188
  page: OperatorPage;
177
189
  /**
@@ -180,11 +192,17 @@ export interface OperatorState {
180
192
  * the fence that keeps one identity's data off another identity's screen.
181
193
  */
182
194
  generation: number;
183
- /** `loading` until the first /ui/data answer decides gated or ready. */
184
- session: "loading" | "gated" | "ready";
195
+ /**
196
+ * `loading` until `/ui/api/config` answers (and, for a maintainer,
197
+ * `/ui/data` too). `denied` is signed in as someone who is neither a
198
+ * maintainer nor a pool member.
199
+ */
200
+ session: "loading" | "gated" | "ready" | "denied";
185
201
  gate: Notice | null;
186
202
  /** True while a signed-in operator's /ui/data is in flight. */
187
203
  refreshing: boolean;
204
+ /** A bounded admission recovery is pending; Retry can supersede it. */
205
+ recoveryPending: boolean;
188
206
  /**
189
207
  * Why the last /ui/data read failed when the session itself was not the
190
208
  * reason: the page stays signed in, with its chrome, and offers a retry.
@@ -204,7 +222,14 @@ export interface OperatorState {
204
222
  */
205
223
  focusIfLost: string | null;
206
224
  data: UiData | null;
207
- contract: OperatorUiContract | null;
225
+ /** Operator contract v2, for either role; null until `/ui/api/config` answers. */
226
+ contract: OperatorUiContractV2 | null;
227
+ /**
228
+ * The role the last good contract confirmed. It outlives a failed refresh
229
+ * that clears `contract`, so the next answer with a different role is
230
+ * still recognized as a different viewer. Only an identity reset clears it.
231
+ */
232
+ role: OperatorRole | null;
208
233
  contractPhase: LoadPhase;
209
234
  health: "loading" | "ok" | "unavailable";
210
235
  connectorFilter: string;
@@ -218,6 +243,11 @@ export interface OperatorState {
218
243
  * new tab: its row offers the link instead of the button.
219
244
  */
220
245
  oauthBlocked: string | null;
246
+ /**
247
+ * The authorization link the route returned for `oauthBlocked`. A member has
248
+ * no `/ui/data` connector to keep it on, so it is held here.
249
+ */
250
+ oauthBlockedUrl: string | null;
221
251
  /** A row action waiting for its in-page confirmation. */
222
252
  confirming: PendingConfirm | null;
223
253
  /** Connectors whose details failed on this side of the deployment. */
@@ -235,6 +265,31 @@ export interface OperatorState {
235
265
  activityCursor: string | null;
236
266
  activitySearch: string;
237
267
  }
268
+ /** Who the contract says this viewer is, or null before it loads. */
269
+ export declare function operatorRole(state: Pick<OperatorState, "contract">): OperatorRole | null;
270
+ /**
271
+ * The role pages and navigation render for. The contract's, or, while a
272
+ * refresh that failed has left no contract on screen, maintainer when the
273
+ * last good contract confirmed one and that maintainer's data is still here:
274
+ * their page stays mounted on its own retry state instead of being torn down
275
+ * mid-recovery. Never a request's permission; the store sends maintainer
276
+ * reads only while the contract itself says maintainer.
277
+ */
278
+ export declare function displayRole(state: Pick<OperatorState, "contract" | "role" | "data">): OperatorRole | null;
279
+ /**
280
+ * A confirmed maintainer's page is mounted without a contract behind it: a
281
+ * refresh failed, or its read ran past the deadline. The shell shows one
282
+ * recovery banner with Retry above whichever page is open, and that page is
283
+ * stale until a contract confirms the role again.
284
+ */
285
+ export declare function contractRecovering(state: Pick<OperatorState, "contract" | "role" | "data">): boolean;
286
+ /**
287
+ * Whether Activity is open to this viewer: maintainers only, on a deployment
288
+ * that records history. Members always read "forbidden".
289
+ */
290
+ export declare function activityReadable(state: Pick<OperatorState, "contract" | "data">): boolean;
291
+ /** The contract's half of `activityReadable`: a maintainer, on a deployment that records. */
292
+ export declare function activityAllowed(state: Pick<OperatorState, "contract">): boolean;
238
293
  export declare function initialState(page: OperatorPage): OperatorState;
239
294
  /**
240
295
  * Drop to the gate and forget the previous operator. Bumping the generation is
@@ -254,7 +309,7 @@ export declare function credentialUnavailableCopy(capability?: CredentialManagem
254
309
  */
255
310
  export declare function connectorStatusLabel(status: string, problem?: UiProblem): string;
256
311
  /** The tone a status carries wherever it is rendered as a badge or a tile. */
257
- export type Tone = "ok" | "warn" | "danger" | "neutral";
312
+ export type Tone = "ok" | "warn" | "danger" | "neutral" | "accent";
258
313
  export declare function connectorStatusTone(status: string): Tone;
259
314
  export declare function problemCopy(problem: UiProblem | undefined): string | null;
260
315
  /**
@@ -263,12 +318,31 @@ export declare function problemCopy(problem: UiProblem | undefined): string | nu
263
318
  * error. The two used to share the red box, so the row contradicted itself.
264
319
  */
265
320
  export declare function problemTone(problem: UiProblem): "warn" | "danger";
321
+ /** What the page knows about a connector beyond its live status: catalog drift and a failed detail read. */
322
+ export interface ConnectorSignals {
323
+ drift?: boolean;
324
+ failed?: boolean;
325
+ }
326
+ /** One connector's health everywhere it is shown: badge tone, status word, and the one action beside it. */
327
+ export interface ConnectorVerdict {
328
+ tone: "ok" | "warn" | "danger" | "neutral";
329
+ label: string;
330
+ /** The link to the Connection tab, or null when the connector is fine. */
331
+ action: "Connect" | "Replace key" | "Inspect" | "Review" | null;
332
+ }
266
333
  /**
267
- * What this identity may do with a connector, in one line. Being able to use it
268
- * is implied by seeing it at all, so the sentence covers authentication, which
269
- * is the part that differs between operators.
334
+ * The verdict the Connectors list, the Overview, Endpoints and a connector's
335
+ * page all show. The server's problem code comes first, then a failed detail
336
+ * read on this page, then a non-ok status, then catalog drift. A problem
337
+ * outranks the status, so a connector whose catalog failed is never
338
+ * "Connected". A failed read marks its connector `connector_unavailable` in
339
+ * the store, so that one code beside a failed read is the page's own and
340
+ * reads "Couldn't load" instead of blaming the downstream.
270
341
  */
271
- export declare function permissionLabel(connector: UiConnector): string;
342
+ export declare function connectorVerdict(live: {
343
+ status: string;
344
+ problem?: UiProblem;
345
+ }, signals?: ConnectorSignals): ConnectorVerdict;
272
346
  /**
273
347
  * Hosted-provider catalog drift, as an operator reads it
274
348
  * ([#343](https://github.com/zackbart/connecta/issues/343)).
@@ -314,6 +388,8 @@ export declare function activityOutcomeBadge(outcome: string): {
314
388
  label: string;
315
389
  tone: Tone;
316
390
  };
391
+ /** Why a call stalled or failed, in words; "" when nothing was recorded. */
392
+ export declare function activityReason(event: UiActivityEvent): string;
317
393
  /** The one-line detail under an address: where it ran, retries, and why it stalled. */
318
394
  export declare function activityDetail(event: UiActivityEvent): string;
319
395
  /**
@@ -321,12 +397,6 @@ export declare function activityDetail(event: UiActivityEvent): string;
321
397
  * beside it is not shown: it can carry a vault's own words.
322
398
  */
323
399
  export declare function credentialProblemCopy(problem: string | undefined): string;
324
- export declare function credentialStateLabel(credential: {
325
- configured: boolean;
326
- fields?: unknown[];
327
- lastFour?: string;
328
- updatedAt?: string;
329
- }): string;
330
400
  /** Gate copy for each browser-auth shape, so the sign-in state is never a blank page. */
331
401
  export declare function gateCopy(kind: string, signedIn: boolean): string;
332
402
  export {};