@econ-v1/domain 7.4.3 → 7.4.5

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.
@@ -1,43 +1,38 @@
1
1
  /** @typedef {import('@econ-v1/ports').HomeNodeClient} HomeNodeClient */
2
+ /** @typedef {import('@econ-v1/ports').OwnerClaimCryptoPort} OwnerClaimCryptoPort */
3
+ /** Claim-key proof for invitation admission; raw codes live only during the request. */
2
4
  export class InviteRedemptionService {
3
- /**
4
- * @param {InviteRedemptionServiceOptions} options
5
- */
5
+ /** @param {InviteRedemptionServiceOptions} options */
6
6
  constructor(options: InviteRedemptionServiceOptions);
7
- /**
8
- * @param {string} code
9
- * @returns {Promise<InviteRedemptionOutcome>}
10
- */
11
- redeem(code: string): Promise<InviteRedemptionOutcome>;
7
+ /** @param {string} code @param {string} [confirmation] @returns {Promise<InviteRedemptionOutcome>} */
8
+ redeem(code: string, confirmation?: string): Promise<InviteRedemptionOutcome>;
9
+ /** @returns {Promise<InviteRedemptionOutcome>} */
10
+ resume(): Promise<InviteRedemptionOutcome>;
12
11
  #private;
13
12
  }
14
13
  export type HomeNodeClient = import("@econ-v1/ports").HomeNodeClient;
15
- /**
16
- * Step 11's real action: redeem an invitation code against the node's PUBLIC
17
- * `POST /api/v2/invite/redeem` (`core/adapters-network/src/http/server/invite.rs`), which
18
- * takes `{ code }` or `{ token }` and needs no authentication — deliberately, because at step
19
- * 11 the browser has no session at all.
20
- *
21
- * The response's `receiver_name` is the name the INVITER registered for this person. Prefilling
22
- * step 12's display name with it (and locking it, see `OnboardingFlowService`) is what makes
23
- * the invitation meaningful rather than decorative: the person who was invited as "Ada
24
- * Lovelace" shows up on the inviter's node as Ada Lovelace. It is null for legacy
25
- * token-shaped invites, which is a normal, non-error case.
26
- *
27
- * Outcomes are closed and text-free. `"rejected"` covers expired, already-used, unknown and
28
- * malformed codes alike — the node's own message would tell an attacker which of those it was.
29
- */
14
+ export type OwnerClaimCryptoPort = import("@econ-v1/ports").OwnerClaimCryptoPort;
30
15
  export type InviteRedemptionOutcome = {
31
16
  readonly kind: "redeemed";
32
- readonly receiverName: string | undefined;
17
+ readonly receiverName: string;
33
18
  } | {
34
19
  readonly kind: "rejected";
20
+ } | {
21
+ readonly kind: "unavailable";
22
+ } | {
23
+ readonly kind: "confirmation-required";
24
+ } | {
25
+ readonly kind: "recovery";
35
26
  };
36
27
  export type InviteRedemptionServiceOptions = {
37
28
  node: Pick<HomeNodeClient, "invoke">;
38
- };
39
- export type InviteRedeemWire = {
40
- ok?: unknown;
41
- receiver_name?: unknown;
29
+ /**
30
+ * Same persisted identity used for owner completion.
31
+ */
32
+ keys: OwnerClaimCryptoPort;
33
+ /**
34
+ * SHA256 lowercase hex of the supplied UTF-8 string.
35
+ */
36
+ hashCode: (code: string) => Promise<string>;
42
37
  };
43
38
  //# sourceMappingURL=invite-redemption-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"invite-redemption-service.d.ts","sourceRoot":"","sources":["../../src/services/invite-redemption-service.js"],"names":[],"mappings":"AACA,wEAAwE;AACxE;IAKE;;OAEG;IACH,qBAFW,8BAA8B,EAIxC;IACD;;;OAGG;IACH,aAHW,MAAM,GACJ,OAAO,CAAC,uBAAuB,CAAC,CAoB5C;;CACF;6BAnCa,OAAO,gBAAgB,EAAE,cAAc;;;;;;;;;;;;;;;;sCAkDtC;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAChF;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE;;UAItB,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC;;;SAI9B,OAAO;oBACP,OAAO"}
