@econ-v1/rpc 6.7.0 → 6.11.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 (79) hide show
  1. package/dist/client-device-pending-contract.d.ts +14 -0
  2. package/dist/client-device-pending-contract.d.ts.map +1 -0
  3. package/dist/client.d.ts +15 -13
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/device-boot-contract.d.ts +7 -0
  6. package/dist/device-boot-contract.d.ts.map +1 -0
  7. package/dist/device-cleanup-contract.d.ts +17 -0
  8. package/dist/device-cleanup-contract.d.ts.map +1 -0
  9. package/dist/dialog-contract.d.ts +26 -0
  10. package/dist/dialog-contract.d.ts.map +1 -0
  11. package/dist/errors.d.ts +33 -6
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/index.d.ts +144 -14
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/message-port-client.d.ts +134 -16
  16. package/dist/message-port-client.d.ts.map +1 -1
  17. package/dist/node-app-memory-contract.d.ts +10 -0
  18. package/dist/node-app-memory-contract.d.ts.map +1 -0
  19. package/dist/onboarding/discovery-contract.d.ts +61 -34
  20. package/dist/onboarding/discovery-contract.d.ts.map +1 -1
  21. package/dist/onboarding/onboarding-contract.d.ts +119 -27
  22. package/dist/onboarding/onboarding-contract.d.ts.map +1 -1
  23. package/dist/onboarding/owner-claim-contract.d.ts +50 -0
  24. package/dist/onboarding/owner-claim-contract.d.ts.map +1 -0
  25. package/dist/pairing-status-contract.d.ts +40 -0
  26. package/dist/pairing-status-contract.d.ts.map +1 -0
  27. package/dist/protocol.d.ts +50 -12
  28. package/dist/protocol.d.ts.map +1 -1
  29. package/dist/stage-context.d.ts +31 -16
  30. package/dist/stage-context.d.ts.map +1 -1
  31. package/dist/sync-cursors-contract.d.ts +6 -0
  32. package/dist/sync-cursors-contract.d.ts.map +1 -0
  33. package/dist/system-notice-contract.d.ts +70 -0
  34. package/dist/system-notice-contract.d.ts.map +1 -0
  35. package/dist/transport-contract.d.ts +11 -0
  36. package/dist/transport-contract.d.ts.map +1 -0
  37. package/dist/transport.d.ts +2 -2
  38. package/dist/transport.d.ts.map +1 -1
  39. package/dist/trust-contract.d.ts +23 -0
  40. package/dist/trust-contract.d.ts.map +1 -0
  41. package/package.json +16 -12
  42. package/src/client-device-pending-contract.js +6 -0
  43. package/src/client.js +26 -0
  44. package/src/device-boot-contract.js +4 -0
  45. package/src/device-cleanup-contract.js +9 -0
  46. package/src/dialog-contract.js +6 -0
  47. package/src/errors.js +111 -0
  48. package/src/index.js +194 -0
  49. package/src/message-port-client.js +628 -0
  50. package/src/node-app-memory-contract.js +10 -0
  51. package/src/onboarding/discovery-contract.js +83 -0
  52. package/src/onboarding/onboarding-contract.js +106 -0
  53. package/src/onboarding/owner-claim-contract.js +59 -0
  54. package/src/pairing-status-contract.js +36 -0
  55. package/src/protocol.js +758 -0
  56. package/src/stage-context.js +27 -0
  57. package/src/sync-cursors-contract.js +3 -0
  58. package/src/system-notice-contract.js +64 -0
  59. package/src/transport-contract.js +11 -0
  60. package/src/transport.js +6 -0
  61. package/src/trust-contract.js +36 -0
  62. package/dist/client.js +0 -2
  63. package/dist/client.js.map +0 -1
  64. package/dist/errors.js +0 -57
  65. package/dist/errors.js.map +0 -1
  66. package/dist/index.js +0 -7
  67. package/dist/index.js.map +0 -1
  68. package/dist/message-port-client.js +0 -365
  69. package/dist/message-port-client.js.map +0 -1
  70. package/dist/onboarding/discovery-contract.js +0 -42
  71. package/dist/onboarding/discovery-contract.js.map +0 -1
  72. package/dist/onboarding/onboarding-contract.js +0 -37
  73. package/dist/onboarding/onboarding-contract.js.map +0 -1
  74. package/dist/protocol.js +0 -295
  75. package/dist/protocol.js.map +0 -1
  76. package/dist/stage-context.js +0 -2
  77. package/dist/stage-context.js.map +0 -1
  78. package/dist/transport.js +0 -2
  79. package/dist/transport.js.map +0 -1
