@layervai/qurl 0.7.0 → 2.0.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/README.md +212 -1
- package/dist/cjs/client.d.ts +19 -1
- package/dist/cjs/client.js +520 -9
- package/dist/cjs/crid.d.ts +2 -1
- package/dist/cjs/crid.js +11 -2
- package/dist/cjs/errors.d.ts +20 -3
- package/dist/cjs/errors.js +31 -4
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/node/agent-runtime.d.ts +108 -0
- package/dist/cjs/node/agent-runtime.js +952 -0
- package/dist/cjs/node/agent-state.d.ts +100 -0
- package/dist/cjs/node/agent-state.js +311 -0
- package/dist/cjs/node/agent-transport.d.ts +17 -0
- package/dist/cjs/node/agent-transport.js +118 -0
- package/dist/cjs/node/deployment.d.ts +6 -0
- package/dist/cjs/node/deployment.js +24 -3
- package/dist/cjs/node/file-agent-state.d.ts +15 -0
- package/dist/cjs/node/file-agent-state.js +137 -0
- package/dist/cjs/node/native-loader.cjs +7 -0
- package/dist/cjs/node/native-loader.d.cts +11 -0
- package/dist/cjs/node/native-udp.d.ts +8 -3
- package/dist/cjs/node/native-udp.js +8 -3
- package/dist/cjs/node/nhp-wire.d.ts +9 -0
- package/dist/cjs/node/nhp-wire.js +24 -7
- package/dist/cjs/node/portal-opener.d.ts +7 -0
- package/dist/cjs/node/portal-opener.js +28 -6
- package/dist/cjs/node/provider.d.ts +19 -0
- package/dist/cjs/node/provider.js +153 -0
- package/dist/cjs/node/qv2.d.ts +3 -1
- package/dist/cjs/node/qv2.js +16 -2
- package/dist/cjs/node/relay.d.ts +12 -0
- package/dist/cjs/node/relay.js +111 -0
- package/dist/cjs/node/sealed-agent-state.d.ts +19 -0
- package/dist/cjs/node/sealed-agent-state.js +193 -0
- package/dist/cjs/node/strict-json.d.ts +1 -1
- package/dist/cjs/node/strict-json.js +8 -3
- package/dist/cjs/node.d.ts +14 -0
- package/dist/cjs/node.js +24 -1
- package/dist/cjs/types.d.ts +97 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/client.d.ts +19 -1
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +521 -10
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/crid.d.ts +2 -1
- package/dist/esm/crid.d.ts.map +1 -1
- package/dist/esm/crid.js +10 -2
- package/dist/esm/crid.js.map +1 -1
- package/dist/esm/errors.d.ts +20 -3
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +29 -3
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/node/agent-runtime.d.ts +109 -0
- package/dist/esm/node/agent-runtime.d.ts.map +1 -0
- package/dist/esm/node/agent-runtime.js +946 -0
- package/dist/esm/node/agent-runtime.js.map +1 -0
- package/dist/esm/node/agent-state.d.ts +101 -0
- package/dist/esm/node/agent-state.d.ts.map +1 -0
- package/dist/esm/node/agent-state.js +298 -0
- package/dist/esm/node/agent-state.js.map +1 -0
- package/dist/esm/node/agent-transport.d.ts +18 -0
- package/dist/esm/node/agent-transport.d.ts.map +1 -0
- package/dist/esm/node/agent-transport.js +114 -0
- package/dist/esm/node/agent-transport.js.map +1 -0
- package/dist/esm/node/deployment.d.ts +6 -0
- package/dist/esm/node/deployment.d.ts.map +1 -1
- package/dist/esm/node/deployment.js +23 -3
- package/dist/esm/node/deployment.js.map +1 -1
- package/dist/esm/node/file-agent-state.d.ts +16 -0
- package/dist/esm/node/file-agent-state.d.ts.map +1 -0
- package/dist/esm/node/file-agent-state.js +134 -0
- package/dist/esm/node/file-agent-state.js.map +1 -0
- package/dist/esm/node/native-loader.cjs +7 -0
- package/dist/esm/node/native-loader.d.cts +11 -0
- package/dist/esm/node/native-udp.d.ts +8 -3
- package/dist/esm/node/native-udp.d.ts.map +1 -1
- package/dist/esm/node/native-udp.js +7 -5
- package/dist/esm/node/native-udp.js.map +1 -1
- package/dist/esm/node/nhp-wire.d.ts +9 -0
- package/dist/esm/node/nhp-wire.d.ts.map +1 -1
- package/dist/esm/node/nhp-wire.js +23 -6
- package/dist/esm/node/nhp-wire.js.map +1 -1
- package/dist/esm/node/portal-opener.d.ts +7 -0
- package/dist/esm/node/portal-opener.d.ts.map +1 -1
- package/dist/esm/node/portal-opener.js +28 -6
- package/dist/esm/node/portal-opener.js.map +1 -1
- package/dist/esm/node/provider.d.ts +20 -0
- package/dist/esm/node/provider.d.ts.map +1 -0
- package/dist/esm/node/provider.js +149 -0
- package/dist/esm/node/provider.js.map +1 -0
- package/dist/esm/node/qv2.d.ts +3 -1
- package/dist/esm/node/qv2.d.ts.map +1 -1
- package/dist/esm/node/qv2.js +17 -3
- package/dist/esm/node/qv2.js.map +1 -1
- package/dist/esm/node/relay.d.ts +13 -0
- package/dist/esm/node/relay.d.ts.map +1 -0
- package/dist/esm/node/relay.js +105 -0
- package/dist/esm/node/relay.js.map +1 -0
- package/dist/esm/node/sealed-agent-state.d.ts +20 -0
- package/dist/esm/node/sealed-agent-state.d.ts.map +1 -0
- package/dist/esm/node/sealed-agent-state.js +190 -0
- package/dist/esm/node/sealed-agent-state.js.map +1 -0
- package/dist/esm/node/strict-json.d.ts +1 -1
- package/dist/esm/node/strict-json.d.ts.map +1 -1
- package/dist/esm/node/strict-json.js +8 -3
- package/dist/esm/node/strict-json.js.map +1 -1
- package/dist/esm/node.d.ts +14 -0
- package/dist/esm/node.d.ts.map +1 -1
- package/dist/esm/node.js +7 -0
- package/dist/esm/node.js.map +1 -1
- package/dist/esm/types.d.ts +97 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +16 -7
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileAgentState = void 0;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const promises_1 = require("node:timers/promises");
|
|
7
|
+
const native_loader_cjs_1 = require("./native-loader.cjs");
|
|
8
|
+
const agent_state_js_1 = require("./agent-state.js");
|
|
9
|
+
const plaintextCodec = {
|
|
10
|
+
async encode(state) {
|
|
11
|
+
return (0, agent_state_js_1.encodeAgentState)(state);
|
|
12
|
+
},
|
|
13
|
+
async decode(raw) {
|
|
14
|
+
return (0, agent_state_js_1.decodeAgentState)(raw);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
/** Own this handle until all clients and lifecycle operations finish. */
|
|
18
|
+
class FileAgentState {
|
|
19
|
+
#native;
|
|
20
|
+
#handle;
|
|
21
|
+
#codec;
|
|
22
|
+
#active = 0;
|
|
23
|
+
#closed = false;
|
|
24
|
+
#locked = false;
|
|
25
|
+
constructor(path, codec = plaintextCodec) {
|
|
26
|
+
let absolute = (0, node_path_1.resolve)(path);
|
|
27
|
+
if (process.platform === "darwin")
|
|
28
|
+
absolute = absolute.replace(/^\/(var|tmp|etc)(?=\/|$)/, "/private/$1");
|
|
29
|
+
this.#native = (0, native_loader_cjs_1.loadNativeStateFS)();
|
|
30
|
+
this.#handle = this.#native.open((0, node_path_1.dirname)(absolute), (0, node_path_1.basename)(absolute));
|
|
31
|
+
this.#codec = codec;
|
|
32
|
+
}
|
|
33
|
+
checkContinuity() {
|
|
34
|
+
if (this.#closed)
|
|
35
|
+
throw new agent_state_js_1.AgentStateError("CLOSED");
|
|
36
|
+
this.#native.check(this.#handle);
|
|
37
|
+
}
|
|
38
|
+
async load(signal) {
|
|
39
|
+
this.checkContinuity();
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
this.#active++;
|
|
42
|
+
let bytes = null;
|
|
43
|
+
try {
|
|
44
|
+
bytes = this.#native.read(this.#handle);
|
|
45
|
+
if (bytes === null)
|
|
46
|
+
throw new agent_state_js_1.AgentStateError("NOT_FOUND");
|
|
47
|
+
const state = await this.#codec.decode(bytes, signal);
|
|
48
|
+
this.checkContinuity();
|
|
49
|
+
signal?.throwIfAborted();
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
bytes?.fill(0);
|
|
54
|
+
this.#active--;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async save(state, signal) {
|
|
58
|
+
// Snapshot before waiting for another writer or an external key wrapper.
|
|
59
|
+
const snapshot = structuredClone(state);
|
|
60
|
+
await this.withLock((locked) => locked.save(snapshot, signal), signal);
|
|
61
|
+
}
|
|
62
|
+
async withLock(operation, signal) {
|
|
63
|
+
this.checkContinuity();
|
|
64
|
+
this.#active++;
|
|
65
|
+
let acquired = false;
|
|
66
|
+
const deadline = AbortSignal.timeout(30_000);
|
|
67
|
+
const waiting = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
68
|
+
try {
|
|
69
|
+
for (;;) {
|
|
70
|
+
waiting.throwIfAborted();
|
|
71
|
+
if (!this.#locked && this.#native.tryLock(this.#handle)) {
|
|
72
|
+
this.#locked = acquired = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
await (0, promises_1.setTimeout)(25, undefined, { signal: waiting });
|
|
76
|
+
}
|
|
77
|
+
let valid = true;
|
|
78
|
+
const requireLease = () => {
|
|
79
|
+
if (!valid)
|
|
80
|
+
throw new agent_state_js_1.AgentStateError("EXPIRED_LOCK_HANDLE");
|
|
81
|
+
this.checkContinuity();
|
|
82
|
+
};
|
|
83
|
+
const locked = {
|
|
84
|
+
load: async (nextSignal) => {
|
|
85
|
+
requireLease();
|
|
86
|
+
const value = await this.load(nextSignal ?? signal);
|
|
87
|
+
requireLease();
|
|
88
|
+
return value;
|
|
89
|
+
},
|
|
90
|
+
save: async (state, nextSignal) => {
|
|
91
|
+
requireLease();
|
|
92
|
+
const raw = (0, agent_state_js_1.encodeAgentState)(state);
|
|
93
|
+
let encoded;
|
|
94
|
+
try {
|
|
95
|
+
encoded = await this.#codec.encode((0, agent_state_js_1.decodeAgentState)(raw), nextSignal ?? signal);
|
|
96
|
+
requireLease();
|
|
97
|
+
(nextSignal ?? signal)?.throwIfAborted();
|
|
98
|
+
this.#native.write(this.#handle, encoded, `.qurl-${(0, node_crypto_1.randomUUID)()}`);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
encoded?.fill(0);
|
|
102
|
+
raw.fill(0);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
withLock: async (fn) => {
|
|
106
|
+
requireLease();
|
|
107
|
+
return fn(locked);
|
|
108
|
+
},
|
|
109
|
+
checkContinuity: requireLease,
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
const result = await operation(locked);
|
|
113
|
+
requireLease();
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
valid = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
this.#active--;
|
|
122
|
+
if (acquired) {
|
|
123
|
+
this.#locked = false;
|
|
124
|
+
this.#native.unlock(this.#handle);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
close() {
|
|
129
|
+
if (this.#closed)
|
|
130
|
+
return;
|
|
131
|
+
if (this.#active)
|
|
132
|
+
throw new agent_state_js_1.AgentStateError("BUSY");
|
|
133
|
+
this.#closed = true;
|
|
134
|
+
this.#native.close(this.#handle);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.FileAgentState = FileAgentState;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const { platform } = require('node:process');
|
|
2
|
+
exports.loadNativeStateFS = () => {
|
|
3
|
+
if (platform !== 'linux' && platform !== 'darwin') {
|
|
4
|
+
throw new Error('Secure qURL state storage requires Linux or macOS');
|
|
5
|
+
}
|
|
6
|
+
return require('@layervai/qurl-state-fs').load();
|
|
7
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function loadNativeStateFS(): NativeStateFS;
|
|
2
|
+
|
|
3
|
+
export interface NativeStateFS {
|
|
4
|
+
open(directory: string, name: string): object;
|
|
5
|
+
check(handle: object): void;
|
|
6
|
+
tryLock(handle: object): boolean;
|
|
7
|
+
unlock(handle: object): void;
|
|
8
|
+
read(handle: object): Buffer | null;
|
|
9
|
+
write(handle: object, bytes: Buffer, temporaryName: string): void;
|
|
10
|
+
close(handle: object): void;
|
|
11
|
+
}
|
|
@@ -17,13 +17,18 @@ interface NativeKnockRuntime {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function nativeKnock(cell: ValidatedCell, devicePrivateKey: Uint8Array, body: Uint8Array, options?: NativeExchangeOptions): Promise<NHPMessage>;
|
|
19
19
|
declare function nativeKnockWithRuntime(cell: ValidatedCell, devicePrivateKey: Uint8Array, body: Uint8Array, options: NativeExchangeOptions, runtime: NativeKnockRuntime): Promise<NHPMessage>;
|
|
20
|
-
declare function resolvePublicAddresses(host: string, maximum: number, signal?: AbortSignal, lookupAddresses?: typeof lookup): Promise<Array<{
|
|
20
|
+
export declare function resolvePublicAddresses(host: string, maximum: number, signal?: AbortSignal, lookupAddresses?: typeof lookup): Promise<Array<{
|
|
21
21
|
address: string;
|
|
22
22
|
family: 4 | 6;
|
|
23
23
|
}>>;
|
|
24
24
|
declare function isPublicAddress(address: string, family: number): boolean;
|
|
25
|
-
declare
|
|
26
|
-
|
|
25
|
+
declare class SocketExchangeError extends Error {
|
|
26
|
+
readonly cause: unknown;
|
|
27
|
+
constructor(cause: unknown);
|
|
28
|
+
}
|
|
29
|
+
export declare function isSocketExchangeError(error: unknown): error is SocketExchangeError;
|
|
30
|
+
export declare function exchangeDatagram(address: string, family: 4 | 6, port: number, packet: Uint8Array, timeoutMs: number, signal?: AbortSignal, oneWay?: boolean): Promise<Uint8Array>;
|
|
31
|
+
declare function sendOwnedDatagram(socket: Socket, packet: Uint8Array, finish: (error: unknown) => void, onSent?: () => void): void;
|
|
27
32
|
export declare const nativeUdpTesting: {
|
|
28
33
|
isPublicAddress: typeof isPublicAddress;
|
|
29
34
|
resolvePublicAddresses: typeof resolvePublicAddresses;
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.nativeUdpTesting = void 0;
|
|
4
4
|
exports.nativeKnock = nativeKnock;
|
|
5
|
+
exports.resolvePublicAddresses = resolvePublicAddresses;
|
|
6
|
+
exports.isSocketExchangeError = isSocketExchangeError;
|
|
7
|
+
exports.exchangeDatagram = exchangeDatagram;
|
|
5
8
|
const promises_1 = require("node:dns/promises");
|
|
6
9
|
const node_dgram_1 = require("node:dgram");
|
|
7
10
|
const node_net_1 = require("node:net");
|
|
@@ -192,7 +195,7 @@ class ReceivedDatagramError extends Error {
|
|
|
192
195
|
function isSocketExchangeError(error) {
|
|
193
196
|
return error instanceof SocketExchangeError;
|
|
194
197
|
}
|
|
195
|
-
function exchangeDatagram(address, family, port, packet, timeoutMs, signal) {
|
|
198
|
+
function exchangeDatagram(address, family, port, packet, timeoutMs, signal, oneWay = false) {
|
|
196
199
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000) {
|
|
197
200
|
return Promise.reject(new Error("native NHP timeout must be from 1 to 60000 milliseconds"));
|
|
198
201
|
}
|
|
@@ -237,11 +240,11 @@ function exchangeDatagram(address, family, port, packet, timeoutMs, signal) {
|
|
|
237
240
|
finish(undefined, reply);
|
|
238
241
|
});
|
|
239
242
|
socket.connect(port, address, () => {
|
|
240
|
-
sendOwnedDatagram(socket, packet, finish);
|
|
243
|
+
sendOwnedDatagram(socket, packet, finish, oneWay ? () => finish(undefined, Buffer.alloc(0)) : undefined);
|
|
241
244
|
});
|
|
242
245
|
});
|
|
243
246
|
}
|
|
244
|
-
function sendOwnedDatagram(socket, packet, finish) {
|
|
247
|
+
function sendOwnedDatagram(socket, packet, finish, onSent) {
|
|
245
248
|
// dgram owns its input until this callback. Use an independent encrypted
|
|
246
249
|
// datagram so nativeKnock can wipe the builder's packet immediately when an
|
|
247
250
|
// abort settles the exchange. Wipe this copy only after libuv releases it.
|
|
@@ -251,6 +254,8 @@ function sendOwnedDatagram(socket, packet, finish) {
|
|
|
251
254
|
try {
|
|
252
255
|
if (error)
|
|
253
256
|
finish(error);
|
|
257
|
+
else
|
|
258
|
+
onSent?.();
|
|
254
259
|
}
|
|
255
260
|
finally {
|
|
256
261
|
outbound.fill(0);
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export declare const NHP_TYPE_KNOCK = 1;
|
|
2
2
|
export declare const NHP_TYPE_ACK = 2;
|
|
3
3
|
export declare const NHP_TYPE_COOKIE = 7;
|
|
4
|
+
export declare const NHP_TYPE_LIST = 5;
|
|
5
|
+
export declare const NHP_TYPE_LIST_RESULT = 6;
|
|
6
|
+
export declare const NHP_TYPE_REKNOCK = 8;
|
|
7
|
+
export declare const NHP_TYPE_OTP = 12;
|
|
8
|
+
export declare const NHP_TYPE_REGISTER = 13;
|
|
9
|
+
export declare const NHP_TYPE_REGISTER_ACK = 14;
|
|
10
|
+
export declare const NHP_TYPE_EXIT = 16;
|
|
4
11
|
export declare const NHP_HEADER_SIZE = 240;
|
|
5
12
|
export declare const NHP_PACKET_SIZE = 4096;
|
|
6
13
|
export declare const NHP_MAX_BODY_SIZE: number;
|
|
@@ -20,6 +27,8 @@ export interface BuildNHPMessageInput {
|
|
|
20
27
|
readonly timestampNanos?: bigint;
|
|
21
28
|
readonly counter?: bigint;
|
|
22
29
|
readonly preamble?: number;
|
|
30
|
+
readonly cookie?: Uint8Array;
|
|
31
|
+
readonly hubProof?: boolean;
|
|
23
32
|
}
|
|
24
33
|
export declare function buildNHPMessage(input: BuildNHPMessageInput): {
|
|
25
34
|
packet: Uint8Array;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.nhpWireTesting = exports.NHP_MAX_BODY_SIZE = exports.NHP_PACKET_SIZE = exports.NHP_HEADER_SIZE = exports.NHP_TYPE_COOKIE = exports.NHP_TYPE_ACK = exports.NHP_TYPE_KNOCK = void 0;
|
|
3
|
+
exports.nhpWireTesting = exports.NHP_MAX_BODY_SIZE = exports.NHP_PACKET_SIZE = exports.NHP_HEADER_SIZE = exports.NHP_TYPE_EXIT = exports.NHP_TYPE_REGISTER_ACK = exports.NHP_TYPE_REGISTER = exports.NHP_TYPE_OTP = exports.NHP_TYPE_REKNOCK = exports.NHP_TYPE_LIST_RESULT = exports.NHP_TYPE_LIST = exports.NHP_TYPE_COOKIE = exports.NHP_TYPE_ACK = exports.NHP_TYPE_KNOCK = void 0;
|
|
4
4
|
exports.buildNHPMessage = buildNHPMessage;
|
|
5
5
|
exports.decryptNHPReply = decryptNHPReply;
|
|
6
6
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -8,6 +8,13 @@ const node_zlib_1 = require("node:zlib");
|
|
|
8
8
|
exports.NHP_TYPE_KNOCK = 1;
|
|
9
9
|
exports.NHP_TYPE_ACK = 2;
|
|
10
10
|
exports.NHP_TYPE_COOKIE = 7;
|
|
11
|
+
exports.NHP_TYPE_LIST = 5;
|
|
12
|
+
exports.NHP_TYPE_LIST_RESULT = 6;
|
|
13
|
+
exports.NHP_TYPE_REKNOCK = 8;
|
|
14
|
+
exports.NHP_TYPE_OTP = 12;
|
|
15
|
+
exports.NHP_TYPE_REGISTER = 13;
|
|
16
|
+
exports.NHP_TYPE_REGISTER_ACK = 14;
|
|
17
|
+
exports.NHP_TYPE_EXIT = 16;
|
|
11
18
|
exports.NHP_HEADER_SIZE = 240;
|
|
12
19
|
exports.NHP_PACKET_SIZE = 4_096;
|
|
13
20
|
exports.NHP_MAX_BODY_SIZE = exports.NHP_PACKET_SIZE - exports.NHP_HEADER_SIZE - 16;
|
|
@@ -26,8 +33,12 @@ function buildNHPMessage(input) {
|
|
|
26
33
|
validateKey(input.serverPublicKey, "server public key");
|
|
27
34
|
if (input.body.byteLength > exports.NHP_MAX_BODY_SIZE)
|
|
28
35
|
throw new Error("NHP application body is too large");
|
|
29
|
-
if (input.type
|
|
36
|
+
if (![1, 5, 8, 12, 13, 16].includes(input.type))
|
|
30
37
|
throw new Error("unsupported NHP initiator message type");
|
|
38
|
+
const cookieRequired = input.type === exports.NHP_TYPE_REKNOCK || input.hubProof === true;
|
|
39
|
+
if ((input.hubProof && input.type !== exports.NHP_TYPE_LIST) ||
|
|
40
|
+
(cookieRequired ? input.cookie?.length !== 32 : input.cookie !== undefined))
|
|
41
|
+
throw new Error("invalid NHP cookie proof");
|
|
31
42
|
const ephemeralPrivateKey = input.ephemeralPrivateKey
|
|
32
43
|
? Buffer.from(input.ephemeralPrivateKey)
|
|
33
44
|
: (0, node_crypto_1.randomBytes)(32);
|
|
@@ -68,14 +79,14 @@ function buildNHPMessage(input) {
|
|
|
68
79
|
const bodyKey = derived[1];
|
|
69
80
|
header.writeUInt8(1, 8);
|
|
70
81
|
header.writeUInt8(1, 9);
|
|
71
|
-
header.writeUInt16BE(0, 10);
|
|
82
|
+
header.writeUInt16BE(input.hubProof ? 4 : 0, 10);
|
|
72
83
|
header.writeBigUInt64BE(counter, 16);
|
|
73
84
|
const payloadSize = input.body.byteLength === 0 ? 0 : input.body.byteLength + 16;
|
|
74
85
|
header.writeUInt32BE(preamble >>> 0, 0);
|
|
75
86
|
header.writeUInt32BE((preamble ^ ((input.type << 16) | payloadSize)) >>> 0, 4);
|
|
76
87
|
const bodyAad = hashParts([...chainHashParts, header.subarray(0, HEADER_COMMON_SIZE)]);
|
|
77
88
|
const sealedBody = input.body.byteLength === 0 ? Buffer.alloc(0) : seal(bodyKey, nonce, input.body, bodyAad);
|
|
78
|
-
headerDigest(input.serverPublicKey, header).copy(header, OFFSET_DIGEST);
|
|
89
|
+
headerDigest(input.serverPublicKey, header, input.cookie).copy(header, OFFSET_DIGEST);
|
|
79
90
|
return { packet: Buffer.concat([header, sealedBody]), counter };
|
|
80
91
|
}
|
|
81
92
|
finally {
|
|
@@ -156,7 +167,8 @@ function decryptNHPReply(devicePrivateKey, expectedServerPublicKey, packetValue)
|
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
function validateReplyProfile(type, flags) {
|
|
159
|
-
if (![exports.NHP_TYPE_ACK, exports.NHP_TYPE_COOKIE].includes(type) ||
|
|
170
|
+
if (![exports.NHP_TYPE_ACK, exports.NHP_TYPE_COOKIE, exports.NHP_TYPE_LIST_RESULT, exports.NHP_TYPE_REGISTER_ACK].includes(type) ||
|
|
171
|
+
(flags !== 0 && flags !== FLAG_COMPRESS)) {
|
|
160
172
|
throw new Error("NHP reply type or flags are outside the reply profile");
|
|
161
173
|
}
|
|
162
174
|
}
|
|
@@ -247,8 +259,13 @@ function open(key, nonce, sealed, aad) {
|
|
|
247
259
|
decipher.setAuthTag(tag);
|
|
248
260
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
249
261
|
}
|
|
250
|
-
function headerDigest(peerStaticPublicKey, header) {
|
|
251
|
-
return hashParts([
|
|
262
|
+
function headerDigest(peerStaticPublicKey, header, cookie) {
|
|
263
|
+
return hashParts([
|
|
264
|
+
INITIAL_HASH,
|
|
265
|
+
peerStaticPublicKey,
|
|
266
|
+
header.subarray(0, OFFSET_DIGEST),
|
|
267
|
+
...(cookie ? [cookie] : []),
|
|
268
|
+
]);
|
|
252
269
|
}
|
|
253
270
|
function trackSecret(secrets, value) {
|
|
254
271
|
secrets.push(value);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PortalProvider } from "./provider.js";
|
|
1
2
|
import type { NHPMessage } from "./nhp-wire.js";
|
|
2
3
|
import { type PortalDeployment, type ValidatedCell, type ValidatedDeployment } from "./deployment.js";
|
|
3
4
|
import { type NativeExchangeOptions } from "./native-udp.js";
|
|
@@ -5,8 +6,14 @@ import { type VerifiedQv2Link } from "./qv2.js";
|
|
|
5
6
|
export interface CreatePortalOpenerOptions {
|
|
6
7
|
/** Full qv2t1 qURL. The opener re-verifies it before each native open. */
|
|
7
8
|
readonly qurl: string;
|
|
9
|
+
/** Independently obtained CRID. When supplied, checked before every native open. */
|
|
10
|
+
readonly expectedCRID?: string;
|
|
8
11
|
/** Public deployment trust. Omit to load QURL_DEPLOYMENT on the first start. */
|
|
9
12
|
readonly deployment?: PortalDeployment;
|
|
13
|
+
readonly provider?: PortalProvider;
|
|
14
|
+
/** Explicit choice. Native UDP is the default and never falls back to relay. */
|
|
15
|
+
readonly transport?: "native" | "relay";
|
|
16
|
+
readonly relayFetch?: typeof globalThis.fetch;
|
|
10
17
|
/** Whole-operation deadline for each NHP open. The default is 15 seconds. */
|
|
11
18
|
readonly openTimeoutMs?: number;
|
|
12
19
|
/** Protected-content fetch implementation. Native NHP opening never uses it. */
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.portalOpenerTesting = exports.PortalInvalidReplyError = exports.PortalBusyError = exports.PortalTooManyRedirectsError = exports.PortalRedirectError = exports.PortalTargetChangedError = exports.PortalOpenTimeoutError = exports.PortalOpenerClosedError = exports.PortalOpenerNotReadyError = exports.PortalOpenerNotStartedError = exports.PortalStateError = exports.PortalDenyError = exports.PortalVerificationError = exports.PortalConfigurationError = void 0;
|
|
4
4
|
exports.createPortalOpener = createPortalOpener;
|
|
5
5
|
exports.createPortalOpenerWithRuntime = createPortalOpenerWithRuntime;
|
|
6
|
+
const relay_js_1 = require("./relay.js");
|
|
6
7
|
const node_crypto_1 = require("node:crypto");
|
|
7
8
|
const node_events_1 = require("node:events");
|
|
8
9
|
const nhp_wire_js_1 = require("./nhp-wire.js");
|
|
@@ -10,6 +11,7 @@ const deployment_js_1 = require("./deployment.js");
|
|
|
10
11
|
const native_udp_js_1 = require("./native-udp.js");
|
|
11
12
|
const strict_json_js_1 = require("./strict-json.js");
|
|
12
13
|
const qv2_js_1 = require("./qv2.js");
|
|
14
|
+
const crid_js_1 = require("../crid.js");
|
|
13
15
|
const SESSION_COOKIE = "qurl_vsession";
|
|
14
16
|
const MAX_REDIRECT_REQUESTS = 10;
|
|
15
17
|
const DEFAULT_OPEN_TIMEOUT_MS = 15_000;
|
|
@@ -146,6 +148,9 @@ function createPortalOpenerWithRuntime(options, runtime) {
|
|
|
146
148
|
if (typeof options.qurl !== "string" || options.qurl.trim() === "") {
|
|
147
149
|
throw new PortalConfigurationError("native portal opener qurl must be a non-empty string");
|
|
148
150
|
}
|
|
151
|
+
if ("expectedCRID" in options && !(0, crid_js_1.parseCrid)(options.expectedCRID, true)) {
|
|
152
|
+
throw new PortalConfigurationError("native portal opener expectedCRID is invalid or unsupported");
|
|
153
|
+
}
|
|
149
154
|
if (options.openTimeoutMs !== undefined &&
|
|
150
155
|
(!Number.isFinite(options.openTimeoutMs) ||
|
|
151
156
|
options.openTimeoutMs < 1 ||
|
|
@@ -155,6 +160,12 @@ function createPortalOpenerWithRuntime(options, runtime) {
|
|
|
155
160
|
if (options.fetch !== undefined && typeof options.fetch !== "function") {
|
|
156
161
|
throw new PortalConfigurationError("native portal opener fetch must be a function");
|
|
157
162
|
}
|
|
163
|
+
if (options.transport !== undefined &&
|
|
164
|
+
options.transport !== "native" &&
|
|
165
|
+
options.transport !== "relay")
|
|
166
|
+
throw new PortalConfigurationError("unsupported portal transport");
|
|
167
|
+
if (options.provider && options.deployment)
|
|
168
|
+
throw new PortalConfigurationError("configure either a provider or a deployment");
|
|
158
169
|
return new NativePortalOpener(options, runtime);
|
|
159
170
|
}
|
|
160
171
|
class NativePortalOpener {
|
|
@@ -162,6 +173,10 @@ class NativePortalOpener {
|
|
|
162
173
|
#fetch;
|
|
163
174
|
#explicitDeployment;
|
|
164
175
|
#openTimeoutMs;
|
|
176
|
+
#expectedCRID;
|
|
177
|
+
#provider;
|
|
178
|
+
#transport;
|
|
179
|
+
#relayFetch;
|
|
165
180
|
#qurl;
|
|
166
181
|
#resolvedDeployment;
|
|
167
182
|
#sessionSecret;
|
|
@@ -183,7 +198,11 @@ class NativePortalOpener {
|
|
|
183
198
|
#consecutiveFailures = 0;
|
|
184
199
|
constructor(options, runtime) {
|
|
185
200
|
this.#qurl = options.qurl;
|
|
201
|
+
this.#expectedCRID = options.expectedCRID;
|
|
186
202
|
this.#explicitDeployment = options.deployment;
|
|
203
|
+
this.#provider = options.provider;
|
|
204
|
+
this.#transport = options.transport ?? "native";
|
|
205
|
+
this.#relayFetch = options.relayFetch;
|
|
187
206
|
this.#runtime = runtime;
|
|
188
207
|
this.#fetch = options.fetch ?? runtime.fetch;
|
|
189
208
|
this.#openTimeoutMs = options.openTimeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS;
|
|
@@ -475,10 +494,13 @@ class NativePortalOpener {
|
|
|
475
494
|
}
|
|
476
495
|
async #performOpen(signal) {
|
|
477
496
|
this.#requireOpen();
|
|
478
|
-
const deployment = this.#
|
|
497
|
+
const deployment = this.#provider
|
|
498
|
+
? (0, deployment_js_1.loadPortalDeployment)(await this.#provider.resolve(signal))
|
|
499
|
+
: this.#loadDeployment();
|
|
500
|
+
signal.throwIfAborted();
|
|
479
501
|
const link = this.#verifyLink(deployment);
|
|
480
502
|
try {
|
|
481
|
-
const cell = validatedCellForLink(link, deployment);
|
|
503
|
+
const cell = this.#transport === "native" ? validatedCellForLink(link, deployment) : undefined;
|
|
482
504
|
this.#sessionSecret ??= (0, node_crypto_1.randomBytes)(32);
|
|
483
505
|
const body = this.#knockBody(link);
|
|
484
506
|
// Start the local validity bound before DNS and UDP I/O. The cell starts
|
|
@@ -486,9 +508,9 @@ class NativePortalOpener {
|
|
|
486
508
|
const startedAtNanos = this.#runtime.nowNanos();
|
|
487
509
|
const startedAtEpochMs = this.#runtime.nowEpochMs();
|
|
488
510
|
try {
|
|
489
|
-
const reply =
|
|
490
|
-
signal
|
|
491
|
-
|
|
511
|
+
const reply = cell
|
|
512
|
+
? await this.#runtime.knock(cell, link.devicePrivateKey, body, { signal })
|
|
513
|
+
: await (0, relay_js_1.relayKnock)(link.claims.relayUrl, deployment.relayAllowlist, link.claims.cellPublicKey, link.devicePrivateKey, body, { signal, fetch: this.#relayFetch });
|
|
492
514
|
try {
|
|
493
515
|
this.#requireOpen();
|
|
494
516
|
if (reply.type === nhp_wire_js_1.NHP_TYPE_COOKIE)
|
|
@@ -674,7 +696,7 @@ class NativePortalOpener {
|
|
|
674
696
|
}
|
|
675
697
|
#verifyLink(deployment) {
|
|
676
698
|
try {
|
|
677
|
-
return (0, qv2_js_1.verifyQv2Link)(this.#qurl, deployment.issuers);
|
|
699
|
+
return (0, qv2_js_1.verifyQv2Link)(this.#qurl, deployment.issuers, this.#expectedCRID);
|
|
678
700
|
}
|
|
679
701
|
catch (error) {
|
|
680
702
|
throw new PortalVerificationError("native qURL credential validation failed", {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
import { type PortalDeployment } from "./deployment.js";
|
|
3
|
+
export interface PortalProvider {
|
|
4
|
+
/** Resolve authenticated, current trust. A failure must not return stale trust. */
|
|
5
|
+
resolve(signal?: AbortSignal): Promise<PortalDeployment>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createStaticProvider(deployment: PortalDeployment): PortalProvider;
|
|
8
|
+
export interface DiscoveryProviderOptions {
|
|
9
|
+
readonly fetcher: (signal?: AbortSignal) => Promise<Uint8Array>;
|
|
10
|
+
readonly pinSHA256?: Uint8Array;
|
|
11
|
+
readonly manifestKeys?: ReadonlyMap<string, KeyObject>;
|
|
12
|
+
readonly requireSignature?: boolean;
|
|
13
|
+
/** Persist the required floor in deployment configuration across restarts. */
|
|
14
|
+
readonly minVersion?: bigint;
|
|
15
|
+
readonly expectedProfile?: string;
|
|
16
|
+
readonly now?: () => number;
|
|
17
|
+
}
|
|
18
|
+
export declare function createHTTPManifestFetcher(value: string, fetch?: typeof globalThis.fetch): DiscoveryProviderOptions["fetcher"];
|
|
19
|
+
export declare function createDiscoveryProvider(options: DiscoveryProviderOptions): PortalProvider;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStaticProvider = createStaticProvider;
|
|
4
|
+
exports.createHTTPManifestFetcher = createHTTPManifestFetcher;
|
|
5
|
+
exports.createDiscoveryProvider = createDiscoveryProvider;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const deployment_js_1 = require("./deployment.js");
|
|
8
|
+
const qv2_js_1 = require("./qv2.js");
|
|
9
|
+
const strict_json_js_1 = require("./strict-json.js");
|
|
10
|
+
const relay_js_1 = require("./relay.js");
|
|
11
|
+
function createStaticProvider(deployment) {
|
|
12
|
+
const snapshot = JSON.parse(JSON.stringify(deployment));
|
|
13
|
+
(0, deployment_js_1.loadPortalDeployment)(snapshot);
|
|
14
|
+
return {
|
|
15
|
+
async resolve(signal) {
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
return JSON.parse(JSON.stringify(snapshot));
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const MANIFEST_DOMAIN = Buffer.from("NHP-QURL-V2-DISCOVERY-MANIFEST\0");
|
|
22
|
+
const HALF_ORDER = Buffer.from("7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8", "hex");
|
|
23
|
+
const LIMIT = 1_048_576;
|
|
24
|
+
const MAX_INT64 = (1n << 63n) - 1n;
|
|
25
|
+
function object(value, keys) {
|
|
26
|
+
if (!(0, strict_json_js_1.isStrictJsonObject)(value) || Object.keys(value).some((key) => !keys.includes(key)))
|
|
27
|
+
throw new Error("invalid discovery schema");
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function decode(value) {
|
|
31
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
32
|
+
throw new Error("invalid discovery base64url");
|
|
33
|
+
const bytes = Buffer.from(value, "base64url");
|
|
34
|
+
if (bytes.toString("base64url") !== value)
|
|
35
|
+
throw new Error("noncanonical discovery base64url");
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
38
|
+
function createHTTPManifestFetcher(value, fetch = globalThis.fetch) {
|
|
39
|
+
const url = new URL(value);
|
|
40
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash)
|
|
41
|
+
throw new Error("discovery URL must be HTTPS without credentials or fragment");
|
|
42
|
+
return async (signal) => {
|
|
43
|
+
const deadline = AbortSignal.timeout(30_000);
|
|
44
|
+
const response = await fetch(url.href, {
|
|
45
|
+
redirect: "error",
|
|
46
|
+
signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
await response.body?.cancel().catch(() => undefined);
|
|
50
|
+
throw new Error(`discovery returned HTTP ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
return (0, relay_js_1.readBoundedBody)(response, LIMIT);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createDiscoveryProvider(options) {
|
|
56
|
+
const pin = options.pinSHA256 === undefined ? undefined : Buffer.from(options.pinSHA256);
|
|
57
|
+
const keys = new Map(options.manifestKeys);
|
|
58
|
+
if (typeof options.fetcher !== "function" ||
|
|
59
|
+
(!pin && keys.size === 0) ||
|
|
60
|
+
(pin && pin.length !== 32) ||
|
|
61
|
+
(options.requireSignature && keys.size === 0))
|
|
62
|
+
throw new Error("discovery requires a fetcher and a valid pin or signing key");
|
|
63
|
+
for (const [kid, key] of keys) {
|
|
64
|
+
if (!kid.trim())
|
|
65
|
+
throw new Error("discovery signing key id is required");
|
|
66
|
+
(0, qv2_js_1.issuerKeyFromSpki)(key.export({ type: "spki", format: "der" }));
|
|
67
|
+
}
|
|
68
|
+
const fetcher = options.fetcher;
|
|
69
|
+
const required = options.requireSignature;
|
|
70
|
+
const profile = options.expectedProfile;
|
|
71
|
+
const now = options.now ?? Date.now;
|
|
72
|
+
let floor = options.minVersion ?? 0n;
|
|
73
|
+
if (typeof floor !== "bigint" || floor < 0n || floor > MAX_INT64)
|
|
74
|
+
throw new Error("invalid discovery version floor");
|
|
75
|
+
return {
|
|
76
|
+
async resolve(signal) {
|
|
77
|
+
signal?.throwIfAborted();
|
|
78
|
+
const envelope = object((0, strict_json_js_1.parseStrictJson)(await fetcher(signal), LIMIT), [
|
|
79
|
+
"manifest_b64",
|
|
80
|
+
"sig_b64",
|
|
81
|
+
"kid",
|
|
82
|
+
]);
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
const bytes = decode(envelope.manifest_b64);
|
|
85
|
+
if (pin && !(0, node_crypto_1.timingSafeEqual)(pin, (0, node_crypto_1.createHash)("sha256").update(bytes).digest()))
|
|
86
|
+
throw new Error("discovery manifest pin mismatch");
|
|
87
|
+
let signed = false;
|
|
88
|
+
if (keys.size && envelope.sig_b64 !== undefined && envelope.sig_b64 !== "") {
|
|
89
|
+
const signature = decode(envelope.sig_b64);
|
|
90
|
+
const key = typeof envelope.kid === "string" ? keys.get(envelope.kid) : undefined;
|
|
91
|
+
if (!key || signature.length !== 64)
|
|
92
|
+
throw new Error("invalid discovery signature");
|
|
93
|
+
if (Buffer.compare(signature.subarray(32), HALF_ORDER) > 0 ||
|
|
94
|
+
!(0, node_crypto_1.verify)("sha256", Buffer.concat([MANIFEST_DOMAIN, bytes]), { key, dsaEncoding: "ieee-p1363" }, signature))
|
|
95
|
+
throw new Error("invalid discovery signature");
|
|
96
|
+
signed = true;
|
|
97
|
+
}
|
|
98
|
+
if ((!pin && !signed) || (required && !signed))
|
|
99
|
+
throw new Error("discovery manifest is not authenticated");
|
|
100
|
+
const manifest = object((0, strict_json_js_1.parseStrictJson)(bytes, LIMIT), [
|
|
101
|
+
"profile",
|
|
102
|
+
"version",
|
|
103
|
+
"issued_at",
|
|
104
|
+
"not_after",
|
|
105
|
+
"issuers",
|
|
106
|
+
"relay_allowlist",
|
|
107
|
+
]);
|
|
108
|
+
const { version, issued_at: issuedAt, not_after: notAfter } = manifest;
|
|
109
|
+
if (typeof version !== "bigint" ||
|
|
110
|
+
version <= 0n ||
|
|
111
|
+
version > MAX_INT64 ||
|
|
112
|
+
typeof issuedAt !== "bigint" ||
|
|
113
|
+
issuedAt <= 0n ||
|
|
114
|
+
issuedAt > MAX_INT64 ||
|
|
115
|
+
typeof notAfter !== "bigint" ||
|
|
116
|
+
notAfter <= issuedAt ||
|
|
117
|
+
notAfter > MAX_INT64 ||
|
|
118
|
+
(manifest.profile !== undefined && typeof manifest.profile !== "string") ||
|
|
119
|
+
(profile && profile !== manifest.profile))
|
|
120
|
+
throw new Error("invalid discovery manifest");
|
|
121
|
+
const seconds = BigInt(Math.floor(now() / 1000));
|
|
122
|
+
if (seconds + 120n < issuedAt)
|
|
123
|
+
throw new Error("discovery manifest is not yet valid");
|
|
124
|
+
if (seconds > notAfter)
|
|
125
|
+
throw new Error("discovery manifest expired");
|
|
126
|
+
if (!Array.isArray(manifest.issuers) ||
|
|
127
|
+
!Array.isArray(manifest.relay_allowlist) ||
|
|
128
|
+
!manifest.relay_allowlist.some((value) => typeof value === "string" && value.trim()))
|
|
129
|
+
throw new Error("discovery manifest has no usable trust");
|
|
130
|
+
const deployment = {
|
|
131
|
+
cells: [],
|
|
132
|
+
issuers: manifest.issuers.map((row) => {
|
|
133
|
+
const issuer = object(row, ["kid", "spki_der_b64"]);
|
|
134
|
+
if (typeof issuer.kid !== "string" || typeof issuer.spki_der_b64 !== "string")
|
|
135
|
+
throw new Error("invalid discovery issuer");
|
|
136
|
+
decode(issuer.spki_der_b64);
|
|
137
|
+
return { kid: issuer.kid, spki_der_b64: issuer.spki_der_b64 };
|
|
138
|
+
}),
|
|
139
|
+
relay_allowlist: manifest.relay_allowlist.map((value) => {
|
|
140
|
+
if (typeof value !== "string")
|
|
141
|
+
throw new Error("invalid discovery relay host");
|
|
142
|
+
return value;
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
(0, deployment_js_1.loadPortalDeployment)(deployment);
|
|
146
|
+
// No await between comparison and advance: concurrent resolutions cannot lower the floor.
|
|
147
|
+
if (version < floor)
|
|
148
|
+
throw new Error("discovery manifest downgrade");
|
|
149
|
+
floor = version;
|
|
150
|
+
return deployment;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
package/dist/cjs/node/qv2.d.ts
CHANGED
|
@@ -6,10 +6,11 @@ export interface VerifiedQv2Link {
|
|
|
6
6
|
readonly kid: string;
|
|
7
7
|
readonly cellPublicKey: Uint8Array;
|
|
8
8
|
readonly resourcePublicKeyB64: string;
|
|
9
|
+
readonly relayUrl: string;
|
|
9
10
|
};
|
|
10
11
|
readonly devicePrivateKey: Uint8Array;
|
|
11
12
|
}
|
|
12
|
-
export declare function verifyQv2Link(qurl: string, issuers: ReadonlyMap<string, KeyObject
|
|
13
|
+
export declare function verifyQv2Link(qurl: string, issuers: ReadonlyMap<string, KeyObject>, expectedCRID?: string): VerifiedQv2Link;
|
|
13
14
|
export declare function issuerKeyFromSpki(spki: Uint8Array): KeyObject;
|
|
14
15
|
export declare function x25519PublicFromPrivate(raw: Uint8Array): Uint8Array;
|
|
15
16
|
export declare function strictBase64Url(value: string): Uint8Array;
|
|
@@ -18,6 +19,7 @@ type ParsedClaims = {
|
|
|
18
19
|
kid: string;
|
|
19
20
|
cellPublicKeyB64: string;
|
|
20
21
|
resourcePublicKeyB64: string;
|
|
22
|
+
relayUrl: string;
|
|
21
23
|
qurlUserPublicKeyB64: string;
|
|
22
24
|
};
|
|
23
25
|
declare function parseClaims(raw: Uint8Array): ParsedClaims;
|