@econ-v1/domain 6.7.0 → 6.10.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.
- package/dist/errors/index.d.ts +96 -25
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/events/index.d.ts +127 -20
- package/dist/events/index.d.ts.map +1 -1
- package/dist/index.d.ts +250 -47
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent.d.ts +25 -4
- package/dist/models/agent.d.ts.map +1 -1
- package/dist/models/app-data.d.ts +84 -0
- package/dist/models/app-data.d.ts.map +1 -0
- package/dist/models/conversation.d.ts +12 -2
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/delegation.d.ts +12 -3
- package/dist/models/delegation.d.ts.map +1 -1
- package/dist/models/device-boot-state.d.ts +12 -0
- package/dist/models/device-boot-state.d.ts.map +1 -0
- package/dist/models/device-identity.d.ts +11 -3
- package/dist/models/device-identity.d.ts.map +1 -1
- package/dist/models/graph.d.ts +26 -3
- package/dist/models/graph.d.ts.map +1 -1
- package/dist/models/index.d.ts +82 -14
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/json.d.ts +5 -5
- package/dist/models/json.d.ts.map +1 -1
- package/dist/models/l402.d.ts +4 -5
- package/dist/models/l402.d.ts.map +1 -1
- package/dist/models/live-query-definition.d.ts +15 -16
- package/dist/models/live-query-definition.d.ts.map +1 -1
- package/dist/models/node-app-memory.d.ts +107 -0
- package/dist/models/node-app-memory.d.ts.map +1 -0
- package/dist/models/outbox-intent.d.ts +3 -3
- package/dist/models/outbox-intent.d.ts.map +1 -1
- package/dist/models/outbox.d.ts +67 -15
- package/dist/models/outbox.d.ts.map +1 -1
- package/dist/models/owner-views.d.ts +28 -28
- package/dist/models/owner-views.d.ts.map +1 -1
- package/dist/models/payment.d.ts +28 -9
- package/dist/models/payment.d.ts.map +1 -1
- package/dist/models/query-diff.d.ts +2 -2
- package/dist/models/query-diff.d.ts.map +1 -1
- package/dist/models/release-manifest.d.ts +32 -47
- package/dist/models/release-manifest.d.ts.map +1 -1
- package/dist/models/repository-write.d.ts +3 -3
- package/dist/models/repository-write.d.ts.map +1 -1
- package/dist/models/revisioned-commands.d.ts +66 -13
- package/dist/models/revisioned-commands.d.ts.map +1 -1
- package/dist/models/revisioned.d.ts +50 -10
- package/dist/models/revisioned.d.ts.map +1 -1
- package/dist/models/settings.d.ts +2 -2
- package/dist/models/settings.d.ts.map +1 -1
- package/dist/models/sync.d.ts +18 -18
- package/dist/models/sync.d.ts.map +1 -1
- package/dist/models/wallet.d.ts +16 -16
- package/dist/models/wallet.d.ts.map +1 -1
- package/dist/queries/certificate-trust.d.ts +13 -0
- package/dist/queries/certificate-trust.d.ts.map +1 -0
- package/dist/queries/conversations.d.ts +38 -7
- package/dist/queries/conversations.d.ts.map +1 -1
- package/dist/queries/discovery.d.ts +10 -10
- package/dist/queries/discovery.d.ts.map +1 -1
- package/dist/queries/notifications.d.ts +24 -4
- package/dist/queries/notifications.d.ts.map +1 -1
- package/dist/queries/onboarding-flow.d.ts +25 -0
- package/dist/queries/onboarding-flow.d.ts.map +1 -0
- package/dist/queries/onboarding-lcd-screen.d.ts +56 -0
- package/dist/queries/onboarding-lcd-screen.d.ts.map +1 -0
- package/dist/queries/onboarding-status.d.ts +10 -10
- package/dist/queries/onboarding-status.d.ts.map +1 -1
- package/dist/queries/revisioned.d.ts +47 -13
- package/dist/queries/revisioned.d.ts.map +1 -1
- package/dist/queries/wallet.d.ts +40 -10
- package/dist/queries/wallet.d.ts.map +1 -1
- package/dist/services/abort-signal.d.ts +54 -0
- package/dist/services/abort-signal.d.ts.map +1 -0
- package/dist/services/certificate-trust-service.d.ts +42 -0
- package/dist/services/certificate-trust-service.d.ts.map +1 -0
- package/dist/services/conversation-service.d.ts +103 -15
- package/dist/services/conversation-service.d.ts.map +1 -1
- package/dist/services/conversation-sync.d.ts +9 -5
- package/dist/services/conversation-sync.d.ts.map +1 -1
- package/dist/services/device-boot-state-service.d.ts +17 -0
- package/dist/services/device-boot-state-service.d.ts.map +1 -0
- package/dist/services/device-cleanup-service.d.ts +47 -0
- package/dist/services/device-cleanup-service.d.ts.map +1 -0
- package/dist/services/discovery-probe-service.d.ts +37 -46
- package/dist/services/discovery-probe-service.d.ts.map +1 -1
- package/dist/services/gossip-service.d.ts +28 -13
- package/dist/services/gossip-service.d.ts.map +1 -1
- package/dist/services/invite-redemption-service.d.ts +43 -0
- package/dist/services/invite-redemption-service.d.ts.map +1 -0
- package/dist/services/lan-candidate-refresh-service.d.ts +22 -7
- package/dist/services/lan-candidate-refresh-service.d.ts.map +1 -1
- package/dist/services/lcd-decision-service.d.ts +58 -0
- package/dist/services/lcd-decision-service.d.ts.map +1 -0
- package/dist/services/onboarding-checkpoint-store.d.ts +54 -0
- package/dist/services/onboarding-checkpoint-store.d.ts.map +1 -0
- package/dist/services/onboarding-flow-service.d.ts +180 -0
- package/dist/services/onboarding-flow-service.d.ts.map +1 -0
- package/dist/services/onboarding-status-service.d.ts +21 -38
- package/dist/services/onboarding-status-service.d.ts.map +1 -1
- package/dist/services/outbox-service.d.ts +105 -29
- package/dist/services/outbox-service.d.ts.map +1 -1
- package/dist/services/owner-claim-service.d.ts +187 -0
- package/dist/services/owner-claim-service.d.ts.map +1 -0
- package/dist/services/owner-view-refresh-service.d.ts +40 -0
- package/dist/services/owner-view-refresh-service.d.ts.map +1 -0
- package/dist/services/owner-view-service.d.ts +49 -4
- package/dist/services/owner-view-service.d.ts.map +1 -1
- package/dist/services/peer-directory-service.d.ts +53 -17
- package/dist/services/peer-directory-service.d.ts.map +1 -1
- package/dist/services/release-manifest-verifier.d.ts +18 -20
- package/dist/services/release-manifest-verifier.d.ts.map +1 -1
- package/dist/services/revisioned-domain-service.d.ts +50 -17
- package/dist/services/revisioned-domain-service.d.ts.map +1 -1
- package/dist/services/runtime-globals.d.ts +39 -0
- package/dist/services/runtime-globals.d.ts.map +1 -0
- package/dist/services/session-service.d.ts +23 -3
- package/dist/services/session-service.d.ts.map +1 -1
- package/dist/services/sync/authoritative-mirror.d.ts +88 -15
- package/dist/services/sync/authoritative-mirror.d.ts.map +1 -1
- package/dist/services/sync/index.d.ts +17 -5
- package/dist/services/sync/index.d.ts.map +1 -1
- package/dist/services/sync/log-sync.d.ts +42 -8
- package/dist/services/sync/log-sync.d.ts.map +1 -1
- package/dist/services/sync/revision-merge.d.ts +80 -13
- package/dist/services/sync/revision-merge.d.ts.map +1 -1
- package/dist/services/sync/strategy.d.ts +11 -1
- package/dist/services/sync/strategy.d.ts.map +1 -1
- package/dist/services/transport-manager.d.ts +84 -29
- package/dist/services/transport-manager.d.ts.map +1 -1
- package/dist/services/wallet-mirror-service.d.ts +81 -18
- package/dist/services/wallet-mirror-service.d.ts.map +1 -1
- package/dist/services/wifi-provisioning-service.d.ts +146 -0
- package/dist/services/wifi-provisioning-service.d.ts.map +1 -0
- package/package.json +18 -12
- package/src/errors/index.js +126 -0
- package/src/events/index.js +124 -0
- package/src/index.js +566 -0
- package/src/models/agent.js +43 -0
- package/src/models/app-data.js +122 -0
- package/src/models/conversation.js +36 -0
- package/{dist → src}/models/delegation.js +18 -1
- package/src/models/device-boot-state.js +3 -0
- package/{dist → src}/models/device-identity.js +18 -2
- package/{dist → src}/models/graph.js +30 -3
- package/src/models/index.js +175 -0
- package/src/models/json.js +18 -0
- package/src/models/l402.js +9 -0
- package/src/models/live-query-definition.js +30 -0
- package/src/models/node-app-memory.js +130 -0
- package/src/models/outbox-intent.js +12 -0
- package/{dist → src}/models/outbox.js +102 -3
- package/src/models/owner-views.js +59 -0
- package/src/models/payment.js +24 -0
- package/src/models/query-diff.js +7 -0
- package/src/models/release-manifest.js +160 -0
- package/src/models/repository-write.js +10 -0
- package/src/models/revisioned-commands.js +92 -0
- package/src/models/revisioned.js +41 -0
- package/src/models/settings.js +16 -0
- package/src/models/sync.js +44 -0
- package/src/models/wallet.js +40 -0
- package/src/queries/certificate-trust.js +31 -0
- package/{dist → src}/queries/conversations.js +54 -3
- package/src/queries/discovery.js +34 -0
- package/{dist → src}/queries/notifications.js +25 -2
- package/src/queries/onboarding-flow.js +48 -0
- package/src/queries/onboarding-lcd-screen.js +100 -0
- package/src/queries/onboarding-status.js +34 -0
- package/src/queries/revisioned.js +128 -0
- package/{dist → src}/queries/wallet.js +35 -1
- package/src/services/abort-signal.js +69 -0
- package/src/services/certificate-trust-service.js +119 -0
- package/{dist → src}/services/conversation-service.js +108 -1
- package/src/services/conversation-sync.js +28 -0
- package/src/services/device-boot-state-service.js +45 -0
- package/src/services/device-cleanup-service.js +112 -0
- package/src/services/discovery-probe-service.js +196 -0
- package/{dist → src}/services/gossip-service.js +54 -4
- package/src/services/invite-redemption-service.js +64 -0
- package/src/services/lan-candidate-refresh-service.js +164 -0
- package/src/services/lcd-decision-service.js +93 -0
- package/src/services/onboarding-checkpoint-store.js +155 -0
- package/src/services/onboarding-flow-service.js +1096 -0
- package/src/services/onboarding-status-service.js +64 -0
- package/{dist → src}/services/outbox-service.js +132 -1
- package/src/services/owner-claim-service.js +347 -0
- package/src/services/owner-view-refresh-service.js +208 -0
- package/src/services/owner-view-service.js +87 -0
- package/{dist → src}/services/peer-directory-service.js +72 -3
- package/src/services/release-manifest-verifier.js +108 -0
- package/{dist → src}/services/revisioned-domain-service.js +189 -9
- package/src/services/runtime-globals.js +174 -0
- package/src/services/session-service.js +50 -0
- package/{dist → src}/services/sync/authoritative-mirror.js +90 -1
- package/src/services/sync/index.js +67 -0
- package/src/services/sync/log-sync.js +60 -0
- package/src/services/sync/revision-merge.js +119 -0
- package/src/services/sync/strategy.js +44 -0
- package/src/services/transport-manager.js +478 -0
- package/{dist → src}/services/wallet-mirror-service.js +226 -6
- package/src/services/wifi-provisioning-service.js +278 -0
- package/dist/errors/index.js +0 -45
- package/dist/errors/index.js.map +0 -1
- package/dist/events/index.js +0 -17
- package/dist/events/index.js.map +0 -1
- package/dist/index.js +0 -31
- package/dist/index.js.map +0 -1
- package/dist/models/agent.js +0 -17
- package/dist/models/agent.js.map +0 -1
- package/dist/models/conversation.js +0 -2
- package/dist/models/conversation.js.map +0 -1
- package/dist/models/delegation.js.map +0 -1
- package/dist/models/device-identity.js.map +0 -1
- package/dist/models/graph.js.map +0 -1
- package/dist/models/index.js +0 -8
- package/dist/models/index.js.map +0 -1
- package/dist/models/json.js +0 -2
- package/dist/models/json.js.map +0 -1
- package/dist/models/l402.js +0 -2
- package/dist/models/l402.js.map +0 -1
- package/dist/models/live-query-definition.js +0 -2
- package/dist/models/live-query-definition.js.map +0 -1
- package/dist/models/outbox-intent.js +0 -2
- package/dist/models/outbox-intent.js.map +0 -1
- package/dist/models/outbox.js.map +0 -1
- package/dist/models/owner-views.js +0 -7
- package/dist/models/owner-views.js.map +0 -1
- package/dist/models/payment.js +0 -3
- package/dist/models/payment.js.map +0 -1
- package/dist/models/query-diff.js +0 -2
- package/dist/models/query-diff.js.map +0 -1
- package/dist/models/release-manifest.js +0 -147
- package/dist/models/release-manifest.js.map +0 -1
- package/dist/models/repository-write.js +0 -2
- package/dist/models/repository-write.js.map +0 -1
- package/dist/models/revisioned-commands.js +0 -35
- package/dist/models/revisioned-commands.js.map +0 -1
- package/dist/models/revisioned.js +0 -2
- package/dist/models/revisioned.js.map +0 -1
- package/dist/models/settings.js +0 -2
- package/dist/models/settings.js.map +0 -1
- package/dist/models/sync.js +0 -2
- package/dist/models/sync.js.map +0 -1
- package/dist/models/wallet.js +0 -2
- package/dist/models/wallet.js.map +0 -1
- package/dist/queries/conversations.js.map +0 -1
- package/dist/queries/discovery.js +0 -27
- package/dist/queries/discovery.js.map +0 -1
- package/dist/queries/notifications.js.map +0 -1
- package/dist/queries/onboarding-status.js +0 -27
- package/dist/queries/onboarding-status.js.map +0 -1
- package/dist/queries/revisioned.js +0 -58
- package/dist/queries/revisioned.js.map +0 -1
- package/dist/queries/wallet.js.map +0 -1
- package/dist/services/conversation-service.js.map +0 -1
- package/dist/services/conversation-sync.js +0 -9
- package/dist/services/conversation-sync.js.map +0 -1
- package/dist/services/discovery-probe-service.js +0 -129
- package/dist/services/discovery-probe-service.js.map +0 -1
- package/dist/services/gossip-service.js.map +0 -1
- package/dist/services/lan-candidate-refresh-service.js +0 -79
- package/dist/services/lan-candidate-refresh-service.js.map +0 -1
- package/dist/services/onboarding-status-service.js +0 -32
- package/dist/services/onboarding-status-service.js.map +0 -1
- package/dist/services/outbox-service.js.map +0 -1
- package/dist/services/owner-view-service.js +0 -21
- package/dist/services/owner-view-service.js.map +0 -1
- package/dist/services/peer-directory-service.js.map +0 -1
- package/dist/services/release-manifest-verifier.js +0 -84
- package/dist/services/release-manifest-verifier.js.map +0 -1
- package/dist/services/revisioned-domain-service.js.map +0 -1
- package/dist/services/session-service.js +0 -20
- package/dist/services/session-service.js.map +0 -1
- package/dist/services/sync/authoritative-mirror.js.map +0 -1
- package/dist/services/sync/index.js +0 -4
- package/dist/services/sync/index.js.map +0 -1
- package/dist/services/sync/log-sync.js +0 -20
- package/dist/services/sync/log-sync.js.map +0 -1
- package/dist/services/sync/revision-merge.js +0 -46
- package/dist/services/sync/revision-merge.js.map +0 -1
- package/dist/services/sync/strategy.js +0 -2
- package/dist/services/sync/strategy.js.map +0 -1
- package/dist/services/transport-manager.js +0 -234
- package/dist/services/transport-manager.js.map +0 -1
- package/dist/services/wallet-mirror-service.js.map +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("@econ-v1/ports").HomeNodeCandidate} HomeNodeCandidate */
|
|
4
|
+
/** @typedef {import("@econ-v1/ports").HomeNodeClient} HomeNodeClient */
|
|
5
|
+
/** @typedef {import("@econ-v1/ports").HomeNodeDialer} HomeNodeDialer */
|
|
6
|
+
/** @typedef {import("./lan-candidate-refresh-service.js").LanCandidateRefreshService} LanCandidateRefreshService */
|
|
7
|
+
|
|
8
|
+
import { DomainError } from "../errors/index.js";
|
|
9
|
+
import { createRuntimeAbortController } from "./runtime-globals.js";
|
|
10
|
+
|
|
11
|
+
/** @typedef {"probing" | "reachable" | "verified" | "unreachable"} DiscoveryCandidateStatus */
|
|
12
|
+
/** @typedef {{ readonly displayName: string, readonly maskedNodeId: string }} DiscoveryFoundNode */
|
|
13
|
+
/** @typedef {{ readonly address: string, readonly node?: DiscoveryFoundNode, readonly status: DiscoveryCandidateStatus }} DiscoveryCandidateRow */
|
|
14
|
+
/** @typedef {{ readonly friendly_name?: unknown, readonly node_id?: unknown }} DiscoveryNodeIdentityWire */
|
|
15
|
+
/** @typedef {{ readonly dialer: HomeNodeDialer, readonly onChange?: (rows: readonly DiscoveryCandidateRow[]) => void, readonly refresh: Pick<LanCandidateRefreshService, "refresh"> }} DiscoveryProbeServiceOptions */
|
|
16
|
+
|
|
17
|
+
export class DiscoveryProbeService {
|
|
18
|
+
/** @readonly @type {HomeNodeDialer} */
|
|
19
|
+
#dialer;
|
|
20
|
+
/** @readonly @type {(rows: readonly DiscoveryCandidateRow[]) => void} */
|
|
21
|
+
#onChange;
|
|
22
|
+
/** @readonly @type {Pick<LanCandidateRefreshService, "refresh">} */
|
|
23
|
+
#refresh;
|
|
24
|
+
/** @type {Map<string, DiscoveryCandidateRow>} */
|
|
25
|
+
#rows = new Map();
|
|
26
|
+
#generation = 0;
|
|
27
|
+
|
|
28
|
+
/** @param {DiscoveryProbeServiceOptions} options */
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.#dialer = options.dialer;
|
|
31
|
+
this.#onChange = options.onChange ?? (() => undefined);
|
|
32
|
+
this.#refresh = options.refresh;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @returns {readonly DiscoveryCandidateRow[]} */
|
|
36
|
+
snapshot() {
|
|
37
|
+
return [...this.#rows.values()];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {readonly string[]} addresses
|
|
42
|
+
* @returns {Promise<readonly DiscoveryCandidateRow[]>}
|
|
43
|
+
*/
|
|
44
|
+
async probe(addresses) {
|
|
45
|
+
const generation = (this.#generation += 1);
|
|
46
|
+
const unique = dedupeText(addresses);
|
|
47
|
+
this.#rows = new Map(unique.map((address) => [address, { address, status: "probing" }]));
|
|
48
|
+
this.#emit();
|
|
49
|
+
|
|
50
|
+
await Promise.all(unique.map((address) => this.#probeOne(generation, address)));
|
|
51
|
+
return this.snapshot();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} address
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
async promote(address) {
|
|
59
|
+
const row = this.#rows.get(address);
|
|
60
|
+
if (row === undefined || row.status !== "verified") {
|
|
61
|
+
throw new DomainError({
|
|
62
|
+
code: "invalid_request",
|
|
63
|
+
message: "Only a verified discovery candidate may be promoted as the home-node route",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const client = await this.#dial(address);
|
|
67
|
+
await this.#refresh.refresh(client);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {number} generation
|
|
72
|
+
* @param {string} rawAddress
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async #probeOne(generation, rawAddress) {
|
|
76
|
+
/** @type {HomeNodeClient} */
|
|
77
|
+
let client;
|
|
78
|
+
try {
|
|
79
|
+
client = await this.#dial(rawAddress);
|
|
80
|
+
} catch {
|
|
81
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "unreachable" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "reachable" });
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
/** @type {unknown} */
|
|
89
|
+
const metadata = await client.invoke({
|
|
90
|
+
operation: "GET /api/v2/node/metadata",
|
|
91
|
+
payload: {},
|
|
92
|
+
});
|
|
93
|
+
if (!isRecord(metadata)) {
|
|
94
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "unreachable" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const nodeId = textOrUndefined(metadata.node_id);
|
|
98
|
+
if (nodeId === undefined) {
|
|
99
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "unreachable" });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const displayName = textOrUndefined(metadata.friendly_name) ?? "Home Node";
|
|
103
|
+
this.#setRow(generation, rawAddress, {
|
|
104
|
+
address: rawAddress,
|
|
105
|
+
node: { displayName, maskedNodeId: maskNodeId(nodeId) },
|
|
106
|
+
status: "verified",
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "unreachable" });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} rawAddress
|
|
115
|
+
* @returns {Promise<HomeNodeClient>}
|
|
116
|
+
*/
|
|
117
|
+
async #dial(rawAddress) {
|
|
118
|
+
const origin = secureLanOrigin(rawAddress);
|
|
119
|
+
if (origin === undefined) {
|
|
120
|
+
throw new DomainError({ code: "invalid_request", message: "Discovery candidate address is invalid" });
|
|
121
|
+
}
|
|
122
|
+
/** @type {HomeNodeCandidate} */
|
|
123
|
+
const candidate = { address: origin, id: rawAddress, path: "lan-direct" };
|
|
124
|
+
const controller = createRuntimeAbortController();
|
|
125
|
+
return await this.#dialer.dial(candidate, controller.signal);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {number} generation
|
|
130
|
+
* @param {string} address
|
|
131
|
+
* @param {DiscoveryCandidateRow} row
|
|
132
|
+
* @returns {void}
|
|
133
|
+
*/
|
|
134
|
+
#setRow(generation, address, row) {
|
|
135
|
+
if (generation !== this.#generation) return;
|
|
136
|
+
if (!this.#rows.has(address)) return;
|
|
137
|
+
this.#rows.set(address, row);
|
|
138
|
+
this.#emit();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @returns {void} */
|
|
142
|
+
#emit() {
|
|
143
|
+
this.#onChange(this.snapshot());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {readonly string[]} values
|
|
149
|
+
* @returns {readonly string[]}
|
|
150
|
+
*/
|
|
151
|
+
function dedupeText(values) {
|
|
152
|
+
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {unknown} value
|
|
157
|
+
* @returns {string | undefined}
|
|
158
|
+
*/
|
|
159
|
+
function textOrUndefined(value) {
|
|
160
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {unknown} value
|
|
165
|
+
* @returns {value is Record<string, unknown>}
|
|
166
|
+
*/
|
|
167
|
+
function isRecord(value) {
|
|
168
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} nodeId
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
function maskNodeId(nodeId) {
|
|
176
|
+
if (nodeId.length <= 10) return nodeId;
|
|
177
|
+
return `${nodeId.slice(0, 6)}····${nodeId.slice(-4)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const LAN_ADDRESS_PATTERN = /^(?:https?:\/\/)?([^/?#:\s]+)(?::(\d{1,5}))?\/?$/u;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} address
|
|
184
|
+
* @returns {string | undefined}
|
|
185
|
+
*/
|
|
186
|
+
export function secureLanOrigin(address) {
|
|
187
|
+
const trimmed = address.trim();
|
|
188
|
+
if (trimmed.length === 0) return undefined;
|
|
189
|
+
const match = LAN_ADDRESS_PATTERN.exec(trimmed);
|
|
190
|
+
if (match === null) return undefined;
|
|
191
|
+
const host = match[1];
|
|
192
|
+
if (host === undefined || host.length === 0) return undefined;
|
|
193
|
+
const port = match[2] === undefined ? undefined : Number(match[2]);
|
|
194
|
+
if (port !== undefined && (port < 1 || port > 65535)) return undefined;
|
|
195
|
+
return port === undefined ? `https://${host}` : `https://${host}:${port}`;
|
|
196
|
+
}
|
|
@@ -1,17 +1,57 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("@econ-v1/ports").PeerTransportPort} PeerTransportPort
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import("./abort-signal.js").AbortSignalPort} AbortSignalPort
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import("./peer-directory-service.js").PeerAdvertisement} PeerAdvertisement
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {PeerAdvertisement & { readonly version: typeof GOSSIP_VERSION; }} GossipAdvertisement
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{ readonly directory: PeerDirectoryService; readonly transport: PeerTransportPort; }} GossipServiceOptions
|
|
17
|
+
*/
|
|
1
18
|
import { normalizeAdvertisement, PeerDirectoryService, } from "./peer-directory-service.js";
|
|
19
|
+
import { decodeUtf8, encodeUtf8 } from "./runtime-globals.js";
|
|
2
20
|
const GOSSIP_VERSION = 1;
|
|
3
21
|
export class GossipService {
|
|
22
|
+
/**
|
|
23
|
+
* @readonly
|
|
24
|
+
* @type {PeerDirectoryService}
|
|
25
|
+
*/
|
|
4
26
|
#directory;
|
|
27
|
+
/**
|
|
28
|
+
* @readonly
|
|
29
|
+
* @type {PeerTransportPort}
|
|
30
|
+
*/
|
|
5
31
|
#transport;
|
|
32
|
+
/**
|
|
33
|
+
* @param {GossipServiceOptions} options
|
|
34
|
+
*/
|
|
6
35
|
constructor(options) {
|
|
7
36
|
this.#directory = options.directory;
|
|
8
37
|
this.#transport = options.transport;
|
|
9
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} topic
|
|
41
|
+
* @param {PeerAdvertisement} advertisement
|
|
42
|
+
* @returns {Promise<void>}
|
|
43
|
+
*/
|
|
10
44
|
async publishAdvertisement(topic, advertisement) {
|
|
11
45
|
const normalized = normalizeAdvertisement(advertisement);
|
|
46
|
+
/** @type {GossipAdvertisement} */
|
|
12
47
|
const value = { ...normalized, version: GOSSIP_VERSION };
|
|
13
|
-
await this.#transport.publish(topic,
|
|
48
|
+
await this.#transport.publish(topic, encodeUtf8(JSON.stringify(value)));
|
|
14
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} topic
|
|
52
|
+
* @param {AbortSignalPort} [signal]
|
|
53
|
+
* @returns {AsyncIterable<GossipAdvertisement>}
|
|
54
|
+
*/
|
|
15
55
|
async *advertisements(topic, signal) {
|
|
16
56
|
const seen = new Set();
|
|
17
57
|
for await (const payload of this.#transport.subscribe(topic, signal)) {
|
|
@@ -26,12 +66,19 @@ export class GossipService {
|
|
|
26
66
|
}
|
|
27
67
|
}
|
|
28
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* @param {Uint8Array} payload
|
|
71
|
+
* @returns {GossipAdvertisement | undefined}
|
|
72
|
+
*/
|
|
29
73
|
function parseAdvertisement(payload) {
|
|
30
74
|
try {
|
|
31
|
-
|
|
75
|
+
/** @type {unknown} */
|
|
76
|
+
const value = JSON.parse(decodeUtf8(payload));
|
|
32
77
|
if (!isRecord(value) || value.version !== GOSSIP_VERSION ||
|
|
33
78
|
!Array.isArray(value.directAddresses) ||
|
|
34
|
-
!value.directAddresses.every(
|
|
79
|
+
!value.directAddresses.every(/**
|
|
80
|
+
* @returns {address is string}
|
|
81
|
+
*/ (address) => typeof address === "string") ||
|
|
35
82
|
typeof value.did !== "string" || typeof value.irohNodeId !== "string" || typeof value.observedAt !== "string") {
|
|
36
83
|
return undefined;
|
|
37
84
|
}
|
|
@@ -47,7 +94,10 @@ function parseAdvertisement(payload) {
|
|
|
47
94
|
return undefined;
|
|
48
95
|
}
|
|
49
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* @param {unknown} value
|
|
99
|
+
* @returns {value is Record<string, unknown>}
|
|
100
|
+
*/
|
|
50
101
|
function isRecord(value) {
|
|
51
102
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
103
|
}
|
|
53
|
-
//# sourceMappingURL=gossip-service.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @typedef {import('@econ-v1/ports').HomeNodeClient} HomeNodeClient */
|
|
3
|
+
export class InviteRedemptionService {
|
|
4
|
+
/**
|
|
5
|
+
* @readonly
|
|
6
|
+
*/
|
|
7
|
+
#node;
|
|
8
|
+
/**
|
|
9
|
+
* @param {InviteRedemptionServiceOptions} options
|
|
10
|
+
*/
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.#node = options.node;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} code
|
|
16
|
+
* @returns {Promise<InviteRedemptionOutcome>}
|
|
17
|
+
*/
|
|
18
|
+
async redeem(code) {
|
|
19
|
+
const trimmed = code.trim();
|
|
20
|
+
if (trimmed.length === 0) return { kind: "rejected" };
|
|
21
|
+
try {
|
|
22
|
+
const wire = await this.#node.invoke({
|
|
23
|
+
operation: "POST /api/v2/invite/redeem",
|
|
24
|
+
payload: { code: trimmed },
|
|
25
|
+
});
|
|
26
|
+
if (wire.ok !== true) return { kind: "rejected" };
|
|
27
|
+
const receiverName =
|
|
28
|
+
typeof wire.receiver_name === "string" &&
|
|
29
|
+
wire.receiver_name.trim().length > 0
|
|
30
|
+
? wire.receiver_name.trim()
|
|
31
|
+
: undefined;
|
|
32
|
+
return { kind: "redeemed", receiverName };
|
|
33
|
+
} catch {
|
|
34
|
+
return { kind: "rejected" };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Step 11's real action: redeem an invitation code against the node's PUBLIC
|
|
40
|
+
* `POST /api/v2/invite/redeem` (`core/adapters-network/src/http/server/invite.rs`), which
|
|
41
|
+
* takes `{ code }` or `{ token }` and needs no authentication — deliberately, because at step
|
|
42
|
+
* 11 the browser has no session at all.
|
|
43
|
+
*
|
|
44
|
+
* The response's `receiver_name` is the name the INVITER registered for this person. Prefilling
|
|
45
|
+
* step 12's display name with it (and locking it, see `OnboardingFlowService`) is what makes
|
|
46
|
+
* the invitation meaningful rather than decorative: the person who was invited as "Ada
|
|
47
|
+
* Lovelace" shows up on the inviter's node as Ada Lovelace. It is null for legacy
|
|
48
|
+
* token-shaped invites, which is a normal, non-error case.
|
|
49
|
+
*
|
|
50
|
+
* Outcomes are closed and text-free. `"rejected"` covers expired, already-used, unknown and
|
|
51
|
+
* malformed codes alike — the node's own message would tell an attacker which of those it was.
|
|
52
|
+
* @typedef {| { readonly kind: "redeemed"; readonly receiverName: string | undefined }
|
|
53
|
+
* | { readonly kind: "rejected" }} InviteRedemptionOutcome
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {object} InviteRedemptionServiceOptions
|
|
57
|
+
* @property {Pick<HomeNodeClient, "invoke">} node
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {object} InviteRedeemWire
|
|
61
|
+
* @property {unknown} [ok]
|
|
62
|
+
* @property {unknown} [receiver_name]
|
|
63
|
+
*/
|
|
64
|
+
// @ts-check
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("@econ-v1/ports").DeviceConnectionMetadata} DeviceConnectionMetadata */
|
|
4
|
+
/** @typedef {import("@econ-v1/ports").HomeNodeCandidate} HomeNodeCandidate */
|
|
5
|
+
/** @typedef {import("@econ-v1/ports").HomeNodeClient} HomeNodeClient */
|
|
6
|
+
/** @typedef {import("@econ-v1/ports").HttpHomeNodeCandidate} HttpHomeNodeCandidate */
|
|
7
|
+
/** @typedef {import("@econ-v1/ports").IdentityPort} IdentityPort */
|
|
8
|
+
|
|
9
|
+
/** @typedef {{ readonly version: 1, readonly node_id: string, readonly wan_origin: string, readonly lan_origins: readonly string[] }} ClientTransportCandidateWire */
|
|
10
|
+
/** @typedef {{ readonly identity: IdentityPort, readonly now?: () => string }} LanCandidateRefreshServiceOptions */
|
|
11
|
+
|
|
12
|
+
import { parseRuntimeUrl } from "./runtime-globals.js";
|
|
13
|
+
import { isPotentiallyTrustworthyOrigin } from "./transport-manager.js";
|
|
14
|
+
|
|
15
|
+
const MAX_LAN_ORIGINS = 8;
|
|
16
|
+
|
|
17
|
+
export class LanCandidateRefreshService {
|
|
18
|
+
/** @readonly @type {IdentityPort} */
|
|
19
|
+
#identity;
|
|
20
|
+
/** @readonly @type {() => string} */
|
|
21
|
+
#now;
|
|
22
|
+
|
|
23
|
+
/** @param {LanCandidateRefreshServiceOptions} options */
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.#identity = options.identity;
|
|
26
|
+
this.#now = options.now ?? (() => new Date().toISOString());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {HomeNodeClient} home
|
|
31
|
+
* @returns {Promise<readonly HomeNodeCandidate[]>}
|
|
32
|
+
*/
|
|
33
|
+
async refresh(home) {
|
|
34
|
+
const current = await this.#identity.loadConnectionMetadata();
|
|
35
|
+
const homeNodeId = current?.homeNodeId;
|
|
36
|
+
if (!isNodeId(homeNodeId))
|
|
37
|
+
throw new TypeError("Home node transport identity is not established");
|
|
38
|
+
/** @type {unknown} */
|
|
39
|
+
const response = await home.invoke({
|
|
40
|
+
operation: "GET /api/v2/client-transport/candidates",
|
|
41
|
+
payload: {},
|
|
42
|
+
});
|
|
43
|
+
const refreshed = parseCandidates(response, homeNodeId);
|
|
44
|
+
/** @type {DeviceConnectionMetadata} */
|
|
45
|
+
/** @type {DeviceConnectionMetadata} */
|
|
46
|
+
const next = {
|
|
47
|
+
directAddresses: refreshed.lan.map((candidate) => candidate.address),
|
|
48
|
+
homeNodeId,
|
|
49
|
+
lastConnectedAt: this.#now(),
|
|
50
|
+
...(current?.relayUrl === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { relayUrl: current.relayUrl }),
|
|
53
|
+
wanOrigin: refreshed.wan.address,
|
|
54
|
+
};
|
|
55
|
+
await this.#identity.storeConnectionMetadata(next);
|
|
56
|
+
return refreshed.lan;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {unknown} value
|
|
62
|
+
* @param {string} expectedHomeNodeId
|
|
63
|
+
* @returns {{ readonly lan: readonly HttpHomeNodeCandidate[], readonly wan: HttpHomeNodeCandidate }}
|
|
64
|
+
*/
|
|
65
|
+
function parseCandidates(value, expectedHomeNodeId) {
|
|
66
|
+
if (
|
|
67
|
+
!isExactCandidateResponse(value) ||
|
|
68
|
+
value.node_id !== expectedHomeNodeId
|
|
69
|
+
) {
|
|
70
|
+
throw new TypeError("Home node transport identity did not match");
|
|
71
|
+
}
|
|
72
|
+
const lan = normalizeLanOrigins(value.lan_origins);
|
|
73
|
+
const wan = canonicalHttpsOrigin(
|
|
74
|
+
value.wan_origin,
|
|
75
|
+
"Home node WAN origin was invalid",
|
|
76
|
+
);
|
|
77
|
+
return {
|
|
78
|
+
lan: lan.map((address) => ({
|
|
79
|
+
address,
|
|
80
|
+
id: expectedHomeNodeId,
|
|
81
|
+
path: "lan-direct",
|
|
82
|
+
})),
|
|
83
|
+
wan: { address: wan, id: expectedHomeNodeId, path: "wan-direct" },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {readonly string[]} origins
|
|
89
|
+
* @returns {readonly string[]}
|
|
90
|
+
*/
|
|
91
|
+
function normalizeLanOrigins(origins) {
|
|
92
|
+
const normalized = new Set();
|
|
93
|
+
for (const origin of origins) {
|
|
94
|
+
normalized.add(
|
|
95
|
+
canonicalHttpsOrigin(origin, "Home node LAN candidates were invalid"),
|
|
96
|
+
);
|
|
97
|
+
if (normalized.size === MAX_LAN_ORIGINS) break;
|
|
98
|
+
}
|
|
99
|
+
return [...normalized];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} value
|
|
104
|
+
* @param {string} message
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function canonicalHttpsOrigin(value, message) {
|
|
108
|
+
/** @type {import("./runtime-globals.js").RuntimeUrl} */
|
|
109
|
+
let url;
|
|
110
|
+
try {
|
|
111
|
+
url = parseRuntimeUrl(value);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new TypeError(message);
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
!isPotentiallyTrustworthyOrigin(url) ||
|
|
117
|
+
url.username !== "" ||
|
|
118
|
+
url.password !== "" ||
|
|
119
|
+
url.hostname === "" ||
|
|
120
|
+
url.origin !== value
|
|
121
|
+
) {
|
|
122
|
+
throw new TypeError(message);
|
|
123
|
+
}
|
|
124
|
+
return url.origin;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {unknown} value
|
|
129
|
+
* @returns {value is ClientTransportCandidateWire}
|
|
130
|
+
*/
|
|
131
|
+
function isExactCandidateResponse(value) {
|
|
132
|
+
if (!isRecord(value)) return false;
|
|
133
|
+
const expected = ["lan_origins", "node_id", "version", "wan_origin"];
|
|
134
|
+
if (
|
|
135
|
+
Object.keys(value).length !== expected.length ||
|
|
136
|
+
!expected.every((key) => Object.hasOwn(value, key))
|
|
137
|
+
)
|
|
138
|
+
return false;
|
|
139
|
+
return (
|
|
140
|
+
value.version === 1 &&
|
|
141
|
+
isNodeId(value.node_id) &&
|
|
142
|
+
typeof value.wan_origin === "string" &&
|
|
143
|
+
Array.isArray(value.lan_origins) &&
|
|
144
|
+
value.lan_origins.every((origin) => typeof origin === "string")
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {unknown} value
|
|
150
|
+
* @returns {value is Record<string, unknown>}
|
|
151
|
+
*/
|
|
152
|
+
function isRecord(value) {
|
|
153
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {unknown} value
|
|
158
|
+
* @returns {value is string}
|
|
159
|
+
*/
|
|
160
|
+
function isNodeId(value) {
|
|
161
|
+
return (
|
|
162
|
+
typeof value === "string" && value.trim() === value && value.length > 0
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @typedef {import('@econ-v1/ports').HomeNodeClient} HomeNodeClient */
|
|
3
|
+
export class LcdDecisionService {
|
|
4
|
+
/**
|
|
5
|
+
* @readonly
|
|
6
|
+
*/
|
|
7
|
+
#node;
|
|
8
|
+
/**
|
|
9
|
+
* @param {LcdDecisionServiceOptions} options
|
|
10
|
+
*/
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.#node = options.node;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} operationHash
|
|
16
|
+
* @returns {Promise<LcdDecisionOutcome>}
|
|
17
|
+
*/
|
|
18
|
+
async confirm(operationHash) {
|
|
19
|
+
return await this.#decide(
|
|
20
|
+
"POST /api/v2/node/lcd-screen/confirm",
|
|
21
|
+
operationHash,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} operationHash
|
|
26
|
+
* @returns {Promise<LcdDecisionOutcome>}
|
|
27
|
+
*/
|
|
28
|
+
async reject(operationHash) {
|
|
29
|
+
return await this.#decide(
|
|
30
|
+
"POST /api/v2/node/lcd-screen/reject",
|
|
31
|
+
operationHash,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} operation
|
|
36
|
+
* @param {string} operationHash
|
|
37
|
+
* @returns {Promise<LcdDecisionOutcome>}
|
|
38
|
+
*/
|
|
39
|
+
async #decide(operation, operationHash) {
|
|
40
|
+
const trimmed = operationHash.trim();
|
|
41
|
+
// The node validates `length(min = 1)` and would answer 400; asking is pointless noise on
|
|
42
|
+
// the node's audit log for what is a client-side programming error.
|
|
43
|
+
if (trimmed.length === 0) return "not-accepted";
|
|
44
|
+
try {
|
|
45
|
+
await this.#node.invoke({
|
|
46
|
+
operation,
|
|
47
|
+
payload: { operation_hash: trimmed },
|
|
48
|
+
});
|
|
49
|
+
return "forwarded";
|
|
50
|
+
} catch {
|
|
51
|
+
// Every failure is the same instruction to the person: the node did not take this tap,
|
|
52
|
+
// so use the node. No error text, no status code, no branch.
|
|
53
|
+
return "not-accepted";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Forwards an LCD-pane tap to the NODE's own public decision routes — the same
|
|
59
|
+
* `onboarding.device.confirm` / `…reject` capability a physical tap on the hardware drives
|
|
60
|
+
* (`core/adapters-network/src/http/server/device_management/proxy.rs:888-975`). Both routes
|
|
61
|
+
* are in `public_routes()` and need no JWT, which is exactly right: at step 10 the browser has
|
|
62
|
+
* no session at all, so this must be constructed over `prePairingNode`.
|
|
63
|
+
*
|
|
64
|
+
* WHAT THIS CLASS IS NOT. It is not an approval. `LcdDecisionOutcome` has two values and
|
|
65
|
+
* neither of them is "approved": `"forwarded"` means the node accepted the *request*, not
|
|
66
|
+
* that the claim is confirmed. Confirmation reaches the flow through exactly one path —
|
|
67
|
+
* `OwnerClaimService.awaitConfirmation`'s poll of `GET /node/operation-status/{hash}` — and
|
|
68
|
+
* nothing here shortcuts it. That is the property design §8.3 exists to protect: the node is
|
|
69
|
+
* the authority, and a browser tap can at most ask.
|
|
70
|
+
*
|
|
71
|
+
* WHY THE FAILURE SIDE IS ONE VALUE. `enforce_physical_presence` answers 403 when a hardware
|
|
72
|
+
* LCD is present and the pending operation is an `onboard` (the physical tap is then the only
|
|
73
|
+
* path), and 503 when it cannot determine presence. Both would ideally be distinguishable
|
|
74
|
+
* here — but `AppError::Forbidden` and `AppError::ServiceUnavailable` do NOT emit the node's
|
|
75
|
+
* `{ success: false, error: { code } }` envelope (`core/foundation/src/error.rs:145-148`,
|
|
76
|
+
* `:252-254`), so `PrePairingHomeNodeClient` collapses both — and a genuine network failure —
|
|
77
|
+
* into `TransportError{ code: "unavailable" }`. Inventing a distinction the wire does not
|
|
78
|
+
* carry would be a lie, and the pane's behaviour is identical for all three anyway: render
|
|
79
|
+
* the press, say "waiting for the node", keep waiting. The pane learns whether a hardware
|
|
80
|
+
* display exists from `OwnerClaimPrepareOutcome.lcdHardware`, which the node states outright
|
|
81
|
+
* at prepare time — see `OnboardingFlowService.lcdScreen()`'s `physicalTapOnly`.
|
|
82
|
+
*
|
|
83
|
+
* The optional `setup_token` field on both requests is deliberately never sent: it is read
|
|
84
|
+
* from a file on the node's own filesystem, which a browser cannot do. A headless node with
|
|
85
|
+
* the code-match gate armed therefore answers `"not-accepted"` here, and the operator confirms
|
|
86
|
+
* through a surface that can read the token. Recorded as a named gap, not papered over.
|
|
87
|
+
* @typedef {"forwarded" | "not-accepted"} LcdDecisionOutcome
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {object} LcdDecisionServiceOptions
|
|
91
|
+
* @property {Pick<HomeNodeClient, "invoke">} node MUST be the pre-pairing client: step 10 runs on an unclaimed node with no session.
|
|
92
|
+
*/
|
|
93
|
+
// @ts-check
|