@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
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
- /** Base64 with URL-safe alphabet and no `=` padding (used throughout keys and wire formats). */
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
- // return Buffer.from(data).toString("base64url"); // no padding by default
119
- const str = utils.encodeBase64(data);
120
- return str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
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
- * @throws TypeError if `data` is not a string
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
- return utils.decodeBase64(data);
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 = (0, tweetnacl_util_1.decodeUTF8)(msg);
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 = (0, tweetnacl_util_1.decodeUTF8)(objStr);
217
- const signatureByteArray = nacl.sign.detached(objDecoded, _secretKey);
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) => nacl.sign.detached.verify((0, tweetnacl_util_1.decodeUTF8)(objStr), signature, _publicKey);
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 = (0, tweetnacl_util_1.decodeUTF8)(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
+ }
@@ -113,6 +113,21 @@ export interface IIsolatedPackageTable {
113
113
  * table. Storage policy, like `localOnly`: changing them needs a higher `versionNumber`.
114
114
  */
115
115
  indexes?: IIsolatedTableIndex[];
116
+ /**
117
+ * The required `id` field holding each row's owner, a user id. The host derives each row's
118
+ * id from its owner (see `signedKey`), signs rows on save with the signed-in user's key, and
119
+ * adds the signature, signing time, table/context binding, and `signedIdSeed` columns. In a
120
+ * group it refuses a row that isn't validly signed by its owner, a Writer, under its derived
121
+ * id. Not on a local-only table. A table has it, and the same primary key, from its first
122
+ * version or never.
123
+ */
124
+ signedBy?: string;
125
+ /**
126
+ * With `signedBy`: the fields that, with the owner, name a row; each row's id is derived from
127
+ * its owner and these fields. Without it, from its owner and a host-chosen seed. A table has
128
+ * the same `signedKey` in every version.
129
+ */
130
+ signedKey?: string[];
116
131
  }
117
132
  /** One index of an isolated package table: field names in order, each ascending unless set. */
118
133
  export interface IIsolatedTableIndex {
@@ -255,6 +270,8 @@ export declare const ISOLATED_PERSISTENT_VAR_HOST_MEMBER = "package.pvar";
255
270
  export declare const ISOLATED_EVENT_HOST_MEMBER = "contract.emit";
256
271
  /** Host-contract member guests use to publish an in-memory provided-contract observable. */
257
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";
258
275
  /**
259
276
  * How deep field lists may nest through object `fields`: a tool's inputs, say, are level 1.
260
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,
@@ -281,7 +283,17 @@ function validatePackageTables(manifest, strict) {
281
283
  if (forbiddenIdentity) {
282
284
  throw new Error(`${tableLabel} must not declare ${forbiddenIdentity}`);
283
285
  }
284
- assertOnlyKeys(table, ["name", "description", "primaryKeyName", "fields", "versionNumber", "localOnly", "indexes"], tableLabel);
286
+ assertOnlyKeys(table, [
287
+ "name",
288
+ "description",
289
+ "primaryKeyName",
290
+ "fields",
291
+ "versionNumber",
292
+ "localOnly",
293
+ "indexes",
294
+ "signedBy",
295
+ "signedKey",
296
+ ], tableLabel);
285
297
  if (typeof table.name !== "string" || table.name.trim() === "") {
286
298
  throw new Error(`${tableLabel}.name is required`);
287
299
  }
@@ -318,6 +330,7 @@ function validatePackageTables(manifest, strict) {
318
330
  table.versionNumber = 1;
319
331
  }
320
332
  (0, package_table_identity_1.validateTableIndexes)(table.indexes, new Set(table.fields.map((field) => field.name)), `${tableLabel}.indexes`, table.localOnly);
333
+ (0, package_table_identity_1.validateSignedTable)(table, tableLabel);
321
334
  }
322
335
  }
323
336
  function validateProvidedContract(contract, index, packageTables, strict) {
@@ -207,6 +207,10 @@ export interface IIsolatedPackageTableDeclaration extends IIsolatedPackageTableR
207
207
  localOnly?: boolean;
208
208
  /** Indexes; see `IIsolatedPackageTable.indexes`. */
209
209
  indexes?: IIsolatedTableIndex[];
210
+ /** The field holding each row's owner; see `IIsolatedPackageTable.signedBy`. */
211
+ signedBy?: string;
212
+ /** The fields naming a row among its owner's; see `IIsolatedPackageTable.signedKey`. */
213
+ signedKey?: string[];
210
214
  }
211
215
  /**
212
216
  * Trusted host capability for isolated-package table persistence.
@@ -347,6 +351,12 @@ export interface IIsolatedPackageRuntimeFactoryOptions {
347
351
  onWorkerOutput?: (output: IIsolatedWorkerOutput) => void;
348
352
  /** This device's id, which package code reads as `ctx.deviceId`. */
