@peers-app/peers-sdk 0.28.11 → 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/contract-resolvers.js +1 -0
- package/dist/contracts/index.d.ts +2 -1
- package/dist/contracts/index.js +5 -2
- package/dist/contracts/remote-contract-transport.d.ts +27 -0
- package/dist/contracts/remote-contract-transport.js +27 -0
- package/dist/contracts/system/group-members-contract.d.ts +25 -0
- package/dist/contracts/system/group-members-contract.js +50 -0
- package/dist/contracts/system/network-contracts.js +1 -1
- package/dist/data/authority-write-checks.d.ts +58 -1
- package/dist/data/authority-write-checks.js +176 -1
- package/dist/data/group-resign.d.ts +30 -0
- package/dist/data/group-resign.js +74 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/orm/index.d.ts +1 -0
- package/dist/data/orm/index.js +1 -0
- package/dist/data/orm/signed-package-table.d.ts +126 -0
- package/dist/data/orm/signed-package-table.js +293 -0
- package/dist/data/orm/table-container.d.ts +22 -2
- package/dist/data/orm/table-container.js +49 -14
- package/dist/data/orm/types.d.ts +12 -0
- package/dist/data/package-table-identity.d.ts +48 -0
- package/dist/data/package-table-identity.js +151 -0
- package/dist/data/signed-row-id.d.ts +43 -0
- package/dist/data/signed-row-id.js +72 -0
- package/dist/define/context.d.ts +9 -0
- package/dist/define/manifest.js +3 -1
- package/dist/define/system-contracts.d.ts +99 -0
- package/dist/define/system-contracts.js +5 -1
- package/dist/define/table.d.ts +32 -0
- package/dist/define/table.js +120 -2
- 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/invites/invite-messages.d.ts +44 -44
- 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 +17 -0
- package/dist/package-loader/isolated-artifact.js +15 -2
- package/dist/package-loader/isolated-runtime.d.ts +10 -0
- package/dist/package-loader/isolated-table-gateway.js +31 -3
- 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/system-ids.d.ts +2 -0
- package/dist/system-ids.js +3 -1
- 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
package/dist/define/table.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PackageTable = void 0;
|
|
4
4
|
exports.defineTable = defineTable;
|
|
5
|
+
const signed_row_id_1 = require("../data/signed-row-id");
|
|
5
6
|
const isolated_runtime_1 = require("../package-loader/isolated-runtime");
|
|
7
|
+
const field_type_1 = require("../types/field-type");
|
|
6
8
|
const empty_fields_1 = require("./empty-fields");
|
|
7
9
|
const table_handle_1 = require("./table-handle");
|
|
8
10
|
/**
|
|
@@ -120,6 +122,102 @@ class PackageTable {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
exports.PackageTable = PackageTable;
|
|
125
|
+
/** The columns Peers adds to a signed table; a schema can't declare them. */
|
|
126
|
+
const SIGNED_TABLE_COLUMNS = [
|
|
127
|
+
"signature",
|
|
128
|
+
"signedAt",
|
|
129
|
+
"signedContextId",
|
|
130
|
+
"signedTableId",
|
|
131
|
+
"signedIdSeed",
|
|
132
|
+
];
|
|
133
|
+
/** The field types a signed table's key fields may have. */
|
|
134
|
+
const SIGNED_KEY_TYPES = new Set([
|
|
135
|
+
field_type_1.FieldType.string,
|
|
136
|
+
field_type_1.FieldType.id,
|
|
137
|
+
field_type_1.FieldType.number,
|
|
138
|
+
field_type_1.FieldType.boolean,
|
|
139
|
+
]);
|
|
140
|
+
/**
|
|
141
|
+
* A schema field's Peers field type and whether a wrapper makes it optional, nullable, an
|
|
142
|
+
* array, or defaulted. `zodPeerId` marks its own def (a refinement) as an id field; walk the
|
|
143
|
+
* wrappers the manifest walks and take the mark from whichever level carries it.
|
|
144
|
+
*/
|
|
145
|
+
function schemaFieldKind(schema, name) {
|
|
146
|
+
let def = schema.shape[name]._def;
|
|
147
|
+
let wrapped = false;
|
|
148
|
+
let isArray = false;
|
|
149
|
+
let fieldType = def._fieldType;
|
|
150
|
+
const typeName = () => def.typeName;
|
|
151
|
+
while (def.innerType || def.schema || def.type) {
|
|
152
|
+
isArray ||= typeName() === "ZodArray";
|
|
153
|
+
wrapped ||= ["ZodOptional", "ZodNullable", "ZodArray", "ZodDefault"].includes(typeName());
|
|
154
|
+
def = (def.innerType ?? def.schema ?? def.type)._def;
|
|
155
|
+
fieldType ??= def._fieldType;
|
|
156
|
+
}
|
|
157
|
+
fieldType ??= {
|
|
158
|
+
ZodString: field_type_1.FieldType.string,
|
|
159
|
+
ZodEnum: field_type_1.FieldType.string,
|
|
160
|
+
ZodNumber: field_type_1.FieldType.number,
|
|
161
|
+
ZodBoolean: field_type_1.FieldType.boolean,
|
|
162
|
+
}[typeName()];
|
|
163
|
+
// A literal is typed by its value, as the manifest types it.
|
|
164
|
+
if (!fieldType && typeName() === "ZodLiteral") {
|
|
165
|
+
fieldType = { string: field_type_1.FieldType.string, number: field_type_1.FieldType.number, boolean: field_type_1.FieldType.boolean }[typeof def.value];
|
|
166
|
+
}
|
|
167
|
+
return { fieldType, wrapped, isArray };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check a `signedBy` declaration without the manifest machinery (this entry stays small): the
|
|
171
|
+
* owner field and the primary key are required, single `zodPeerId` fields and differ; any
|
|
172
|
+
* `signedKey` fields are distinct, required scalars other than those two; the table isn't
|
|
173
|
+
* local-only; and the schema leaves the signature columns to Peers. The build checks the
|
|
174
|
+
* manifest again.
|
|
175
|
+
*/
|
|
176
|
+
function validateSignedByField(definition) {
|
|
177
|
+
const { name, signedBy, signedKey, schema, localOnly, primaryKeyName } = definition;
|
|
178
|
+
const label = `Table ${name}`;
|
|
179
|
+
if (typeof signedBy !== "string" || !(signedBy in schema.shape)) {
|
|
180
|
+
throw new Error(`${label}.signedBy names a field the table doesn't have: "${signedBy}"`);
|
|
181
|
+
}
|
|
182
|
+
const owner = schemaFieldKind(schema, signedBy);
|
|
183
|
+
if (owner.wrapped || owner.fieldType !== field_type_1.FieldType.id) {
|
|
184
|
+
throw new Error(`${label}.signedBy must name a required field of type id, a user id`);
|
|
185
|
+
}
|
|
186
|
+
if (localOnly) {
|
|
187
|
+
throw new Error(`${label}.signedBy can't be set on a local-only table: nothing syncs`);
|
|
188
|
+
}
|
|
189
|
+
if (primaryKeyName === signedBy) {
|
|
190
|
+
throw new Error(`${label}: a signed table's primary key can't be its owner field; use signedKey: [] for one row per owner`);
|
|
191
|
+
}
|
|
192
|
+
const primaryKey = schemaFieldKind(schema, primaryKeyName);
|
|
193
|
+
if (primaryKey.fieldType !== field_type_1.FieldType.id || primaryKey.isArray) {
|
|
194
|
+
throw new Error(`${label}: a signed table's primary key must be a zodPeerId`);
|
|
195
|
+
}
|
|
196
|
+
if (signedKey !== undefined) {
|
|
197
|
+
if (!Array.isArray(signedKey))
|
|
198
|
+
throw new Error(`${label}.signedKey must be a list of fields`);
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
for (const field of signedKey) {
|
|
201
|
+
if (typeof field !== "string" || !(field in schema.shape)) {
|
|
202
|
+
throw new Error(`${label}.signedKey names a field the table doesn't have: "${field}"`);
|
|
203
|
+
}
|
|
204
|
+
if (seen.has(field))
|
|
205
|
+
throw new Error(`${label}.signedKey lists "${field}" twice`);
|
|
206
|
+
seen.add(field);
|
|
207
|
+
if (field === primaryKeyName || field === signedBy) {
|
|
208
|
+
throw new Error(`${label}.signedKey can't list "${field}": the owner and the id are already part of a row's identity`);
|
|
209
|
+
}
|
|
210
|
+
const kind = schemaFieldKind(schema, field);
|
|
211
|
+
if (kind.wrapped || !kind.fieldType || !SIGNED_KEY_TYPES.has(kind.fieldType)) {
|
|
212
|
+
throw new Error(`${label}.signedKey field "${field}" must be a required string, id, number, or boolean`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const reserved = SIGNED_TABLE_COLUMNS.find((column) => column in schema.shape);
|
|
217
|
+
if (reserved) {
|
|
218
|
+
throw new Error(`${label} declares "${reserved}", which the host adds to a signed table itself`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
123
221
|
/**
|
|
124
222
|
* Declare one of the package's tables from a zod schema.
|
|
125
223
|
*
|
|
@@ -138,12 +236,32 @@ function defineTable(definition) {
|
|
|
138
236
|
if (!(definition.primaryKeyName in definition.schema.shape)) {
|
|
139
237
|
throw new Error(`Table ${definition.name} primary key "${definition.primaryKeyName}" is not in its schema`);
|
|
140
238
|
}
|
|
239
|
+
if (definition.signedBy !== undefined) {
|
|
240
|
+
validateSignedByField(definition);
|
|
241
|
+
}
|
|
242
|
+
else if (definition.signedKey !== undefined) {
|
|
243
|
+
throw new Error(`Table ${definition.name}.signedKey needs signedBy`);
|
|
244
|
+
}
|
|
141
245
|
const TableClass = definition.tableClass ?? PackageTable;
|
|
142
246
|
if (typeof TableClass !== "function" ||
|
|
143
247
|
!(TableClass.prototype instanceof PackageTable || TableClass === PackageTable)) {
|
|
144
248
|
throw new Error(`Table ${definition.name} tableClass must extend PackageTable`);
|
|
145
249
|
}
|
|
146
|
-
const frozen = Object.freeze({
|
|
250
|
+
const frozen = Object.freeze({
|
|
251
|
+
...definition,
|
|
252
|
+
...(definition.signedKey ? { signedKey: Object.freeze([...definition.signedKey]) } : {}),
|
|
253
|
+
});
|
|
147
254
|
const table = (ctx) => new TableClass(ctx, frozen);
|
|
148
|
-
|
|
255
|
+
const rowId = (row) => {
|
|
256
|
+
const { name, signedBy, signedKey, schema } = frozen;
|
|
257
|
+
if (!signedBy || !signedKey) {
|
|
258
|
+
throw new Error(`Table ${name} has no signedKey, so its row ids come from Peers when rows are first saved`);
|
|
259
|
+
}
|
|
260
|
+
const identityRow = Object.fromEntries([signedBy, ...signedKey].map((field) => [
|
|
261
|
+
field,
|
|
262
|
+
schema.shape[field].parse(row[field], { path: [field] }),
|
|
263
|
+
]));
|
|
264
|
+
return (0, signed_row_id_1.signedRowIdFor)({ name, signedBy, signedKey }, identityRow);
|
|
265
|
+
};
|
|
266
|
+
return Object.freeze(Object.assign(table, { kind: "table", definition: frozen, rowId }));
|
|
149
267
|
}
|
|
@@ -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);
|
|
@@ -655,17 +655,6 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
655
655
|
signedAt?: number | undefined;
|
|
656
656
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
657
657
|
};
|
|
658
|
-
kind: "group-approval";
|
|
659
|
-
membership: {
|
|
660
|
-
role: import("../data").GroupMemberRole;
|
|
661
|
-
signature: string;
|
|
662
|
-
userId: string;
|
|
663
|
-
groupId: string;
|
|
664
|
-
groupMemberId: string;
|
|
665
|
-
deleted?: boolean | undefined;
|
|
666
|
-
publicKey?: string | undefined;
|
|
667
|
-
signedAt?: number | undefined;
|
|
668
|
-
};
|
|
669
658
|
seed: {
|
|
670
659
|
users: {
|
|
671
660
|
name: string;
|
|
@@ -703,6 +692,17 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
703
692
|
signedAt?: number | undefined;
|
|
704
693
|
}[];
|
|
705
694
|
};
|
|
695
|
+
kind: "group-approval";
|
|
696
|
+
membership: {
|
|
697
|
+
role: import("../data").GroupMemberRole;
|
|
698
|
+
signature: string;
|
|
699
|
+
userId: string;
|
|
700
|
+
groupId: string;
|
|
701
|
+
groupMemberId: string;
|
|
702
|
+
deleted?: boolean | undefined;
|
|
703
|
+
publicKey?: string | undefined;
|
|
704
|
+
signedAt?: number | undefined;
|
|
705
|
+
};
|
|
706
706
|
approver: {
|
|
707
707
|
publicKey: string;
|
|
708
708
|
userId: string;
|
|
@@ -735,17 +735,6 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
735
735
|
signedAt?: number | undefined;
|
|
736
736
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
737
737
|
};
|
|
738
|
-
kind: "group-approval";
|
|
739
|
-
membership: {
|
|
740
|
-
role: import("../data").GroupMemberRole;
|
|
741
|
-
signature: string;
|
|
742
|
-
userId: string;
|
|
743
|
-
groupId: string;
|
|
744
|
-
groupMemberId: string;
|
|
745
|
-
deleted?: boolean | undefined;
|
|
746
|
-
publicKey?: string | undefined;
|
|
747
|
-
signedAt?: number | undefined;
|
|
748
|
-
};
|
|
749
738
|
seed: {
|
|
750
739
|
users: {
|
|
751
740
|
name: string;
|
|
@@ -783,6 +772,17 @@ export declare const groupApprovalSchema: z.ZodObject<{
|
|
|
783
772
|
signedAt?: number | undefined;
|
|
784
773
|
}[];
|
|
785
774
|
};
|
|
775
|
+
kind: "group-approval";
|
|
776
|
+
membership: {
|
|
777
|
+
role: import("../data").GroupMemberRole;
|
|
778
|
+
signature: string;
|
|
779
|
+
userId: string;
|
|
780
|
+
groupId: string;
|
|
781
|
+
groupMemberId: string;
|
|
782
|
+
deleted?: boolean | undefined;
|
|
783
|
+
publicKey?: string | undefined;
|
|
784
|
+
signedAt?: number | undefined;
|
|
785
|
+
};
|
|
786
786
|
approver: {
|
|
787
787
|
publicKey: string;
|
|
788
788
|
userId: string;
|
|
@@ -1607,17 +1607,6 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1607
1607
|
signedAt?: number | undefined;
|
|
1608
1608
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
1609
1609
|
};
|
|
1610
|
-
kind: "group-approval";
|
|
1611
|
-
membership: {
|
|
1612
|
-
role: import("../data").GroupMemberRole;
|
|
1613
|
-
signature: string;
|
|
1614
|
-
userId: string;
|
|
1615
|
-
groupId: string;
|
|
1616
|
-
groupMemberId: string;
|
|
1617
|
-
deleted?: boolean | undefined;
|
|
1618
|
-
publicKey?: string | undefined;
|
|
1619
|
-
signedAt?: number | undefined;
|
|
1620
|
-
};
|
|
1621
1610
|
seed: {
|
|
1622
1611
|
users: {
|
|
1623
1612
|
name: string;
|
|
@@ -1655,6 +1644,17 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1655
1644
|
signedAt?: number | undefined;
|
|
1656
1645
|
}[];
|
|
1657
1646
|
};
|
|
1647
|
+
kind: "group-approval";
|
|
1648
|
+
membership: {
|
|
1649
|
+
role: import("../data").GroupMemberRole;
|
|
1650
|
+
signature: string;
|
|
1651
|
+
userId: string;
|
|
1652
|
+
groupId: string;
|
|
1653
|
+
groupMemberId: string;
|
|
1654
|
+
deleted?: boolean | undefined;
|
|
1655
|
+
publicKey?: string | undefined;
|
|
1656
|
+
signedAt?: number | undefined;
|
|
1657
|
+
};
|
|
1658
1658
|
approver: {
|
|
1659
1659
|
publicKey: string;
|
|
1660
1660
|
userId: string;
|
|
@@ -1687,17 +1687,6 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1687
1687
|
signedAt?: number | undefined;
|
|
1688
1688
|
publicRole?: import("../data").GroupMemberRole | undefined;
|
|
1689
1689
|
};
|
|
1690
|
-
kind: "group-approval";
|
|
1691
|
-
membership: {
|
|
1692
|
-
role: import("../data").GroupMemberRole;
|
|
1693
|
-
signature: string;
|
|
1694
|
-
userId: string;
|
|
1695
|
-
groupId: string;
|
|
1696
|
-
groupMemberId: string;
|
|
1697
|
-
deleted?: boolean | undefined;
|
|
1698
|
-
publicKey?: string | undefined;
|
|
1699
|
-
signedAt?: number | undefined;
|
|
1700
|
-
};
|
|
1701
1690
|
seed: {
|
|
1702
1691
|
users: {
|
|
1703
1692
|
name: string;
|
|
@@ -1735,6 +1724,17 @@ export declare const inviteMessageSchema: z.ZodDiscriminatedUnion<"kind", [z.Zod
|
|
|
1735
1724
|
signedAt?: number | undefined;
|
|
1736
1725
|
}[];
|
|
1737
1726
|
};
|
|
1727
|
+
kind: "group-approval";
|
|
1728
|
+
membership: {
|
|
1729
|
+
role: import("../data").GroupMemberRole;
|
|
1730
|
+
signature: string;
|
|
1731
|
+
userId: string;
|
|
1732
|
+
groupId: string;
|
|
1733
|
+
groupMemberId: string;
|
|
1734
|
+
deleted?: boolean | undefined;
|
|
1735
|
+
publicKey?: string | undefined;
|
|
1736
|
+
signedAt?: number | undefined;
|
|
1737
|
+
};
|
|
1738
1738
|
approver: {
|
|
1739
1739
|
publicKey: string;
|
|
1740
1740
|
userId: string;
|
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). */
|