@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.
Files changed (55) hide show
  1. package/dist/contracts/contract-resolvers.js +1 -0
  2. package/dist/contracts/index.d.ts +2 -1
  3. package/dist/contracts/index.js +5 -2
  4. package/dist/contracts/remote-contract-transport.d.ts +27 -0
  5. package/dist/contracts/remote-contract-transport.js +27 -0
  6. package/dist/contracts/system/group-members-contract.d.ts +25 -0
  7. package/dist/contracts/system/group-members-contract.js +50 -0
  8. package/dist/contracts/system/network-contracts.js +1 -1
  9. package/dist/data/authority-write-checks.d.ts +58 -1
  10. package/dist/data/authority-write-checks.js +176 -1
  11. package/dist/data/group-resign.d.ts +30 -0
  12. package/dist/data/group-resign.js +74 -0
  13. package/dist/data/index.d.ts +1 -0
  14. package/dist/data/index.js +1 -0
  15. package/dist/data/orm/index.d.ts +1 -0
  16. package/dist/data/orm/index.js +1 -0
  17. package/dist/data/orm/signed-package-table.d.ts +126 -0
  18. package/dist/data/orm/signed-package-table.js +293 -0
  19. package/dist/data/orm/table-container.d.ts +22 -2
  20. package/dist/data/orm/table-container.js +49 -14
  21. package/dist/data/orm/types.d.ts +12 -0
  22. package/dist/data/package-table-identity.d.ts +48 -0
  23. package/dist/data/package-table-identity.js +151 -0
  24. package/dist/data/signed-row-id.d.ts +43 -0
  25. package/dist/data/signed-row-id.js +72 -0
  26. package/dist/define/context.d.ts +9 -0
  27. package/dist/define/manifest.js +3 -1
  28. package/dist/define/system-contracts.d.ts +99 -0
  29. package/dist/define/system-contracts.js +5 -1
  30. package/dist/define/table.d.ts +32 -0
  31. package/dist/define/table.js +120 -2
  32. package/dist/device/binary-peer-connection-v2.d.ts +2 -2
  33. package/dist/device/binary-peer-connection-v2.js +51 -13
  34. package/dist/device/tx-encoding.d.ts +22 -1
  35. package/dist/device/tx-encoding.js +63 -26
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/invites/invite-messages.d.ts +44 -44
  39. package/dist/keys.d.ts +7 -2
  40. package/dist/keys.js +61 -12
  41. package/dist/native-codecs.d.ts +34 -0
  42. package/dist/native-codecs.js +16 -0
  43. package/dist/package-loader/isolated-artifact.d.ts +17 -0
  44. package/dist/package-loader/isolated-artifact.js +15 -2
  45. package/dist/package-loader/isolated-runtime.d.ts +10 -0
  46. package/dist/package-loader/isolated-table-gateway.js +31 -3
  47. package/dist/package-loader/package-loader.js +1 -0
  48. package/dist/serial-json.d.ts +7 -0
  49. package/dist/serial-json.js +16 -2
  50. package/dist/system-ids.d.ts +2 -0
  51. package/dist/system-ids.js +3 -1
  52. package/dist/types/peer-device.d.ts +6 -0
  53. package/dist/types/peer-device.js +7 -1
  54. package/dist/utils.js +2 -2
  55. package/package.json +1 -1
@@ -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({ ...definition });
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
- return Object.freeze(Object.assign(table, { kind: "table", definition: frozen }));
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 Uint8Array) and transparent
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 Uint8Array) and transparent
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
- const callbacks = {};
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[message.callbackId];
497
+ const callback = callbacks.get(message.callbackId);
473
498
  if (callback) {
474
- delete callbacks[message.callbackId];
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
- // Handle closure. Cipher teardown waits until queued RPC replies have sealed;
743
- // see {@link releaseCipherAfterTurn}.
744
- peer.on("close", () => {
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
- export declare function txEncode(data: unknown): Uint8Array;
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
- function txEncode(data) {
18
- const noCycles = (0, serial_json_1.toJSON)(data); // remove cycles and encode dates, etc.
19
- // First encode to msgpack
20
- const encoded = (0, msgpack_1.encode)(noCycles);
21
- let body;
22
- let flag;
23
- if (encoded.length > COMPRESSION_THRESHOLD) {
24
- // Try to compress - use fast compression level
25
- const compressed = (0, fflate_1.compressSync)(encoded, { level: COMPRESSION_LEVEL });
26
- // Only use compression if it actually helps significantly
27
- if (compressed.length < encoded.length * MIN_COMPRESSION_RATIO) {
28
- body = compressed;
29
- flag = 1; // compressed
30
- }
31
- else {
32
- // Compression didn't help much, skip it
33
- body = encoded;
34
- flag = 0; // not compressed
35
- }
36
- }
37
- else {
38
- body = encoded;
39
- flag = 0; // not compressed
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] = flag;
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
- body = (0, fflate_1.decompressSync)(body);
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
@@ -33,6 +33,7 @@ export * from "./invites";
33
33
  export * from "./keys";
34
34
  export * from "./logging";
35
35
  export * from "./mentions";
36
+ export * from "./native-codecs";
36
37
  export * from "./observable";
37
38
  export * from "./package-installer";
38
39
  export * from "./package-loader";
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
- /** Base64 with URL-safe alphabet and no `=` padding (used throughout keys and wire formats). */
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
- * @throws TypeError if `data` is not a string
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). */