@econ-v1/domain 6.5.0 → 6.6.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.
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/models/owner-views.d.ts +1 -1
- package/dist/models/owner-views.d.ts.map +1 -1
- package/dist/models/release-manifest.d.ts +64 -0
- package/dist/models/release-manifest.d.ts.map +1 -0
- package/dist/models/release-manifest.js +147 -0
- package/dist/models/release-manifest.js.map +1 -0
- package/dist/queries/discovery.d.ts +13 -0
- package/dist/queries/discovery.d.ts.map +1 -0
- package/dist/queries/discovery.js +27 -0
- package/dist/queries/discovery.js.map +1 -0
- package/dist/queries/onboarding-status.d.ts +13 -0
- package/dist/queries/onboarding-status.d.ts.map +1 -0
- package/dist/queries/onboarding-status.js +27 -0
- package/dist/queries/onboarding-status.js.map +1 -0
- package/dist/services/discovery-probe-service.d.ts +60 -0
- package/dist/services/discovery-probe-service.d.ts.map +1 -0
- package/dist/services/discovery-probe-service.js +129 -0
- package/dist/services/discovery-probe-service.js.map +1 -0
- package/dist/services/lan-candidate-refresh-service.d.ts.map +1 -1
- package/dist/services/lan-candidate-refresh-service.js +61 -14
- package/dist/services/lan-candidate-refresh-service.js.map +1 -1
- package/dist/services/onboarding-status-service.d.ts +46 -0
- package/dist/services/onboarding-status-service.d.ts.map +1 -0
- package/dist/services/onboarding-status-service.js +32 -0
- package/dist/services/onboarding-status-service.js.map +1 -0
- package/dist/services/release-manifest-verifier.d.ts +37 -0
- package/dist/services/release-manifest-verifier.d.ts.map +1 -0
- package/dist/services/release-manifest-verifier.js +84 -0
- package/dist/services/release-manifest-verifier.js.map +1 -0
- package/dist/services/transport-manager.d.ts +25 -6
- package/dist/services/transport-manager.d.ts.map +1 -1
- package/dist/services/transport-manager.js +185 -84
- package/dist/services/transport-manager.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { DomainError } from "../errors/index.js";
|
|
2
|
+
export class DiscoveryProbeService {
|
|
3
|
+
#dialer;
|
|
4
|
+
#onChange;
|
|
5
|
+
#refresh;
|
|
6
|
+
#rows = new Map();
|
|
7
|
+
#generation = 0;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.#dialer = options.dialer;
|
|
10
|
+
this.#onChange = options.onChange ?? (() => undefined);
|
|
11
|
+
this.#refresh = options.refresh;
|
|
12
|
+
}
|
|
13
|
+
snapshot() {
|
|
14
|
+
return [...this.#rows.values()];
|
|
15
|
+
}
|
|
16
|
+
/** Probes every address in parallel, replacing the previous candidate set. */
|
|
17
|
+
async probe(addresses) {
|
|
18
|
+
const generation = (this.#generation += 1);
|
|
19
|
+
const unique = dedupeText(addresses);
|
|
20
|
+
this.#rows = new Map(unique.map((address) => [address, { address, status: "probing" }]));
|
|
21
|
+
this.#emit();
|
|
22
|
+
await Promise.all(unique.map((address) => this.#probeOne(generation, address)));
|
|
23
|
+
return this.snapshot();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Promotes a previously verified candidate as the device's home-node route. Fail-closed:
|
|
27
|
+
* throws (never silently accepts) unless `probe()` most recently reported this exact
|
|
28
|
+
* address as `"verified"` — "continue anyway" on an unverified or stale candidate is not
|
|
29
|
+
* offered.
|
|
30
|
+
*/
|
|
31
|
+
async promote(address) {
|
|
32
|
+
const row = this.#rows.get(address);
|
|
33
|
+
if (row === undefined || row.status !== "verified") {
|
|
34
|
+
throw new DomainError({
|
|
35
|
+
code: "invalid_request",
|
|
36
|
+
message: "Only a verified discovery candidate may be promoted as the home-node route",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const client = await this.#dial(address);
|
|
40
|
+
await this.#refresh.refresh(client);
|
|
41
|
+
}
|
|
42
|
+
async #probeOne(generation, rawAddress) {
|
|
43
|
+
let client;
|
|
44
|
+
try {
|
|
45
|
+
client = await this.#dial(rawAddress);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "unreachable" });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "reachable" });
|
|
52
|
+
try {
|
|
53
|
+
const metadata = await client.invoke({
|
|
54
|
+
operation: "GET /api/v2/node/metadata",
|
|
55
|
+
payload: {},
|
|
56
|
+
});
|
|
57
|
+
const nodeId = textOrUndefined(metadata.node_id);
|
|
58
|
+
if (nodeId === undefined) {
|
|
59
|
+
// Reachable, but it never proved a stable node identity — fail-closed, never
|
|
60
|
+
// "verified" on reachability alone.
|
|
61
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "unreachable" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const displayName = textOrUndefined(metadata.friendly_name) ?? "Home Node";
|
|
65
|
+
this.#setRow(generation, rawAddress, {
|
|
66
|
+
address: rawAddress,
|
|
67
|
+
node: { displayName, maskedNodeId: maskNodeId(nodeId) },
|
|
68
|
+
status: "verified",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
this.#setRow(generation, rawAddress, { address: rawAddress, status: "unreachable" });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async #dial(rawAddress) {
|
|
76
|
+
const origin = secureLanOrigin(rawAddress);
|
|
77
|
+
if (origin === undefined) {
|
|
78
|
+
throw new DomainError({ code: "invalid_request", message: "Discovery candidate address is invalid" });
|
|
79
|
+
}
|
|
80
|
+
const candidate = { address: origin, id: rawAddress, path: "lan-direct" };
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
return await this.#dialer.dial(candidate, controller.signal);
|
|
83
|
+
}
|
|
84
|
+
#setRow(generation, address, row) {
|
|
85
|
+
if (generation !== this.#generation)
|
|
86
|
+
return; // superseded by a later probe() call
|
|
87
|
+
if (!this.#rows.has(address))
|
|
88
|
+
return;
|
|
89
|
+
this.#rows.set(address, row);
|
|
90
|
+
this.#emit();
|
|
91
|
+
}
|
|
92
|
+
#emit() {
|
|
93
|
+
this.#onChange(this.snapshot());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function dedupeText(values) {
|
|
97
|
+
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
98
|
+
}
|
|
99
|
+
function textOrUndefined(value) {
|
|
100
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
101
|
+
}
|
|
102
|
+
function maskNodeId(nodeId) {
|
|
103
|
+
if (nodeId.length <= 10)
|
|
104
|
+
return nodeId;
|
|
105
|
+
return `${nodeId.slice(0, 6)}····${nodeId.slice(-4)}`;
|
|
106
|
+
}
|
|
107
|
+
const LAN_ADDRESS_PATTERN = /^(?:https?:\/\/)?([^/?#:\s]+)(?::(\d{1,5}))?\/?$/u;
|
|
108
|
+
/**
|
|
109
|
+
* Normalizes a user-typed (or metadata-sourced) LAN address into a strict HTTPS origin —
|
|
110
|
+
* the shape `probeLanCandidate`/`secureOrigin` (`@econ-v1/adapters-web`) require — or
|
|
111
|
+
* `undefined` if it is not a well-formed host[:port]. Mirrors
|
|
112
|
+
* `peer-directory-service.ts`'s `toSecureAddress`.
|
|
113
|
+
*/
|
|
114
|
+
export function secureLanOrigin(address) {
|
|
115
|
+
const trimmed = address.trim();
|
|
116
|
+
if (trimmed.length === 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
const match = LAN_ADDRESS_PATTERN.exec(trimmed);
|
|
119
|
+
if (match === null)
|
|
120
|
+
return undefined;
|
|
121
|
+
const host = match[1];
|
|
122
|
+
if (host === undefined || host.length === 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
const port = match[2] === undefined ? undefined : Number(match[2]);
|
|
125
|
+
if (port !== undefined && (port < 1 || port > 65535))
|
|
126
|
+
return undefined;
|
|
127
|
+
return port === undefined ? `https://${host}` : `https://${host}:${port}`;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=discovery-probe-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery-probe-service.js","sourceRoot":"","sources":["../../src/services/discovery-probe-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lan-candidate-refresh-service.d.ts","sourceRoot":"","sources":["../../src/services/lan-candidate-refresh-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"lan-candidate-refresh-service.d.ts","sourceRoot":"","sources":["../../src/services/lan-candidate-refresh-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,iBAAiB,EACjB,cAAc,EACd,YAAY,EACb,MAAM,gBAAgB,CAAC;AAWxB,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,qBAAa,0BAA0B;;gBAIzB,OAAO,EAAE,iCAAiC;IAKhD,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC;CAmB3E"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const MAX_LAN_ORIGINS = 8;
|
|
2
2
|
export class LanCandidateRefreshService {
|
|
3
3
|
#identity;
|
|
4
4
|
#now;
|
|
@@ -7,26 +7,73 @@ export class LanCandidateRefreshService {
|
|
|
7
7
|
this.#now = options.now ?? (() => new Date().toISOString());
|
|
8
8
|
}
|
|
9
9
|
async refresh(home) {
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const current = await this.#identity.loadConnectionMetadata();
|
|
11
|
+
const homeNodeId = current?.homeNodeId;
|
|
12
|
+
if (!isNodeId(homeNodeId))
|
|
13
|
+
throw new TypeError("Home node transport identity is not established");
|
|
14
|
+
const response = await home.invoke({
|
|
15
|
+
operation: "GET /api/v2/client-transport/candidates",
|
|
12
16
|
payload: {},
|
|
13
17
|
});
|
|
14
|
-
const
|
|
15
|
-
const refreshed = nodeMetadataCandidates(metadata);
|
|
16
|
-
const directAddresses = secureAddresses([
|
|
17
|
-
...(current?.directAddresses ?? []),
|
|
18
|
-
...refreshed.map((candidate) => candidate.address),
|
|
19
|
-
]);
|
|
18
|
+
const refreshed = parseCandidates(response, homeNodeId);
|
|
20
19
|
const next = {
|
|
21
|
-
directAddresses,
|
|
20
|
+
directAddresses: refreshed.lan.map((candidate) => candidate.address),
|
|
21
|
+
homeNodeId,
|
|
22
22
|
lastConnectedAt: this.#now(),
|
|
23
23
|
...(current?.relayUrl === undefined ? {} : { relayUrl: current.relayUrl }),
|
|
24
|
+
wanOrigin: refreshed.wan.address,
|
|
24
25
|
};
|
|
25
26
|
await this.#identity.storeConnectionMetadata(next);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
return refreshed.lan;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function parseCandidates(value, expectedHomeNodeId) {
|
|
31
|
+
if (!isExactCandidateResponse(value) || value.node_id !== expectedHomeNodeId) {
|
|
32
|
+
throw new TypeError("Home node transport identity did not match");
|
|
30
33
|
}
|
|
34
|
+
const lan = normalizeLanOrigins(value.lan_origins);
|
|
35
|
+
const wan = canonicalHttpsOrigin(value.wan_origin, "Home node WAN origin was invalid");
|
|
36
|
+
return {
|
|
37
|
+
lan: lan.map((address) => ({ address, id: expectedHomeNodeId, path: "lan-direct" })),
|
|
38
|
+
wan: { address: wan, id: expectedHomeNodeId, path: "wan-direct" },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function normalizeLanOrigins(origins) {
|
|
42
|
+
const normalized = new Set();
|
|
43
|
+
for (const origin of origins) {
|
|
44
|
+
normalized.add(canonicalHttpsOrigin(origin, "Home node LAN candidates were invalid"));
|
|
45
|
+
if (normalized.size === MAX_LAN_ORIGINS)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return [...normalized];
|
|
49
|
+
}
|
|
50
|
+
function canonicalHttpsOrigin(value, message) {
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new TypeError(message);
|
|
57
|
+
}
|
|
58
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hostname === "" || url.origin !== value) {
|
|
59
|
+
throw new TypeError(message);
|
|
60
|
+
}
|
|
61
|
+
return url.origin;
|
|
62
|
+
}
|
|
63
|
+
function isExactCandidateResponse(value) {
|
|
64
|
+
if (!isRecord(value))
|
|
65
|
+
return false;
|
|
66
|
+
const record = value;
|
|
67
|
+
const expected = ["lan_origins", "node_id", "version", "wan_origin"];
|
|
68
|
+
if (Object.keys(record).length !== expected.length || !expected.every((key) => Object.hasOwn(record, key)))
|
|
69
|
+
return false;
|
|
70
|
+
return record.version === 1 && isNodeId(record.node_id) && typeof record.wan_origin === "string" &&
|
|
71
|
+
Array.isArray(record.lan_origins) && record.lan_origins.every((origin) => typeof origin === "string");
|
|
72
|
+
}
|
|
73
|
+
function isRecord(value) {
|
|
74
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
function isNodeId(value) {
|
|
77
|
+
return typeof value === "string" && value.trim() === value && value.length > 0;
|
|
31
78
|
}
|
|
32
79
|
//# sourceMappingURL=lan-candidate-refresh-service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lan-candidate-refresh-service.js","sourceRoot":"","sources":["../../src/services/lan-candidate-refresh-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lan-candidate-refresh-service.js","sourceRoot":"","sources":["../../src/services/lan-candidate-refresh-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { HomeNodeClient } from "@econ-v1/ports";
|
|
2
|
+
/**
|
|
3
|
+
* The #1815 onboarding-status coordinator (Tasks B+C). Derives which shell surface an
|
|
4
|
+
* unclaimed/claimed/unreachable node should show, so onboarding can mount by default for a
|
|
5
|
+
* fresh (unclaimed) node instead of only behind the `?onboarding=1` dev flag
|
|
6
|
+
* (`docs/superpowers/plans/2026-07-26-client-node-onboarding-status-coordinator.md`).
|
|
7
|
+
*
|
|
8
|
+
* Derivation order (fail-safe: an unrecognized outcome never invents "ready" or
|
|
9
|
+
* "needs-onboarding" — it falls back to `needs-pairing`, the pre-#1815 default path):
|
|
10
|
+
* 1. The browser already holds a valid device session → `ready` (never calls the node).
|
|
11
|
+
* 2. The node is reachable and reports `claimed: false` (no owner yet) → `needs-onboarding`,
|
|
12
|
+
* scenario `new` (first-owner setup creates the owner).
|
|
13
|
+
* 3. The node is reachable and reports `claimed: true` (has an owner) → `needs-pairing`
|
|
14
|
+
* (additional-device pairing — the existing default behavior).
|
|
15
|
+
* 4. The node has not been reached at all (a `TransportError` from `node.invoke`, e.g. no
|
|
16
|
+
* route/candidate promoted yet) → `bootstrap-required` (drive discovery first).
|
|
17
|
+
* 5. Any other failure or an unrecognized response shape → `needs-pairing` (no regression
|
|
18
|
+
* from the pre-#1815 default — the shell still shows the pairing gate, which handles
|
|
19
|
+
* both "already paired" and "not yet paired" on its own).
|
|
20
|
+
*/
|
|
21
|
+
export type OnboardingLifecycleState = "bootstrap-required" | "needs-onboarding" | "needs-pairing" | "ready";
|
|
22
|
+
export interface OnboardingLifecycleStatusRow {
|
|
23
|
+
readonly id: "onboarding-status";
|
|
24
|
+
/** Present only when `state === "needs-onboarding"` — always `"new"` today (first-owner
|
|
25
|
+
* setup); a future scenario would be added here, not overloaded onto this field. */
|
|
26
|
+
readonly scenario?: "new";
|
|
27
|
+
readonly state: OnboardingLifecycleState;
|
|
28
|
+
}
|
|
29
|
+
/** The subset of `GET /api/auth/onboarding-status`'s wire shape this service reads. */
|
|
30
|
+
export interface OnboardingStatusWire {
|
|
31
|
+
readonly claimed?: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface OnboardingStatusServiceOptions {
|
|
34
|
+
/** Synchronous read of whether the browser currently holds an established device session
|
|
35
|
+
* (`DeviceSessionClient.currentSession() !== undefined` in the composition root). Kept as
|
|
36
|
+
* a plain predicate — rather than this service depending on `DeviceSessionClient` itself —
|
|
37
|
+
* so the service stays platform-free and trivially fakeable in tests. */
|
|
38
|
+
readonly hasActiveSession: () => boolean;
|
|
39
|
+
readonly node: Pick<HomeNodeClient, "invoke">;
|
|
40
|
+
}
|
|
41
|
+
export declare class OnboardingStatusService {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(options: OnboardingStatusServiceOptions);
|
|
44
|
+
status(): Promise<OnboardingLifecycleStatusRow>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=onboarding-status-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboarding-status-service.d.ts","sourceRoot":"","sources":["../../src/services/onboarding-status-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,wBAAwB,GAChC,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,GACf,OAAO,CAAC;AAEZ,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,EAAE,EAAE,mBAAmB,CAAC;IACjC;wFACoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,wBAAwB,CAAC;CAC1C;AAED,uFAAuF;AACvF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;6EAGyE;IACzE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,OAAO,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;CAC/C;AAED,qBAAa,uBAAuB;;gBAItB,OAAO,EAAE,8BAA8B;IAK7C,MAAM,IAAI,OAAO,CAAC,4BAA4B,CAAC;CAoBtD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TransportError } from "../errors/index.js";
|
|
2
|
+
export class OnboardingStatusService {
|
|
3
|
+
#hasActiveSession;
|
|
4
|
+
#node;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.#hasActiveSession = options.hasActiveSession;
|
|
7
|
+
this.#node = options.node;
|
|
8
|
+
}
|
|
9
|
+
async status() {
|
|
10
|
+
if (this.#hasActiveSession())
|
|
11
|
+
return { id: "onboarding-status", state: "ready" };
|
|
12
|
+
let claimed;
|
|
13
|
+
try {
|
|
14
|
+
const response = await this.#node.invoke({
|
|
15
|
+
operation: "GET /api/auth/onboarding-status",
|
|
16
|
+
payload: {},
|
|
17
|
+
});
|
|
18
|
+
claimed = typeof response.claimed === "boolean" ? response.claimed : undefined;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof TransportError)
|
|
22
|
+
return { id: "onboarding-status", state: "bootstrap-required" };
|
|
23
|
+
return { id: "onboarding-status", state: "needs-pairing" };
|
|
24
|
+
}
|
|
25
|
+
if (claimed === undefined)
|
|
26
|
+
return { id: "onboarding-status", state: "needs-pairing" };
|
|
27
|
+
return claimed
|
|
28
|
+
? { id: "onboarding-status", state: "needs-pairing" }
|
|
29
|
+
: { id: "onboarding-status", scenario: "new", state: "needs-onboarding" };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=onboarding-status-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboarding-status-service.js","sourceRoot":"","sources":["../../src/services/onboarding-status-service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAkDpD,MAAM,OAAO,uBAAuB;IACzB,iBAAiB,CAAgB;IACjC,KAAK,CAAiC;IAE/C,YAAY,OAAuC;QACjD,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;QAClD,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,IAAI,CAAC,iBAAiB,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAEjF,IAAI,OAA4B,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAuB;gBAC7D,SAAS,EAAE,iCAAiC;gBAC5C,OAAO,EAAE,EAAE;aACZ,CAAC,CAAC;YACH,OAAO,GAAG,OAAO,QAAQ,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;QACjF,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,KAAK,YAAY,cAAc;gBAAE,OAAO,EAAE,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;YACrG,OAAO,EAAE,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;QAC7D,CAAC;QAED,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,EAAE,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;QACtF,OAAO,OAAO;YACZ,CAAC,CAAC,EAAE,EAAE,EAAE,mBAAmB,EAAE,KAAK,EAAE,eAAe,EAAE;YACrD,CAAC,CAAC,EAAE,EAAE,EAAE,mBAAmB,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC9E,CAAC;CACF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CryptoPort } from "@econ-v1/ports";
|
|
2
|
+
import { type SignedReleaseManifest } from "../models/release-manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* The #1815 Phase 1 release-manifest verifier (gap-audit §3 "Phase 1"; lifecycle spec §9).
|
|
5
|
+
* Pure with respect to the manifest's *content* — the only injected capability is the
|
|
6
|
+
* `CryptoPort` subset it needs (`digestSha256`, `verifyEd25519`), so this runs identically
|
|
7
|
+
* in a unit test (fake crypto) and in the browser service worker (real WebCrypto, see
|
|
8
|
+
* `client/adapters-web/src/identity/webcrypto-crypto-port.ts`). Never throws: every failure
|
|
9
|
+
* path resolves to one of the six stable reason codes below so a caller can log/telemetry it
|
|
10
|
+
* without risking a raw error message (and its potential detail) leaking anywhere.
|
|
11
|
+
*
|
|
12
|
+
* Check order is deliberate: structural validity first (so nothing downstream ever reads an
|
|
13
|
+
* un-typechecked field), then signature (nothing about an unauthenticated manifest's content
|
|
14
|
+
* is trusted for the policy checks that follow), then the manifest's own internal
|
|
15
|
+
* self-consistency, then policy (channel/protocol/schema).
|
|
16
|
+
*/
|
|
17
|
+
export declare const RELEASE_MANIFEST_REJECTION_REASONS: readonly ["malformed", "bad-signature", "hash-mismatch", "incompatible-protocol", "incompatible-schema", "channel-not-allowed"];
|
|
18
|
+
export type ReleaseManifestRejectionReason = (typeof RELEASE_MANIFEST_REJECTION_REASONS)[number];
|
|
19
|
+
export type ReleaseManifestVerification = {
|
|
20
|
+
readonly manifest: SignedReleaseManifest;
|
|
21
|
+
readonly ok: true;
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly reason: ReleaseManifestRejectionReason;
|
|
25
|
+
};
|
|
26
|
+
export interface ReleaseManifestTrustedKey {
|
|
27
|
+
readonly keyId: string;
|
|
28
|
+
readonly publicKey: Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
export interface ReleaseManifestVerifyOptions {
|
|
31
|
+
readonly allowedChannels: readonly string[];
|
|
32
|
+
readonly cryptoPort: Pick<CryptoPort, "digestSha256" | "verifyEd25519">;
|
|
33
|
+
readonly currentProtocolVersion: number;
|
|
34
|
+
readonly currentStorageSchemaVersion: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function verifyReleaseManifest(candidate: unknown, trustedPublicKeys: readonly ReleaseManifestTrustedKey[], options: ReleaseManifestVerifyOptions): Promise<ReleaseManifestVerification>;
|
|
37
|
+
//# sourceMappingURL=release-manifest-verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-manifest-verifier.d.ts","sourceRoot":"","sources":["../../src/services/release-manifest-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAEvC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kCAAkC,iIAOrC,CAAC;AAEX,MAAM,MAAM,8BAA8B,GAAG,CAAC,OAAO,kCAAkC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjG,MAAM,MAAM,2BAA2B,GACnC;IAAE,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GAC/D;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAA;CAAE,CAAC;AAE5E,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,cAAc,GAAG,eAAe,CAAC,CAAC;IACxE,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,2BAA2B,EAAE,MAAM,CAAC;CAC9C;AAED,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,OAAO,EAClB,iBAAiB,EAAE,SAAS,yBAAyB,EAAE,EACvD,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAmCtC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { canonicalOutboxPayload } from "../models/outbox.js";
|
|
2
|
+
import { parseSignedReleaseManifest, unsignedReleaseManifestToJson, } from "../models/release-manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* The #1815 Phase 1 release-manifest verifier (gap-audit §3 "Phase 1"; lifecycle spec §9).
|
|
5
|
+
* Pure with respect to the manifest's *content* — the only injected capability is the
|
|
6
|
+
* `CryptoPort` subset it needs (`digestSha256`, `verifyEd25519`), so this runs identically
|
|
7
|
+
* in a unit test (fake crypto) and in the browser service worker (real WebCrypto, see
|
|
8
|
+
* `client/adapters-web/src/identity/webcrypto-crypto-port.ts`). Never throws: every failure
|
|
9
|
+
* path resolves to one of the six stable reason codes below so a caller can log/telemetry it
|
|
10
|
+
* without risking a raw error message (and its potential detail) leaking anywhere.
|
|
11
|
+
*
|
|
12
|
+
* Check order is deliberate: structural validity first (so nothing downstream ever reads an
|
|
13
|
+
* un-typechecked field), then signature (nothing about an unauthenticated manifest's content
|
|
14
|
+
* is trusted for the policy checks that follow), then the manifest's own internal
|
|
15
|
+
* self-consistency, then policy (channel/protocol/schema).
|
|
16
|
+
*/
|
|
17
|
+
export const RELEASE_MANIFEST_REJECTION_REASONS = [
|
|
18
|
+
"malformed",
|
|
19
|
+
"bad-signature",
|
|
20
|
+
"hash-mismatch",
|
|
21
|
+
"incompatible-protocol",
|
|
22
|
+
"incompatible-schema",
|
|
23
|
+
"channel-not-allowed",
|
|
24
|
+
];
|
|
25
|
+
export async function verifyReleaseManifest(candidate, trustedPublicKeys, options) {
|
|
26
|
+
const manifest = parseSignedReleaseManifest(candidate);
|
|
27
|
+
if (manifest === undefined)
|
|
28
|
+
return { ok: false, reason: "malformed" };
|
|
29
|
+
const trustedKey = trustedPublicKeys.find((key) => key.keyId === manifest.signingKeyId);
|
|
30
|
+
if (trustedKey === undefined)
|
|
31
|
+
return { ok: false, reason: "bad-signature" };
|
|
32
|
+
const signatureBytes = hexToBytes(manifest.signature);
|
|
33
|
+
if (signatureBytes === undefined)
|
|
34
|
+
return { ok: false, reason: "malformed" };
|
|
35
|
+
const canonicalPayload = new TextEncoder().encode(canonicalOutboxPayload(unsignedReleaseManifestToJson(manifest)));
|
|
36
|
+
let signatureValid;
|
|
37
|
+
try {
|
|
38
|
+
signatureValid = await options.cryptoPort.verifyEd25519(canonicalPayload, signatureBytes, trustedKey.publicKey);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false, reason: "bad-signature" };
|
|
42
|
+
}
|
|
43
|
+
if (!signatureValid)
|
|
44
|
+
return { ok: false, reason: "bad-signature" };
|
|
45
|
+
const expectedShellVersion = await deriveShellVersion(manifest.shellAssets, options.cryptoPort);
|
|
46
|
+
if (expectedShellVersion !== manifest.shellVersion)
|
|
47
|
+
return { ok: false, reason: "hash-mismatch" };
|
|
48
|
+
if (!options.allowedChannels.includes(manifest.channel))
|
|
49
|
+
return { ok: false, reason: "channel-not-allowed" };
|
|
50
|
+
if (isOutOfRange(options.currentProtocolVersion, manifest.protocolCompatibility)) {
|
|
51
|
+
return { ok: false, reason: "incompatible-protocol" };
|
|
52
|
+
}
|
|
53
|
+
if (isOutOfRange(options.currentStorageSchemaVersion, manifest.storageSchemaCompatibility)) {
|
|
54
|
+
return { ok: false, reason: "incompatible-schema" };
|
|
55
|
+
}
|
|
56
|
+
return { manifest, ok: true };
|
|
57
|
+
}
|
|
58
|
+
/** Mirrors `system/pwa/scripts/build-service-worker.mjs`'s `shellVersion` derivation
|
|
59
|
+
* (sha256 of the URL-sorted asset list, first 16 hex chars) so a validly-signed manifest
|
|
60
|
+
* whose signer computed `shellVersion` inconsistently with its own `shellAssets` is caught
|
|
61
|
+
* even though the signature itself only proves authenticity, not internal consistency. */
|
|
62
|
+
async function deriveShellVersion(shellAssets, cryptoPort) {
|
|
63
|
+
const sorted = [...shellAssets].sort((left, right) => left.url.localeCompare(right.url));
|
|
64
|
+
const payload = JSON.stringify(sorted.map((asset) => ({ sha256: asset.sha256, url: asset.url })));
|
|
65
|
+
const digest = await cryptoPort.digestSha256(new TextEncoder().encode(payload));
|
|
66
|
+
return digest.slice(0, 16);
|
|
67
|
+
}
|
|
68
|
+
function isOutOfRange(value, range) {
|
|
69
|
+
return value < range.min || value > range.max;
|
|
70
|
+
}
|
|
71
|
+
function hexToBytes(hex) {
|
|
72
|
+
if (hex.length % 2 !== 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
75
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
76
|
+
const byteHex = hex.slice(index * 2, index * 2 + 2);
|
|
77
|
+
const byte = Number.parseInt(byteHex, 16);
|
|
78
|
+
if (Number.isNaN(byte))
|
|
79
|
+
return undefined;
|
|
80
|
+
bytes[index] = byte;
|
|
81
|
+
}
|
|
82
|
+
return bytes;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=release-manifest-verifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-manifest-verifier.js","sourceRoot":"","sources":["../../src/services/release-manifest-verifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
import type { DelayPort,
|
|
1
|
+
import type { DelayPort, DirectHomeNodeCandidate, DirectTransportPath, HomeNodeClient, TransportState } from "@econ-v1/ports";
|
|
2
2
|
export interface TransportManagerOptions {
|
|
3
|
-
readonly candidates: readonly
|
|
4
|
-
readonly dial: (candidate:
|
|
3
|
+
readonly candidates: readonly DirectHomeNodeCandidate[];
|
|
4
|
+
readonly dial: (candidate: DirectHomeNodeCandidate, signal: AbortSignal) => Promise<HomeNodeClient>;
|
|
5
5
|
readonly delay?: DelayPort;
|
|
6
|
-
/**
|
|
6
|
+
/** Retained for source compatibility; candidates are selected by closed priority tiers. */
|
|
7
7
|
readonly priorityGraceMs?: number;
|
|
8
8
|
/** Upper bound for one candidate dial. */
|
|
9
9
|
readonly candidateTimeoutMs?: number;
|
|
10
10
|
readonly now?: () => string;
|
|
11
11
|
readonly onState?: (state: TransportState) => void;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
export interface TransportConnectOptions {
|
|
14
|
+
readonly higherPriorityThan?: DirectTransportPath;
|
|
15
|
+
readonly path?: DirectTransportPath;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Direct (non-relayed) home-node transport policy: browser-trusted LAN
|
|
19
|
+
* HTTPS, then hash-pinned LAN WebTransport, then WAN HTTPS/WSS (ticket
|
|
20
|
+
* #1823). Adapters own I/O; this class owns selection only.
|
|
21
|
+
*/
|
|
14
22
|
export declare class TransportManager {
|
|
15
23
|
#private;
|
|
16
24
|
constructor(options: TransportManagerOptions);
|
|
17
25
|
get state(): TransportState;
|
|
26
|
+
get selectedCandidate(): DirectHomeNodeCandidate | undefined;
|
|
18
27
|
subscribe(listener: (state: TransportState) => void): () => void;
|
|
19
|
-
|
|
28
|
+
replaceCandidates(candidates: readonly DirectHomeNodeCandidate[]): void;
|
|
29
|
+
connect(options?: TransportConnectOptions): Promise<HomeNodeClient>;
|
|
30
|
+
markConnected(path: DirectTransportPath): void;
|
|
20
31
|
markOffline(): void;
|
|
32
|
+
abort(): void;
|
|
21
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Canonicalize a bare HTTPS origin. Named for its HTTP origin, but this rule
|
|
36
|
+
* — exact `https://` scheme, no credentials, no path/query/fragment — is the
|
|
37
|
+
* same origin shape a WebTransport candidate's `address` uses too (ticket
|
|
38
|
+
* #1823): the client-RPC path is appended when dialing, not advertised.
|
|
39
|
+
*/
|
|
40
|
+
export declare function canonicalHttpCandidateOrigin(value: string): string;
|
|
22
41
|
//# sourceMappingURL=transport-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport-manager.d.ts","sourceRoot":"","sources":["../../src/services/transport-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"transport-manager.d.ts","sourceRoot":"","sources":["../../src/services/transport-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAK9H,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACxD,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,uBAAuB,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACpG,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,0CAA0C;IAC1C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,mBAAmB,CAAC;IAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAID;;;;GAIG;AACH,qBAAa,gBAAgB;;gBAef,OAAO,EAAE,uBAAuB;IAS5C,IAAI,KAAK,IAAI,cAAc,CAAwB;IACnD,IAAI,iBAAiB,IAAI,uBAAuB,GAAG,SAAS,CAE3D;IAED,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAKhE,iBAAiB,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,GAAG,IAAI;IAIvE,OAAO,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,cAAc,CAAC;IAYvE,aAAa,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI;IAI9C,WAAW,IAAI,IAAI;IAInB,KAAK,IAAI,IAAI;CA+Fd;AA4BD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBlE"}
|