@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.
Files changed (286) hide show
  1. package/dist/errors/index.d.ts +96 -25
  2. package/dist/errors/index.d.ts.map +1 -1
  3. package/dist/events/index.d.ts +127 -20
  4. package/dist/events/index.d.ts.map +1 -1
  5. package/dist/index.d.ts +250 -47
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/models/agent.d.ts +25 -4
  8. package/dist/models/agent.d.ts.map +1 -1
  9. package/dist/models/app-data.d.ts +84 -0
  10. package/dist/models/app-data.d.ts.map +1 -0
  11. package/dist/models/conversation.d.ts +12 -2
  12. package/dist/models/conversation.d.ts.map +1 -1
  13. package/dist/models/delegation.d.ts +12 -3
  14. package/dist/models/delegation.d.ts.map +1 -1
  15. package/dist/models/device-boot-state.d.ts +12 -0
  16. package/dist/models/device-boot-state.d.ts.map +1 -0
  17. package/dist/models/device-identity.d.ts +11 -3
  18. package/dist/models/device-identity.d.ts.map +1 -1
  19. package/dist/models/graph.d.ts +26 -3
  20. package/dist/models/graph.d.ts.map +1 -1
  21. package/dist/models/index.d.ts +82 -14
  22. package/dist/models/index.d.ts.map +1 -1
  23. package/dist/models/json.d.ts +5 -5
  24. package/dist/models/json.d.ts.map +1 -1
  25. package/dist/models/l402.d.ts +4 -5
  26. package/dist/models/l402.d.ts.map +1 -1
  27. package/dist/models/live-query-definition.d.ts +15 -16
  28. package/dist/models/live-query-definition.d.ts.map +1 -1
  29. package/dist/models/node-app-memory.d.ts +107 -0
  30. package/dist/models/node-app-memory.d.ts.map +1 -0
  31. package/dist/models/outbox-intent.d.ts +3 -3
  32. package/dist/models/outbox-intent.d.ts.map +1 -1
  33. package/dist/models/outbox.d.ts +67 -15
  34. package/dist/models/outbox.d.ts.map +1 -1
  35. package/dist/models/owner-views.d.ts +28 -28
  36. package/dist/models/owner-views.d.ts.map +1 -1
  37. package/dist/models/payment.d.ts +28 -9
  38. package/dist/models/payment.d.ts.map +1 -1
  39. package/dist/models/query-diff.d.ts +2 -2
  40. package/dist/models/query-diff.d.ts.map +1 -1
  41. package/dist/models/release-manifest.d.ts +32 -47
  42. package/dist/models/release-manifest.d.ts.map +1 -1
  43. package/dist/models/repository-write.d.ts +3 -3
  44. package/dist/models/repository-write.d.ts.map +1 -1
  45. package/dist/models/revisioned-commands.d.ts +66 -13
  46. package/dist/models/revisioned-commands.d.ts.map +1 -1
  47. package/dist/models/revisioned.d.ts +50 -10
  48. package/dist/models/revisioned.d.ts.map +1 -1
  49. package/dist/models/settings.d.ts +2 -2
  50. package/dist/models/settings.d.ts.map +1 -1
  51. package/dist/models/sync.d.ts +18 -18
  52. package/dist/models/sync.d.ts.map +1 -1
  53. package/dist/models/wallet.d.ts +16 -16
  54. package/dist/models/wallet.d.ts.map +1 -1
  55. package/dist/queries/certificate-trust.d.ts +13 -0
  56. package/dist/queries/certificate-trust.d.ts.map +1 -0
  57. package/dist/queries/conversations.d.ts +38 -7
  58. package/dist/queries/conversations.d.ts.map +1 -1
  59. package/dist/queries/discovery.d.ts +10 -10
  60. package/dist/queries/discovery.d.ts.map +1 -1
  61. package/dist/queries/notifications.d.ts +24 -4
  62. package/dist/queries/notifications.d.ts.map +1 -1
  63. package/dist/queries/onboarding-flow.d.ts +25 -0
  64. package/dist/queries/onboarding-flow.d.ts.map +1 -0
  65. package/dist/queries/onboarding-lcd-screen.d.ts +56 -0
  66. package/dist/queries/onboarding-lcd-screen.d.ts.map +1 -0
  67. package/dist/queries/onboarding-status.d.ts +10 -10
  68. package/dist/queries/onboarding-status.d.ts.map +1 -1
  69. package/dist/queries/revisioned.d.ts +47 -13
  70. package/dist/queries/revisioned.d.ts.map +1 -1
  71. package/dist/queries/wallet.d.ts +40 -10
  72. package/dist/queries/wallet.d.ts.map +1 -1
  73. package/dist/services/abort-signal.d.ts +54 -0
  74. package/dist/services/abort-signal.d.ts.map +1 -0
  75. package/dist/services/certificate-trust-service.d.ts +42 -0
  76. package/dist/services/certificate-trust-service.d.ts.map +1 -0
  77. package/dist/services/conversation-service.d.ts +103 -15
  78. package/dist/services/conversation-service.d.ts.map +1 -1
  79. package/dist/services/conversation-sync.d.ts +9 -5
  80. package/dist/services/conversation-sync.d.ts.map +1 -1
  81. package/dist/services/device-boot-state-service.d.ts +17 -0
  82. package/dist/services/device-boot-state-service.d.ts.map +1 -0
  83. package/dist/services/device-cleanup-service.d.ts +47 -0
  84. package/dist/services/device-cleanup-service.d.ts.map +1 -0
  85. package/dist/services/discovery-probe-service.d.ts +37 -46
  86. package/dist/services/discovery-probe-service.d.ts.map +1 -1
  87. package/dist/services/gossip-service.d.ts +28 -13
  88. package/dist/services/gossip-service.d.ts.map +1 -1
  89. package/dist/services/invite-redemption-service.d.ts +43 -0
  90. package/dist/services/invite-redemption-service.d.ts.map +1 -0
  91. package/dist/services/lan-candidate-refresh-service.d.ts +22 -7
  92. package/dist/services/lan-candidate-refresh-service.d.ts.map +1 -1
  93. package/dist/services/lcd-decision-service.d.ts +58 -0
  94. package/dist/services/lcd-decision-service.d.ts.map +1 -0
  95. package/dist/services/onboarding-checkpoint-store.d.ts +54 -0
  96. package/dist/services/onboarding-checkpoint-store.d.ts.map +1 -0
  97. package/dist/services/onboarding-flow-service.d.ts +180 -0
  98. package/dist/services/onboarding-flow-service.d.ts.map +1 -0
  99. package/dist/services/onboarding-status-service.d.ts +21 -38
  100. package/dist/services/onboarding-status-service.d.ts.map +1 -1
  101. package/dist/services/outbox-service.d.ts +105 -29
  102. package/dist/services/outbox-service.d.ts.map +1 -1
  103. package/dist/services/owner-claim-service.d.ts +187 -0
  104. package/dist/services/owner-claim-service.d.ts.map +1 -0
  105. package/dist/services/owner-view-refresh-service.d.ts +40 -0
  106. package/dist/services/owner-view-refresh-service.d.ts.map +1 -0
  107. package/dist/services/owner-view-service.d.ts +49 -4
  108. package/dist/services/owner-view-service.d.ts.map +1 -1
  109. package/dist/services/peer-directory-service.d.ts +53 -17
  110. package/dist/services/peer-directory-service.d.ts.map +1 -1
  111. package/dist/services/release-manifest-verifier.d.ts +18 -20
  112. package/dist/services/release-manifest-verifier.d.ts.map +1 -1
  113. package/dist/services/revisioned-domain-service.d.ts +50 -17
  114. package/dist/services/revisioned-domain-service.d.ts.map +1 -1
  115. package/dist/services/runtime-globals.d.ts +39 -0
  116. package/dist/services/runtime-globals.d.ts.map +1 -0
  117. package/dist/services/session-service.d.ts +23 -3
  118. package/dist/services/session-service.d.ts.map +1 -1
  119. package/dist/services/sync/authoritative-mirror.d.ts +88 -15
  120. package/dist/services/sync/authoritative-mirror.d.ts.map +1 -1
  121. package/dist/services/sync/index.d.ts +17 -5
  122. package/dist/services/sync/index.d.ts.map +1 -1
  123. package/dist/services/sync/log-sync.d.ts +42 -8
  124. package/dist/services/sync/log-sync.d.ts.map +1 -1
  125. package/dist/services/sync/revision-merge.d.ts +80 -13
  126. package/dist/services/sync/revision-merge.d.ts.map +1 -1
  127. package/dist/services/sync/strategy.d.ts +11 -1
  128. package/dist/services/sync/strategy.d.ts.map +1 -1
  129. package/dist/services/transport-manager.d.ts +84 -29
  130. package/dist/services/transport-manager.d.ts.map +1 -1
  131. package/dist/services/wallet-mirror-service.d.ts +81 -18
  132. package/dist/services/wallet-mirror-service.d.ts.map +1 -1
  133. package/dist/services/wifi-provisioning-service.d.ts +146 -0
  134. package/dist/services/wifi-provisioning-service.d.ts.map +1 -0
  135. package/package.json +18 -12
  136. package/src/errors/index.js +126 -0
  137. package/src/events/index.js +124 -0
  138. package/src/index.js +566 -0
  139. package/src/models/agent.js +43 -0
  140. package/src/models/app-data.js +122 -0
  141. package/src/models/conversation.js +36 -0
  142. package/{dist → src}/models/delegation.js +18 -1
  143. package/src/models/device-boot-state.js +3 -0
  144. package/{dist → src}/models/device-identity.js +18 -2
  145. package/{dist → src}/models/graph.js +30 -3
  146. package/src/models/index.js +175 -0
  147. package/src/models/json.js +18 -0
  148. package/src/models/l402.js +9 -0
  149. package/src/models/live-query-definition.js +30 -0
  150. package/src/models/node-app-memory.js +130 -0
  151. package/src/models/outbox-intent.js +12 -0
  152. package/{dist → src}/models/outbox.js +102 -3
  153. package/src/models/owner-views.js +59 -0
  154. package/src/models/payment.js +24 -0
  155. package/src/models/query-diff.js +7 -0
  156. package/src/models/release-manifest.js +160 -0
  157. package/src/models/repository-write.js +10 -0
  158. package/src/models/revisioned-commands.js +92 -0
  159. package/src/models/revisioned.js +41 -0
  160. package/src/models/settings.js +16 -0
  161. package/src/models/sync.js +44 -0
  162. package/src/models/wallet.js +40 -0
  163. package/src/queries/certificate-trust.js +31 -0
  164. package/{dist → src}/queries/conversations.js +54 -3
  165. package/src/queries/discovery.js +34 -0
  166. package/{dist → src}/queries/notifications.js +25 -2
  167. package/src/queries/onboarding-flow.js +48 -0
  168. package/src/queries/onboarding-lcd-screen.js +100 -0
  169. package/src/queries/onboarding-status.js +34 -0
  170. package/src/queries/revisioned.js +128 -0
  171. package/{dist → src}/queries/wallet.js +35 -1
  172. package/src/services/abort-signal.js +69 -0
  173. package/src/services/certificate-trust-service.js +119 -0
  174. package/{dist → src}/services/conversation-service.js +108 -1
  175. package/src/services/conversation-sync.js +28 -0
  176. package/src/services/device-boot-state-service.js +45 -0
  177. package/src/services/device-cleanup-service.js +112 -0
  178. package/src/services/discovery-probe-service.js +196 -0
  179. package/{dist → src}/services/gossip-service.js +54 -4
  180. package/src/services/invite-redemption-service.js +64 -0
  181. package/src/services/lan-candidate-refresh-service.js +164 -0
  182. package/src/services/lcd-decision-service.js +93 -0
  183. package/src/services/onboarding-checkpoint-store.js +155 -0
  184. package/src/services/onboarding-flow-service.js +1096 -0
  185. package/src/services/onboarding-status-service.js +64 -0
  186. package/{dist → src}/services/outbox-service.js +132 -1
  187. package/src/services/owner-claim-service.js +347 -0
  188. package/src/services/owner-view-refresh-service.js +208 -0
  189. package/src/services/owner-view-service.js +87 -0
  190. package/{dist → src}/services/peer-directory-service.js +72 -3
  191. package/src/services/release-manifest-verifier.js +108 -0
  192. package/{dist → src}/services/revisioned-domain-service.js +189 -9
  193. package/src/services/runtime-globals.js +174 -0
  194. package/src/services/session-service.js +50 -0
  195. package/{dist → src}/services/sync/authoritative-mirror.js +90 -1
  196. package/src/services/sync/index.js +67 -0
  197. package/src/services/sync/log-sync.js +60 -0
  198. package/src/services/sync/revision-merge.js +119 -0
  199. package/src/services/sync/strategy.js +44 -0
  200. package/src/services/transport-manager.js +478 -0
  201. package/{dist → src}/services/wallet-mirror-service.js +226 -6
  202. package/src/services/wifi-provisioning-service.js +278 -0
  203. package/dist/errors/index.js +0 -45
  204. package/dist/errors/index.js.map +0 -1
  205. package/dist/events/index.js +0 -17
  206. package/dist/events/index.js.map +0 -1
  207. package/dist/index.js +0 -31
  208. package/dist/index.js.map +0 -1
  209. package/dist/models/agent.js +0 -17
  210. package/dist/models/agent.js.map +0 -1
  211. package/dist/models/conversation.js +0 -2
  212. package/dist/models/conversation.js.map +0 -1
  213. package/dist/models/delegation.js.map +0 -1
  214. package/dist/models/device-identity.js.map +0 -1
  215. package/dist/models/graph.js.map +0 -1
  216. package/dist/models/index.js +0 -8
  217. package/dist/models/index.js.map +0 -1
  218. package/dist/models/json.js +0 -2
  219. package/dist/models/json.js.map +0 -1
  220. package/dist/models/l402.js +0 -2
  221. package/dist/models/l402.js.map +0 -1
  222. package/dist/models/live-query-definition.js +0 -2
  223. package/dist/models/live-query-definition.js.map +0 -1
  224. package/dist/models/outbox-intent.js +0 -2
  225. package/dist/models/outbox-intent.js.map +0 -1
  226. package/dist/models/outbox.js.map +0 -1
  227. package/dist/models/owner-views.js +0 -7
  228. package/dist/models/owner-views.js.map +0 -1
  229. package/dist/models/payment.js +0 -3
  230. package/dist/models/payment.js.map +0 -1
  231. package/dist/models/query-diff.js +0 -2
  232. package/dist/models/query-diff.js.map +0 -1
  233. package/dist/models/release-manifest.js +0 -147
  234. package/dist/models/release-manifest.js.map +0 -1
  235. package/dist/models/repository-write.js +0 -2
  236. package/dist/models/repository-write.js.map +0 -1
  237. package/dist/models/revisioned-commands.js +0 -35
  238. package/dist/models/revisioned-commands.js.map +0 -1
  239. package/dist/models/revisioned.js +0 -2
  240. package/dist/models/revisioned.js.map +0 -1
  241. package/dist/models/settings.js +0 -2
  242. package/dist/models/settings.js.map +0 -1
  243. package/dist/models/sync.js +0 -2
  244. package/dist/models/sync.js.map +0 -1
  245. package/dist/models/wallet.js +0 -2
  246. package/dist/models/wallet.js.map +0 -1
  247. package/dist/queries/conversations.js.map +0 -1
  248. package/dist/queries/discovery.js +0 -27
  249. package/dist/queries/discovery.js.map +0 -1
  250. package/dist/queries/notifications.js.map +0 -1
  251. package/dist/queries/onboarding-status.js +0 -27
  252. package/dist/queries/onboarding-status.js.map +0 -1
  253. package/dist/queries/revisioned.js +0 -58
  254. package/dist/queries/revisioned.js.map +0 -1
  255. package/dist/queries/wallet.js.map +0 -1
  256. package/dist/services/conversation-service.js.map +0 -1
  257. package/dist/services/conversation-sync.js +0 -9
  258. package/dist/services/conversation-sync.js.map +0 -1
  259. package/dist/services/discovery-probe-service.js +0 -129
  260. package/dist/services/discovery-probe-service.js.map +0 -1
  261. package/dist/services/gossip-service.js.map +0 -1
  262. package/dist/services/lan-candidate-refresh-service.js +0 -79
  263. package/dist/services/lan-candidate-refresh-service.js.map +0 -1
  264. package/dist/services/onboarding-status-service.js +0 -32
  265. package/dist/services/onboarding-status-service.js.map +0 -1
  266. package/dist/services/outbox-service.js.map +0 -1
  267. package/dist/services/owner-view-service.js +0 -21
  268. package/dist/services/owner-view-service.js.map +0 -1
  269. package/dist/services/peer-directory-service.js.map +0 -1
  270. package/dist/services/release-manifest-verifier.js +0 -84
  271. package/dist/services/release-manifest-verifier.js.map +0 -1
  272. package/dist/services/revisioned-domain-service.js.map +0 -1
  273. package/dist/services/session-service.js +0 -20
  274. package/dist/services/session-service.js.map +0 -1
  275. package/dist/services/sync/authoritative-mirror.js.map +0 -1
  276. package/dist/services/sync/index.js +0 -4
  277. package/dist/services/sync/index.js.map +0 -1
  278. package/dist/services/sync/log-sync.js +0 -20
  279. package/dist/services/sync/log-sync.js.map +0 -1
  280. package/dist/services/sync/revision-merge.js +0 -46
  281. package/dist/services/sync/revision-merge.js.map +0 -1
  282. package/dist/services/sync/strategy.js +0 -2
  283. package/dist/services/sync/strategy.js.map +0 -1
  284. package/dist/services/transport-manager.js +0 -234
  285. package/dist/services/transport-manager.js.map +0 -1
  286. 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, new TextEncoder().encode(JSON.stringify(value)));
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
- const value = JSON.parse(new TextDecoder().decode(payload));
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((address) => typeof address === "string") ||
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