@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.
- package/dist/services/invite-redemption-service.d.ts +23 -28
- package/dist/services/invite-redemption-service.d.ts.map +1 -1
- package/dist/services/onboarding-flow-service.d.ts +2 -2
- package/dist/services/onboarding-flow-service.d.ts.map +1 -1
- package/dist/services/onboarding-status-service.d.ts +3 -0
- package/dist/services/onboarding-status-service.d.ts.map +1 -1
- package/dist/services/owner-claim-service.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/services/invite-redemption-service.js +46 -46
- package/src/services/onboarding-flow-service.js +80 -7
- package/src/services/onboarding-status-service.js +5 -1
- package/src/services/owner-claim-service.js +3 -2
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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":"
|
|
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":"AAaA,8KAA8K;AAC9K,uCADW,SAAS,CAAC,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,CAAC,CAaxK;AA0DF;
|
|
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;
|
|
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,
|
|
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
|
+
"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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
24
|
-
payload: {
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return { kind: "
|
|
33
|
-
|
|
34
|
-
|
|
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 === "
|
|
430
|
-
this.#
|
|
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
|
|
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
|
|
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
|
/**
|