349
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>;
350
360
  }
351
361
  /**
352
362
  * Host callback that builds an {@link IIsolatedPackageRuntime} for one
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createMemoryIsolatedPackageTableGateway = createMemoryIsolatedPackageTableGateway;
4
4
  exports.createIsolatedPackageTableGateway = createIsolatedPackageTableGateway;
5
+ const lodash_1 = require("lodash");
6
+ const authority_write_checks_1 = require("../data/authority-write-checks");
5
7
  const table_container_1 = require("../data/orm/table-container");
6
8
  const types_1 = require("../data/orm/types");
7
9
  const package_table_identity_1 = require("../data/package-table-identity");
@@ -44,12 +46,16 @@ function validateDeclaration(declaration) {
44
46
  throw new Error("Isolated table localOnly must be a boolean");
45
47
  }
46
48
  (0, package_table_identity_1.validateTableIndexes)(declaration.indexes, fieldNames, "Isolated table indexes", declaration.localOnly);
47
- const { localOnly, ...rest } = declaration;
49
+ // A stored definition's fields already carry the host's signature columns.
50
+ (0, package_table_identity_1.validateSignedTable)(declaration, "Isolated table", { hostFields: true });
51
+ const { localOnly, signedBy, signedKey, ...rest } = declaration;
48
52
  return {
49
53
  ...rest,
50
54
  fields: declaration.fields.map((field) => ({ ...field })),
51
55
  versionNumber: versionNumber(declaration),
52
56
  ...(localOnly ? { localOnly: true } : {}),
57
+ ...(signedBy === undefined ? {} : { signedBy }),
58
+ ...(signedKey === undefined ? {} : { signedKey: [...signedKey] }),
53
59
  };
54
60
  }
55
61
  function toMetaData(rawDeclaration) {
@@ -60,11 +66,14 @@ function toMetaData(rawDeclaration) {
60
66
  name: declaration.tableName,
61
67
  description: declaration.description,
62
68
  primaryKeyName: declaration.primaryKeyName,
63
- fields: declaration.fields,
69
+ // A signed table stores the host's signature columns beside the package's fields.
70
+ fields: declaration.signedBy ? (0, package_table_identity_1.withSignedTableFields)(declaration.fields) : declaration.fields,
64
71
  versionNumber: versionNumber(declaration),
65
72
  // Only when set: stored definitions hash their metadata, and older ones have no such keys.
66
73
  ...(declaration.localOnly ? { localOnly: true } : {}),
67
74
  ...(declaration.indexes?.length ? { indexes: declaration.indexes } : {}),
75
+ ...(declaration.signedBy === undefined ? {} : { signedBy: declaration.signedBy }),
76
+ ...(declaration.signedKey === undefined ? {} : { signedKey: declaration.signedKey }),
68
77
  };
69
78
  }
70
79
  function normalizedMetaData(metaData) {
@@ -92,6 +101,15 @@ function selectNewerDefinition(existing, incoming) {
92
101
  if (existingVersion === incomingVersion && !sameDefinition(existing, incoming)) {
93
102
  throw new Error(`Isolated table "${(0, package_table_identity_1.packageScopedTableName)(requiredPackageId(incoming), incoming.name)}" has different schemas at version ${incomingVersion}`);
94
103
  }
104
+ if (existing.signedBy !== incoming.signedBy) {
105
+ throw new Error(`Isolated table "${(0, package_table_identity_1.packageScopedTableName)(requiredPackageId(incoming), incoming.name)}" is signed from its first version or never; signedBy can't change`);
106
+ }
107
+ if (!(0, lodash_1.isEqual)(existing.signedKey, incoming.signedKey)) {
108
+ throw new Error(`Isolated table "${(0, package_table_identity_1.packageScopedTableName)(requiredPackageId(incoming), incoming.name)}" derives row ids from its signedKey, so signedKey can't change`);
109
+ }
110
+ if (existing.signedBy && existing.primaryKeyName !== incoming.primaryKeyName) {
111
+ throw new Error(`Isolated table "${(0, package_table_identity_1.packageScopedTableName)(requiredPackageId(incoming), incoming.name)}" keeps its derived row ids in its primary key, so the primary key can't change`);
112
+ }
95
113
  const decision = (0, table_container_1.checkVersionedUpdate)(existingVersion, incomingVersion, existing, incoming);
96
114
  if (decision === "update") {
97
115
  return incoming;
@@ -114,6 +132,8 @@ function declarationFromMetaData(metaData) {
114
132
  versionNumber: metaData.versionNumber,
115
133
  localOnly: metaData.localOnly,
116
134
  ...(metaData.indexes?.length ? { indexes: metaData.indexes } : {}),
135
+ ...(metaData.signedBy === undefined ? {} : { signedBy: metaData.signedBy }),
136
+ ...(metaData.signedKey === undefined ? {} : { signedKey: metaData.signedKey }),
117
137
  });
118
138
  }
119
139
  function metaDataFromStoredDefinition(record, incoming) {
@@ -136,6 +156,9 @@ function metaDataFromStoredDefinition(record, incoming) {
136
156
  }
137
157
  function registerDeclaration(declarations, byTableId, rawDeclaration) {
138
158
  const declaration = validateDeclaration(rawDeclaration);
159
+ // Package declarations leave the host columns out. Only internal reconstruction of a
160
+ // stored definition (declarationFromMetaData) may already include those columns.
161
+ (0, package_table_identity_1.validateSignedTable)(declaration, "Isolated table");
139
162
  const key = tableKey(declaration);
140
163
  const tableId = (0, package_table_identity_1.packageScopedTableId)(declaration.packageId, declaration.tableName);
141
164
  const existingKey = byTableId.get(tableId);
@@ -430,9 +453,11 @@ function createIsolatedPackageTableGateway(dataContext) {
430
453
  if (plan.existingRecord) {
431
454
  await definitionsTable.save(plan.existingRecord, { restoreIfDeleted: true });
432
455
  }
433
- else {
456
+ else if (!plan.activeMetaData.signedBy) {
434
457
  await definitionsTable.delete(requiredTableId(plan.activeMetaData));
435
458
  }
459
+ // A signed declaration is permanent even if activation fails. Retaining an unused
460
+ // definition is safe; deleting it would reopen the unsigned-definition bypass.
436
461
  }
437
462
  catch (rollbackError) {
438
463
  failures.push(rollbackError);
@@ -445,6 +470,9 @@ function createIsolatedPackageTableGateway(dataContext) {
445
470
  }
446
471
  for (const definition of definitions) {
447
472
  dataContext.tableContainer.registerTableDefinition(definition, { overwrite: true });
473
+ // A signed table's check is keyed by its physical name; register it with the table, so
474
+ // the host finds it when it builds the table's data source.
475
+ (0, authority_write_checks_1.tableWriteCheckFor)(definition.metaData);
448
476
  }
449
477
  declarations.clear();
450
478
  byTableId.clear();
@@ -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];
@@ -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_`.
@@ -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;
@@ -213,3 +213,5 @@ export declare const packagesContractId = "00mvdhk66tdqrj3hy0bacmj32";
213
213
  * Allocated once with newid() for System Host Shell v1.
214
214
  */
