@peers-app/peers-sdk 0.28.0 → 0.28.1

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.
@@ -10,6 +10,16 @@ const tool_catalog_fields_1 = require("../tool-catalog-fields");
10
10
  const data_query_1 = require("./data-query");
11
11
  const data_query_sqlite_1 = require("./data-query.sqlite");
12
12
  const types_1 = require("./types");
13
+ /**
14
+ * The SQLite name {@link SQLDataSource} gives one of a table's indexes.
15
+ *
16
+ * @param tableName - Full (physical) table name.
17
+ * @param index - One entry of `ITableMetaData.indexes`.
18
+ */
19
+ function sqlIndexName(tableName, index) {
20
+ const fieldNames = index.fields.map((f) => `_${typeof f === "string" ? f : f.name}`).join("");
21
+ return `${tableName}_${fieldNames}_${(0, utils_1.simpleHash)(JSON.stringify(index))}`;
22
+ }
13
23
  /** SQLite-backed table storage with additive schema migration and serialized initialization. */
14
24
  class SQLDataSource {
15
25
  db;
@@ -79,22 +89,12 @@ class SQLDataSource {
79
89
  // create indexes if they don't exist
80
90
  if (this.metaData.indexes) {
81
91
  for (const index of this.metaData.indexes) {
82
- let fieldNames = "";
92
+ // Quoted: a field may be named like an SQL keyword (`order`, `group`).
83
93
  const fields = index.fields
84
- .map((f) => {
85
- if (typeof f === "string") {
86
- fieldNames += `_${f}`;
87
- return f;
88
- }
89
- else {
90
- fieldNames += `_${f.name}`;
91
- return `${f.name} ${f.order}`;
92
- }
93
- })
94
+ .map((f) => (typeof f === "string" ? `"${f}"` : `"${f.name}" ${f.order}`))
94
95
  .join(", ");
95
- const hash = (0, utils_1.simpleHash)(JSON.stringify(index));
96
96
  const unique = index.unique ? "UNIQUE" : "";
97
- const indexName = `${this.tableName}_${fieldNames}_${hash}`;
97
+ const indexName = sqlIndexName(this.tableName, index);
98
98
  indexNames.push(indexName);
99
99
  const sql = `CREATE ${unique} INDEX IF NOT EXISTS "${indexName}" ON "${this.tableName}"(${fields});`;
100
100
  try {
@@ -102,6 +102,9 @@ class SQLDataSource {
102
102
  }
103
103
  catch (e) {
104
104
  console.error(`Error creating index "${indexName}" on table "${this.tableName}": ${String(e)}`);
105
+ // A package must not accept writes without its declared uniqueness constraint.
106
+ if (this.metaData.packageId && index.unique)
107
+ throw e;
105
108
  }
106
109
  }
107
110
  }
@@ -160,9 +160,12 @@ class TableContainer {
160
160
  this.savingDefinitions = true;
161
161
  const tableName = (0, utils_1.getFullTableName)(metaData);
162
162
  try {
163
- // Lazy import to avoid circular dependency at module load time
164
- const { TableDefinitions } = require("../table-definitions-table");
165
- const tableDefsTable = TableDefinitions();
163
+ // Lazy import to avoid circular dependency at module load time; it registers the
164
+ // TableDefinitions system table. Use this container's own copy: a definition belongs to
165
+ // the data context the table lives in, not whichever group is active.
166
+ require("../table-definitions-table");
167
+ const definitions = table_definitions_system_1.systemTableDefinitions.TableDefinitions;
168
+ const tableDefsTable = this.getTable(definitions.metaData, definitions.schema);
166
169
  const record = {
167
170
  tableId: metaData.tableId,
168
171
  name: tableName,
@@ -13,6 +13,21 @@ import type { ITableDefinitionRecord } from "./table-definitions-table";
13
13
  * @throws When the identifier is empty or contains SQL syntax.
14
14
  */
15
15
  export declare function validateIsolatedSqlIdentifier(identifier: string, label?: string): string;
16
+ /**
17
+ * Validate a table's index declarations: each lists at least one of the table's fields, in
18
+ * order, by name or as `{ name, order }` with `"ASC"` or `"DESC"`.
19
+ *
20
+ * @param indexes - The declared indexes, if any.
21
+ * @param fieldNames - The table's field names.
22
+ * @param label - Where the indexes come from, for errors.
23
+ * @param allowUnique - Whether an index may be unique. A package may make one unique only on a
24
+ * local-only table: on a synced table two devices can each write a row the other's unique
25
+ * index refuses, and package tables have no conflict resolver. A stored definition may have
26
+ * one anyway (an old-style package's).
27
+ * @throws When an index is malformed, names a field the table lacks, or is unique when not
28
+ * allowed.
29
+ */
30
+ export declare function validateTableIndexes(indexes: unknown, fieldNames: ReadonlySet<string>, label: string, allowUnique?: boolean): void;
16
31
  /**
17
32
  * Validate a package-local table name.
18
33
  *
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateIsolatedSqlIdentifier = validateIsolatedSqlIdentifier;
4
+ exports.validateTableIndexes = validateTableIndexes;
4
5
  exports.validatePackageTableName = validatePackageTableName;
5
6
  exports.packageScopedTableName = packageScopedTableName;
6
7
  exports.packageScopedTableId = packageScopedTableId;
@@ -29,6 +30,53 @@ function validateIsolatedSqlIdentifier(identifier, label = "Isolated SQL identif
29
30
  }
30
31
  return identifier;
31
32
  }
33
+ /**
34
+ * Validate a table's index declarations: each lists at least one of the table's fields, in
35
+ * order, by name or as `{ name, order }` with `"ASC"` or `"DESC"`.
36
+ *
37
+ * @param indexes - The declared indexes, if any.
38
+ * @param fieldNames - The table's field names.
39
+ * @param label - Where the indexes come from, for errors.
40
+ * @param allowUnique - Whether an index may be unique. A package may make one unique only on a
41
+ * local-only table: on a synced table two devices can each write a row the other's unique
42
+ * index refuses, and package tables have no conflict resolver. A stored definition may have
43
+ * one anyway (an old-style package's).
44
+ * @throws When an index is malformed, names a field the table lacks, or is unique when not
45
+ * allowed.
46
+ */
47
+ function validateTableIndexes(indexes, fieldNames, label, allowUnique = false) {
48
+ if (indexes === undefined)
49
+ return;
50
+ if (!Array.isArray(indexes))
51
+ throw new Error(`${label} must be an array`);
52
+ for (const [position, index] of indexes.entries()) {
53
+ const at = `${label}[${position}]`;
54
+ const { fields, unique } = (index ?? {});
55
+ if (!index ||
56
+ typeof index !== "object" ||
57
+ Object.keys(index).some((key) => key !== "fields" && key !== "unique") ||
58
+ !Array.isArray(fields) ||
59
+ fields.length === 0) {
60
+ throw new Error(`${at} must be { fields: [...] } with at least one field`);
61
+ }
62
+ if (unique !== undefined && (unique !== true || !allowUnique)) {
63
+ throw new Error(`${at} can be unique only on a local-only table`);
64
+ }
65
+ for (const field of fields) {
66
+ const name = typeof field === "string"
67
+ ? field
68
+ : field &&
69
+ typeof field === "object" &&
70
+ Object.keys(field).every((key) => key === "name" || key === "order") &&
71
+ (field.order === "ASC" || field.order === "DESC")
72
+ ? field.name
73
+ : undefined;
74
+ if (typeof name !== "string" || !fieldNames.has(name)) {
75
+ throw new Error(`${at} names a field the table doesn't have`);
76
+ }
77
+ }
78
+ }
79
+ }
32
80
  /**
33
81
  * Validate a package-local table name.
34
82
  *
@@ -105,18 +153,6 @@ function validateTableDefinitionRecord(record) {
105
153
  }
106
154
  if (!names.has(meta.primaryKeyName))
107
155
  throw new Error("Missing primary key column");
108
- if (meta.indexes !== undefined && !Array.isArray(meta.indexes))
109
- throw new Error("Invalid indexes");
110
- for (const index of meta.indexes ?? []) {
111
- if (!Array.isArray(index.fields) || !index.fields.length)
112
- throw new Error("Missing index fields");
113
- for (const field of index.fields) {
114
- const name = typeof field === "string" ? field : field.name;
115
- if (!names.has(name))
116
- throw new Error("Index must name a declared column");
117
- if (typeof field !== "string" && field.order !== "ASC" && field.order !== "DESC") {
118
- throw new Error("Invalid index order");
119
- }
120
- }
121
- }
156
+ // Only what reaches SQL here: an old-style package may keep a unique index on a synced table.
157
+ validateTableIndexes(meta.indexes, names, "Table indexes", true);
122
158
  }
@@ -17,6 +17,7 @@ exports.resolveDevicePackageVersion = resolveDevicePackageVersion;
17
17
  exports.hasDeviceFollowOverride = hasDeviceFollowOverride;
18
18
  exports.readStoredPackageLocalPath = readStoredPackageLocalPath;
19
19
  const user_context_singleton_1 = require("../context/user-context-singleton");
20
+ const keys_1 = require("../keys");
20
21
  const assistants_1 = require("./assistants");
21
22
  const file_types_1 = require("./files/file.types");
22
23
  const group_member_roles_1 = require("./group-member-roles");
@@ -100,12 +101,23 @@ async function assertPackageVersionRunnable(pkg, pv, dataContext, localDev = fal
100
101
  if (!localDev) {
101
102
  const prefs = packagePrefsVar(pkg.packageId, dataContext);
102
103
  await prefs.loadingPromise;
103
- const deviceSelected = prefs()?.activePackageVersionId === pv.packageVersionId;
104
+ let selection = prefs();
105
+ const deviceSelected = selection?.activePackageVersionId === pv.packageVersionId;
106
+ if (selection && deviceSelected && !selection.activePackageVersionTag) {
107
+ // A selection saved before 0.28.0 has no tag. Record this version's tag once when this
108
+ // device's own user signed the version (their own build or release), so it keeps running;
109
+ // a version someone else retagged still needs a fresh activation.
110
+ const me = await dataContext.userContext.getMe();
111
+ if (me?.publicKey && (0, keys_1.signedByKey)(pv) === me.publicKey) {
112
+ selection = { ...selection, activePackageVersionTag: pv.versionTag };
113
+ prefs(selection);
114
+ }
115
+ }
104
116
  if (!deviceSelected && !(await isPackageRowAdminSigned(pkg, dataContext))) {
105
117
  throw new Error("A group package selection needs an Admin's signature before it can run");
106
118
  }
107
119
  if (pv.versionTag === "dev" &&
108
- (!deviceSelected || prefs()?.activePackageVersionTag !== "dev")) {
120
+ (!deviceSelected || selection?.activePackageVersionTag !== "dev")) {
109
121
  throw new Error("Activate this dev version on this device before running it");
110
122
  }
111
123
  }
@@ -11,7 +11,7 @@ export { AccessLevel } from "../data/access-level";
11
11
  export type { DataFilter, IDataQueryParams } from "../data/orm/data-query";
12
12
  export type { IDataChangedEvent } from "../data/orm/subscribable.data-source";
13
13
  export { type Observable, observable, type Subscription } from "../observable";
14
- export { isid, newid } from "../peer-id";
14
+ export { idFromKey, isid, newid } from "../peer-id";
15
15
  export { FieldType, type IField } from "../types/field-type";
16
16
  export { zodPeerId } from "../types/zod-types";
17
17
  export * from "./context";
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.writePackageVar = exports.userVar = exports.readPackageVar = exports.groupVar = exports.groupUserVar = exports.groupDeviceVar = exports.deviceVar = exports.isImplementedObservable = exports.defineObservable = exports.declareObservable = exports.generatedField = exports.defineEvent = exports.zodPeerId = exports.FieldType = exports.newid = exports.isid = exports.observable = exports.AccessLevel = exports.z = void 0;
17
+ exports.writePackageVar = exports.userVar = exports.readPackageVar = exports.groupVar = exports.groupUserVar = exports.groupDeviceVar = exports.deviceVar = exports.isImplementedObservable = exports.defineObservable = exports.declareObservable = exports.generatedField = exports.defineEvent = exports.zodPeerId = exports.FieldType = exports.newid = exports.isid = exports.idFromKey = exports.observable = exports.AccessLevel = exports.z = void 0;
18
18
  /**
19
19
  * `@peers-app/peers-sdk/define`: declare an isolated package.
20
20
  *
@@ -30,6 +30,7 @@ Object.defineProperty(exports, "AccessLevel", { enumerable: true, get: function
30
30
  var observable_1 = require("../observable");
31
31
  Object.defineProperty(exports, "observable", { enumerable: true, get: function () { return observable_1.observable; } });
32
32
  var peer_id_1 = require("../peer-id");
33
+ Object.defineProperty(exports, "idFromKey", { enumerable: true, get: function () { return peer_id_1.idFromKey; } });
33
34
  Object.defineProperty(exports, "isid", { enumerable: true, get: function () { return peer_id_1.isid; } });
34
35
  Object.defineProperty(exports, "newid", { enumerable: true, get: function () { return peer_id_1.newid; } });
35
36
  var field_type_1 = require("../types/field-type");
@@ -501,7 +501,7 @@ function observableValueShape(name, schema) {
501
501
  return empty ? { ...shape, optional: true } : shape;
502
502
  }
503
503
  function tableManifest(table) {
504
- const { name, description, primaryKeyName, schema, versionNumber, localOnly } = table.definition;
504
+ const { name, description, primaryKeyName, schema, versionNumber, localOnly, indexes } = table.definition;
505
505
  return {
506
506
  name,
507
507
  description,
@@ -509,6 +509,14 @@ function tableManifest(table) {
509
509
  fields: describing(`Table ${name}`, () => irFields(schema)),
510
510
  ...(versionNumber === undefined ? {} : { versionNumber }),
511
511
  ...(localOnly ? { localOnly: true } : {}),
512
+ ...(indexes?.length
513
+ ? {
514
+ indexes: indexes.map((index) => ({
515
+ fields: index.fields.map((f) => typeof f === "string" ? f : { name: f.name, order: f.order }),
516
+ ...(index.unique ? { unique: true } : {}),
517
+ })),
518
+ }
519
+ : {}),
512
520
  };
513
521
  }
514
522
  /** One contract's manifest entry, with name-only references to its tables. */
@@ -16,6 +16,12 @@ export interface IPackageTableDefinition<S extends z.AnyZodObject = z.AnyZodObje
16
16
  versionNumber?: number;
17
17
  /** Keep rows on this device only. They never sync. */
18
18
  localOnly?: boolean;
19
+ /**
20
+ * Indexes for the fields queries filter or sort by, each in order (a field name, or a name with
21
+ * `"ASC"` or `"DESC"`). A local-only table can also make one `unique`. Changing them is a
22
+ * schema change, so raise `versionNumber`.
23
+ */
24
+ indexes?: IPackageTableIndex<S>[];
19
25
  /**
20
26
  * The table's class, to override its methods: `Feeds(ctx)` returns one, and so does every
21
27
  * consumer call through a contract, so an override is always the logic in use. Defaults to
@@ -23,6 +29,15 @@ export interface IPackageTableDefinition<S extends z.AnyZodObject = z.AnyZodObje
23
29
  */
24
30
  tableClass?: PackageTableClass<S>;
25
31
  }
32
+ /** One index of a {@link defineTable} table: its fields in order, each ascending unless set. */
33
+ export interface IPackageTableIndex<S extends z.AnyZodObject = z.AnyZodObject> {
34
+ fields: (Extract<keyof z.infer<S>, string> | {
35
+ name: Extract<keyof z.infer<S>, string>;
36
+ order: "ASC" | "DESC";
37
+ })[];
38
+ /** No two rows share these values. Only on a `localOnly` table: synced rows can collide. */
39
+ unique?: true;
40
+ }
26
41
  /** A class extending {@link PackageTable} for a table's schema. */
27
42
  export type PackageTableClass<S extends z.AnyZodObject = z.AnyZodObject> = new (ctx: IPackageContext, definition: Readonly<IPackageTableDefinition<S>>) => PackageTable<S>;
28
43
  /** One table's methods, bound to one handler call. */
@@ -125,6 +125,20 @@ export interface IIsolatedPackageTable {
125
125
  * of a contract's table shape.
126
126
  */
127
127
  localOnly?: boolean;
128
+ /**
129
+ * Indexes for the fields queries filter or sort by, each in order. Unique only on a local-only
130
+ * table. Storage policy, like `localOnly`: changing them needs a higher `versionNumber`.
131
+ */
132
+ indexes?: IIsolatedTableIndex[];
133
+ }
134
+ /** One index of an isolated package table: field names in order, each ascending unless set. */
135
+ export interface IIsolatedTableIndex {
136
+ fields: (string | {
137
+ name: string;
138
+ order: "ASC" | "DESC";
139
+ })[];
140
+ /** No two rows share these values. Local-only tables only. */
141
+ unique?: true;
128
142
  }
129
143
  /** Name-only reference that exposes one package table through a contract. */
130
144
  export interface IIsolatedContractTableRef {
@@ -19,6 +19,7 @@ exports.findPackageTable = findPackageTable;
19
19
  exports.toConsumedContracts = toConsumedContracts;
20
20
  const contract_definition_schema_1 = require("../contracts/contract-definition-schema");
21
21
  const access_level_1 = require("../data/access-level");
22
+ const package_table_identity_1 = require("../data/package-table-identity");
22
23
  const persistent_var_identity_1 = require("../data/persistent-var-identity");
23
24
  const field_type_1 = require("../types/field-type");
24
25
  const utils_1 = require("../utils");
@@ -300,7 +301,7 @@ function validatePackageTables(manifest, strict) {
300
301
  if (forbiddenIdentity) {
301
302
  throw new Error(`${tableLabel} must not declare ${forbiddenIdentity}`);
302
303
  }
303
- assertOnlyKeys(table, ["name", "description", "primaryKeyName", "fields", "versionNumber", "localOnly"], tableLabel);
304
+ assertOnlyKeys(table, ["name", "description", "primaryKeyName", "fields", "versionNumber", "localOnly", "indexes"], tableLabel);
304
305
  if (typeof table.name !== "string" || table.name.trim() === "") {
305
306
  throw new Error(`${tableLabel}.name is required`);
306
307
  }
@@ -336,6 +337,7 @@ function validatePackageTables(manifest, strict) {
336
337
  else {
337
338
  table.versionNumber = 1;
338
339
  }
340
+ (0, package_table_identity_1.validateTableIndexes)(table.indexes, new Set(table.fields.map((field) => field.name)), `${tableLabel}.indexes`, table.localOnly);
339
341
  }
340
342
  }
341
343
  function validateProvidedContract(contract, index, packageTables, guestApi, strict) {
@@ -4,7 +4,7 @@ import type { IContractCall, IContractProviderEndpoint } from "../contracts/cont
4
4
  import type { ContractRegistry } from "../contracts/registry";
5
5
  import type { PersistentVarScope } from "../data/persistent-vars";
6
6
  import type { IField } from "../types/field-type";
7
- import type { IIsolatedContractToolRef, IIsolatedInstalledPackage, IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
7
+ import type { IIsolatedContractToolRef, IIsolatedInstalledPackage, IIsolatedInstallResult, IIsolatedPackageArtifact, IIsolatedTableIndex } from "./isolated-artifact";
8
8
  import type { IPackageWorkerHealth } from "./package-runtime-state";
9
9
  /** Standard table methods exposed through isolated contract-table references. */
10
10
  export declare const ISOLATED_CONTRACT_TABLE_OPERATIONS: readonly ["get", "list", "count", "findOne", "save", "insert", "update", "delete"];
@@ -165,6 +165,8 @@ export interface IIsolatedPackageTableDeclaration extends IIsolatedPackageTableR
165
165
  versionNumber?: number;
166
166
  /** Rows stay on the writing device; see `IIsolatedPackageTable.localOnly`. */
167
167
  localOnly?: boolean;
168
+ /** Indexes; see `IIsolatedPackageTable.indexes`. */
169
+ indexes?: IIsolatedTableIndex[];
168
170
  }
169
171
  /**
170
172
  * Trusted host capability for isolated-package table persistence.
@@ -43,6 +43,7 @@ function validateDeclaration(declaration) {
43
43
  if (declaration.localOnly !== undefined && typeof declaration.localOnly !== "boolean") {
44
44
  throw new Error("Isolated table localOnly must be a boolean");
45
45
  }
46
+ (0, package_table_identity_1.validateTableIndexes)(declaration.indexes, fieldNames, "Isolated table indexes", declaration.localOnly);
46
47
  const { localOnly, ...rest } = declaration;
47
48
  return {
48
49
  ...rest,
@@ -61,8 +62,9 @@ function toMetaData(rawDeclaration) {
61
62
  primaryKeyName: declaration.primaryKeyName,
62
63
  fields: declaration.fields,
63
64
  versionNumber: versionNumber(declaration),
64
- // Only when set: stored definitions hash their metadata, and older ones have no such key.
65
+ // Only when set: stored definitions hash their metadata, and older ones have no such keys.
65
66
  ...(declaration.localOnly ? { localOnly: true } : {}),
67
+ ...(declaration.indexes?.length ? { indexes: declaration.indexes } : {}),
66
68
  };
67
69
  }
68
70
  function normalizedMetaData(metaData) {
@@ -111,6 +113,7 @@ function declarationFromMetaData(metaData) {
111
113
  fields: metaData.fields,
112
114
  versionNumber: metaData.versionNumber,
113
115
  localOnly: metaData.localOnly,
116
+ ...(metaData.indexes?.length ? { indexes: metaData.indexes } : {}),
114
117
  });
115
118
  }
116
119
  function metaDataFromStoredDefinition(record, incoming) {
package/dist/peer-id.d.ts CHANGED
@@ -1,5 +1,24 @@
1
1
  /** Builds a random string of base-36 characters with unbiased distribution (tweetnacl-backed). */
2
2
  export declare function cryptoRandomString(length: number): string;
3
+ /**
4
+ * A 25-character peer id derived from `input`: `prefix`, then base-36 characters of its SHA-512
5
+ * hash. The same input always gives the same id. Each kind of derived id has its own prefix, so
6
+ * kinds can't collide.
7
+ *
8
+ * @param prefix - Fixed leading characters naming the kind of id.
9
+ * @param input - What the id is derived from.
10
+ */
11
+ export declare function deterministicPeerId(prefix: string, input: string): string;
12
+ /**
13
+ * A peer id derived from `key`, the same on every device. Rows different devices save under the
14
+ * same key are then one row, which sync merges as ordinary updates: how a synced table keeps one
15
+ * row per key, where a unique index can't (two devices could each save a row the other's index
16
+ * refuses). Start the key with what it identifies (`` `store:${name}` ``) so ids for different
17
+ * things can't collide.
18
+ *
19
+ * @param key - What the row stands for.
20
+ */
21
+ export declare function idFromKey(key: string): string;
3
22
  /**
4
23
  * Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
5
24
  * Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
package/dist/peer-id.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MAX_ID = exports.MIN_ID = void 0;
4
4
  exports.cryptoRandomString = cryptoRandomString;
5
+ exports.deterministicPeerId = deterministicPeerId;
6
+ exports.idFromKey = idFromKey;
5
7
  exports.newid = newid;
6
8
  exports.isid = isid;
7
9
  exports.idTime = idTime;
@@ -35,6 +37,45 @@ function cryptoRandomString(length) {
35
37
  }
36
38
  return s.substring(0, length);
37
39
  }
40
+ /**
41
+ * Converts a byte array to a base-36 string of the requested length, using unbiased modular reduction.
42
+ * Bytes >= 252 are skipped to avoid bias (same technique as {@link cryptoRandomString}).
43
+ */
44
+ function bytesToBase36(bytes, length) {
45
+ let s = "";
46
+ for (let i = 0; i < bytes.length && s.length < length; i++) {
47
+ const n = bytes[i];
48
+ if (n >= 252)
49
+ continue;
50
+ s += (n % 36).toString(36);
51
+ }
52
+ return s.substring(0, length);
53
+ }
54
+ /**
55
+ * A 25-character peer id derived from `input`: `prefix`, then base-36 characters of its SHA-512
56
+ * hash. The same input always gives the same id. Each kind of derived id has its own prefix, so
57
+ * kinds can't collide.
58
+ *
59
+ * @param prefix - Fixed leading characters naming the kind of id.
60
+ * @param input - What the id is derived from.
61
+ */
62
+ function deterministicPeerId(prefix, input) {
63
+ const hashSuffixLength = 25 - prefix.length;
64
+ const hash = nacl.hash(new TextEncoder().encode(input));
65
+ return prefix + bytesToBase36(hash, hashSuffixLength);
66
+ }
67
+ /**
68
+ * A peer id derived from `key`, the same on every device. Rows different devices save under the
69
+ * same key are then one row, which sync merges as ordinary updates: how a synced table keeps one
70
+ * row per key, where a unique index can't (two devices could each save a row the other's index
71
+ * refuses). Start the key with what it identifies (`` `store:${name}` ``) so ids for different
72
+ * things can't collide.
73
+ *
74
+ * @param key - What the row stands for.
75
+ */
76
+ function idFromKey(key) {
77
+ return deterministicPeerId("000key00", key);
78
+ }
38
79
  /**
39
80
  * Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
40
81
  * Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
package/dist/utils.js CHANGED
@@ -20,18 +20,18 @@ exports.retryOnErrorOrTimeout = retryOnErrorOrTimeout;
20
20
  exports.readResponseTextCapped = readResponseTextCapped;
21
21
  exports.getFullTableName = getFullTableName;
22
22
  const _ = require("lodash");
23
- const nacl = require("tweetnacl");
23
+ const peer_id_1 = require("./peer-id");
24
24
  const peer_device_1 = require("./types/peer-device");
25
25
  const stableStringify = require("fast-json-stable-stringify");
26
- var peer_id_1 = require("./peer-id");
27
- Object.defineProperty(exports, "cryptoRandomString", { enumerable: true, get: function () { return peer_id_1.cryptoRandomString; } });
28
- Object.defineProperty(exports, "idDate", { enumerable: true, get: function () { return peer_id_1.idDate; } });
29
- Object.defineProperty(exports, "idRandNums", { enumerable: true, get: function () { return peer_id_1.idRandNums; } });
30
- Object.defineProperty(exports, "idTime", { enumerable: true, get: function () { return peer_id_1.idTime; } });
31
- Object.defineProperty(exports, "isid", { enumerable: true, get: function () { return peer_id_1.isid; } });
32
- Object.defineProperty(exports, "MAX_ID", { enumerable: true, get: function () { return peer_id_1.MAX_ID; } });
33
- Object.defineProperty(exports, "MIN_ID", { enumerable: true, get: function () { return peer_id_1.MIN_ID; } });
34
- Object.defineProperty(exports, "newid", { enumerable: true, get: function () { return peer_id_1.newid; } });
26
+ var peer_id_2 = require("./peer-id");
27
+ Object.defineProperty(exports, "cryptoRandomString", { enumerable: true, get: function () { return peer_id_2.cryptoRandomString; } });
28
+ Object.defineProperty(exports, "idDate", { enumerable: true, get: function () { return peer_id_2.idDate; } });
29
+ Object.defineProperty(exports, "idRandNums", { enumerable: true, get: function () { return peer_id_2.idRandNums; } });
30
+ Object.defineProperty(exports, "idTime", { enumerable: true, get: function () { return peer_id_2.idTime; } });
31
+ Object.defineProperty(exports, "isid", { enumerable: true, get: function () { return peer_id_2.isid; } });
32
+ Object.defineProperty(exports, "MAX_ID", { enumerable: true, get: function () { return peer_id_2.MAX_ID; } });
33
+ Object.defineProperty(exports, "MIN_ID", { enumerable: true, get: function () { return peer_id_2.MIN_ID; } });
34
+ Object.defineProperty(exports, "newid", { enumerable: true, get: function () { return peer_id_2.newid; } });
35
35
  /** Fast non-cryptographic 32-bit string hash (sign-insensitive) for display keys and bucketing. */
36
36
  function simpleHash(str) {
37
37
  let hash = 0;
@@ -52,25 +52,6 @@ const DETERMINISTIC_PVAR_PREFIX = "000pvar0";
52
52
  const DETERMINISTIC_TABLE_PREFIX = "000tbl00";
53
53
  /** Well-known prefix for deterministic group-membership IDs. */
54
54
  const DETERMINISTIC_GROUP_MEMBER_PREFIX = "000gmbr0";
55
- /**
56
- * Converts a byte array to a base-36 string of the requested length, using unbiased modular reduction.
57
- * Bytes >= 252 are skipped to avoid bias (same technique as {@link cryptoRandomString}).
58
- */
59
- function bytesToBase36(bytes, length) {
60
- let s = "";
61
- for (let i = 0; i < bytes.length && s.length < length; i++) {
62
- const n = bytes[i];
63
- if (n >= 252)
64
- continue;
65
- s += (n % 36).toString(36);
66
- }
67
- return s.substring(0, length);
68
- }
69
- function deterministicPeerId(prefix, input) {
70
- const hashSuffixLength = 25 - prefix.length;
71
- const hash = nacl.hash(new TextEncoder().encode(input));
72
- return prefix + bytesToBase36(hash, hashSuffixLength);
73
- }
74
55
  /**
75
56
  * Generates a deterministic 25-character peer ID for a persistent variable.
76
57
  * The ID has the fixed prefix `000pvar0` (8 chars) followed by 17 base-36 characters
@@ -81,7 +62,7 @@ function deterministicPeerId(prefix, input) {
81
62
  * (already includes group suffixes for groupDevice/groupUser scopes)
82
63
  */
83
64
  function deterministicPvarId(name) {
84
- return deterministicPeerId(DETERMINISTIC_PVAR_PREFIX, name);
65
+ return (0, peer_id_1.deterministicPeerId)(DETERMINISTIC_PVAR_PREFIX, name);
85
66
  }
86
67
  /**
87
68
  * Returns whether a peer ID was generated by {@link deterministicPvarId}
@@ -99,7 +80,7 @@ function isDeterministicPvarId(id) {
99
80
  * @param identity Stable package-scoped table identity.
100
81
  */
101
82
  function deterministicTableId(identity) {
102
- return deterministicPeerId(DETERMINISTIC_TABLE_PREFIX, identity);
83
+ return (0, peer_id_1.deterministicPeerId)(DETERMINISTIC_TABLE_PREFIX, identity);
103
84
  }
104
85
  /**
105
86
  * Return whether a peer ID was generated by {@link deterministicTableId}.
@@ -120,7 +101,7 @@ function isDeterministicTableId(id) {
120
101
  * @param userId The member.
121
102
  */
122
103
  function deterministicGroupMemberId(groupId, userId) {
123
- return deterministicPeerId(DETERMINISTIC_GROUP_MEMBER_PREFIX, `${groupId}:${userId}`);
104
+ return (0, peer_id_1.deterministicPeerId)(DETERMINISTIC_GROUP_MEMBER_PREFIX, `${groupId}:${userId}`);
124
105
  }
125
106
  /**
126
107
  * Return whether a peer ID was generated by {@link deterministicGroupMemberId}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"