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