@econ-v1/domain 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 (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,208 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {import("@econ-v1/ports").RepositoryJsonValue} RepositoryJsonValue */
4
+ /** @typedef {import("../events/index.js").DomainEvent} DomainEvent */
5
+ /** @typedef {import("../models/owner-views.js").FeedItem} FeedItem */
6
+ /** @typedef {import("../models/owner-views.js").HomeSummary} HomeSummary */
7
+ /** @typedef {import("../models/owner-views.js").OperationalSummary} OperationalSummary */
8
+ /** @typedef {import("../models/owner-views.js").OwnerViewRepository} OwnerViewRepository */
9
+ /** @typedef {import("../models/owner-views.js").PeerDirectoryEntry} PeerDirectoryEntry */
10
+
11
+ const OWNER_VIEW_PROJECTION_KEYS = Object.freeze({
12
+ feed: "feed:default",
13
+ home: "home",
14
+ operations: "operations",
15
+ peers: "peers:default",
16
+ });
17
+
18
+ export class OwnerViewRefreshService {
19
+ /** @readonly @type {OwnerViewRepository} */
20
+ #network;
21
+ /** @readonly @type {() => string} */
22
+ #now;
23
+ /** @readonly @type {OwnerViewProjectionStore} */
24
+ #projections;
25
+ /** @readonly @type {(event: DomainEvent) => void} */
26
+ #publish;
27
+ /** @readonly @type {() => Promise<void>} */
28
+ #validatePersistence;
29
+
30
+ /** @param {OwnerViewRefreshServiceOptions} options */
31
+ constructor(options) {
32
+ this.#network = options.network;
33
+ this.#now = options.now ?? (() => new Date().toISOString());
34
+ this.#projections = options.projections;
35
+ this.#publish = options.publish ?? (() => undefined);
36
+ this.#validatePersistence = options.validatePersistence ?? (async () => undefined);
37
+ }
38
+
39
+ /** @returns {Promise<void>} */
40
+ async refreshAll() {
41
+ const homeValue = await this.#network.readHomeSummary();
42
+ const home = homeValue === undefined ? null : parseHome(homeValue);
43
+ const peers = [...parsePeers(await this.#network.listPeers({}))];
44
+ const feed = [...parseFeed(await this.#network.listFeed({}))];
45
+ const operationsValue = await this.#network.readOperationalSummary();
46
+ const operations = operationsValue === undefined ? null : parseOperational(operationsValue);
47
+
48
+ await this.#persist([
49
+ { key: OWNER_VIEW_PROJECTION_KEYS.home, payload: home },
50
+ { key: OWNER_VIEW_PROJECTION_KEYS.peers, payload: peers },
51
+ { key: OWNER_VIEW_PROJECTION_KEYS.feed, payload: feed },
52
+ { key: OWNER_VIEW_PROJECTION_KEYS.operations, payload: operations },
53
+ ]);
54
+ }
55
+
56
+ /** @returns {Promise<void>} */
57
+ async refreshHome() {
58
+ const value = await this.#network.readHomeSummary();
59
+ await this.#persist([{
60
+ key: OWNER_VIEW_PROJECTION_KEYS.home,
61
+ payload: value === undefined ? null : parseHome(value),
62
+ }]);
63
+ }
64
+
65
+ /** @returns {Promise<void>} */
66
+ async refreshPeers() {
67
+ const value = await this.#network.listPeers({});
68
+ await this.#persist([{
69
+ key: OWNER_VIEW_PROJECTION_KEYS.peers,
70
+ payload: [...parsePeers(value)],
71
+ }]);
72
+ }
73
+
74
+ /** @returns {Promise<void>} */
75
+ async refreshFeed() {
76
+ const value = await this.#network.listFeed({});
77
+ await this.#persist([{
78
+ key: OWNER_VIEW_PROJECTION_KEYS.feed,
79
+ payload: [...parseFeed(value)],
80
+ }]);
81
+ }
82
+
83
+ /** @returns {Promise<void>} */
84
+ async refreshOperational() {
85
+ const value = await this.#network.readOperationalSummary();
86
+ await this.#persist([{
87
+ key: OWNER_VIEW_PROJECTION_KEYS.operations,
88
+ payload: value === undefined ? null : parseOperational(value),
89
+ }]);
90
+ }
91
+
92
+ /** @param {readonly { readonly key: string, readonly payload: RepositoryJsonValue }[]} batch */
93
+ async #persist(batch) {
94
+ const cachedAt = requiredTimestamp(this.#now(), "owner-view cachedAt");
95
+ await this.#validatePersistence();
96
+ await this.#projections.writeBatch(batch, cachedAt);
97
+ for (const { key } of batch) {
98
+ this.#publish({
99
+ occurredAt: cachedAt,
100
+ payload: { action: "updated", entityId: key, entityType: "owner-view" },
101
+ type: "domain.entity-changed",
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ /** @param {unknown} value @returns {HomeSummary} */
108
+ function parseHome(value) {
109
+ const record = exactRecord(value, ["displayName", "homeNodeId", "lastSyncedAt", "status"], "home summary");
110
+ const lastSyncedAt = optionalTimestamp(record.lastSyncedAt, "home summary lastSyncedAt");
111
+ return {
112
+ displayName: requiredString(record.displayName, "home summary displayName"),
113
+ homeNodeId: requiredString(record.homeNodeId, "home summary homeNodeId"),
114
+ ...(lastSyncedAt === undefined ? {} : { lastSyncedAt }),
115
+ status: oneOf(record.status, ["offline", "online", "unknown"], "home summary status"),
116
+ };
117
+ }
118
+
119
+ /** @param {unknown} value @returns {readonly PeerDirectoryEntry[]} */
120
+ function parsePeers(value) {
121
+ if (!Array.isArray(value)) throw invalid("peer directory");
122
+ return value.map((entry, index) => {
123
+ const label = `peer directory[${index}]`;
124
+ const record = exactRecord(entry, ["displayName", "id", "lastSeenAt", "status"], label);
125
+ const lastSeenAt = optionalTimestamp(record.lastSeenAt, `${label} lastSeenAt`);
126
+ return {
127
+ displayName: requiredString(record.displayName, `${label} displayName`),
128
+ id: requiredString(record.id, `${label} id`),
129
+ ...(lastSeenAt === undefined ? {} : { lastSeenAt }),
130
+ status: oneOf(record.status, ["offline", "online", "unknown"], `${label} status`),
131
+ };
132
+ });
133
+ }
134
+
135
+ /** @param {unknown} value @returns {readonly FeedItem[]} */
136
+ function parseFeed(value) {
137
+ if (!Array.isArray(value)) throw invalid("feed");
138
+ return value.map((entry, index) => {
139
+ const label = `feed[${index}]`;
140
+ const record = exactRecord(entry, ["body", "createdAt", "id", "kind", "read", "title"], label);
141
+ if (typeof record.read !== "boolean") throw invalid(`${label} read`);
142
+ return {
143
+ body: requiredString(record.body, `${label} body`),
144
+ createdAt: requiredTimestamp(record.createdAt, `${label} createdAt`),
145
+ id: requiredString(record.id, `${label} id`),
146
+ kind: oneOf(record.kind, ["activity", "message", "system"], `${label} kind`),
147
+ read: record.read,
148
+ title: requiredString(record.title, `${label} title`),
149
+ };
150
+ });
151
+ }
152
+
153
+ /** @param {unknown} value @returns {OperationalSummary} */
154
+ function parseOperational(value) {
155
+ const record = exactRecord(value, ["outboxDepth", "storage", "sync", "transport"], "operational summary");
156
+ if (!Number.isSafeInteger(record.outboxDepth) || /** @type {number} */ (record.outboxDepth) < 0) {
157
+ throw invalid("operational summary outboxDepth");
158
+ }
159
+ return {
160
+ outboxDepth: /** @type {number} */ (record.outboxDepth),
161
+ storage: oneOf(record.storage, ["blocked", "ready", "recovering"], "operational summary storage"),
162
+ sync: oneOf(record.sync, ["current", "stale", "syncing"], "operational summary sync"),
163
+ transport: oneOf(record.transport, ["iroh-relay", "lan-direct", "lan-webtransport", "offline", "wan-direct", "webrtc-direct"], "operational summary transport"),
164
+ };
165
+ }
166
+
167
+ /** @param {unknown} value @param {readonly string[]} allowed @param {string} label */
168
+ function exactRecord(value, allowed, label) {
169
+ if (!isRecord(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw invalid(label);
170
+ return value;
171
+ }
172
+
173
+ /** @param {unknown} value @param {string} label */
174
+ function requiredString(value, label) {
175
+ if (typeof value !== "string" || value.trim() !== value || value.length === 0) throw invalid(label);
176
+ return value;
177
+ }
178
+
179
+ /** @param {unknown} value @param {string} label */
180
+ function requiredTimestamp(value, label) {
181
+ const timestamp = requiredString(value, label);
182
+ if (!Number.isFinite(Date.parse(timestamp))) throw invalid(label);
183
+ return timestamp;
184
+ }
185
+
186
+ /** @param {unknown} value @param {string} label */
187
+ function optionalTimestamp(value, label) {
188
+ return value === undefined ? undefined : requiredTimestamp(value, label);
189
+ }
190
+
191
+ /** @template {string} T @param {unknown} value @param {readonly T[]} values @param {string} label @returns {T} */
192
+ function oneOf(value, values, label) {
193
+ if (typeof value !== "string" || !values.includes(/** @type {T} */ (value))) throw invalid(label);
194
+ return /** @type {T} */ (value);
195
+ }
196
+
197
+ /** @param {string} label */
198
+ function invalid(label) {
199
+ return new TypeError(`Invalid ${label}`);
200
+ }
201
+
202
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
203
+ function isRecord(value) {
204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
205
+ }
206
+
207
+ /** @typedef {{ read(key: string): Promise<{ readonly cachedAt: string, readonly payload: RepositoryJsonValue } | undefined>, writeBatch(batch: readonly { readonly key: string, readonly payload: RepositoryJsonValue }[], cachedAt: string): Promise<void> }} OwnerViewProjectionStore */
208
+ /** @typedef {{ readonly network: OwnerViewRepository, readonly now?: () => string, readonly projections: OwnerViewProjectionStore, readonly publish?: (event: DomainEvent) => void, readonly validatePersistence?: () => Promise<void> }} OwnerViewRefreshServiceOptions */
@@ -0,0 +1,87 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {import("../models/owner-views.js").FeedItem} FeedItem
5
+ */
6
+ /**
7
+ * @typedef {import("../models/owner-views.js").FeedQuery} FeedQuery
8
+ */
9
+ /**
10
+ * @typedef {import("../models/owner-views.js").HomeSummary} HomeSummary
11
+ */
12
+ /**
13
+ * @typedef {import("../models/owner-views.js").OperationalSummary} OperationalSummary
14
+ */
15
+ /**
16
+ * @typedef {import("../models/owner-views.js").OwnerViewRepository} OwnerViewRepository
17
+ */
18
+ /**
19
+ * @typedef {import("../models/owner-views.js").PeerDirectoryEntry} PeerDirectoryEntry
20
+ */
21
+ /**
22
+ * @typedef {import("../models/owner-views.js").PeerDirectoryQuery} PeerDirectoryQuery
23
+ */
24
+ export class OwnerViewService {
25
+ /**
26
+ * @readonly
27
+ * @type {OwnerViewRepository}
28
+ */
29
+ #repository;
30
+ /** @readonly @type {OwnerViewRepository} */
31
+ #onlineRepository;
32
+ /**
33
+ * @param {OwnerViewRepository} repository Local repository for declared default projections.
34
+ * @param {OwnerViewRepository} [onlineRepository=repository] Online repository for undeclared filtered queries.
35
+ */
36
+ constructor(repository, onlineRepository = repository) {
37
+ this.#repository = repository;
38
+ this.#onlineRepository = onlineRepository;
39
+ }
40
+ /**
41
+ * @returns {Promise<readonly HomeSummary[]>}
42
+ */
43
+ async readHome() {
44
+ const summary = await this.#repository.readHomeSummary();
45
+ return summary === undefined ? [] : [summary];
46
+ }
47
+ /**
48
+ * @param {PeerDirectoryQuery} params
49
+ * @returns {Promise<readonly PeerDirectoryEntry[]>}
50
+ */
51
+ async listPeers(params) {
52
+ return await (isDefaultPeerQuery(params)
53
+ ? this.#repository.listPeers(params)
54
+ : this.#onlineRepository.listPeers(params));
55
+ }
56
+ /**
57
+ * @param {FeedQuery} params
58
+ * @returns {Promise<readonly FeedItem[]>}
59
+ */
60
+ async listFeed(params) {
61
+ return await (isDefaultFeedQuery(params)
62
+ ? this.#repository.listFeed(params)
63
+ : this.#onlineRepository.listFeed(params));
64
+ }
65
+ /**
66
+ * @returns {Promise<readonly OperationalSummary[]>}
67
+ */
68
+ async readOperational() {
69
+ const summary = await this.#repository.readOperationalSummary();
70
+ return summary === undefined ? [] : [summary];
71
+ }
72
+ }
73
+
74
+ const DEFAULT_LIST_LIMIT = 50;
75
+
76
+ /** @param {PeerDirectoryQuery} params */
77
+ function isDefaultPeerQuery(params) {
78
+ return params.status === undefined
79
+ && (params.limit ?? DEFAULT_LIST_LIMIT) === DEFAULT_LIST_LIMIT;
80
+ }
81
+
82
+ /** @param {FeedQuery} params */
83
+ function isDefaultFeedQuery(params) {
84
+ return params.before === undefined
85
+ && params.unreadOnly === undefined
86
+ && (params.limit ?? DEFAULT_LIST_LIMIT) === DEFAULT_LIST_LIMIT;
87
+ }
@@ -1,6 +1,24 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {import("@econ-v1/ports").HomeNodeCandidate} HomeNodeCandidate
5
+ */
6
+ /**
7
+ * @typedef {import("@econ-v1/ports").HomeNodeClient} HomeNodeClient
8
+ */
9
+ /**
10
+ * @typedef {{ readonly directAddresses: readonly string[]; readonly did: string; readonly irohNodeId: string; readonly observedAt: string; }} PeerAdvertisement
11
+ */
12
+ /**
13
+ * @typedef {{ readonly iroh_direct_addresses?: unknown; readonly iroh_endpoint_id?: unknown; readonly listening_addresses?: unknown; readonly node_id?: unknown; }} NodeMetadataWire
14
+ */
1
15
  const MAX_DIRECT_ADDRESSES = 8;
2
16
  export class PeerDirectoryService {
3
17
  #peers = new Map();
18
+ /**
19
+ * @param {PeerAdvertisement} advertisement
20
+ * @returns {boolean}
21
+ */
4
22
  upsert(advertisement) {
5
23
  const normalized = normalizeAdvertisement(advertisement);
6
24
  const current = this.#peers.get(normalized.irohNodeId);
@@ -9,15 +27,30 @@ export class PeerDirectoryService {
9
27
  this.#peers.set(normalized.irohNodeId, normalized);
10
28
  return true;
11
29
  }
30
+ /**
31
+ * @param {string} irohNodeId
32
+ * @returns {boolean}
33
+ */
12
34
  remove(irohNodeId) {
13
35
  return this.#peers.delete(irohNodeId);
14
36
  }
37
+ /**
38
+ * @param {string} irohNodeId
39
+ * @returns {PeerAdvertisement | undefined}
40
+ */
15
41
  get(irohNodeId) {
16
42
  return this.#peers.get(irohNodeId);
17
43
  }
44
+ /**
45
+ * @returns {readonly PeerAdvertisement[]}
46
+ */
18
47
  list() {
19
48
  return [...this.#peers.values()].sort((left, right) => right.observedAt.localeCompare(left.observedAt));
20
49
  }
50
+ /**
51
+ * @param {HomeNodeClient} home
52
+ * @returns {Promise<readonly HomeNodeCandidate[]>}
53
+ */
21
54
  async refreshLanCandidates(home) {
22
55
  const metadata = await home.invoke({
23
56
  operation: "GET /api/v2/node/metadata",
@@ -26,6 +59,10 @@ export class PeerDirectoryService {
26
59
  return nodeMetadataCandidates(metadata);
27
60
  }
28
61
  }
62
+ /**
63
+ * @param {PeerAdvertisement} advertisement
64
+ * @returns {PeerAdvertisement}
65
+ */
29
66
  export function normalizeAdvertisement(advertisement) {
30
67
  requireText(advertisement.did, "Peer advertisement did");
31
68
  requireText(advertisement.irohNodeId, "Peer advertisement irohNodeId");
@@ -37,6 +74,10 @@ export function normalizeAdvertisement(advertisement) {
37
74
  observedAt: advertisement.observedAt,
38
75
  };
39
76
  }
77
+ /**
78
+ * @param {NodeMetadataWire} metadata
79
+ * @returns {readonly HomeNodeCandidate[]}
80
+ */
40
81
  export function nodeMetadataCandidates(metadata) {
41
82
  const id = textOrUndefined(metadata.iroh_endpoint_id) ?? textOrUndefined(metadata.node_id);
42
83
  if (id === undefined)
@@ -47,25 +88,54 @@ export function nodeMetadataCandidates(metadata) {
47
88
  ]).slice(0, MAX_DIRECT_ADDRESSES);
48
89
  return addresses.map((address) => ({ address, id, path: "lan-direct" }));
49
90
  }
91
+ /**
92
+ * @param {readonly string[]} addresses
93
+ * @returns {readonly string[]}
94
+ */
50
95
  export function uniqueAddresses(addresses) {
51
96
  return [...new Set(addresses.map((address) => address.trim()).filter((address) => address.length > 0))];
52
97
  }
98
+ /**
99
+ * @param {readonly string[]} addresses
100
+ * @returns {readonly string[]}
101
+ */
53
102
  export function secureAddresses(addresses) {
54
103
  return uniqueAddresses(addresses)
55
104
  .map(toSecureAddress)
56
- .filter((address) => address !== undefined)
105
+ .filter(/**
106
+ * @returns {address is string}
107
+ */ (address) => address !== undefined)
57
108
  .slice(0, MAX_DIRECT_ADDRESSES);
58
109
  }
110
+ /**
111
+ * @param {unknown} value
112
+ * @returns {readonly string[]}
113
+ */
59
114
  function stringArray(value) {
60
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
115
+ return Array.isArray(value) ? value.filter(/**
116
+ * @returns {item is string}
117
+ */ (item) => typeof item === "string") : [];
61
118
  }
119
+ /**
120
+ * @param {unknown} value
121
+ * @returns {string | undefined}
122
+ */
62
123
  function textOrUndefined(value) {
63
124
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
64
125
  }
126
+ /**
127
+ * @param {string} value
128
+ * @param {string} label
129
+ * @returns {void}
130
+ */
65
131
  function requireText(value, label) {
66
132
  if (typeof value !== "string" || value.trim().length === 0)
67
133
  throw new TypeError(`${label} must be non-empty`);
68
134
  }
135
+ /**
136
+ * @param {string} address
137
+ * @returns {string | undefined}
138
+ */
69
139
  function toSecureAddress(address) {
70
140
  const value = address.includes("://") ? address : `https://${address}`;
71
141
  const match = /^https:\/\/([^/?#:\s]+)(?::(\d{1,5}))?(?:\/)?$/u.exec(value);
@@ -76,4 +146,3 @@ function toSecureAddress(address) {
76
146
  return undefined;
77
147
  return port === undefined ? `https://${match[1]}` : `https://${match[1]}:${port}`;
78
148
  }
79
- //# sourceMappingURL=peer-directory-service.js.map
@@ -0,0 +1,108 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {import("@econ-v1/ports").CryptoPort} CryptoPort */
4
+ /** @typedef {import("../models/release-manifest.js").ReleaseManifestAsset} ReleaseManifestAsset */
5
+ /** @typedef {import("../models/release-manifest.js").SignedReleaseManifest} SignedReleaseManifest */
6
+
7
+ import { canonicalOutboxPayload } from "../models/outbox.js";
8
+ import {
9
+ parseSignedReleaseManifest,
10
+ unsignedReleaseManifestToJson,
11
+ } from "../models/release-manifest.js";
12
+ import { encodeUtf8 } from "./runtime-globals.js";
13
+
14
+ /** @type {readonly ["malformed", "bad-signature", "hash-mismatch", "incompatible-protocol", "incompatible-schema", "channel-not-allowed"]} */
15
+ export const RELEASE_MANIFEST_REJECTION_REASONS = [
16
+ "malformed",
17
+ "bad-signature",
18
+ "hash-mismatch",
19
+ "incompatible-protocol",
20
+ "incompatible-schema",
21
+ "channel-not-allowed",
22
+ ];
23
+
24
+ /** @typedef {(typeof RELEASE_MANIFEST_REJECTION_REASONS)[number]} ReleaseManifestRejectionReason */
25
+ /** @typedef {{ readonly manifest: SignedReleaseManifest, readonly ok: true } | { readonly ok: false, readonly reason: ReleaseManifestRejectionReason }} ReleaseManifestVerification */
26
+ /** @typedef {{ readonly keyId: string, readonly publicKey: Uint8Array }} ReleaseManifestTrustedKey */
27
+ /** @typedef {{ readonly allowedChannels: readonly string[], readonly cryptoPort: Pick<CryptoPort, "digestSha256" | "verifyEd25519">, readonly currentProtocolVersion: number, readonly currentStorageSchemaVersion: number }} ReleaseManifestVerifyOptions */
28
+
29
+ /**
30
+ * @param {unknown} candidate
31
+ * @param {readonly ReleaseManifestTrustedKey[]} trustedPublicKeys
32
+ * @param {ReleaseManifestVerifyOptions} options
33
+ * @returns {Promise<ReleaseManifestVerification>}
34
+ */
35
+ export async function verifyReleaseManifest(candidate, trustedPublicKeys, options) {
36
+ const manifest = parseSignedReleaseManifest(candidate);
37
+ if (manifest === undefined) return { ok: false, reason: "malformed" };
38
+
39
+ const trustedKey = trustedPublicKeys.find((key) => key.keyId === manifest.signingKeyId);
40
+ if (trustedKey === undefined) return { ok: false, reason: "bad-signature" };
41
+
42
+ const signatureBytes = hexToBytes(manifest.signature);
43
+ if (signatureBytes === undefined) return { ok: false, reason: "malformed" };
44
+
45
+ const canonicalPayload = encodeUtf8(
46
+ canonicalOutboxPayload(unsignedReleaseManifestToJson(manifest)),
47
+ );
48
+ /** @type {boolean} */
49
+ let signatureValid;
50
+ try {
51
+ signatureValid = await options.cryptoPort.verifyEd25519(canonicalPayload, signatureBytes, trustedKey.publicKey);
52
+ } catch {
53
+ return { ok: false, reason: "bad-signature" };
54
+ }
55
+ if (!signatureValid) return { ok: false, reason: "bad-signature" };
56
+
57
+ const expectedShellVersion = await deriveShellVersion(manifest.shellAssets, options.cryptoPort);
58
+ if (expectedShellVersion !== manifest.shellVersion) return { ok: false, reason: "hash-mismatch" };
59
+
60
+ if (!options.allowedChannels.includes(manifest.channel)) return { ok: false, reason: "channel-not-allowed" };
61
+
62
+ if (isOutOfRange(options.currentProtocolVersion, manifest.protocolCompatibility)) {
63
+ return { ok: false, reason: "incompatible-protocol" };
64
+ }
65
+
66
+ if (isOutOfRange(options.currentStorageSchemaVersion, manifest.storageSchemaCompatibility)) {
67
+ return { ok: false, reason: "incompatible-schema" };
68
+ }
69
+
70
+ return { manifest, ok: true };
71
+ }
72
+
73
+ /**
74
+ * @param {readonly ReleaseManifestAsset[]} shellAssets
75
+ * @param {Pick<CryptoPort, "digestSha256">} cryptoPort
76
+ * @returns {Promise<string>}
77
+ */
78
+ async function deriveShellVersion(shellAssets, cryptoPort) {
79
+ const sorted = [...shellAssets].sort((left, right) => left.url.localeCompare(right.url));
80
+ const payload = JSON.stringify(sorted.map((asset) => ({ sha256: asset.sha256, url: asset.url })));
81
+ const digest = await cryptoPort.digestSha256(encodeUtf8(payload));
82
+ return digest.slice(0, 16);
83
+ }
84
+
85
+ /**
86
+ * @param {number} value
87
+ * @param {{ readonly max: number, readonly min: number }} range
88
+ * @returns {boolean}
89
+ */
90
+ function isOutOfRange(value, range) {
91
+ return value < range.min || value > range.max;
92
+ }
93
+
94
+ /**
95
+ * @param {string} hex
96
+ * @returns {Uint8Array | undefined}
97
+ */
98
+ function hexToBytes(hex) {
99
+ if (hex.length % 2 !== 0) return undefined;
100
+ const bytes = new Uint8Array(hex.length / 2);
101
+ for (let index = 0; index < bytes.length; index += 1) {
102
+ const byteHex = hex.slice(index * 2, index * 2 + 2);
103
+ const byte = Number.parseInt(byteHex, 16);
104
+ if (Number.isNaN(byte)) return undefined;
105
+ bytes[index] = byte;
106
+ }
107
+ return bytes;
108
+ }