@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,64 @@
1
+ // @ts-check
2
+ /** @import { HomeNodeClient } from "@econ-v1/ports" */
3
+ /** @typedef {| "bootstrap-required" | "needs-onboarding" | "needs-pairing" | "ready"} OnboardingLifecycleState */
4
+ /** @typedef {{ readonly id: "onboarding-status"; readonly invitationsEnabled: boolean; readonly scenario?: "new"; readonly state: OnboardingLifecycleState; }} OnboardingLifecycleStatusRow */
5
+ /** @typedef {{ readonly claimed?: unknown; readonly invitations_enabled?: unknown; }} OnboardingStatusWire */
6
+ /** @typedef {{ readonly hasActiveSession: () => boolean; readonly locallyOnboarded?: () => Promise<boolean>; readonly node: Pick<HomeNodeClient, "invoke">; }} OnboardingStatusServiceOptions */
7
+ import { TransportError } from "../errors/index.js";
8
+ export class OnboardingStatusService {
9
+ /** @type {() => boolean} */
10
+ #hasActiveSession;
11
+ /** @type {(() => Promise<boolean>) | undefined} */
12
+ #locallyOnboarded;
13
+ /** @type {Pick<HomeNodeClient, "invoke">} */
14
+ #node;
15
+ #invitationsEnabled = false;
16
+ /** @param {OnboardingStatusServiceOptions} options */
17
+ constructor(options) {
18
+ this.#hasActiveSession = options.hasActiveSession;
19
+ this.#locallyOnboarded = options.locallyOnboarded;
20
+ this.#node = options.node;
21
+ }
22
+ /** Synchronous read of the last flag the node reported. Injected into
23
+ * `OnboardingFlowService` as a getter so a value arriving after boot still takes effect. */
24
+ /** @returns {boolean} */
25
+ invitationsEnabled() {
26
+ return this.#invitationsEnabled;
27
+ }
28
+ /** @returns {Promise<OnboardingLifecycleStatusRow>} */
29
+ async status() {
30
+ if (this.#hasActiveSession()) {
31
+ return { id: "onboarding-status", invitationsEnabled: this.#invitationsEnabled, state: "ready" };
32
+ }
33
+ try {
34
+ if (await this.#locallyOnboarded?.() === true) {
35
+ return { id: "onboarding-status", invitationsEnabled: this.#invitationsEnabled, state: "ready" };
36
+ }
37
+ }
38
+ catch {
39
+ // fall through to the network derivation
40
+ }
41
+ /** @type {boolean | undefined} */
42
+ let claimed;
43
+ try {
44
+ const response = await this.#node.invoke({
45
+ operation: "GET /api/auth/onboarding-status",
46
+ payload: {},
47
+ });
48
+ claimed = typeof response.claimed === "boolean" ? response.claimed : undefined;
49
+ this.#invitationsEnabled = response.invitations_enabled === true;
50
+ }
51
+ catch (error) {
52
+ if (error instanceof TransportError) {
53
+ return { id: "onboarding-status", invitationsEnabled: this.#invitationsEnabled, state: "bootstrap-required" };
54
+ }
55
+ return { id: "onboarding-status", invitationsEnabled: this.#invitationsEnabled, state: "needs-pairing" };
56
+ }
57
+ const invitationsEnabled = this.#invitationsEnabled;
58
+ if (claimed === undefined)
59
+ return { id: "onboarding-status", invitationsEnabled, state: "needs-pairing" };
60
+ return claimed
61
+ ? { id: "onboarding-status", invitationsEnabled, state: "needs-pairing" }
62
+ : { id: "onboarding-status", invitationsEnabled, scenario: "new", state: "needs-onboarding" };
63
+ }
64
+ }
@@ -1,28 +1,89 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {import("../models/outbox.js").OutboxEntry} OutboxEntry
5
+ */
6
+ /**
7
+ * @typedef {import("../models/outbox.js").OutboxIntentInput} OutboxIntentInput
8
+ */
9
+ /**
10
+ * @typedef {import("../models/outbox.js").OutboxState} OutboxState
11
+ */
12
+ /**
13
+ * @typedef {{ create(entry: OutboxEntry): Promise<void>; delete(id: string): Promise<void>; list(domain?: string): Promise<readonly OutboxEntry[]>; read(id: string): Promise<OutboxEntry | undefined>; update(entry: OutboxEntry): Promise<void>; }} OutboxPersistence
14
+ */
15
+ /**
16
+ * @typedef {{ readonly maxAttempts?: number; readonly now?: () => string; }} OutboxServiceOptions
17
+ */
18
+ /**
19
+ * @typedef {{ readonly leaseMs?: number; readonly now?: string; readonly revalidateAuthoritative?: () => Promise<boolean>; }} ClaimOptions
20
+ */
21
+ /**
22
+ * @typedef {{ readonly now?: string; readonly owner?: string; readonly retryable: boolean; }} FailureOptions
23
+ */
1
24
  import { canonicalOutboxPayload, createOutboxIntent, validateOutboxIntent, } from "../models/outbox.js";
25
+ /**
26
+ * @extends {Error}
27
+ */
2
28
  export class OutboxIdempotencyConflictError extends Error {
29
+ /**
30
+ * @readonly
31
+ * @type {string}
32
+ */
3
33
  idempotencyKey;
34
+ /**
35
+ * @param {string} idempotencyKey
36
+ */
4
37
  constructor(idempotencyKey) {
5
38
  super("Outbox idempotency key belongs to a different command");
6
39
  this.name = "OutboxIdempotencyConflictError";
7
40
  this.idempotencyKey = idempotencyKey;
8
41
  }
9
42
  }
43
+ /**
44
+ * @extends {Error}
45
+ */
10
46
  export class OutboxTransitionError extends Error {
47
+ /**
48
+ * @param {string} message
49
+ */
11
50
  constructor(message) {
12
51
  super(message);
13
52
  this.name = "OutboxTransitionError";
14
53
  }
15
54
  }
55
+ /**
56
+ * @extends {OutboxTransitionError}
57
+ */
16
58
  export class OutboxRevalidationRequiredError extends OutboxTransitionError {
59
+ /**
60
+ * @param {string} id
61
+ */
17
62
  constructor(id) {
18
63
  super(`Authoritative outbox intent ${id} requires fresh-state revalidation`);
19
64
  this.name = "OutboxRevalidationRequiredError";
20
65
  }
21
66
  }
22
67
  export class OutboxService {
68
+ /**
69
+ * @readonly
70
+ * @type {number}
71
+ */
23
72
  #maxAttempts;
73
+ /**
74
+ * @readonly
75
+ * @type {() => string}
76
+ */
24
77
  #now;
78
+ /**
79
+ * @readonly
80
+ * @type {OutboxPersistence}
81
+ */
25
82
  #persistence;
83
+ /**
84
+ * @param {OutboxPersistence} persistence
85
+ * @param {OutboxServiceOptions} [options]
86
+ */
26
87
  constructor(persistence, options = {}) {
27
88
  this.#maxAttempts = options.maxAttempts ?? 3;
28
89
  if (!Number.isInteger(this.#maxAttempts) || this.#maxAttempts < 1) {
@@ -31,6 +92,10 @@ export class OutboxService {
31
92
  this.#now = options.now ?? (() => new Date().toISOString());
32
93
  this.#persistence = persistence;
33
94
  }
95
+ /**
96
+ * @param {OutboxIntentInput} input
97
+ * @returns {Promise<OutboxEntry>}
98
+ */
34
99
  async enqueue(input) {
35
100
  const entry = createOutboxIntent(input, { maxAttempts: this.#maxAttempts, now: this.#now() });
36
101
  const existing = (await this.#persistence.list()).find((candidate) => candidate.idempotencyKey === entry.idempotencyKey);
@@ -42,6 +107,12 @@ export class OutboxService {
42
107
  await this.#persistence.create(entry);
43
108
  return entry;
44
109
  }
110
+ /**
111
+ * @param {string} domain
112
+ * @param {string} owner
113
+ * @param {ClaimOptions} [options]
114
+ * @returns {Promise<OutboxEntry | undefined>}
115
+ */
45
116
  async claimNext(domain, owner, options = {}) {
46
117
  requireNonEmpty(owner, "lease owner");
47
118
  const now = options.now ?? this.#now();
@@ -71,11 +142,22 @@ export class OutboxService {
71
142
  }
72
143
  return undefined;
73
144
  }
145
+ /**
146
+ * @param {string} id
147
+ * @param {string} owner
148
+ * @returns {Promise<OutboxEntry>}
149
+ */
74
150
  async acknowledge(id, owner, now = this.#now()) {
75
151
  const current = await this.requireEntry(id);
76
152
  this.assertInflightOwner(current, owner);
77
153
  return await this.save(current, { lease: undefined, state: "acked", updatedAt: now });
78
154
  }
155
+ /**
156
+ * @param {string} id
157
+ * @param {string} reason
158
+ * @param {FailureOptions} options
159
+ * @returns {Promise<OutboxEntry>}
160
+ */
79
161
  async fail(id, reason, options) {
80
162
  requireNonEmpty(reason, "failure reason");
81
163
  const current = await this.requireEntry(id);
@@ -88,6 +170,12 @@ export class OutboxService {
88
170
  ? { lease: undefined, state: "failed", terminalReason: reason, updatedAt: options.now ?? this.#now() }
89
171
  : { lease: undefined, state: "pending", updatedAt: options.now ?? this.#now() });
90
172
  }
173
+ /**
174
+ * @param {string} id
175
+ * @param {string} reason
176
+ * @param {string} [owner]
177
+ * @returns {Promise<OutboxEntry>}
178
+ */
91
179
  async conflict(id, reason, owner, now = this.#now()) {
92
180
  requireNonEmpty(reason, "conflict reason");
93
181
  const current = await this.requireEntry(id);
@@ -97,6 +185,10 @@ export class OutboxService {
97
185
  throw new OutboxTransitionError(`Outbox intent ${id} is not inflight`);
98
186
  return await this.save(current, { conflictReason: reason, lease: undefined, state: "conflict", updatedAt: now });
99
187
  }
188
+ /**
189
+ * @param {string} id
190
+ * @returns {Promise<boolean>}
191
+ */
100
192
  async cancel(id) {
101
193
  const current = await this.#persistence.read(id);
102
194
  if (current === undefined)
@@ -106,6 +198,10 @@ export class OutboxService {
106
198
  await this.#persistence.delete(id);
107
199
  return true;
108
200
  }
201
+ /**
202
+ * @param {string} [domain]
203
+ * @returns {Promise<number>}
204
+ */
109
205
  async recoverExpiredLeases(now = this.#now(), domain) {
110
206
  let recovered = 0;
111
207
  for (const entry of await this.#persistence.list(domain)) {
@@ -116,36 +212,71 @@ export class OutboxService {
116
212
  }
117
213
  return recovered;
118
214
  }
215
+ /**
216
+ * @private
217
+ * @param {string} id
218
+ * @returns {Promise<OutboxEntry>}
219
+ */
119
220
  async requireEntry(id) {
120
221
  const entry = await this.#persistence.read(id);
121
222
  if (entry === undefined)
122
223
  throw new OutboxTransitionError(`Outbox intent was not found: ${id}`);
123
224
  return validateOutboxIntent(entry);
124
225
  }
226
+ /**
227
+ * @private
228
+ * @param {OutboxEntry} entry
229
+ * @param {string} owner
230
+ * @returns {void}
231
+ */
125
232
  assertInflightOwner(entry, owner) {
126
233
  if (entry.state !== "inflight" || entry.lease?.owner !== owner) {
127
234
  throw new OutboxTransitionError(`Outbox lease is not held by ${owner}`);
128
235
  }
129
236
  }
237
+ /**
238
+ * @private
239
+ * @param {OutboxEntry} current
240
+ * @param {Omit<Partial<OutboxEntry>, "lease"> & { readonly lease?: OutboxEntry["lease"]; readonly state: OutboxState; }} changes
241
+ * @returns {Promise<OutboxEntry>}
242
+ */
130
243
  async save(current, changes) {
131
244
  const updated = validateOutboxIntent({ ...current, ...changes });
132
245
  await this.#persistence.update(updated);
133
246
  return updated;
134
247
  }
135
248
  }
249
+ /**
250
+ * @param {OutboxEntry} left
251
+ * @param {OutboxEntry} right
252
+ * @returns {number}
253
+ */
136
254
  function compareEntries(left, right) {
137
255
  return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
138
256
  }
257
+ /**
258
+ * @param {OutboxState} state
259
+ * @returns {boolean}
260
+ */
139
261
  function isTerminal(state) {
140
262
  return state === "acked" || state === "failed" || state === "conflict";
141
263
  }
264
+ /**
265
+ * @param {OutboxEntry} left
266
+ * @param {OutboxEntry} right
267
+ * @returns {boolean}
268
+ */
142
269
  function sameCommand(left, right) {
143
270
  return left.domain === right.domain &&
144
271
  left.authority === right.authority &&
145
272
  canonicalOutboxPayload(left.payload) === canonicalOutboxPayload(right.payload);
146
273
  }
274
+ /**
275
+ * @param {string} value
276
+ * @param {string} label
277
+ * @returns {void}
278
+ */
147
279
  function requireNonEmpty(value, label) {
148
280
  if (value.trim() === "")
149
281
  throw new TypeError(`${label} must be a non-empty string`);
150
282
  }
151
- //# sourceMappingURL=outbox-service.js.map
@@ -0,0 +1,347 @@
1
+ // @ts-check
2
+ /** @typedef {import('@econ-v1/ports').DelayPort} DelayPort */
3
+ /** @typedef {import('@econ-v1/ports').HomeNodeClient} HomeNodeClient */
4
+ /** @typedef {import('@econ-v1/ports').OwnerClaimCryptoPort} OwnerClaimCryptoPort */
5
+ /** @typedef {import('@econ-v1/ports').OwnerClaimSessionPort} OwnerClaimSessionPort */
6
+ /** @typedef {import('@econ-v1/ports').OwnerClaimSessionTokens} OwnerClaimSessionTokens */
7
+ const POLL_INTERVAL_MS = 2000;
8
+ const CONFIRMATION_TIMEOUT_MS = 5 * 60 * 1000;
9
+ export class OwnerClaimService {
10
+ /**
11
+ * @readonly
12
+ */
13
+ #crypto;
14
+ /**
15
+ * @readonly
16
+ */
17
+ #delay;
18
+ /**
19
+ * @readonly
20
+ */
21
+ #node;
22
+ /**
23
+ * @readonly
24
+ */
25
+ #session;
26
+ /** @type {{ confirmed: boolean, operationHash: string | undefined, requiresConfirmation: boolean } | undefined} */
27
+ #pending;
28
+ /**
29
+ * @param {OwnerClaimServiceOptions} options
30
+ */
31
+ constructor(options) {
32
+ this.#crypto = options.keys;
33
+ this.#delay = options.delay;
34
+ this.#node = options.node;
35
+ this.#session = options.session;
36
+ }
37
+ /**
38
+ * Generates this attempt's ephemeral key and asks the node to start its
39
+ * physical-confirmation gate for it. Safe to call again after a failed `completeClaim()` or
40
+ * an expired code — each call replaces whatever claim was previously pending, which is
41
+ * exactly what step 10's "Request a new code" needs.
42
+ *
43
+ * `accessCode`, when supplied, is sent as `access_code` — the shared secret that lets the
44
+ * node bind and auto-confirm this operation without a physical LCD tap. It is omitted from
45
+ * the payload entirely (never sent as `undefined`/`null`) when not supplied, and it is never
46
+ * logged.
47
+ * @param {string} [accessCode]
48
+ * @returns {Promise<OwnerClaimPrepareOutcome>}
49
+ */
50
+ async prepareClaim(accessCode) {
51
+ const publicKeyHex = await this.#crypto.publicKey();
52
+ const prepared = await this.#node.invoke({
53
+ operation: "POST /api/v2/node/onboarding-prepare",
54
+ payload: {
55
+ public_key_multibase: publicKeyHex,
56
+ ...(accessCode !== undefined ? { access_code: accessCode } : {}),
57
+ },
58
+ });
59
+ const requiresConfirmation = prepared.requires_lcd === true;
60
+ const confirmed = prepared.confirmed === true;
61
+ this.#pending = {
62
+ confirmed,
63
+ operationHash: prepared.operation_hash,
64
+ requiresConfirmation,
65
+ };
66
+ return {
67
+ confirmed,
68
+ lcdHardware: prepared.lcd_hardware === true,
69
+ matchCode: prepared.match_code ?? "",
70
+ operationHash: prepared.operation_hash,
71
+ requiresConfirmation,
72
+ };
73
+ }
74
+ /**
75
+ * Waits for the node's physical confirmation. This is the ONLY thing that can put the
76
+ * pending claim into a completable state — no browser click, no timer, no LCD-pane tap
77
+ * substitutes for the node reporting `confirmed`.
78
+ *
79
+ * The poll cadence comes from the injected `DelayPort`, so this contains no timer of its
80
+ * own and stays testable with a zero-wait fake.
81
+ * @param {string} operationHash
82
+ * @returns {Promise<OwnerClaimConfirmation>}
83
+ */
84
+ async awaitConfirmation(operationHash) {
85
+ const deadline = Date.now() + CONFIRMATION_TIMEOUT_MS;
86
+ for (;;) {
87
+ const status = await this.#node.invoke({
88
+ operation: `GET /api/v2/node/operation-status/${encodeURIComponent(operationHash)}`,
89
+ payload: {},
90
+ });
91
+ if (status.confirmed === true) {
92
+ // Only the claim this hash actually belongs to may be unlocked: `prepareClaim()` can
93
+ // be called again ("Request a new code") while an earlier poll is still in flight, and
94
+ // a stale confirmation must never carry over onto the claim that superseded it.
95
+ const pending = this.#pending;
96
+ if (pending !== undefined && pending.operationHash === operationHash)
97
+ pending.confirmed = true;
98
+ return "confirmed";
99
+ }
100
+ if (status.status === "rejected") return "rejected";
101
+ if (Date.now() >= deadline) return "timed-out";
102
+ await this.#delay.wait(POLL_INTERVAL_MS);
103
+ }
104
+ }
105
+ /**
106
+ * Runs the legacy challenge -> sign -> `complete-onboarding` sequence to actually create the
107
+ * owner + DID, using the identity supplied HERE rather than one captured at prepare time.
108
+ * Resolves `"claimed"` only when the node's own completion call reported success.
109
+ *
110
+ * RETRY-SAFE BY DESIGN: neither `#pending` nor the ephemeral key is touched until the node's
111
+ * completion call is known to have SUCCEEDED. A thrown challenge/complete call, or an explicit
112
+ * `success: false`, leaves `#pending` (and the key) exactly as they were, so the caller can
113
+ * invoke `completeClaim()` again on this same instance — reaching the node a second time
114
+ * without a new `prepareClaim()` — for as long as the underlying failure might be transient
115
+ * (a dropped connection, a momentarily-busy node). Clearing state on failure, or in a
116
+ * `finally`, would turn every transient hiccup into a forced restart of the whole claim.
117
+ * @param {OwnerClaimIdentity} identity
118
+ * @returns {Promise<OwnerClaimOutcome>}
119
+ */
120
+ async completeClaim(identity) {
121
+ const pending = this.#pending;
122
+ if (pending === undefined) return "not-prepared";
123
+ // Refuse without touching the node: an owner record must never be attempted, let alone
124
+ // created, before the node itself reported the physical confirmation.
125
+ if (pending.requiresConfirmation && !pending.confirmed) return "rejected";
126
+ const onboardingConfirmation = pending.requiresConfirmation
127
+ ? pending.operationHash
128
+ : undefined;
129
+ /** @type {OnboardingCompleteWire} */
130
+ let result;
131
+ try {
132
+ const challenge = await this.#node.invoke({
133
+ operation: "POST /api/auth/onboarding-challenge",
134
+ payload: {},
135
+ });
136
+ const signature = await this.#crypto.sign(challenge.challenge);
137
+ const publicKeyHex = await this.#crypto.publicKey();
138
+ // OPTIONAL and best-effort: a descriptor-build failure must never turn into a rejected
139
+ // claim, so it gets its own inner try/catch rather than sharing the outer one (which
140
+ // would still swallow it into "rejected" — the same outcome, but for the wrong reason,
141
+ // and the outer catch's comment promises retry-safety specifically for the challenge/
142
+ // complete round trip, not for this). `?.` also covers session ports (like
143
+ // `createNullOwnerClaimSession()`) that never implement the optional member at all.
144
+ let genesisDevice;
145
+ try {
146
+ genesisDevice = await this.#session.describeGenesisDevice?.(challenge.challenge);
147
+ } catch {
148
+ genesisDevice = undefined;
149
+ }
150
+ result = await this.#node.invoke({
151
+ operation: "POST /api/auth/complete-onboarding",
152
+ payload: {
153
+ challenge_id: challenge.challenge_id,
154
+ device_info: this.#crypto.deviceInfo(),
155
+ first_name: identity.displayName,
156
+ last_name: "",
157
+ onboarding_confirmation: onboardingConfirmation,
158
+ public_key: publicKeyHex,
159
+ signature,
160
+ username: identity.username,
161
+ ...(genesisDevice === undefined ? {} : { genesis_device: genesisDevice }),
162
+ },
163
+ });
164
+ } catch {
165
+ // RETRY-SAFE: `#pending` and the ephemeral key survive untouched — see the doc comment.
166
+ return "rejected";
167
+ }
168
+ if (result.success !== true) return "rejected";
169
+ // PAST THIS LINE THE OWNER AND DID EXIST ON THE NODE. Nothing below may report the claim
170
+ // as failed — the worst honest answer is "claimed, but this browser is not paired". That is
171
+ // why the session work sits outside the try/catch above rather than inside it: a throw from
172
+ // `adopt` inside that block would have been swallowed into `"rejected"` and told a person
173
+ // their setup failed after it had already succeeded.
174
+ //
175
+ // This is also the ONLY point (besides `cancel()`) where the pending claim is dropped and
176
+ // the ephemeral key is reset — the claim it belonged to is now settled, one way or another.
177
+ this.#pending = undefined;
178
+ this.#crypto.reset();
179
+ const tokens = readSessionTokens(result);
180
+ if (tokens === undefined) return "claimed-unpaired";
181
+ try {
182
+ return (await this.#session.adopt(tokens))
183
+ ? "claimed"
184
+ : "claimed-unpaired";
185
+ } catch {
186
+ return "claimed-unpaired";
187
+ }
188
+ }
189
+ /** Drops any pending claim without contacting the node — used when the person abandons or
190
+ * restarts the flow before `completeClaim()` runs.
191
+ * @returns {void}
192
+ */
193
+ cancel() {
194
+ this.#pending = undefined;
195
+ this.#crypto.reset();
196
+ }
197
+ }
198
+ /**
199
+ * All THREE fields must be present and non-empty before any of them is used. A node that
200
+ * answers with a token but no expiry is a node whose session contract this client does not
201
+ * understand, and guessing an expiry would produce a session that silently dies mid-use.
202
+ *
203
+ * `refresh_token` is required-present for the same reason but deliberately NOT returned: see
204
+ * `OwnerClaimSessionTokens`.
205
+ * @param {OnboardingCompleteWire} wire
206
+ * @returns {OwnerClaimSessionTokens | undefined}
207
+ */
208
+ function readSessionTokens(wire) {
209
+ const accessToken = typeof wire.token === "string" ? wire.token.trim() : "";
210
+ const expiresAt =
211
+ typeof wire.token_expires_at === "string"
212
+ ? wire.token_expires_at.trim()
213
+ : "";
214
+ const refreshToken =
215
+ typeof wire.refresh_token === "string" ? wire.refresh_token.trim() : "";
216
+ if (
217
+ accessToken.length === 0 ||
218
+ expiresAt.length === 0 ||
219
+ refreshToken.length === 0
220
+ ) {
221
+ return undefined;
222
+ }
223
+ return { accessToken, expiresAt };
224
+ }
225
+ /** @typedef {"confirmed" | "rejected" | "timed-out"} OwnerClaimConfirmation */
226
+ /**
227
+ * `claimed-unpaired` is a SUCCESS, not a failure: the owner + DID now exist on the node. It
228
+ * says only that this browser did not end up holding a device session, so the shell must run
229
+ * its ordinary pairing flow. Collapsing it into `rejected` would tell a person their setup
230
+ * failed after it had already, irreversibly, succeeded.
231
+ * @typedef {| "claimed"
232
+ * | "claimed-unpaired"
233
+ * | "not-prepared"
234
+ * | "rejected"
235
+ * | "timed-out"} OwnerClaimOutcome
236
+ */
237
+ /**
238
+ * First-owner onboarding — "claim this unclaimed node" (#1815 DID-blocker fix).
239
+ *
240
+ * The PWA's onboarding-gate walks a person through identity + physical-confirmation
241
+ * screens, but until this service existed nothing behind them ever created an owner or
242
+ * a DID on the backend (`.superpowers/sdd/did-blocker-report.md`): `core.did.onboard` —
243
+ * the only code path that writes `did_documents` — is called exclusively from
244
+ * `AuthApplicationService::complete()`, which backs the legacy, JWT-shaped
245
+ * `POST /api/auth/complete-onboarding` (`system/ui`-only until now).
246
+ *
247
+ * This service drives that SAME legacy contract from the PWA, reusing the existing,
248
+ * already-reviewed backend surface rather than inventing a parallel owner-creation
249
+ * path (see the blocker report §5): an ephemeral secp256k1 keypair
250
+ * (`OwnerClaimCryptoPort` — deliberately distinct from the Ed25519 `IdentityPort` used
251
+ * for client-device pairing afterwards), `POST /api/v2/node/onboarding-prepare` to
252
+ * start the node's physical-confirmation gate, a poll of
253
+ * `GET /api/v2/node/operation-status/{hash}` for that confirmation, then
254
+ * `POST /api/auth/{onboarding-challenge,complete-onboarding}` to actually create the
255
+ * owner + DID.
256
+ *
257
+ * The claim now also carries the browser's OWN pairing identity: `completeClaim()` asks the
258
+ * session port for an optional signed `genesis_device` descriptor and forwards it verbatim to
259
+ * `complete-onboarding`, so the node's genesis DID device IS this browser (not a synthetic
260
+ * placeholder) and the client-device pairing that follows reconciles onto that same row instead
261
+ * of minting a duplicate. Building or forwarding it can never turn a successful claim into a
262
+ * rejected one — see the inner try/catch around it below.
263
+ *
264
+ * SECURITY: this service never decides whether the claim is authorized — the node
265
+ * does. `completeClaim()` only ever *attempts* the completion call; the host's
266
+ * `validate_physical_confirmation` gate (`core/domain/src/services/
267
+ * auth_application_service.rs`) is the sole authority on whether a real physical
268
+ * confirmation happened, and refuses the request otherwise
269
+ * (`enforce_physical_presence`, `core/adapters-network/src/http/server/
270
+ * device_management/proxy.rs`, refuses browser/remote confirmation of an "onboard"
271
+ * operation outright when the node has a screen — only a physical LCD tap counts).
272
+ * A caller of this service who never gets the node's screen tapped simply never
273
+ * reaches "claimed": `awaitConfirmation()` resolves `"rejected"`/`"timed-out"` and
274
+ * `completeClaim()` then refuses, rather than reporting success.
275
+ *
276
+ * Once a client's `prepareClaim()` succeeds, the service holds exactly one pending claim —
277
+ * matching the legacy flow's "ONE device keypair for the whole onboarding attempt"
278
+ * invariant (the node's confirmation is bound to the specific public key sent to
279
+ * `onboarding-prepare`, and the app-did miniapp allows only one pending "onboard"
280
+ * operation node-wide).
281
+ *
282
+ * SEQUENCING (design §6.1) — `prepare(identity)`/`complete()` were split into
283
+ * `prepareClaim()` / `awaitConfirmation()` / `completeClaim(identity)` because the flow puts
284
+ * the match code at step 10 and identity at step 12, deliberately: nothing is written until
285
+ * physical access has been proven. That split is possible with NO backend change because
286
+ * `onboarding-prepare`'s payload is only `{ public_key_multibase }` — the identity that used
287
+ * to be handed to `prepare()` was stashed purely client-side and read back at completion.
288
+ *
289
+ * The security property is preserved and strengthened: `completeClaim()` refuses outright
290
+ * (without contacting the node) unless `awaitConfirmation()` has already observed the node's
291
+ * own `confirmed` for THIS claim's operation hash, so no owner record can exist until physical
292
+ * confirmation AND identity submission have both happened, in that order. The node's
293
+ * `validate_physical_confirmation` gate remains the sole authority; this client-side guard
294
+ * only stops a pointless, misleading round trip.
295
+ * @typedef {object} OwnerClaimIdentity
296
+ * @property {string} displayName
297
+ * @property {string} username
298
+ */
299
+ /**
300
+ * @typedef {object} OwnerClaimPrepareOutcome
301
+ * @property {boolean} confirmed `true` when the node already bound and auto-confirmed this operation server-side (an
302
+ * access code was supplied and matched) — `awaitConfirmation()` is unnecessary and
303
+ * `completeClaim()` proceeds straight to the challenge/complete sequence, with no LCD
304
+ * match-code step in between.
305
+ * @property {boolean} lcdHardware
306
+ * @property {string} matchCode
307
+ * @property {string | undefined} operationHash The handle `awaitConfirmation()` polls and `completeClaim()` echoes back as
308
+ * `onboarding_confirmation`. Absent when the node's confirmation gate is disabled.
309
+ * @property {boolean} requiresConfirmation
310
+ */
311
+ /**
312
+ * @typedef {object} OwnerClaimServiceOptions
313
+ * @property {OwnerClaimCryptoPort} keys The ephemeral secp256k1 identity for this onboarding attempt (see `OwnerClaimCryptoPort`'s
314
+ * doc comment). Named `keys`, not `crypto`, so this stays a plain interface field rather
315
+ * than reading as a reference to the browser's `crypto` global.
316
+ * @property {Pick<DelayPort, "wait">} delay
317
+ * @property {Pick<HomeNodeClient, "invoke">} node
318
+ * @property {OwnerClaimSessionPort} session Turns the completion response's tokens into a real device session. Required, not optional:
319
+ * defaulting it would make a wiring mistake look like a node that simply issued no tokens.
320
+ */
321
+ /**
322
+ * @typedef {object} OnboardingPrepareWire
323
+ * @property {boolean} [confirmed] Present (and `true`) only when the node auto-confirmed this operation from a supplied
324
+ * access code; omitted entirely otherwise — never sent as an explicit `false`.
325
+ * @property {boolean} [lcd_hardware]
326
+ * @property {string} [operation_hash]
327
+ * @property {string} [match_code]
328
+ * @property {boolean} requires_lcd
329
+ */
330
+ /**
331
+ * @typedef {object} OperationStatusWire
332
+ * @property {boolean} confirmed
333
+ * @property {string} [status]
334
+ */
335
+ /**
336
+ * @typedef {object} OnboardingChallengeWire
337
+ * @property {string} challenge
338
+ * @property {string} challenge_id
339
+ */
340
+ /**
341
+ * @typedef {object} OnboardingCompleteWire
342
+ * @property {unknown} [refresh_token]
343
+ * @property {boolean} success
344
+ * @property {unknown} [token]
345
+ * @property {unknown} [token_expires_at]
346
+ */
347
+ // @ts-check