1
+ {"version":3,"file":"invite-redemption-service.d.ts","sourceRoot":"","sources":["../../src/services/invite-redemption-service.js"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,oFAAoF;AAEpF,wFAAwF;AACxF;IAIE,sDAAsD;IACtD,qBADY,8BAA8B,EAKzC;IACD,sGAAsG;IACtG,aADY,MAAM,iBAAe,MAAM,GAA2B,OAAO,CAAC,uBAAuB,CAAC,CAKjG;IACD,kDAAkD;IAClD,UADc,OAAO,CAAC,uBAAuB,CAAC,CAG7C;;CA+BF;6BAtDa,OAAO,gBAAgB,EAAE,cAAc;mCACvC,OAAO,gBAAgB,EAAE,oBAAoB;sCAsD7C;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE;;UAG5M,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC;;;;UAC9B,oBAAoB;;;;cACpB,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC"}
@@ -98,7 +98,7 @@ export type OnboardingFlowApprovalState = "waiting" | "approved" | "rejected" |
98
98
  * node's outright refusal of this device's claim — the code is already bound to a different
99
99
  * device, or the supplied/missing code did not match.
100
100
  */
101
- export type OnboardingFlowReasonCode = "approval-expired" | "approval-rejected" | "wifi-credentials-rejected" | "wifi-scan-failed" | "wifi-unreachable" | "invitation-invalid" | "claim-failed" | "code-already-bound" | "access-code-invalid";
101
+ export type OnboardingFlowReasonCode = "approval-expired" | "approval-rejected" | "wifi-credentials-rejected" | "wifi-scan-failed" | "wifi-unreachable" | "invitation-invalid" | "invitation-unavailable" | "claim-failed" | "code-already-bound" | "access-code-invalid";
102
102
  export type OnboardingFlowRow = OnboardingFlowRowBase & ({
103
103
  readonly networks: readonly OnboardingFlowNetwork[];
104
104
  readonly probing: boolean;
@@ -131,7 +131,7 @@ export type OnboardingFlowRow = OnboardingFlowRowBase & ({
131
131
  readonly stepId: Exclude<OnboardingFlowStepId, "choose-network" | "handoff" | "identity" | "invitation" | "match-code" | "network-password" | "trusted">;
132
132
  });
133
133
  export type OnboardingFlowClaimPort = Pick<OwnerClaimService, "awaitConfirmation" | "cancel" | "completeClaim" | "prepareClaim">;
134
- export type OnboardingFlowInvitePort = Pick<InviteRedemptionService, "redeem">;
134
+ export type OnboardingFlowInvitePort = Pick<InviteRedemptionService, "redeem" | "resume">;
135
135
  export type OnboardingFlowWifiPort = Pick<WifiProvisioningService, "awaitHandoff" | "awaitReconnect" | "configure" | "scan" | "status">;
136
136
  export type OnboardingFlowLcdPort = Pick<LcdDecisionService, "confirm" | "reject">;
137
137
  export type OnboardingFlowNetwork = {
@@ -1 +1 @@
1
- {"version":3,"file":"onboarding-flow-service.d.ts","sourceRoot":"","sources":["../../src/services/onboarding-flow-service.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"onboarding-flow-service.d.ts","sourceRoot":"","sources":["../../src/services/onboarding-flow-service.js"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@ export class OnboardingStatusService {
4
4
  /** Synchronous read of the last flag the node reported. Injected into
5
5
  * `OnboardingFlowService` as a getter so a value arriving after boot still takes effect. */
6
6
  /** @returns {boolean} */
7
+ /** @returns {boolean} */
8
+ invitationRequired(): boolean;
7
9
  invitationsEnabled(): boolean;
8
10
  /** @returns {Promise<OnboardingLifecycleStatusRow>} */
9
11
  status(): Promise<OnboardingLifecycleStatusRow>;
@@ -19,6 +21,7 @@ export type OnboardingLifecycleStatusRow = {
19
21
  export type OnboardingStatusWire = {
20
22
  readonly claimed?: unknown;
21
23
  readonly invitations_enabled?: unknown;
24
+ readonly admission_policy?: unknown;
22
25
  };
23
26
  export type OnboardingStatusServiceOptions = {
24
27
  readonly hasActiveSession: () => boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"onboarding-status-service.d.ts","sourceRoot":"","sources":["../../src/services/onboarding-status-service.js"],"names":[],"mappings":"AAOA;IAQI,sDAAsD;IACtD,qBADY,8BAA8B,EAKzC;IACD;gGAC4F;IAC5F,yBAAyB;IACzB,sBADc,OAAO,CAGpB;IACD,uDAAuD;IACvD,UADc,OAAO,CAAC,4BAA4B,CAAC,CAmClD;;CACJ;uCA7DgB,oBAAoB,GAAG,kBAAkB,GAAG,eAAe,GAAG,OAAO;2CACvE;IAAE,QAAQ,CAAC,EAAE,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,wBAAwB,CAAC;CAAE;mCAChJ;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAAE;6CACvE;IAAE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;CAAE;oCAJ3H,gBAAgB"}
1
+ {"version":3,"file":"onboarding-status-service.d.ts","sourceRoot":"","sources":["../../src/services/onboarding-status-service.js"],"names":[],"mappings":"AAOA;IASI,sDAAsD;IACtD,qBADY,8BAA8B,EAKzC;IACD;gGAC4F;IAC5F,yBAAyB;IACzB,yBAAyB;IACzB,sBADc,OAAO,CACoC;IACzD,8BAEC;IACD,uDAAuD;IACvD,UADc,OAAO,CAAC,4BAA4B,CAAC,CAoClD;;CACJ;uCAjEgB,oBAAoB,GAAG,kBAAkB,GAAG,eAAe,GAAG,OAAO;2CACvE;IAAE,QAAQ,CAAC,EAAE,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,wBAAwB,CAAC;CAAE;mCAChJ;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAAE;6CAC5G;IAAE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;CAAE;oCAJ3H,gBAAgB"}
@@ -1 +1 @@
1
- {"version":3,"file":"owner-claim-service.d.ts","sourceRoot":"","sources":["../../src/services/owner-claim-service.js"],"names":[],"mappings":"AAQA;IAmBE;;OAEG;IACH,qBAFW,wBAAwB,EAOlC;IACD;;;;;;;;;;;;OAYG;IACH,0BAHW,MAAM,GACJ,OAAO,CAAC,wBAAwB,CAAC,CAyB7C;IACD;;;;;;;;;OASG;IACH,iCAHW,MAAM,GACJ,OAAO,CAAC,sBAAsB,CAAC,CAsB3C;IACD;;;;;;;;;;;;;;OAcG;IACH,wBAHW,kBAAkB,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAsEtC;IACD;;;OAGG;IACH,UAFa,IAAI,CAKhB;;CACF;wBAnMa,OAAO,gBAAgB,EAAE,SAAS;6BAClC,OAAO,gBAAgB,EAAE,cAAc;mCACvC,OAAO,gBAAgB,EAAE,oBAAoB;oCAC7C,OAAO,gBAAgB,EAAE,qBAAqB;sCAC9C,OAAO,gBAAgB,EAAE,uBAAuB;qCA2NhD,WAAW,GAAG,UAAU,GAAG,WAAW;;;;;;;gCAMrC,SAAS,GACjB,kBAAkB,GAClB,cAAc,GACd,UAAU,GACV,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6DJ,MAAM;cACN,MAAM;;;;;;;;;eAIN,OAAO;iBAIP,OAAO;eACP,MAAM;;;;;mBACN,MAAM,GAAG,SAAS;0BAElB,OAAO;;;;;;;;UAIP,oBAAoB;WAGpB,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;UACvB,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC;;;;;aAC9B,qBAAqB;;;;;;;gBAKrB,OAAO;mBAEP,OAAO;qBACP,MAAM;iBACN,MAAM;kBACN,OAAO;;;eAIP,OAAO;aACP,MAAM;;;eAIN,MAAM;kBACN,MAAM;;;oBAIN,OAAO;aACP,OAAO;YACP,OAAO;uBACP,OAAO"}
1
+ {"version":3,"file":"owner-claim-service.d.ts","sourceRoot":"","sources":["../../src/services/owner-claim-service.js"],"names":[],"mappings":"AAQA;IAmBE;;OAEG;IACH,qBAFW,wBAAwB,EAOlC;IACD;;;;;;;;;;;;OAYG;IACH,0BAHW,MAAM,GACJ,OAAO,CAAC,wBAAwB,CAAC,CAyB7C;IACD;;;;;;;;;OASG;IACH,iCAHW,MAAM,GACJ,OAAO,CAAC,sBAAsB,CAAC,CAsB3C;IACD;;;;;;;;;;;;;;OAcG;IACH,wBAHW,kBAAkB,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAsEtC;IACD;;;OAGG;IACH,UAFa,IAAI,CAMhB;;CACF;wBApMa,OAAO,gBAAgB,EAAE,SAAS;6BAClC,OAAO,gBAAgB,EAAE,cAAc;mCACvC,OAAO,gBAAgB,EAAE,oBAAoB;oCAC7C,OAAO,gBAAgB,EAAE,qBAAqB;sCAC9C,OAAO,gBAAgB,EAAE,uBAAuB;qCA4NhD,WAAW,GAAG,UAAU,GAAG,WAAW;;;;;;;gCAMrC,SAAS,GACjB,kBAAkB,GAClB,cAAc,GACd,UAAU,GACV,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6DJ,MAAM;cACN,MAAM;;;;;;;;;eAIN,OAAO;iBAIP,OAAO;eACP,MAAM;;;;;mBACN,MAAM,GAAG,SAAS;0BAElB,OAAO;;;;;;;;UAIP,oBAAoB;WAGpB,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;UACvB,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC;;;;;aAC9B,qBAAqB;;;;;;;gBAKrB,OAAO;mBAEP,OAAO;qBACP,MAAM;iBACN,MAAM;kBACN,OAAO;;;eAIP,OAAO;aACP,MAAM;;;eAIN,MAAM;kBACN,MAAM;;;oBAIN,OAAO;aACP,OAAO;YACP,OAAO;uBACP,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@econ-v1/domain",
3
- "version": "7.4.3",
3
+ "version": "7.4.5",
4
4
  "description": "Platform-neutral domain contracts and errors for Node clients",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "Node contributors",
@@ -57,6 +57,6 @@
57
57
  "access": "public"
58
58
  },
59
59
  "dependencies": {
60
- "@econ-v1/ports": "7.4.3"
60
+ "@econ-v1/ports": "7.4.5"
61
61
  }
62
62
  }
@@ -1,64 +1,64 @@
1
1
  // @ts-check
2
+ import { DomainError } from "../errors/index.js";
2
3
  /** @typedef {import('@econ-v1/ports').HomeNodeClient} HomeNodeClient */
4
+ /** @typedef {import('@econ-v1/ports').OwnerClaimCryptoPort} OwnerClaimCryptoPort */
5
+
6
+ /** Claim-key proof for invitation admission; raw codes live only during the request. */
3
7
  export class InviteRedemptionService {
4
- /**
5
- * @readonly
6
- */
7
8
  #node;
8
- /**
9
- * @param {InviteRedemptionServiceOptions} options
10
- */
9
+ #keys;
10
+ #hashCode;
11
+ /** @param {InviteRedemptionServiceOptions} options */
11
12
  constructor(options) {
12
13
  this.#node = options.node;
14
+ this.#keys = options.keys;
15
+ this.#hashCode = options.hashCode;
13
16
  }
14
- /**
15
- * @param {string} code
16
- * @returns {Promise<InviteRedemptionOutcome>}
17
- */
18
- async redeem(code) {
17
+ /** @param {string} code @param {string} [confirmation] @returns {Promise<InviteRedemptionOutcome>} */
18
+ async redeem(code, confirmation) {
19
19
  const trimmed = code.trim();
20
- if (trimmed.length === 0) return { kind: "rejected" };
20
+ if (trimmed.length === 0 || trimmed.length > 2900) return { kind: "rejected" };
21
+ return await this.#prove("redeem", trimmed, confirmation);
22
+ }
23
+ /** @returns {Promise<InviteRedemptionOutcome>} */
24
+ async resume() {
25
+ return await this.#prove("resume", "");
26
+ }
27
+ /** @param {"redeem" | "resume"} purpose @param {string} code @param {string} [confirmation] @returns {Promise<InviteRedemptionOutcome>} */
28
+ async #prove(purpose, code, confirmation) {
21
29
  try {
30
+ const publicKey = await this.#keys.publicKey();
31
+ const codeHash = purpose === "redeem" ? await this.#hashCode(code) : "";
32
+ const challenge = await this.#node.invoke({
33
+ operation: "POST /api/v2/invite/challenge",
34
+ payload: { public_key: publicKey, purpose, code_hash: codeHash, ...(purpose === "redeem" && confirmation !== undefined ? { onboarding_confirmation: confirmation } : {}) },
35
+ });
36
+ if (typeof challenge.challenge_id !== "string" || typeof challenge.challenge !== "string")
37
+ return { kind: "unavailable" };
38
+ const signature = await this.#keys.sign(challenge.challenge);
22
39
  const wire = await this.#node.invoke({
23
- operation: "POST /api/v2/invite/redeem",
24
- payload: { code: trimmed },
40
+ operation: `POST /api/v2/invite/${purpose}`,
41
+ payload: { public_key: publicKey, challenge_id: challenge.challenge_id, signature,
42
+ ...(purpose === "redeem" ? { code, ...(confirmation !== undefined ? { onboarding_confirmation: confirmation } : {}) } : {}) },
25
43
  });
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" };
44
+ if (purpose === "resume" && wire.ok === true && ["legacy_recovery", "genesis"].includes(wire.source)) return { kind: "recovery" };
45
+ if (wire.ok === false) return { kind: "rejected" };
46
+ if (wire.ok !== true || typeof wire.receiver_name !== "string" || !wire.receiver_name.trim())
47
+ return { kind: "unavailable" };
48
+ return { kind: "redeemed", receiverName: wire.receiver_name.trim() };
49
+ } catch (error) {
50
+ if (purpose === "redeem" && error instanceof DomainError && error.code === "forbidden") return { kind: "confirmation-required" };
51
+ // Only explicit refusals mean invalid admission. Network/provider/store failures can retry.
52
+ if (error instanceof DomainError && ["invalid_request", "invalid_payload", "validation_failed", "not_found", "conflict", "unauthorized", "forbidden"].includes(error.code))
53
+ return { kind: "rejected" };
54
+ return { kind: "unavailable" };
35
55
  }
36
56
  }
37
57
  }
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
- */
58
+ /** @typedef {{ readonly kind: "redeemed"; readonly receiverName: string } | { readonly kind: "rejected" } | { readonly kind: "unavailable" } | { readonly kind: "confirmation-required" } | { readonly kind: "recovery" }} InviteRedemptionOutcome */
55
59
  /**
56
60
  * @typedef {object} InviteRedemptionServiceOptions
57
61
  * @property {Pick<HomeNodeClient, "invoke">} node
62
+ * @property {OwnerClaimCryptoPort} keys Same persisted identity used for owner completion.
63
+ * @property {(code: string) => Promise<string>} hashCode SHA256 lowercase hex of the supplied UTF-8 string.
58
64
  */
59
- /**
60
- * @typedef {object} InviteRedeemWire
61
- * @property {unknown} [ok]
62
- * @property {unknown} [receiver_name]
63
- */
64
- // @ts-check
@@ -95,6 +95,7 @@ export class OnboardingFlowService {
95
95
  #invitationsEnabled;
96
96
  /** @readonly @type {OnboardingFlowInvitePort} */
97
97
  #invites;
98
+ #admissionGeneration = 0;
98
99
  /** @readonly @type {OnboardingFlowLcdPort | undefined} */
99
100
  #lcd;
100
101
  /** @readonly @type {() => string} */
@@ -133,6 +134,7 @@ export class OnboardingFlowService {
133
134
  #networks = [];
134
135
  /** @type {string | undefined} */
135
136
  #operationHash;
137
+ #admissionRestored = false;
136
138
  /** @type {OnboardingFlowReasonCode | undefined} */
137
139
  #reasonCode;
138
140
  /** @default 0 */
@@ -260,6 +262,8 @@ export class OnboardingFlowService {
260
262
  * @returns {void}
261
263
  */
262
264
  start() {
265
+ this.#admissionGeneration += 1;
266
+ this.#submittingIdentity = false;
263
267
  // Latched HERE, before anything else — including the checkpoint restore right below, which
264
268
  // consults `#activeSteps()` (and therefore `#accessCodeMode`) to validate a resumed step id.
265
269
  // See `#accessCodeMode`'s own doc comment for why this is the only place it is ever set.
@@ -275,6 +279,7 @@ export class OnboardingFlowService {
275
279
  // Neither the invitation's name nor the session flag is checkpointed: both are facts about
276
280
  // an in-memory attempt, and a resumed flow must re-earn them rather than assert them.
277
281
  this.#lockedDisplayName = undefined;
282
+ this.#admissionRestored = false;
278
283
  // Deliberately not restored from the checkpoint: it is an answer about THIS claim attempt,
279
284
  // and a resumed flow re-earns it at the next prepare. Until then the pane offers a real
280
285
  // confirm, which a node with a display simply refuses — the safe direction.
@@ -288,6 +293,7 @@ export class OnboardingFlowService {
288
293
  this.#scanning = false;
289
294
  this.#sessionEstablished = false;
290
295
  this.#stepId = restored ?? FIRST_PWA_STEP;
296
+ if (this.#stepId === "trusted" && this.#invitationRequired()) this.#stepId = "identity";
291
297
  this.#probing = this.#stepId === "choose-network";
292
298
  // The security of the checkpointed SSID is deliberately NOT persisted: the password is not
293
299
  // resumable either, so step 5 is re-shown rather than replayed, and WPA2 is the demanding
@@ -307,6 +313,7 @@ export class OnboardingFlowService {
307
313
  // access" moment — independent of which step a resumed checkpoint or the Wi-Fi skip-check
308
314
  // below lands on, rather than waiting for a click through the now-absent match-code step.
309
315
  if (this.#accessCodeMode) void this.#prepareCode();
316
+ if ((this.#stepId === "identity" || this.#stepId === "invitation") && this.#invitationRequired()) void this.#restoreAdmission();
310
317
  // A node whose Wi-Fi was configured on its own captive portal (or any earlier attempt)
311
318
  // needs no credentials from this flow: asking again is the double-prompt this probe
312
319
  // exists to kill, and RESUBMITTING working credentials is worse than redundant — the node
@@ -422,12 +429,28 @@ export class OnboardingFlowService {
422
429
  * @returns {Promise<void>}
423
430
  */
424
431
  async redeemInvitation(code) {
425
- if (this.#stepId !== "invitation") return;
432
+ if (this.#stepId !== "invitation" || this.#submittingIdentity) return;
433
+ // The restore action sends no token; whitespace input still follows ordinary validation.
434
+ if (code === "") { await this.#restoreAdmission(); return; }
435
+ const generation = ++this.#admissionGeneration;
436
+ this.#submittingIdentity = true;
437
+ this.#publish(this.#stepId);
426
438
  /** @type {Awaited<ReturnType<OnboardingFlowInvitePort["redeem"]>>} */
427
- const outcome = await this.#invites.redeem(code);
439
+ const outcome = await this.#invites.redeem(code, this.#operationHash);
440
+ if (generation !== this.#admissionGeneration) return;
441
+ this.#submittingIdentity = false;
428
442
  if (this.#stepId !== "invitation") return;
429
- if (outcome.kind === "rejected") {
430
- this.#reasonCode = "invitation-invalid";
443
+ if (outcome.kind === "confirmation-required") {
444
+ this.#submittingIdentity = true;
445
+ const prepared = await this.#prepareCode();
446
+ if (generation !== this.#admissionGeneration) return;
447
+ this.#submittingIdentity = false;
448
+ if (prepared) this.#enterMatchCode();
449
+ else this.#publish(this.#stepId);
450
+ return;
451
+ }
452
+ if (outcome.kind !== "redeemed") {
453
+ this.#reasonCode = outcome.kind === "unavailable" ? "invitation-unavailable" : "invitation-invalid";
431
454
  this.#publish(this.#stepId);
432
455
  return;
433
456
  }
@@ -443,11 +466,50 @@ export class OnboardingFlowService {
443
466
  // runs (reason code set + published) but is immediately superseded by the `#moveTo` below,
444
467
  // which always resets the reason code; the completion-failure belt in `submitIdentity()`
445
468
  // remains the backstop if the operation is ALSO stale by the time identity is submitted.
469
+ this.#submittingIdentity = true;
446
470
  await this.#prepareCode();
471
+ if (generation !== this.#admissionGeneration) return;
472
+ this.#submittingIdentity = false;
447
473
  if (this.#stepId !== "invitation") return;
448
474
  }
449
475
  this.#moveTo(this.#nextStepAfter("invitation"));
450
476
  }
477
+ /** Restore only host-authorized identity; a checkpoint never grants admission.
478
+ * @returns {Promise<boolean>} */
479
+ async #restoreAdmission() {
480
+ const generation = ++this.#admissionGeneration;
481
+ this.#submittingIdentity = true;
482
+ this.#publish(this.#stepId);
483
+ const outcome = await this.#invites.resume();
484
+ if (generation !== this.#admissionGeneration) return false;
485
+ this.#submittingIdentity = false;
486
+ if (this.#stepId !== "identity" && this.#stepId !== "invitation") return false;
487
+ if (outcome.kind === "rejected" || outcome.kind === "confirmation-required") {
488
+ this.#moveTo("invitation");
489
+ return false;
490
+ }
491
+ if (outcome.kind === "unavailable") {
492
+ this.#reasonCode = "invitation-unavailable";
493
+ this.#publish(this.#stepId);
494
+ return false;
495
+ }
496
+ this.#admissionRestored = true;
497
+ this.#lockedDisplayName = outcome.kind === "redeemed" ? outcome.receiverName : undefined;
498
+ // The owner service's prepared operation is memory-only. A reload must re-prepare the
499
+ // same persistent key; admission never substitutes for the physical-presence gate.
500
+ if (!this.#accessCodeMode) {
501
+ this.#submittingIdentity = true;
502
+ const prepared = await this.#prepareCode();
503
+ if (generation !== this.#admissionGeneration) return false;
504
+ this.#submittingIdentity = false;
505
+ if (!prepared) { this.#publish(this.#stepId); return false; }
506
+ if (this.#requiresConfirmation) { this.#enterMatchCode(); return false; }
507
+ }
508
+ if (this.#stepId === "invitation") this.#moveTo("identity");
509
+ this.#reasonCode = undefined;
510
+ this.#publish(this.#stepId);
511
+ return true;
512
+ }
451
513
  /**
452
514
  * @param {string} ssid
453
515
  * @param {OnboardingFlowWifiSecurity} security
@@ -494,6 +556,9 @@ export class OnboardingFlowService {
494
556
  // disabled "Setting up your node…" button instead of a dead one, and a second submit while
495
557
  // one is in flight is refused rather than racing two owner creations.
496
558
  if (this.#submittingIdentity) return;
559
+ if (this.#invitationRequired() && !this.#admissionRestored && this.#lockedDisplayName === undefined) {
560
+ if (!await this.#restoreAdmission()) return;
561
+ }
497
562
  this.#submittingIdentity = true;
498
563
  this.#reasonCode = undefined;
499
564
  this.#publish(this.#stepId);
@@ -512,6 +577,10 @@ export class OnboardingFlowService {
512
577
  this.#submittingIdentity = false;
513
578
  if (this.#stepId !== "identity") return;
514
579
  if (outcome !== "claimed" && outcome !== "claimed-unpaired") {
580
+ if (outcome === "not-prepared" && !this.#accessCodeMode) {
581
+ if (await this.#prepareCode() && this.#requiresConfirmation) this.#enterMatchCode();
582
+ if (this.#stepId !== "identity") return;
583
+ }
515
584
  if (this.#accessCodeMode) {
516
585
  // Belt for the op-expiry case (design §5.4): the node's confirmed_at freshness window
517
586
  // may have lapsed while the person filled in their identity, so re-fire the idempotent
@@ -583,6 +652,8 @@ export class OnboardingFlowService {
583
652
  * @returns {void}
584
653
  */
585
654
  cancel() {
655
+ this.#admissionGeneration += 1;
656
+ this.#submittingIdentity = false;
586
657
  this.#claimGeneration += 1;
587
658
  this.#claim.cancel();
588
659
  this.#accessCodes.clear();
@@ -595,6 +666,7 @@ export class OnboardingFlowService {
595
666
  this.#lcdTapPending = false;
596
667
  this.#networks = [];
597
668
  this.#operationHash = undefined;
669
+ this.#admissionRestored = false;
598
670
  this.#probeGeneration += 1;
599
671
  this.#probing = false;
600
672
  this.#reasonCode = undefined;
@@ -813,7 +885,7 @@ export class OnboardingFlowService {
813
885
  // The node's confirmation gate is disabled (no screen): there is nothing to wait for and
814
886
  // nothing to compare, so the flow must not stall on a confirmation that never arrives.
815
887
  this.#approvalState = "approved";
816
- this.#moveTo(this.#nextStepAfter("match-code"));
888
+ this.#moveTo((this.#admissionRestored || this.#lockedDisplayName !== undefined) ? "identity" : this.#nextStepAfter("match-code"));
817
889
  return;
818
890
  }
819
891
  this.#moveTo("match-code");
@@ -960,7 +1032,7 @@ export class OnboardingFlowService {
960
1032
  if (generation !== this.#claimGeneration) return;
961
1033
  if (confirmation === "confirmed") {
962
1034
  this.#approvalState = "approved";
963
- this.#moveTo(this.#nextStepAfter("match-code"));
1035
+ this.#moveTo((this.#admissionRestored || this.#lockedDisplayName !== undefined) ? "identity" : this.#nextStepAfter("match-code"));
964
1036
  return;
965
1037
  }
966
1038
  this.#approvalState =
@@ -1050,6 +1122,7 @@ function accessCodeReasonCode(error) {
1050
1122
  * | "wifi-scan-failed"
1051
1123
  * | "wifi-unreachable"
1052
1124
  * | "invitation-invalid"
1125
+ * | "invitation-unavailable"
1053
1126
  * | "claim-failed"
1054
1127
  * | "code-already-bound"
1055
1128
  * | "access-code-invalid"} OnboardingFlowReasonCode
@@ -1109,7 +1182,7 @@ function accessCodeReasonCode(error) {
1109
1182
  * "awaitConfirmation" | "cancel" | "completeClaim" | "prepareClaim"
1110
1183
  * >} OnboardingFlowClaimPort
1111
1184
  */
1112
- /** @typedef {Pick<InviteRedemptionService, "redeem">} OnboardingFlowInvitePort */
1185
+ /** @typedef {Pick<InviteRedemptionService, "redeem" | "resume">} OnboardingFlowInvitePort */
1113
1186
  /**
1114
1187
  * @typedef {Pick<
1115
1188
  * WifiProvisioningService,
@@ -2,7 +2,7 @@
2
2
  /** @import { HomeNodeClient } from "@econ-v1/ports" */
3
3
  /** @typedef {| "bootstrap-required" | "needs-onboarding" | "needs-pairing" | "ready"} OnboardingLifecycleState */
4
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 */
5
+ /** @typedef {{ readonly claimed?: unknown; readonly invitations_enabled?: unknown; readonly admission_policy?: unknown; }} OnboardingStatusWire */
6
6
  /** @typedef {{ readonly hasActiveSession: () => boolean; readonly locallyOnboarded?: () => Promise<boolean>; readonly node: Pick<HomeNodeClient, "invoke">; }} OnboardingStatusServiceOptions */
7
7
  import { TransportError } from "../errors/index.js";
8
8
  export class OnboardingStatusService {
@@ -13,6 +13,7 @@ export class OnboardingStatusService {
13
13
  /** @type {Pick<HomeNodeClient, "invoke">} */
14
14
  #node;
15
15
  #invitationsEnabled = false;
16
+ #invitationRequired = true;
16
17
  /** @param {OnboardingStatusServiceOptions} options */
17
18
  constructor(options) {
18
19
  this.#hasActiveSession = options.hasActiveSession;
@@ -22,6 +23,8 @@ export class OnboardingStatusService {
22
23
  /** Synchronous read of the last flag the node reported. Injected into
23
24
  * `OnboardingFlowService` as a getter so a value arriving after boot still takes effect. */
24
25
  /** @returns {boolean} */
26
+ /** @returns {boolean} */
27
+ invitationRequired() { return this.#invitationRequired; }
25
28
  invitationsEnabled() {
26
29
  return this.#invitationsEnabled;
27
30
  }
@@ -47,6 +50,7 @@ export class OnboardingStatusService {
47
50
  });
48
51
  claimed = typeof response.claimed === "boolean" ? response.claimed : undefined;
49
52
  this.#invitationsEnabled = response.invitations_enabled === true;
53
+ this.#invitationRequired = response.admission_policy !== "genesis";
50
54
  }
51
55
  catch (error) {
52
56
  if (error instanceof TransportError) {
@@ -172,7 +172,7 @@ export class OwnerClaimService {
172
172
  // `adopt` inside that block would have been swallowed into `"rejected"` and told a person
173
173
  // their setup failed after it had already succeeded.
174
174
  //
175
- // This is also the ONLY point (besides `cancel()`) where the pending claim is dropped and
175
+ // This is also the ONLY point where the pending claim is dropped and
176
176
  // the ephemeral key is reset — the claim it belonged to is now settled, one way or another.
177
177
  this.#pending = undefined;
178
178
  this.#crypto.reset();
@@ -192,7 +192,8 @@ export class OwnerClaimService {
192
192
  */
193
193
  cancel() {
194
194
  this.#pending = undefined;
195
- this.#crypto.reset();
195
+ // Admission may already be bound to this key, even if its response was lost.
196
+ // UI cancellation must preserve the identity needed to resume that admission.
196
197
  }
197
198
  }
198
199
  /**