215
215
  export declare const hostShellContractId = "00mveexhw7ezjfghxomlo011g";
216
+ /** Local-only System Group Members v1; allocated once with newid(). */
217
+ export declare const groupMembersContractId = "00mvzfzkuf4djwjrsxq9cnaxt";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hostShellContractId = exports.packagesContractId = exports.desktopToolsContractId = exports.hostToolsContractId = exports.toolCatalogContractId = exports.schedulerToolIds = exports.schedulerContractId = exports.contractCatalogToolIds = exports.contractCatalogContractId = exports.httpToolIds = exports.httpContractId = exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.syncCheckContractId = exports.appUpdatesContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCoreRevokedPublishPublicKeys = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
3
+ exports.groupMembersContractId = exports.hostShellContractId = exports.packagesContractId = exports.desktopToolsContractId = exports.hostToolsContractId = exports.toolCatalogContractId = exports.schedulerToolIds = exports.schedulerContractId = exports.contractCatalogToolIds = exports.contractCatalogContractId = exports.httpToolIds = exports.httpContractId = exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.syncCheckContractId = exports.appUpdatesContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCoreRevokedPublishPublicKeys = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
4
4
  /** Stable user ID that owns built-in Peers system records. */
5
5
  exports.peersRootUserId = "000peers0user000000000001";
6
6
  /** Default assistant used when no user-selected assistant is available. */
@@ -220,3 +220,5 @@ exports.packagesContractId = "00mvdhk66tdqrj3hy0bacmj32";
220
220
  * Allocated once with newid() for System Host Shell v1.
221
221
  */
222
222
  exports.hostShellContractId = "00mveexhw7ezjfghxomlo011g";
223
+ /** Local-only System Group Members v1; allocated once with newid(). */
224
+ exports.groupMembersContractId = "00mvzfzkuf4djwjrsxq9cnaxt";
@@ -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(`connection has been closed`);
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(`connection has been closed`);
306
+ throw new Error(peer_device_1.CONNECTION_CLOSED_MESSAGE);
307
307
  }
308
308
  }
309
309
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.28.11",
3
+ "version": "0.29.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"