@@ -0,0 +1,83 @@
1
+ // @ts-check
2
+
3
+ import { isJsonObject } from "../errors.js";
4
+
5
+ /**
6
+ * The #1815 interim "search the node on the LAN" seam. Client-only vertical slice: the
7
+ * discovery screen probes explicit candidate addresses (manually entered today, a signed
8
+ * bootstrap descriptor's `lan_candidates` in the Phase-2 follow-on — see
9
+ * `docs/superpowers/plans/2026-07-25-client-node-lan-discovery.md`) and the kernel reports
10
+ * live per-candidate progress. Mirrors `./onboarding-contract.js`'s idiom: locked wire-name
11
+ * constants + JSON-safe view/payload types + a runtime validator per command payload.
12
+ *
13
+ * `DISCOVERY_QUERY` is both a one-shot `query()` and a `liveQuery()` name (the kernel's
14
+ * `LiveQueryDefinition` row type is `DiscoveryCandidateView`, keyed by `address`) — the same
15
+ * dual usage every other query in `@econ-v1/kernel`'s `LiveQueryEngine` supports.
16
+ */
17
+
18
+ export const DISCOVERY_QUERY = "onboarding.discovery.v1";
19
+ export const DISCOVERY_PROBE_COMMAND = "onboarding.discovery.probe.v1";
20
+ export const DISCOVERY_PROMOTE_COMMAND = "onboarding.discovery.promote.v1";
21
+ export const DISCOVER_NODE_COMMAND = "onboarding.discovery.discover-node.v1";
22
+
23
+ /** @typedef {"probing" | "reachable" | "verified" | "unreachable"} DiscoveryCandidateStatus */
24
+
25
+ /**
26
+ * The verified node's non-secret projection: nothing here is (or derives) a routable
27
+ * address, macaroon, or key — only what the found-node card renders.
28
+ *
29
+ * @typedef {{
30
+ * readonly displayName: string,
31
+ * readonly maskedNodeId: string
32
+ * }} DiscoveryFoundNode
33
+ */
34
+
35
+ /**
36
+ * @typedef {{
37
+ * readonly address: string,
38
+ * readonly node?: DiscoveryFoundNode,
39
+ * readonly status: DiscoveryCandidateStatus
40
+ * }} DiscoveryCandidateView
41
+ */
42
+
43
+ /** @typedef {{ readonly addresses: readonly string[] }} ProbeCandidatesPayload */
44
+ /** @typedef {{ readonly address: string }} PromoteCandidatePayload */
45
+ /** @typedef {{ readonly address: string }} DiscoverNodePayload */
46
+ /** @typedef {{ readonly address: string, readonly id: string, readonly path: "lan-direct" }} NodeDiscoveryCandidate */
47
+ /**
48
+ * @typedef {
49
+ * | { readonly status: "verified", readonly candidate: NodeDiscoveryCandidate }
50
+ * | { readonly status: "reached-but-invalid", readonly reason: string }
51
+ * | { readonly status: "unreachable-or-untrusted" }
52
+ * } NodeDiscoveryResult
53
+ */
54
+
55
+ /**
56
+ * @param {unknown} value
57
+ * @returns {value is ProbeCandidatesPayload}
58
+ */
59
+ export function isProbeCandidatesPayload(value) {
60
+ if (!isJsonObject(value)) return false;
61
+ const { addresses } = value;
62
+ return Array.isArray(addresses)
63
+ && addresses.length > 0
64
+ && addresses.every((item) => typeof item === "string" && item.trim().length > 0);
65
+ }
66
+
67
+ /**
68
+ * @param {unknown} value
69
+ * @returns {value is PromoteCandidatePayload}
70
+ */
71
+ export function isPromoteCandidatePayload(value) {
72
+ if (!isJsonObject(value)) return false;
73
+ return typeof value.address === "string" && value.address.trim().length > 0;
74
+ }
75
+
76
+ /**
77
+ * @param {unknown} value
78
+ * @returns {value is DiscoverNodePayload}
79
+ */
80
+ export function isDiscoverNodePayload(value) {
81
+ if (!isJsonObject(value)) return false;
82
+ return typeof value.address === "string" && value.address.trim().length > 0;
83
+ }
@@ -0,0 +1,106 @@
1
+ // @ts-check
2
+ /** @typedef {"new" | "pair" | "wifi" | "expired" | "lost"} OnboardingScenario */
3
+ /** @typedef {"scan" | "invite" | "identity" | "matchcode" | "handoff" | "done"} OnboardingScreen */
4
+ /** @typedef {"connect" | "install" | "secure" | "identify" | "ready"} OnboardingPhase */
5
+ /** @typedef {| "bootstrap-required" | "needs-onboarding" | "needs-pairing" | "ready"} OnboardingLifecycleState */
6
+ /** @typedef {{ readonly invitationsEnabled?: boolean; readonly scenario?: OnboardingScenario; readonly state: OnboardingLifecycleState; }} OnboardingLifecycleStatus */
7
+ /** @typedef {{ readonly id: string; readonly maskedNodeId: string; readonly name: string; readonly ownership: "checking" | "owned" | "unowned"; }} OnboardingDiscoveryCandidate */
8
+ /** @typedef {| "waiting" | "approved" | "rejected" | "expired" | "cancelled"} OnboardingApprovalState */
9
+ /** @typedef {{ readonly approvalExpiresAt?: string; readonly approvalState?: OnboardingApprovalState; readonly candidates?: readonly OnboardingDiscoveryCandidate[]; readonly matchcodeDigits?: readonly string[]; readonly phase: OnboardingPhase; readonly reasonCode?: string; readonly scenario: OnboardingScenario; readonly screen: OnboardingScreen; }} OnboardingView */
10
+ /** @typedef {OnboardingView} OnboardingEvent */
11
+ /** @typedef {(typeof ONBOARDING_STEP_IDS)[number]} OnboardingStepId */
12
+ /** @typedef {| "onboard_qr" | "onboard_open_browser" | "onboard_wifi_setup" | "wifi_setup_connecting" | "onboard_home_qr" | "onboard_wifi_joined" | "onboard_working" | "onboard_code" | "onboard_trusted" | "onboard_rejected"} OnboardingLcdScreenId */
13
+ /** @typedef {"open" | "wep" | "wpa2" | "wpa3"} OnboardingWifiSecurity */
14
+ /**
15
+ * Onboarding access-code rail (2026-08-10 design): a code-active node refused this device's
16
+ * claim outright — either the code is already bound to a different device
17
+ * (`code-already-bound`) or the supplied/missing code did not match (`access-code-invalid`,
18
+ * covering both `ACCESS_CODE_INVALID` and `ACCESS_CODE_REQUIRED`).
19
+ * @typedef {| "approval-expired" | "approval-rejected" | "wifi-credentials-rejected" | "wifi-scan-failed" | "wifi-unreachable" | "invitation-invalid" | "claim-failed" | "code-already-bound" | "access-code-invalid"} OnboardingReasonCode
20
+ */
21
+ /** @typedef {{ readonly security: OnboardingWifiSecurity; readonly signal: number; readonly ssid: string; }} OnboardingNetwork */
22
+ /** @typedef {{ readonly lcdScreenId: OnboardingLcdScreenId; readonly phase: OnboardingPhase; readonly reasonCode?: OnboardingReasonCode; readonly stepIndex: number; readonly totalSteps: number; }} OnboardingFlowBase */
23
+ /** @typedef {| (OnboardingFlowBase & { readonly networks: readonly OnboardingNetwork[]; readonly scanning: boolean; readonly stepId: "choose-network"; }) | (OnboardingFlowBase & { readonly security: OnboardingWifiSecurity; readonly ssid: string; readonly stepId: "network-password"; }) | (OnboardingFlowBase & { readonly approvalState: OnboardingApprovalState; readonly digits: readonly string[]; readonly remainingSeconds: number; readonly stepId: "match-code"; }) | (OnboardingFlowBase & { readonly stepId: "invitation" }) | (OnboardingFlowBase & { readonly displayName: string; readonly displayNameLocked: boolean; readonly stepId: "identity"; readonly submitting: boolean; }) | (OnboardingFlowBase & { readonly sessionEstablished: boolean; readonly stepId: "trusted"; }) | (OnboardingFlowBase & { readonly ssid: string; readonly stepId: "handoff"; }) | (OnboardingFlowBase & { readonly stepId: Exclude< OnboardingStepId, | "choose-network" | "handoff" | "identity" | "invitation" | "match-code" | "network-password" | "trusted" >; })} OnboardingFlowView */
24
+ /** @typedef {{ readonly children?: readonly OnboardingLcdScreenTree[]; readonly id: string; readonly type: string; readonly [extra: string]: unknown; }} OnboardingLcdScreenTree */
25
+ /** @typedef {Readonly<Record<string, number | string>>} OnboardingLcdScreenState */
26
+ /** @typedef {"not-registered" | "unreachable"} OnboardingLcdUnavailableReason */
27
+ /** @typedef {{ readonly awaitingNode: boolean; readonly physicalTapOnly: boolean; readonly screenId: OnboardingLcdScreenId; readonly state: OnboardingLcdScreenState; }} OnboardingLcdScreenBase */
28
+ /** @typedef {| (OnboardingLcdScreenBase & { readonly kind: "authored" }) | (OnboardingLcdScreenBase & { readonly kind: "tree"; readonly tree: OnboardingLcdScreenTree }) | (OnboardingLcdScreenBase & { readonly kind: "unavailable"; readonly reason: OnboardingLcdUnavailableReason; })} OnboardingLcdScreenView */
29
+ /** @typedef {| { readonly kind: "advance" } | { readonly kind: "select-network"; readonly security: OnboardingWifiSecurity; readonly ssid: string; } | { readonly kind: "rescan-networks" } | { readonly kind: "reselect-network" } | { readonly kind: "submit-network-password"; readonly secret: string } | { readonly decision: "continue-in-browser" | "installed"; readonly kind: "install-decision" } | { readonly componentId: string; readonly kind: "lcd-tap" } | { readonly kind: "advance-discovery" } | { readonly code: string; readonly kind: "redeem-invitation" } | { readonly displayName: string; readonly kind: "submit-identity"; readonly username: string; } | { readonly kind: "submit-wifi"; readonly secret: string; readonly ssid: string } | { readonly kind: "retry-approval" } | { readonly kind: "reconcile-completion" } | { readonly kind: "choose-path"; readonly scenario: OnboardingScenario } | { readonly kind: "cancel" }} OnboardingCommand */
30
+ import { isJsonObject } from "../errors.js";
31
+ export const ONBOARDING_STATUS_QUERY = /** @type {const} */ ("onboarding.status.v1");
32
+ export const ONBOARDING_COMMAND = /** @type {const} */ ("onboarding.command.v1");
33
+ export const ONBOARDING_EVENTS = /** @type {const} */ ("onboarding.events.v1");
34
+ export const ONBOARDING_FLOW_QUERY = /** @type {const} */ ("onboarding.flow.v1");
35
+ export const ONBOARDING_LCD_SCREEN_QUERY = /** @type {const} */ ("onboarding.lcd-screen.v1");
36
+ /**
37
+ * The pager's declarative order. `stepIndex`/`totalSteps` are DERIVED from this list with
38
+ * gated rows removed (`invitation` today), so invitations-off yields ten steps and shifts
39
+ * every later index with no second table and no hole — the total can never be a constant.
40
+ *
41
+ * `begin-setup` ("Connect") is absent by design: its discovery scan ran only after the shell
42
+ * had ALREADY reached the node over HTTP, so on the shipped path it discovered nothing and was
43
+ * auto-advanced. Its screen still exists off-rail for the unreachable-node bootstrap case —
44
+ * see `OnboardingFlowService`'s `STEP_TABLE` comment.
45
+ */
46
+ export const ONBOARDING_STEP_IDS = /** @type {const} */ ([
47
+ "join-qr",
48
+ "open-browser",
49
+ "choose-network",
50
+ "network-password",
51
+ "handoff",
52
+ "way-back",
53
+ "install",
54
+ "match-code",
55
+ "invitation",
56
+ "identity",
57
+ "trusted",
58
+ ]);
59
+ /** @param {unknown} value @returns {value is OnboardingCommand} */
60
+ export function isOnboardingCommand(value) {
61
+ if (!isJsonObject(value) || typeof value.kind !== "string")
62
+ return false;
63
+ switch (value.kind) {
64
+ case "redeem-invitation":
65
+ return typeof value.code === "string";
66
+ case "advance":
67
+ case "advance-discovery":
68
+ case "rescan-networks":
69
+ case "reselect-network":
70
+ case "retry-approval":
71
+ case "reconcile-completion":
72
+ case "cancel":
73
+ return true;
74
+ case "lcd-tap":
75
+ return typeof value.componentId === "string" && value.componentId.length > 0;
76
+ case "select-network":
77
+ return typeof value.ssid === "string"
78
+ && value.ssid.length > 0
79
+ && isOnboardingWifiSecurity(value.security);
80
+ case "submit-network-password":
81
+ return typeof value.secret === "string";
82
+ case "install-decision":
83
+ return value.decision === "installed" || value.decision === "continue-in-browser";
84
+ case "submit-identity":
85
+ return typeof value.username === "string"
86
+ && typeof value.displayName === "string";
87
+ case "submit-wifi":
88
+ return typeof value.ssid === "string" && typeof value.secret === "string";
89
+ case "choose-path":
90
+ return isOnboardingScenario(value.scenario);
91
+ default:
92
+ return false;
93
+ }
94
+ }
95
+ /** @param {unknown} value @returns {value is OnboardingWifiSecurity} */
96
+ function isOnboardingWifiSecurity(value) {
97
+ return value === "open" || value === "wep" || value === "wpa2" || value === "wpa3";
98
+ }
99
+ /** @param {unknown} value @returns {value is OnboardingScenario} */
100
+ function isOnboardingScenario(value) {
101
+ return value === "new"
102
+ || value === "pair"
103
+ || value === "wifi"
104
+ || value === "expired"
105
+ || value === "lost";
106
+ }
@@ -0,0 +1,59 @@
1
+ // @ts-check
2
+
3
+ import { isJsonObject } from "../errors.js";
4
+ /**
5
+ * First-owner "claim this unclaimed node" seam (#1815 DID-blocker fix,
6
+ * `.superpowers/sdd/did-blocker-report.md`). Deliberately separate from the generic
7
+ * (unwired) `ONBOARDING_COMMAND` in `./onboarding-contract.ts` — the same precedent as
8
+ * `DISCOVER_NODE_COMMAND` in `./discovery-contract.ts`, which is also "wired separately"
9
+ * rather than folded into a bigger, not-yet-built generic seam. `prepare` and `complete`
10
+ * are two commands (not one) because a person may spend anywhere from seconds to
11
+ * minutes physically walking to the node between them; `client/domain`'s
12
+ * `OwnerClaimService` holds the pending claim between the two calls and only unlocks it
13
+ * once the node itself has reported the physical confirmation.
14
+ * @type {"onboarding.owner-claim.prepare.v1"}
15
+ */
16
+ export const OWNER_CLAIM_PREPARE_COMMAND = "onboarding.owner-claim.prepare.v1";
17
+ export const OWNER_CLAIM_COMPLETE_COMMAND = "onboarding.owner-claim.complete.v1";
18
+ /**
19
+ * @param {unknown} value
20
+ * @returns {value is OwnerClaimCompletePayload}
21
+ */
22
+ export function isOwnerClaimCompletePayload(value) {
23
+ if (!isJsonObject(value))
24
+ return false;
25
+ return typeof value.displayName === "string" && value.displayName.trim().length > 0
26
+ && typeof value.username === "string" && value.username.trim().length > 0;
27
+ }
28
+ /**
29
+ * `claimed-unpaired` is a SUCCESS: the owner + DID exist on the node, but the completion
30
+ * response carried no usable session tokens (or adopting them did not produce a session), so
31
+ * the shell must fall back to its ordinary device-pairing flow. Collapsing it into `claimed`
32
+ * would make the UI assert a trust relationship the browser does not actually hold.
33
+ * @typedef {| "claimed"
34
+ * | "claimed-unpaired"
35
+ * | "not-prepared"
36
+ * | "rejected"
37
+ * | "timed-out"} OwnerClaimCompleteOutcome
38
+ */
39
+ /**
40
+ * `prepare` carries NO payload (design §6.1): the node's `onboarding-prepare` route only ever
41
+ * received `{ public_key_multibase }`, and the identity that used to ride along was stashed
42
+ * purely client-side. Moving it to `complete` is what lets the match code appear at step 10
43
+ * while identity is not collected until step 12 — nothing is written until physical access
44
+ * has been proven.
45
+ * @typedef {Object} OwnerClaimPrepareResult
46
+ * @property {boolean} lcdHardware
47
+ * @property {string} matchCode
48
+ * @property {string} operationHash Empty string when the node's confirmation gate is disabled.
49
+ * @property {boolean} requiresConfirmation
50
+ */
51
+ /**
52
+ * @typedef {Object} OwnerClaimCompletePayload
53
+ * @property {string} displayName
54
+ * @property {string} username
55
+ */
56
+ /**
57
+ * @typedef {Object} OwnerClaimCompleteResult
58
+ * @property {OwnerClaimCompleteOutcome} outcome
59
+ */
@@ -0,0 +1,36 @@
1
+ // @ts-check
2
+ export const CLIENT_DEVICE_PAIRING_STATUS_QUERY = "client-device.pairing-status.v1";
3
+ /**
4
+ * Device-pairing wire-error codes shared verbatim across the Rust host and this TS client
5
+ * boundary (review recommendation, PR #2274). Canonical contract table:
6
+ * `docs/development/CAPABILITY_REGISTRY.md` § "Device-pairing wire-error contract" (mirrored
7
+ * on the Rust side by `core/domain/src/services/client_device_pairing_service.rs`'s
8
+ * `CLIENT_DEVICE_WIRE_ERROR_*` constants). These are the exact strings that travel over the
9
+ * wire — changing any value here is a cross-repo wire-contract change, not a local rename;
10
+ * every producer/consumer in that table must move together.
11
+ */
12
+ export const CLIENT_DEVICE_WIRE_ERROR_NAME_TAKEN = "name_taken";
13
+ /** @see CLIENT_DEVICE_WIRE_ERROR_NAME_TAKEN */
14
+ export const CLIENT_DEVICE_WIRE_ERROR_LAST_DEVICE = "last_device";
15
+ /** @see CLIENT_DEVICE_WIRE_ERROR_NAME_TAKEN */
16
+ export const CLIENT_DEVICE_WIRE_ERROR_LAST_DEVICE_PROTECTED = "last_device_protected";
17
+ /** @see CLIENT_DEVICE_WIRE_ERROR_NAME_TAKEN */
18
+ export const CLIENT_DEVICE_WIRE_ERROR_DEVICE_NOT_REGISTERED = "device_not_registered";
19
+ /**
20
+ * Kernel-owned view of the CURRENT pairing attempt this browser has in flight (design M4,
21
+ * `docs/superpowers/plans/2026-08-11-pwa-trusted-devices.md`). Single-slot — there is never
22
+ * more than one attempt live at a time — so the row's `id` is always the literal
23
+ * `"pairing-status"`, mirroring `client-device.cleanup.v1`'s single fixed-id row
24
+ * (`client/kernel/src/queries/device-cleanup.js`).
25
+ *
26
+ * `status` is a superset of what `<node-pairing-gate>` renders directly: `"idle"` (no attempt
27
+ * has begun, or the last one was reset), `"pairing"` (an attempt is in flight, matches the
28
+ * gate's own `"pairing"` display status), `"ready"` (the owner approved — or session
29
+ * establishment simply succeeded on its own), `"declined"`, `"revoked"`, `"expired"`, and
30
+ * `"name_conflict"` (the requested device name collided with another active/pending device —
31
+ * `pairing-status-service.js`'s `isNameTaken`). `"error"` is reserved for a future
32
+ * non-transient failure this service does not yet classify — `PairingStatusService` never
33
+ * emits it today; every unclassified failure keeps the row at `"pairing"` and lets the next
34
+ * poll attempt retry (see that file's doc comment).
35
+ * @typedef {{ readonly id: "pairing-status", readonly status: "idle"|"pairing"|"ready"|"declined"|"revoked"|"expired"|"name_conflict"|"error", readonly message?: string }} ClientDevicePairingStatusRow
36
+ */