@peers-app/peers-sdk 0.28.12 → 0.29.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/contracts/index.d.ts +1 -1
- package/dist/contracts/index.js +4 -2
- package/dist/contracts/remote-contract-transport.d.ts +27 -0
- package/dist/contracts/remote-contract-transport.js +27 -0
- package/dist/contracts/system/network-contracts.js +1 -1
- package/dist/define/context.d.ts +9 -0
- package/dist/device/binary-peer-connection-v2.d.ts +2 -2
- package/dist/device/binary-peer-connection-v2.js +51 -13
- package/dist/device/tx-encoding.d.ts +22 -1
- package/dist/device/tx-encoding.js +63 -26
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.d.ts +7 -2
- package/dist/keys.js +61 -12
- package/dist/native-codecs.d.ts +34 -0
- package/dist/native-codecs.js +16 -0
- package/dist/package-loader/isolated-artifact.d.ts +2 -0
- package/dist/package-loader/isolated-artifact.js +3 -1
- package/dist/package-loader/isolated-runtime.d.ts +6 -0
- package/dist/package-loader/package-loader.js +1 -0
- package/dist/serial-json.d.ts +7 -0
- package/dist/serial-json.js +16 -2
- package/dist/types/peer-device.d.ts +6 -0
- package/dist/types/peer-device.js +7 -1
- package/dist/utils.js +2 -2
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ export { hostContractConsumer, type IHostContractConsumer, type IHostContractCon
|
|
|
14
14
|
export { getHostContractTransport, type HostContractTransportListener, registerHostContractTransport, subscribeHostContractTransport, waitForHostContractTransport, } from "./host-contract-transport";
|
|
15
15
|
export { PersistentContractRegistry } from "./persistent-registry";
|
|
16
16
|
export { ContractRegistry, type IContractProviderRegistration } from "./registry";
|
|
17
|
-
export { createRemoteContractEndpoint, getRemoteContractTransportFactory, type IRemoteContractHost, type IRemoteContractTarget, type RemoteContractTransportFactory, resolveRemoteTargetEndpoint, setRemoteContractTransportFactory, } from "./remote-contract-transport";
|
|
17
|
+
export { connectToRemoteDevice, createRemoteContractEndpoint, getRemoteContractTransportFactory, type IRemoteContractHost, type IRemoteContractTarget, type RemoteContractTransportFactory, type RemoteDeviceConnector, resolveRemoteTargetEndpoint, setRemoteContractTransportFactory, setRemoteDeviceConnector, } from "./remote-contract-transport";
|
|
18
18
|
export * from "./system/app-updates-contract";
|
|
19
19
|
export { CONTRACT_CATALOG_VERSION, contractCatalogProviderPackageId, getContractCatalogDefinition, type IContractCatalogConsumer, type IContractDefinitionWithPolicy, type IContractProviderInfo, type IContractToolPolicy, type IExposedContractInfo, type IExposedToolInfo, type IGetContractDefinitionInput, type IListContractProvidersInput, } from "./system/contract-catalog-contract";
|
|
20
20
|
export * from "./system/desktop-tools-contract";
|
package/dist/contracts/index.js
CHANGED
|
@@ -15,8 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.ContractRegistry = exports.PersistentContractRegistry = exports.waitForHostContractTransport = exports.subscribeHostContractTransport = exports.registerHostContractTransport = exports.getHostContractTransport = exports.hostContractConsumer = exports.hostContract = exports.freezeContracts = exports.FROZEN_CONTRACTS_FILE = exports.describeFrozenContractChanges = exports.checkFrozenContracts = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.createLocalContractConsumer = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.inProcessTransportPair = exports.connectionContractTransport = exports.CONTRACT_REQUEST_CHANNEL = exports.CONTRACT_NOTIFY_CHANNEL = exports.verifyRemoteContractIdentity = exports.registerContractResolver = exports.isSelfTrustedContractCaller = exports.isLocalOnlyContractId = exports.handleContractCall = exports.getContractResolver = exports.createUserContextContractProviderRouter = exports.createLocalUserContextContractProviderRouter = exports.authorizeRemoteContractCall = exports.sameUserContractPermissionCheck = exports.requireWorkflowRunContext = exports.createRemoteContractConsumer = exports.createContractProviderSession = exports.createContractProviderEndpoint = exports.createContractProvider = exports.createContractConsumer = exports.CONTRACT_CALL_RPC = exports.contractProvidersSchema = exports.contractProvidersMetaData = exports.ContractProvidersTable = exports.ContractProviders = exports.RetiredContractProviderEndpointError = exports.createContractProviderRouter = exports.PackageBuilder = exports.definePackage = exports.ContractBuilder = void 0;
|
|
18
|
-
exports.
|
|
19
|
-
exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractShapeChanges = exports.contractKey = exports.registerSystemContract = void 0;
|
|
18
|
+
exports.installSystemContracts = exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = exports.getToolAccessControlContractDefinition = exports.schedulerProviderPackageId = exports.SCHEDULER_MAX_DELAY_MS = exports.SCHEDULER_CONTRACT_VERSION = exports.getSchedulerContractDefinition = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.localFoldersProviderPackageId = exports.LOCAL_FOLDERS_RESERVED_DIR = exports.LOCAL_FOLDERS_READ_RANGE_MAX = exports.LOCAL_FOLDERS_JOB_TTL_MS = exports.LOCAL_FOLDERS_HAVE_BATCH = exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES = exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS = exports.LOCAL_FOLDERS_CONTRACT_VERSION = exports.LOCAL_FOLDERS_CHUNK_MIN = exports.LOCAL_FOLDERS_CHUNK_MAX = exports.LOCAL_FOLDERS_CHUNK_AVG = exports.LOCAL_FOLDERS_ASSEMBLE_STALL_MS = exports.getLocalFoldersContractDefinition = exports.INVITES_CONTRACT_VERSION = exports.getInvitesContractDefinition = exports.IDENTITY_CONTRACT_VERSION = exports.getIdentityContractDefinition = exports.httpProviderPackageId = exports.HTTP_MAX_TIMEOUT_MS = exports.HTTP_MAX_BODY_BYTES = exports.HTTP_DEFAULT_TIMEOUT_MS = exports.HTTP_CONTRACT_VERSION = exports.getHttpContractDefinition = exports.getDeviceOperationsContractDefinition = exports.deviceOperationsToolIds = exports.deviceOperationsProviderPackageId = exports.DEVICE_OPERATIONS_MAX_TOOLS = exports.DEVICE_OPERATIONS_MAX_ROWS = exports.DEVICE_OPERATIONS_MAX_RESULT_BYTES = exports.DEVICE_OPERATIONS_MAX_QUERY_LENGTH = exports.DEVICE_OPERATIONS_CONTRACT_VERSION = exports.getContractCatalogDefinition = exports.contractCatalogProviderPackageId = exports.CONTRACT_CATALOG_VERSION = exports.setRemoteDeviceConnector = exports.setRemoteContractTransportFactory = exports.resolveRemoteTargetEndpoint = exports.getRemoteContractTransportFactory = exports.createRemoteContractEndpoint = exports.connectToRemoteDevice = void 0;
|
|
19
|
+
exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractShapeChanges = exports.contractKey = exports.registerSystemContract = exports.listSystemContracts = exports.isSystemContractId = void 0;
|
|
20
20
|
__exportStar(require("./action-dispatch"), exports);
|
|
21
21
|
var builder_1 = require("./builder");
|
|
22
22
|
Object.defineProperty(exports, "ContractBuilder", { enumerable: true, get: function () { return builder_1.ContractBuilder; } });
|
|
@@ -84,10 +84,12 @@ Object.defineProperty(exports, "PersistentContractRegistry", { enumerable: true,
|
|
|
84
84
|
var registry_1 = require("./registry");
|
|
85
85
|
Object.defineProperty(exports, "ContractRegistry", { enumerable: true, get: function () { return registry_1.ContractRegistry; } });
|
|
86
86
|
var remote_contract_transport_1 = require("./remote-contract-transport");
|
|
87
|
+
Object.defineProperty(exports, "connectToRemoteDevice", { enumerable: true, get: function () { return remote_contract_transport_1.connectToRemoteDevice; } });
|
|
87
88
|
Object.defineProperty(exports, "createRemoteContractEndpoint", { enumerable: true, get: function () { return remote_contract_transport_1.createRemoteContractEndpoint; } });
|
|
88
89
|
Object.defineProperty(exports, "getRemoteContractTransportFactory", { enumerable: true, get: function () { return remote_contract_transport_1.getRemoteContractTransportFactory; } });
|
|
89
90
|
Object.defineProperty(exports, "resolveRemoteTargetEndpoint", { enumerable: true, get: function () { return remote_contract_transport_1.resolveRemoteTargetEndpoint; } });
|
|
90
91
|
Object.defineProperty(exports, "setRemoteContractTransportFactory", { enumerable: true, get: function () { return remote_contract_transport_1.setRemoteContractTransportFactory; } });
|
|
92
|
+
Object.defineProperty(exports, "setRemoteDeviceConnector", { enumerable: true, get: function () { return remote_contract_transport_1.setRemoteDeviceConnector; } });
|
|
91
93
|
__exportStar(require("./system/app-updates-contract"), exports);
|
|
92
94
|
var contract_catalog_contract_1 = require("./system/contract-catalog-contract");
|
|
93
95
|
Object.defineProperty(exports, "CONTRACT_CATALOG_VERSION", { enumerable: true, get: function () { return contract_catalog_contract_1.CONTRACT_CATALOG_VERSION; } });
|
|
@@ -43,6 +43,33 @@ export declare function setRemoteContractTransportFactory(factory: RemoteContrac
|
|
|
43
43
|
* @param userContext Optional owning user context.
|
|
44
44
|
*/
|
|
45
45
|
export declare function getRemoteContractTransportFactory(userContext?: UserContext): RemoteContractTransportFactory | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Host-registered function that asks for a direct connection to one device, for a caller about
|
|
48
|
+
* to make calls that benefit from one (a large transfer, say). Calls never dial on their own:
|
|
49
|
+
* they use a direct connection when one exists and the mesh otherwise.
|
|
50
|
+
*
|
|
51
|
+
* Resolves `true` when a verified direct connection to `target.deviceId` exists, `false` when
|
|
52
|
+
* none could be made in time (unreachable, no free connection slot, a recent failed attempt).
|
|
53
|
+
* Never rejects for an unreachable device, and promises nothing about how long the connection
|
|
54
|
+
* stays up. Only devices that share `target.dataContextId` answer.
|
|
55
|
+
*/
|
|
56
|
+
export type RemoteDeviceConnector = (target: IRemoteContractTarget) => Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Register (or clear, with `undefined`) the {@link RemoteDeviceConnector} for one user context.
|
|
59
|
+
* A connection manager registers its own beside its transport factory.
|
|
60
|
+
*
|
|
61
|
+
* @param connector Connector, or `undefined` to unregister.
|
|
62
|
+
* @param userContext Owning user context.
|
|
63
|
+
*/
|
|
64
|
+
export declare function setRemoteDeviceConnector(connector: RemoteDeviceConnector | undefined, userContext: UserContext): void;
|
|
65
|
+
/**
|
|
66
|
+
* Ask for a direct connection to `target.deviceId` through the connector registered for
|
|
67
|
+
* `userContext`. Resolves `false` when none is registered (a host without device connections).
|
|
68
|
+
*
|
|
69
|
+
* @param target Device to connect to, and the data context it must share.
|
|
70
|
+
* @param userContext Owning user context.
|
|
71
|
+
*/
|
|
72
|
+
export declare function connectToRemoteDevice(target: IRemoteContractTarget, userContext: UserContext | undefined): Promise<boolean>;
|
|
46
73
|
/**
|
|
47
74
|
* Shared chokepoint for trusted local hosts: when an authorized route names a device
|
|
48
75
|
* other than this one, return a forwarding endpoint; otherwise return `undefined` so the
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.setRemoteContractTransportFactory = setRemoteContractTransportFactory;
|
|
4
4
|
exports.getRemoteContractTransportFactory = getRemoteContractTransportFactory;
|
|
5
|
+
exports.setRemoteDeviceConnector = setRemoteDeviceConnector;
|
|
6
|
+
exports.connectToRemoteDevice = connectToRemoteDevice;
|
|
5
7
|
exports.resolveRemoteTargetEndpoint = resolveRemoteTargetEndpoint;
|
|
6
8
|
exports.createRemoteContractEndpoint = createRemoteContractEndpoint;
|
|
7
9
|
const contract_transport_1 = require("./contract-transport");
|
|
@@ -47,6 +49,31 @@ function setRemoteContractTransportFactory(factory, opts = {}) {
|
|
|
47
49
|
function getRemoteContractTransportFactory(userContext) {
|
|
48
50
|
return (userContext && factoriesByUserContext.get(userContext)) ?? defaultFactory;
|
|
49
51
|
}
|
|
52
|
+
const connectorsByUserContext = new WeakMap();
|
|
53
|
+
/**
|
|
54
|
+
* Register (or clear, with `undefined`) the {@link RemoteDeviceConnector} for one user context.
|
|
55
|
+
* A connection manager registers its own beside its transport factory.
|
|
56
|
+
*
|
|
57
|
+
* @param connector Connector, or `undefined` to unregister.
|
|
58
|
+
* @param userContext Owning user context.
|
|
59
|
+
*/
|
|
60
|
+
function setRemoteDeviceConnector(connector, userContext) {
|
|
61
|
+
if (connector)
|
|
62
|
+
connectorsByUserContext.set(userContext, connector);
|
|
63
|
+
else
|
|
64
|
+
connectorsByUserContext.delete(userContext);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ask for a direct connection to `target.deviceId` through the connector registered for
|
|
68
|
+
* `userContext`. Resolves `false` when none is registered (a host without device connections).
|
|
69
|
+
*
|
|
70
|
+
* @param target Device to connect to, and the data context it must share.
|
|
71
|
+
* @param userContext Owning user context.
|
|
72
|
+
*/
|
|
73
|
+
function connectToRemoteDevice(target, userContext) {
|
|
74
|
+
const connector = userContext && connectorsByUserContext.get(userContext);
|
|
75
|
+
return connector ? connector(target) : Promise.resolve(false);
|
|
76
|
+
}
|
|
50
77
|
/**
|
|
51
78
|
* Shared chokepoint for trusted local hosts: when an authorized route names a device
|
|
52
79
|
* other than this one, return a forwarding endpoint; otherwise return `undefined` so the
|
|
@@ -158,7 +158,7 @@ function getNetworkControlContractDefinition() {
|
|
|
158
158
|
},
|
|
159
159
|
{
|
|
160
160
|
name: "connectToDevice",
|
|
161
|
-
usageDescription: "
|
|
161
|
+
usageDescription: "Connect directly to one known device, waiting up to 15 seconds, and report whether a direct connection exists.",
|
|
162
162
|
inputFields: deviceInput,
|
|
163
163
|
outputFields: resultFields,
|
|
164
164
|
},
|
package/dist/define/context.d.ts
CHANGED
|
@@ -29,6 +29,15 @@ export interface IPackageContext {
|
|
|
29
29
|
readonly deviceId: string;
|
|
30
30
|
/** Run a tool on a contract this package consumes (or provides), as this call's caller. */
|
|
31
31
|
callConsumedTool(contractId: string, version: number, toolName: string, args?: unknown, options?: IPackageProviderOptions): Promise<unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Ask for a direct connection to another device in this package's data context, before calls
|
|
34
|
+
* that benefit from one (a large transfer, say). Remote calls use a direct connection when one
|
|
35
|
+
* exists and the Peers mesh otherwise; they never dial on their own. Resolves `true` when a
|
|
36
|
+
* direct connection exists (at most about 15 seconds), `false` when none could be made; never
|
|
37
|
+
* rejects for an unreachable device. The connection may still drop later, and calls then go
|
|
38
|
+
* through the mesh.
|
|
39
|
+
*/
|
|
40
|
+
connectToDevice(deviceId: string): Promise<boolean>;
|
|
32
41
|
/** Run a tool on another device. The remote device authorizes the call. */
|
|
33
42
|
callRemoteTool(contractId: string, version: number, deviceId: string, toolName: string, args?: unknown): Promise<unknown>;
|
|
34
43
|
/** Subscribe to an event on a consumed or provided contract. */
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* separated modes:
|
|
6
6
|
*
|
|
7
7
|
* 1. **RPC mode** (emit/on): request/response interactions with automatic
|
|
8
|
-
* encoding (txEncode for objects, passthrough for
|
|
9
|
-
* chunking for large payloads.
|
|
8
|
+
* encoding (txEncode with bytes kept binary for objects, passthrough for a
|
|
9
|
+
* top-level Uint8Array) and transparent chunking for large payloads.
|
|
10
10
|
*
|
|
11
11
|
* 2. **Stream mode** (sendRawBytes/onRawBytes): continuous byte transfer for
|
|
12
12
|
* file syncing, log streaming, etc. Each sendRawBytes
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* separated modes:
|
|
7
7
|
*
|
|
8
8
|
* 1. **RPC mode** (emit/on): request/response interactions with automatic
|
|
9
|
-
* encoding (txEncode for objects, passthrough for
|
|
10
|
-
* chunking for large payloads.
|
|
9
|
+
* encoding (txEncode with bytes kept binary for objects, passthrough for a
|
|
10
|
+
* top-level Uint8Array) and transparent chunking for large payloads.
|
|
11
11
|
*
|
|
12
12
|
* 2. **Stream mode** (sendRawBytes/onRawBytes): continuous byte transfer for
|
|
13
13
|
* file syncing, log streaming, etc. Each sendRawBytes
|
|
@@ -27,6 +27,7 @@ exports.createBinaryPeerSocket = createBinaryPeerSocket;
|
|
|
27
27
|
exports.wrapBinaryPeer = wrapBinaryPeer;
|
|
28
28
|
exports.wrapWrtc = wrapWrtc;
|
|
29
29
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
30
|
+
const peer_device_1 = require("../types/peer-device");
|
|
30
31
|
const utils_1 = require("../utils");
|
|
31
32
|
const connection_1 = require("./connection");
|
|
32
33
|
const frame_cipher_1 = require("./frame-cipher");
|
|
@@ -100,6 +101,8 @@ function decodeStreamId(bytes) {
|
|
|
100
101
|
streamIdStringCache.set(key, cached);
|
|
101
102
|
return cached;
|
|
102
103
|
}
|
|
104
|
+
/** RPC payloads carry bytes as msgpack binary, not base64 text. */
|
|
105
|
+
const RPC_ENCODING = { binaryBytes: true };
|
|
103
106
|
function encodeMessage(message) {
|
|
104
107
|
let isUint8ArrayData = false;
|
|
105
108
|
if (message.type === "call" && message.args instanceof Uint8Array) {
|
|
@@ -122,18 +125,18 @@ function encodeMessage(message) {
|
|
|
122
125
|
dataBytes = message.args;
|
|
123
126
|
}
|
|
124
127
|
else {
|
|
125
|
-
dataBytes = (0, tx_encoding_1.txEncode)(message.args);
|
|
128
|
+
dataBytes = (0, tx_encoding_1.txEncode)(message.args, RPC_ENCODING);
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
else if (message.type === "callback") {
|
|
129
132
|
if (message.error !== undefined) {
|
|
130
|
-
dataBytes = (0, tx_encoding_1.txEncode)({ error: message.error });
|
|
133
|
+
dataBytes = (0, tx_encoding_1.txEncode)({ error: message.error }, RPC_ENCODING);
|
|
131
134
|
}
|
|
132
135
|
else if (isUint8ArrayData) {
|
|
133
136
|
dataBytes = message.result;
|
|
134
137
|
}
|
|
135
138
|
else {
|
|
136
|
-
dataBytes = (0, tx_encoding_1.txEncode)({ result: message.result });
|
|
139
|
+
dataBytes = (0, tx_encoding_1.txEncode)({ result: message.result }, RPC_ENCODING);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
else {
|
|
@@ -244,7 +247,8 @@ function buildStreamCont(sidBytes, chunkData) {
|
|
|
244
247
|
*/
|
|
245
248
|
function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receiveLimitOverrides = {}) {
|
|
246
249
|
const handlers = {};
|
|
247
|
-
|
|
250
|
+
/** Reply callbacks for this side's calls that are still waiting, by callback id. */
|
|
251
|
+
const callbacks = new Map();
|
|
248
252
|
const rawBytesHandlers = {};
|
|
249
253
|
let isClosed = false;
|
|
250
254
|
const stats = (0, socket_type_1.createSocketStats)();
|
|
@@ -432,6 +436,26 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
|
|
|
432
436
|
streamReassemblyBuffers.clear();
|
|
433
437
|
inFlightReassemblyBytes = 0;
|
|
434
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Fail every call still waiting for a reply. The peer can no longer answer, so waiting would
|
|
441
|
+
* only hold each caller until its own timer fired; failing now lets it move on (a remote
|
|
442
|
+
* contract call's next attempt picks another route).
|
|
443
|
+
*/
|
|
444
|
+
function rejectPendingCalls() {
|
|
445
|
+
const pending = [...callbacks.values()];
|
|
446
|
+
callbacks.clear();
|
|
447
|
+
// A transport may close synchronously from send(), inside emit().
|
|
448
|
+
queueMicrotask(() => {
|
|
449
|
+
for (const callback of pending) {
|
|
450
|
+
try {
|
|
451
|
+
callback(new Error(peer_device_1.CONNECTION_CLOSED_MESSAGE));
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
console.error(`Error failing a pending ${protocol} call on close:`, err);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
435
459
|
function closeForProtocolViolation(reason) {
|
|
436
460
|
if (isClosed) {
|
|
437
461
|
return;
|
|
@@ -441,6 +465,7 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
|
|
|
441
465
|
clearAllReassemblies();
|
|
442
466
|
destroyFrameCipher();
|
|
443
467
|
socket.connected = false;
|
|
468
|
+
rejectPendingCalls();
|
|
444
469
|
peer.destroy();
|
|
445
470
|
}
|
|
446
471
|
function reserveReassembly(key, totalSize, maximumMessageBytes, onTimeout) {
|
|
@@ -469,9 +494,9 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
|
|
|
469
494
|
function handleIncomingRPC(bytes) {
|
|
470
495
|
const message = decodeMessage(bytes);
|
|
471
496
|
if (message.type === "callback") {
|
|
472
|
-
const callback = callbacks
|
|
497
|
+
const callback = callbacks.get(message.callbackId);
|
|
473
498
|
if (callback) {
|
|
474
|
-
delete
|
|
499
|
+
callbacks.delete(message.callbackId);
|
|
475
500
|
callback(message.error, message.result);
|
|
476
501
|
}
|
|
477
502
|
return;
|
|
@@ -739,16 +764,21 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
|
|
|
739
764
|
console.error(`Error parsing ${protocol} frame:`, err);
|
|
740
765
|
}
|
|
741
766
|
});
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
767
|
+
/**
|
|
768
|
+
* Handle closure, from the peer or from {@link ISocket.disconnect}. Calls waiting for a reply
|
|
769
|
+
* fail now. Cipher teardown waits until queued replies to the peer's calls have sealed; see
|
|
770
|
+
* {@link releaseCipherAfterTurn}.
|
|
771
|
+
*/
|
|
772
|
+
function markClosed() {
|
|
745
773
|
if (isClosed)
|
|
746
774
|
return;
|
|
747
775
|
isClosed = true;
|
|
748
776
|
socket.connected = false;
|
|
749
777
|
clearAllReassemblies();
|
|
778
|
+
rejectPendingCalls();
|
|
750
779
|
releaseCipherAfterTurn();
|
|
751
|
-
}
|
|
780
|
+
}
|
|
781
|
+
peer.on("close", markClosed);
|
|
752
782
|
// ─── ISocket Implementation ──────────────────────────────────────
|
|
753
783
|
const socket = {
|
|
754
784
|
id: connectionId,
|
|
@@ -763,10 +793,16 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
|
|
|
763
793
|
plaintextStreamsAllowed = allowed;
|
|
764
794
|
},
|
|
765
795
|
emit(eventName, args, callback) {
|
|
796
|
+
if (isClosed) {
|
|
797
|
+
// Nothing can answer; fail on a later turn so the caller never sees a synchronous reply.
|
|
798
|
+
queueMicrotask(() => callback(new Error(peer_device_1.CONNECTION_CLOSED_MESSAGE)));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
766
801
|
const callbackId = (0, utils_1.newid)();
|
|
767
|
-
callbacks[callbackId] = callback;
|
|
768
802
|
const message = { type: "call", eventName, args, callbackId };
|
|
803
|
+
// Encoded first: a call that can't be encoded throws to its caller and leaves no callback.
|
|
769
804
|
const encoded = encodeMessage(message);
|
|
805
|
+
callbacks.set(callbackId, callback);
|
|
770
806
|
sendRPC(encoded);
|
|
771
807
|
},
|
|
772
808
|
on(eventName, handler) {
|
|
@@ -783,6 +819,8 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
|
|
|
783
819
|
}
|
|
784
820
|
},
|
|
785
821
|
disconnect() {
|
|
822
|
+
// The WebRTC peers don't report their own destroy() as a close.
|
|
823
|
+
markClosed();
|
|
786
824
|
peer.destroy();
|
|
787
825
|
},
|
|
788
826
|
sendRawBytes: sendStreamData,
|
|
@@ -1,2 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/** Options for {@link txEncode}. */
|
|
2
|
+
export interface TxEncodeOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Write `Uint8Array` and `Buffer` values as msgpack binary instead of {@link toJSON}'s base64
|
|
5
|
+
* strings. Device connections' RPC messages use it; a payload that is signed or boxed keeps the
|
|
6
|
+
* default, so its format is unchanged. {@link txDecode} reads both.
|
|
7
|
+
*/
|
|
8
|
+
binaryBytes?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Encodes `data` for a device connection or a box: {@link toJSON} (cycles, dates, bytes), msgpack,
|
|
12
|
+
* then gzip when that saves at least 20% (judged from a 64 KiB sample first for a large payload).
|
|
13
|
+
* The first byte is a flag: 0 plain, 1 gzip. With `binaryBytes`, bytes stay binary (see
|
|
14
|
+
* {@link TxEncodeOptions}).
|
|
15
|
+
*/
|
|
16
|
+
export declare function txEncode(data: unknown, options?: TxEncodeOptions): Uint8Array;
|
|
17
|
+
/**
|
|
18
|
+
* Inverse of {@link txEncode}, with or without `binaryBytes`. Bytes that were written as binary
|
|
19
|
+
* come back as views into `data` (or into its inflated copy), so a caller that keeps a small piece
|
|
20
|
+
* of a large message for long should copy it.
|
|
21
|
+
* @throws If `data` is empty, has an unknown flag, or doesn't inflate or decode
|
|
22
|
+
*/
|
|
2
23
|
export declare function txDecode(data: Uint8Array): unknown;
|
|
@@ -4,6 +4,7 @@ exports.txEncode = txEncode;
|
|
|
4
4
|
exports.txDecode = txDecode;
|
|
5
5
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
6
6
|
const fflate_1 = require("fflate");
|
|
7
|
+
const native_codecs_1 = require("../native-codecs");
|
|
7
8
|
const serial_json_1 = require("../serial-json");
|
|
8
9
|
// Minimum size before attempting compression (bytes)
|
|
9
10
|
// Small payloads don't benefit much from compression
|
|
@@ -14,35 +15,70 @@ const MIN_COMPRESSION_RATIO = 0.8;
|
|
|
14
15
|
// Compression level: 1 (fastest) to 9 (best compression)
|
|
15
16
|
// Level 1 is ~5-10x faster than level 6 with reasonable compression
|
|
16
17
|
const COMPRESSION_LEVEL = 1;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
/**
|
|
19
|
+
* A payload over twice this size is gzipped only when its first this-many bytes compress well, so
|
|
20
|
+
* bytes that are already compressed (Git packs, media, ciphertext) skip gzip at both ends.
|
|
21
|
+
*/
|
|
22
|
+
const COMPRESSION_SAMPLE_BYTES = 64 * 1024;
|
|
23
|
+
/**
|
|
24
|
+
* msgpack extension type for a Node `Buffer` in a `binaryBytes` payload. Without it a `Buffer`
|
|
25
|
+
* would arrive as a plain `Uint8Array`, and {@link toJSON} encodes the two differently, so a
|
|
26
|
+
* signature over a received value that held a `Buffer` would no longer verify.
|
|
27
|
+
*/
|
|
28
|
+
const BUFFER_EXTENSION_TYPE = 1;
|
|
29
|
+
const extensionCodec = new msgpack_1.ExtensionCodec();
|
|
30
|
+
extensionCodec.register({
|
|
31
|
+
type: BUFFER_EXTENSION_TYPE,
|
|
32
|
+
encode: (value) => (Buffer.isBuffer(value) ? value : null),
|
|
33
|
+
decode: (data) => Buffer.from(data.buffer, data.byteOffset, data.byteLength),
|
|
34
|
+
});
|
|
35
|
+
function gzip(data) {
|
|
36
|
+
// Both produce gzip, which either side inflates.
|
|
37
|
+
return ((0, native_codecs_1.getNativeCodecs)()?.gzip(data, COMPRESSION_LEVEL) ??
|
|
38
|
+
(0, fflate_1.compressSync)(data, { level: COMPRESSION_LEVEL }));
|
|
39
|
+
}
|
|
40
|
+
/** True when gzip saves enough of `data` to be worth inflating at the other end. */
|
|
41
|
+
function compressesWell(data, compressed) {
|
|
42
|
+
return compressed.length < data.length * MIN_COMPRESSION_RATIO;
|
|
43
|
+
}
|
|
44
|
+
/** `encoded` gzipped when that saves at least 20%, else `undefined`. */
|
|
45
|
+
function compressIfWorthwhile(encoded) {
|
|
46
|
+
if (encoded.length <= COMPRESSION_THRESHOLD)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (encoded.length > COMPRESSION_SAMPLE_BYTES * 2) {
|
|
49
|
+
const sample = encoded.subarray(0, COMPRESSION_SAMPLE_BYTES);
|
|
50
|
+
if (!compressesWell(sample, gzip(sample)))
|
|
51
|
+
return undefined;
|
|
40
52
|
}
|
|
53
|
+
const compressed = gzip(encoded);
|
|
54
|
+
return compressesWell(encoded, compressed) ? compressed : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Encodes `data` for a device connection or a box: {@link toJSON} (cycles, dates, bytes), msgpack,
|
|
58
|
+
* then gzip when that saves at least 20% (judged from a 64 KiB sample first for a large payload).
|
|
59
|
+
* The first byte is a flag: 0 plain, 1 gzip. With `binaryBytes`, bytes stay binary (see
|
|
60
|
+
* {@link TxEncodeOptions}).
|
|
61
|
+
*/
|
|
62
|
+
function txEncode(data, options) {
|
|
63
|
+
// Remove cycles and encode dates, etc.
|
|
64
|
+
const tree = options?.binaryBytes ? (0, serial_json_1.toJSONKeepingBytes)(data) : (0, serial_json_1.toJSON)(data);
|
|
65
|
+
const encoded = (0, msgpack_1.encode)(tree, { extensionCodec });
|
|
66
|
+
const compressed = compressIfWorthwhile(encoded);
|
|
67
|
+
const body = compressed ?? encoded;
|
|
41
68
|
const out = new Uint8Array(1 + body.length);
|
|
42
|
-
out[0] =
|
|
69
|
+
out[0] = compressed ? 1 : 0;
|
|
43
70
|
out.set(body, 1);
|
|
44
71
|
return out;
|
|
45
72
|
}
|
|
73
|
+
function isGzip(data) {
|
|
74
|
+
return data.length > 2 && data[0] === 0x1f && data[1] === 0x8b;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Inverse of {@link txEncode}, with or without `binaryBytes`. Bytes that were written as binary
|
|
78
|
+
* come back as views into `data` (or into its inflated copy), so a caller that keeps a small piece
|
|
79
|
+
* of a large message for long should copy it.
|
|
80
|
+
* @throws If `data` is empty, has an unknown flag, or doesn't inflate or decode
|
|
81
|
+
*/
|
|
46
82
|
function txDecode(data) {
|
|
47
83
|
if (data.length === 0) {
|
|
48
84
|
throw new Error("Empty payload");
|
|
@@ -50,13 +86,14 @@ function txDecode(data) {
|
|
|
50
86
|
const flag = data[0];
|
|
51
87
|
let body = data.subarray(1);
|
|
52
88
|
if (flag === 1) {
|
|
53
|
-
|
|
89
|
+
const native = (0, native_codecs_1.getNativeCodecs)();
|
|
90
|
+
body = native && isGzip(body) ? native.gunzip(body) : (0, fflate_1.decompressSync)(body);
|
|
54
91
|
}
|
|
55
92
|
else if (flag !== 0) {
|
|
56
93
|
// Optional: future versioning or legacy handling
|
|
57
94
|
throw new Error(`Unknown tx flag: ${flag}`);
|
|
58
95
|
}
|
|
59
|
-
const unpacked = (0, msgpack_1.decode)(body);
|
|
96
|
+
const unpacked = (0, msgpack_1.decode)(body, { extensionCodec });
|
|
60
97
|
const restored = (0, serial_json_1.fromJSON)(unpacked); // restore cycles, dates, etc.
|
|
61
98
|
return restored;
|
|
62
99
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ __exportStar(require("./invites"), exports);
|
|
|
55
55
|
__exportStar(require("./keys"), exports);
|
|
56
56
|
__exportStar(require("./logging"), exports);
|
|
57
57
|
__exportStar(require("./mentions"), exports);
|
|
58
|
+
__exportStar(require("./native-codecs"), exports);
|
|
58
59
|
__exportStar(require("./observable"), exports);
|
|
59
60
|
__exportStar(require("./package-installer"), exports);
|
|
60
61
|
__exportStar(require("./package-loader"), exports);
|
package/dist/keys.d.ts
CHANGED
|
@@ -21,11 +21,16 @@ export declare function hashObject(obj: {
|
|
|
21
21
|
* legacy insertion-order fallback that keeps pre-existing signatures valid.
|
|
22
22
|
*/
|
|
23
23
|
export declare function canonicalJSONString(obj: any): string;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Base64 with URL-safe alphabet and no `=` padding (used throughout keys and wire formats).
|
|
26
|
+
* Uses `Buffer` (native in Node, the `buffer` package in browsers).
|
|
27
|
+
*/
|
|
25
28
|
export declare function encodeBase64(data: Uint8Array): string;
|
|
26
29
|
/**
|
|
27
30
|
* Inverse of {@link encodeBase64}; accepts standard or URL-safe base64 and repads as needed.
|
|
28
|
-
*
|
|
31
|
+
* Accepts exactly what `tweetnacl-util` did (whole groups of four, then at most two `=`), and
|
|
32
|
+
* returns a fresh `Uint8Array` that owns its buffer.
|
|
33
|
+
* @throws TypeError if `data` is not a string or not valid base64
|
|
29
34
|
*/
|
|
30
35
|
export declare function decodeBase64(data: string): Uint8Array;
|
|
31
36
|
/** Random token as base64url string (`size` raw bytes, default 32). */
|
package/dist/keys.js
CHANGED
|
@@ -33,9 +33,9 @@ const sha2_1 = require("@noble/hashes/sha2");
|
|
|
33
33
|
const buffer_1 = require("buffer");
|
|
34
34
|
const ed2curve = require("ed2curve");
|
|
35
35
|
const nacl = require("tweetnacl");
|
|
36
|
-
const utils = require("tweetnacl-util");
|
|
37
36
|
const tweetnacl_util_1 = require("tweetnacl-util");
|
|
38
37
|
const tx_encoding_1 = require("./device/tx-encoding");
|
|
38
|
+
const native_codecs_1 = require("./native-codecs");
|
|
39
39
|
const serial_json_1 = require("./serial-json");
|
|
40
40
|
globalThis.Buffer = buffer_1.Buffer; // shim for browsers/RN
|
|
41
41
|
const _stableStringify = require("fast-json-stable-stringify");
|
|
@@ -113,15 +113,42 @@ function canonicalJSONString(obj) {
|
|
|
113
113
|
function legacySignatureJSONString(obj) {
|
|
114
114
|
return (0, serial_json_1.toJSONString)(obj);
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
const utf8Encoder = new TextEncoder();
|
|
117
|
+
const supportsBase64Url = buffer_1.Buffer.isEncoding("base64url");
|
|
118
|
+
/**
|
|
119
|
+
* UTF-8 bytes of `str`, rejecting what `tweetnacl-util`'s `decodeUTF8` rejected (non-strings and
|
|
120
|
+
* lone surrogates), so signatures are unchanged. `TextEncoder` is native and far faster.
|
|
121
|
+
* @throws TypeError if `str` is not a string; URIError if it has a lone surrogate
|
|
122
|
+
*/
|
|
123
|
+
function decodeUTF8(str) {
|
|
124
|
+
if (typeof str !== "string")
|
|
125
|
+
throw new TypeError("expected string");
|
|
126
|
+
const wellFormed = str.isWellFormed;
|
|
127
|
+
if (wellFormed ? !wellFormed.call(str) : /[\uD800-\uDFFF]/.test(str)) {
|
|
128
|
+
// Slow path only when a surrogate may be unpaired; matches encodeURIComponent's check.
|
|
129
|
+
encodeURIComponent(str);
|
|
130
|
+
}
|
|
131
|
+
return utf8Encoder.encode(str);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Base64 with URL-safe alphabet and no `=` padding (used throughout keys and wire formats).
|
|
135
|
+
* Uses `Buffer` (native in Node, the `buffer` package in browsers).
|
|
136
|
+
*/
|
|
117
137
|
function encodeBase64(data) {
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
// Keep tweetnacl-util's coercion and errors for callers passing other array-like values.
|
|
139
|
+
if (!(data instanceof Uint8Array)) {
|
|
140
|
+
return (0, tweetnacl_util_1.encodeBase64)(data).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
141
|
+
}
|
|
142
|
+
const bytes = buffer_1.Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
143
|
+
if (supportsBase64Url)
|
|
144
|
+
return bytes.toString("base64url");
|
|
145
|
+
return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
121
146
|
}
|
|
122
147
|
/**
|
|
123
148
|
* Inverse of {@link encodeBase64}; accepts standard or URL-safe base64 and repads as needed.
|
|
124
|
-
*
|
|
149
|
+
* Accepts exactly what `tweetnacl-util` did (whole groups of four, then at most two `=`), and
|
|
150
|
+
* returns a fresh `Uint8Array` that owns its buffer.
|
|
151
|
+
* @throws TypeError if `data` is not a string or not valid base64
|
|
125
152
|
*/
|
|
126
153
|
function decodeBase64(data) {
|
|
127
154
|
if (typeof data !== "string") {
|
|
@@ -131,7 +158,11 @@ function decodeBase64(data) {
|
|
|
131
158
|
while (data.length % 4 !== 0) {
|
|
132
159
|
data += "=";
|
|
133
160
|
}
|
|
134
|
-
|
|
161
|
+
// A single character class, so it doesn't grow V8's regex stack on large inputs.
|
|
162
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
|
|
163
|
+
throw new TypeError("invalid encoding");
|
|
164
|
+
}
|
|
165
|
+
return new Uint8Array(buffer_1.Buffer.from(data, "base64"));
|
|
135
166
|
}
|
|
136
167
|
/** Random token as base64url string (`size` raw bytes, default 32). */
|
|
137
168
|
function newToken(size = 32) {
|
|
@@ -178,10 +209,28 @@ function derivePublicBoxKey(publicKey) {
|
|
|
178
209
|
const converted = ed2curve.convertPublicKey(bytes);
|
|
179
210
|
return converted ? encodeBase64(converted) : null;
|
|
180
211
|
}
|
|
212
|
+
/** Ed25519 detached signature; native when the host installed it (identical bytes), else tweetnacl. */
|
|
213
|
+
function signDetached(message, secretKey) {
|
|
214
|
+
const native = secretKey.length === 64 ? (0, native_codecs_1.getNativeCodecs)()?.sign(message, secretKey) : undefined;
|
|
215
|
+
return native ?? nacl.sign.detached(message, secretKey);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Ed25519 detached verify. A native "valid" is trusted (OpenSSL is at least as strict as tweetnacl:
|
|
219
|
+
* it also rejects a non-canonical S); anything else is decided by tweetnacl, so every signature
|
|
220
|
+
* tweetnacl accepted still verifies, and its size errors still throw.
|
|
221
|
+
*/
|
|
222
|
+
function verifyDetached(message, signature, publicKey) {
|
|
223
|
+
const native = (0, native_codecs_1.getNativeCodecs)();
|
|
224
|
+
if (native && signature.length === 64 && publicKey.length === 32) {
|
|
225
|
+
if (native.verify(message, signature, publicKey))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return nacl.sign.detached.verify(message, signature, publicKey);
|
|
229
|
+
}
|
|
181
230
|
/** Signs a UTF-8 message with Ed25519; returns base64url signed message (NaCl `sign` combined format). */
|
|
182
231
|
function signMessageWithSecretKey(msg, secretKey) {
|
|
183
232
|
const _secretKey = decodeBase64(secretKey);
|
|
184
|
-
const msgDecoded =
|
|
233
|
+
const msgDecoded = decodeUTF8(msg);
|
|
185
234
|
const msgSigned = nacl.sign(msgDecoded, _secretKey);
|
|
186
235
|
return encodeBase64(msgSigned);
|
|
187
236
|
}
|
|
@@ -213,8 +262,8 @@ function signObjectWithSecretKey(obj, secretKey) {
|
|
|
213
262
|
obj = { __isNull: true };
|
|
214
263
|
}
|
|
215
264
|
const objStr = canonicalJSONString(obj);
|
|
216
|
-
const objDecoded =
|
|
217
|
-
const signatureByteArray =
|
|
265
|
+
const objDecoded = decodeUTF8(objStr);
|
|
266
|
+
const signatureByteArray = signDetached(objDecoded, _secretKey);
|
|
218
267
|
return {
|
|
219
268
|
contents: obj,
|
|
220
269
|
signature: encodeBase64(signatureByteArray),
|
|
@@ -231,7 +280,7 @@ function signObjectWithSecretKey(obj, secretKey) {
|
|
|
231
280
|
function openSignedObject(signedObj) {
|
|
232
281
|
const _publicKey = decodeBase64(signedObj.publicKey);
|
|
233
282
|
const signature = decodeBase64(signedObj.signature);
|
|
234
|
-
const verify = (objStr) =>
|
|
283
|
+
const verify = (objStr) => verifyDetached(decodeUTF8(objStr), signature, _publicKey);
|
|
235
284
|
const valid = verify(canonicalJSONString(signedObj.contents)) ||
|
|
236
285
|
verify(legacySignatureJSONString(signedObj.contents));
|
|
237
286
|
if (!valid) {
|
|
@@ -317,7 +366,7 @@ function openBoxWithSecretKey(box, mySecretKey) {
|
|
|
317
366
|
}
|
|
318
367
|
function encryptString(data, secretKey) {
|
|
319
368
|
const _secretKey = decodeBase64(secretKey);
|
|
320
|
-
const _data =
|
|
369
|
+
const _data = decodeUTF8(data);
|
|
321
370
|
const nonce = nacl.randomBytes(24);
|
|
322
371
|
const encryptedData = nacl.secretbox(_data, nonce, _secretKey.slice(0, 32));
|
|
323
372
|
return `${encodeBase64(encryptedData)}~${encodeBase64(nonce)}`;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native implementations a Node host can install for the two most expensive steps of a device
|
|
3
|
+
* message: gzip and Ed25519. Without them the pure-JS paths (`fflate`, `tweetnacl`) run, so
|
|
4
|
+
* browsers keep working; with them a large device message costs a fraction of the CPU.
|
|
5
|
+
*
|
|
6
|
+
* Every implementation must be interchangeable with the pure-JS one on the wire: a peer without
|
|
7
|
+
* native codecs must accept what this one sends, and the reverse.
|
|
8
|
+
*/
|
|
9
|
+
export interface INativeCodecs {
|
|
10
|
+
/** gzip `data` at `level` (1-9). Any valid gzip stream will do; receivers only inflate it. */
|
|
11
|
+
gzip(data: Uint8Array, level: number): Uint8Array;
|
|
12
|
+
/** Inflate one gzip stream (from {@link INativeCodecs.gzip} or from `fflate`). */
|
|
13
|
+
gunzip(data: Uint8Array): Uint8Array;
|
|
14
|
+
/**
|
|
15
|
+
* Ed25519 detached signature of `message` with a tweetnacl-format 64-byte secret key (seed then
|
|
16
|
+
* public key). Ed25519 is deterministic, so this must equal tweetnacl's signature byte for byte;
|
|
17
|
+
* return `undefined` when that can't be guaranteed (for example, a key whose public half doesn't
|
|
18
|
+
* match its seed) and the caller signs with tweetnacl.
|
|
19
|
+
*/
|
|
20
|
+
sign(message: Uint8Array, secretKey: Uint8Array): Uint8Array | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Whether `signature` is a valid Ed25519 signature of `message` by the 32-byte `publicKey`.
|
|
23
|
+
* Callers fall back to tweetnacl when this returns false, so it may be stricter than tweetnacl
|
|
24
|
+
* but never more lenient.
|
|
25
|
+
*/
|
|
26
|
+
verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Install (or with no argument, remove) native codecs for this process. Node hosts call this once
|
|
30
|
+
* at startup, before any device message is signed or sent.
|
|
31
|
+
*/
|
|
32
|
+
export declare function installNativeCodecs(codecs?: INativeCodecs): void;
|
|
33
|
+
/** The installed native codecs, or `undefined` when the pure-JS paths should run. */
|
|
34
|
+
export declare function getNativeCodecs(): INativeCodecs | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installNativeCodecs = installNativeCodecs;
|
|
4
|
+
exports.getNativeCodecs = getNativeCodecs;
|
|
5
|
+
let installed;
|
|
6
|
+
/**
|
|
7
|
+
* Install (or with no argument, remove) native codecs for this process. Node hosts call this once
|
|
8
|
+
* at startup, before any device message is signed or sent.
|
|
9
|
+
*/
|
|
10
|
+
function installNativeCodecs(codecs) {
|
|
11
|
+
installed = codecs;
|
|
12
|
+
}
|
|
13
|
+
/** The installed native codecs, or `undefined` when the pure-JS paths should run. */
|
|
14
|
+
function getNativeCodecs() {
|
|
15
|
+
return installed;
|
|
16
|
+
}
|
|
@@ -270,6 +270,8 @@ export declare const ISOLATED_PERSISTENT_VAR_HOST_MEMBER = "package.pvar";
|
|
|
270
270
|
export declare const ISOLATED_EVENT_HOST_MEMBER = "contract.emit";
|
|
271
271
|
/** Host-contract member guests use to publish an in-memory provided-contract observable. */
|
|
272
272
|
export declare const ISOLATED_OBSERVABLE_HOST_MEMBER = "package.observable";
|
|
273
|
+
/** Host-contract member guests use to ask for a direct connection to another device. */
|
|
274
|
+
export declare const ISOLATED_CONNECT_HOST_MEMBER = "device.connect";
|
|
273
275
|
/**
|
|
274
276
|
* How deep field lists may nest through object `fields`: a tool's inputs, say, are level 1.
|
|
275
277
|
* Deeper nesting is refused, so a hostile manifest can't exhaust the validator's stack.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ISOLATED_MAX_FIELD_DEPTH = exports.ISOLATED_OBSERVABLE_HOST_MEMBER = exports.ISOLATED_EVENT_HOST_MEMBER = exports.ISOLATED_PERSISTENT_VAR_HOST_MEMBER = exports.ISOLATED_TABLE_HOST_MEMBER = exports.ISOLATED_CONSUME_HOST_MEMBER = exports.ISOLATED_TOOL_TIMEOUT_MAX_MS = exports.ISOLATED_GUEST_API_VERSION = exports.ISOLATED_PACKAGE_MAGIC = void 0;
|
|
3
|
+
exports.ISOLATED_MAX_FIELD_DEPTH = exports.ISOLATED_CONNECT_HOST_MEMBER = exports.ISOLATED_OBSERVABLE_HOST_MEMBER = exports.ISOLATED_EVENT_HOST_MEMBER = exports.ISOLATED_PERSISTENT_VAR_HOST_MEMBER = exports.ISOLATED_TABLE_HOST_MEMBER = exports.ISOLATED_CONSUME_HOST_MEMBER = exports.ISOLATED_TOOL_TIMEOUT_MAX_MS = exports.ISOLATED_GUEST_API_VERSION = exports.ISOLATED_PACKAGE_MAGIC = void 0;
|
|
4
4
|
exports.enumValueFits = enumValueFits;
|
|
5
5
|
exports.looksLikeIsolatedPackageArtifact = looksLikeIsolatedPackageArtifact;
|
|
6
6
|
exports.validateIsolatedPackageArtifact = validateIsolatedPackageArtifact;
|
|
@@ -45,6 +45,8 @@ exports.ISOLATED_PERSISTENT_VAR_HOST_MEMBER = "package.pvar";
|
|
|
45
45
|
exports.ISOLATED_EVENT_HOST_MEMBER = "contract.emit";
|
|
46
46
|
/** Host-contract member guests use to publish an in-memory provided-contract observable. */
|
|
47
47
|
exports.ISOLATED_OBSERVABLE_HOST_MEMBER = "package.observable";
|
|
48
|
+
/** Host-contract member guests use to ask for a direct connection to another device. */
|
|
49
|
+
exports.ISOLATED_CONNECT_HOST_MEMBER = "device.connect";
|
|
48
50
|
const FIELD_TYPES = new Set(Object.values(field_type_1.FieldType));
|
|
49
51
|
const OBSERVABLE_FIELD_TYPES = new Set([
|
|
50
52
|
field_type_1.FieldType.id,
|
|
@@ -351,6 +351,12 @@ export interface IIsolatedPackageRuntimeFactoryOptions {
|
|
|
351
351
|
onWorkerOutput?: (output: IIsolatedWorkerOutput) => void;
|
|
352
352
|
/** This device's id, which package code reads as `ctx.deviceId`. */
|
|
353
353
|
getDeviceId?: () => string | undefined;
|
|
354
|
+
/**
|
|
355
|
+
* Ask for a direct connection to `deviceId` within this data context, for package code's
|
|
356
|
+
* `ctx.connectToDevice`. Resolves whether a verified direct connection exists; never rejects
|
|
357
|
+
* for an unreachable device. Without it, `ctx.connectToDevice` resolves false.
|
|
358
|
+
*/
|
|
359
|
+
connectToDevice?: (deviceId: string) => Promise<boolean>;
|
|
354
360
|
}
|
|
355
361
|
/**
|
|
356
362
|
* Host callback that builds an {@link IIsolatedPackageRuntime} for one
|
|
@@ -127,6 +127,7 @@ class PackageLoader {
|
|
|
127
127
|
},
|
|
128
128
|
onWorkerOutput: (output) => this.logWorkerOutput(output),
|
|
129
129
|
getDeviceId: () => this.dataContext.userContext?.deviceId(),
|
|
130
|
+
connectToDevice: (deviceId) => (0, remote_contract_transport_1.connectToRemoteDevice)({ deviceId, dataContextId: this.dataContext.dataContextId }, this.dataContext.userContext),
|
|
130
131
|
});
|
|
131
132
|
this.unsubscribePackageHealth = this.isolatedRuntime.subscribePackageHealth?.((packageId) => {
|
|
132
133
|
const previous = this.runtimeStates()[packageId];
|
package/dist/serial-json.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export declare function isObject(x: any): x is Record<string, any>;
|
|
|
26
26
|
* @returns A new tree; the input is never modified. Top-level primitives are returned as-is.
|
|
27
27
|
*/
|
|
28
28
|
export declare function toJSON(obj: any): any;
|
|
29
|
+
/**
|
|
30
|
+
* {@link toJSON}, except that `Uint8Array` and `Buffer` values stay in the tree as they are (the
|
|
31
|
+
* same instances), for an encoder that writes bytes natively: `txEncode` with `binaryBytes`
|
|
32
|
+
* hands the tree to msgpack. {@link fromJSON} passes them through, so the round trip still
|
|
33
|
+
* restores everything else. Never sign this output; signatures cover {@link toJSON}'s.
|
|
34
|
+
*/
|
|
35
|
+
export declare function toJSONKeepingBytes(obj: any): any;
|
|
29
36
|
/**
|
|
30
37
|
* Reverses {@link toJSON}: restores the sentinel values, shared objects and cycles, and keys
|
|
31
38
|
* escaped with `__DOLLAR_`.
|
package/dist/serial-json.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isObject = isObject;
|
|
4
4
|
exports.toJSON = toJSON;
|
|
5
|
+
exports.toJSONKeepingBytes = toJSONKeepingBytes;
|
|
5
6
|
exports.fromJSON = fromJSON;
|
|
6
7
|
exports.toJSONString = toJSONString;
|
|
7
8
|
exports.fromJSONString = fromJSONString;
|
|
@@ -40,6 +41,19 @@ const ARRAY_TAG_PREFIX = "__this_ref:";
|
|
|
40
41
|
* @returns A new tree; the input is never modified. Top-level primitives are returned as-is.
|
|
41
42
|
*/
|
|
42
43
|
function toJSON(obj) {
|
|
44
|
+
return encodeTree(obj, false);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* {@link toJSON}, except that `Uint8Array` and `Buffer` values stay in the tree as they are (the
|
|
48
|
+
* same instances), for an encoder that writes bytes natively: `txEncode` with `binaryBytes`
|
|
49
|
+
* hands the tree to msgpack. {@link fromJSON} passes them through, so the round trip still
|
|
50
|
+
* restores everything else. Never sign this output; signatures cover {@link toJSON}'s.
|
|
51
|
+
*/
|
|
52
|
+
function toJSONKeepingBytes(obj) {
|
|
53
|
+
return encodeTree(obj, true);
|
|
54
|
+
}
|
|
55
|
+
/** The walk behind {@link toJSON} and {@link toJSONKeepingBytes}. */
|
|
56
|
+
function encodeTree(obj, keepBytes) {
|
|
43
57
|
if (!_.isObject(obj)) {
|
|
44
58
|
return obj;
|
|
45
59
|
}
|
|
@@ -76,9 +90,9 @@ function toJSON(obj) {
|
|
|
76
90
|
if (_.isError(value))
|
|
77
91
|
return `__ERROR ${value.stack}`;
|
|
78
92
|
if (Buffer.isBuffer(value))
|
|
79
|
-
return `__BUFFER ${value.toString("base64")}`;
|
|
93
|
+
return keepBytes ? value : `__BUFFER ${value.toString("base64")}`;
|
|
80
94
|
if (value instanceof Uint8Array)
|
|
81
|
-
return `__UINT8ARRAY ${Buffer.from(value).toString("base64")}`;
|
|
95
|
+
return keepBytes ? value : `__UINT8ARRAY ${Buffer.from(value).toString("base64")}`;
|
|
82
96
|
const known = seen.get(value);
|
|
83
97
|
if (known) {
|
|
84
98
|
const target = known.copy;
|
|
@@ -73,6 +73,12 @@ export interface IDeviceConnection {
|
|
|
73
73
|
/** Whether the connection has already closed. */
|
|
74
74
|
closed?: boolean;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Message of the error a device RPC fails with when its connection closes before the reply
|
|
78
|
+
* arrives, or when it is sent on a connection that has already closed. Sync treats it as an
|
|
79
|
+
* expected disconnect, not a failure.
|
|
80
|
+
*/
|
|
81
|
+
export declare const CONNECTION_CLOSED_MESSAGE = "connection has been closed";
|
|
76
82
|
/** Shared tuning constants for device synchronization and routing. */
|
|
77
83
|
export declare const PeerDeviceConsts: Readonly<{
|
|
78
84
|
readonly MAX_CONNECTIONS: 30;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PeerDeviceConsts = void 0;
|
|
3
|
+
exports.PeerDeviceConsts = exports.CONNECTION_CLOSED_MESSAGE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Message of the error a device RPC fails with when its connection closes before the reply
|
|
6
|
+
* arrives, or when it is sent on a connection that has already closed. Sync treats it as an
|
|
7
|
+
* expected disconnect, not a failure.
|
|
8
|
+
*/
|
|
9
|
+
exports.CONNECTION_CLOSED_MESSAGE = "connection has been closed";
|
|
4
10
|
/** Shared tuning constants for device synchronization and routing. */
|
|
5
11
|
exports.PeerDeviceConsts = Object.freeze({
|
|
6
12
|
// TODO set this based on device type (desktops and servers should be able to handle 100 connections)
|
package/dist/utils.js
CHANGED
|
@@ -293,7 +293,7 @@ async function retryOnErrorOrTimeout({ fn, connection, opts = {}, }) {
|
|
|
293
293
|
timeoutCount++;
|
|
294
294
|
finalError = undefined;
|
|
295
295
|
if (connection.closed) {
|
|
296
|
-
throw new Error(
|
|
296
|
+
throw new Error(peer_device_1.CONNECTION_CLOSED_MESSAGE);
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
catch (error) {
|
|
@@ -303,7 +303,7 @@ async function retryOnErrorOrTimeout({ fn, connection, opts = {}, }) {
|
|
|
303
303
|
if (opts.shouldRetry?.(error) === false)
|
|
304
304
|
throw error;
|
|
305
305
|
if (connection.closed) {
|
|
306
|
-
throw new Error(
|
|
306
|
+
throw new Error(peer_device_1.CONNECTION_CLOSED_MESSAGE);
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
finally {
|