@peers-app/peers-sdk 0.27.5 → 0.28.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 (81) hide show
  1. package/dist/context/user-context.d.ts +7 -1
  2. package/dist/context/user-context.js +4 -9
  3. package/dist/contracts/contract-proxy.d.ts +5 -0
  4. package/dist/contracts/contract-proxy.js +2 -2
  5. package/dist/contracts/contract-resolvers.d.ts +2 -2
  6. package/dist/contracts/contract-resolvers.js +14 -15
  7. package/dist/contracts/system/network-contracts.d.ts +1 -1
  8. package/dist/contracts/tool-catalog-service.js +1 -1
  9. package/dist/data/assistants.d.ts +3 -3
  10. package/dist/data/authority-write-checks.d.ts +42 -0
  11. package/dist/data/authority-write-checks.js +324 -0
  12. package/dist/data/change-tracking.d.ts +4 -4
  13. package/dist/data/create-group.js +1 -0
  14. package/dist/data/devices.d.ts +27 -4
  15. package/dist/data/devices.js +31 -0
  16. package/dist/data/group-authority.d.ts +111 -0
  17. package/dist/data/group-authority.js +281 -0
  18. package/dist/data/group-members.d.ts +8 -2
  19. package/dist/data/group-members.js +41 -18
  20. package/dist/data/group-permissions.d.ts +15 -31
  21. package/dist/data/group-permissions.js +25 -95
  22. package/dist/data/group-resign.d.ts +38 -0
  23. package/dist/data/group-resign.js +71 -0
  24. package/dist/data/groups.d.ts +11 -0
  25. package/dist/data/groups.js +31 -13
  26. package/dist/data/index.d.ts +3 -0
  27. package/dist/data/index.js +3 -0
  28. package/dist/data/invites.d.ts +14 -0
  29. package/dist/data/orm/index.d.ts +1 -0
  30. package/dist/data/orm/index.js +1 -0
  31. package/dist/data/orm/sql.data-source.d.ts +2 -2
  32. package/dist/data/orm/sql.data-source.js +8 -9
  33. package/dist/data/orm/table-container.js +14 -8
  34. package/dist/data/orm/write-checks.d.ts +89 -0
  35. package/dist/data/orm/write-checks.js +115 -0
  36. package/dist/data/package-table-identity.d.ts +6 -0
  37. package/dist/data/package-table-identity.js +51 -0
  38. package/dist/data/package-version-resolver.d.ts +15 -0
  39. package/dist/data/package-version-resolver.js +58 -3
  40. package/dist/data/package-versions.d.ts +20 -14
  41. package/dist/data/package-versions.js +15 -9
  42. package/dist/data/packages.d.ts +6 -1
  43. package/dist/data/packages.js +15 -20
  44. package/dist/data/persistent-vars.js +2 -39
  45. package/dist/data/sync-recovery-queue.d.ts +4 -4
  46. package/dist/data/sync-recovery-queue.js +7 -1
  47. package/dist/data/tool-access-approvals.d.ts +3 -3
  48. package/dist/data/user-permissions.d.ts +33 -2
  49. package/dist/data/user-permissions.js +87 -10
  50. package/dist/data/users.d.ts +49 -12
  51. package/dist/data/users.js +21 -1
  52. package/dist/data/workflows.d.ts +1 -1
  53. package/dist/device/binary-peer-connection-v2.d.ts +1 -1
  54. package/dist/device/binary-peer-connection-v2.js +1 -1
  55. package/dist/device/connection.d.ts +1 -1
  56. package/dist/device/frame-cipher.d.ts +1 -1
  57. package/dist/device/frame-cipher.js +1 -1
  58. package/dist/device/get-trust-level-fn.js +3 -6
  59. package/dist/device-pairing/types.d.ts +26 -26
  60. package/dist/identity/user-row-change.d.ts +3 -1
  61. package/dist/identity/user-row-change.js +33 -18
  62. package/dist/invites/group-approval.js +23 -24
  63. package/dist/invites/invite-messages.d.ts +250 -28
  64. package/dist/invites/invite-token.d.ts +18 -0
  65. package/dist/invites/invite-token.js +2 -0
  66. package/dist/keys.d.ts +7 -0
  67. package/dist/keys.js +27 -0
  68. package/dist/package-installer/package-installer.d.ts +1 -1
  69. package/dist/package-installer/package-installer.js +8 -3
  70. package/dist/package-installer/package-propagation.d.ts +2 -0
  71. package/dist/package-installer/package-propagation.js +20 -38
  72. package/dist/package-installer/package-remote-checker.js +15 -3
  73. package/dist/package-loader/isolated-artifact.js +0 -2
  74. package/dist/package-loader/package-loader.js +6 -0
  75. package/dist/types/peer-device.d.ts +2 -0
  76. package/dist/types/workflow.d.ts +2 -2
  77. package/package.json +1 -1
  78. package/dist/data/package-permissions.d.ts +0 -14
  79. package/dist/data/package-permissions.js +0 -26
  80. package/dist/data/package-version-permissions.d.ts +0 -14
  81. package/dist/data/package-version-permissions.js +0 -30
@@ -47,6 +47,7 @@ export declare const inviteSchema: z.ZodObject<{
47
47
  type: "peers-services" | "https";
48
48
  url: string;
49
49
  }>, "many">>;
50
+ anchorsSignature: z.ZodOptional<z.ZodString>;
50
51
  }, "strict", z.ZodTypeAny, {
51
52
  publicKey: string;
52
53
  userId: string;
@@ -55,6 +56,7 @@ export declare const inviteSchema: z.ZodObject<{
55
56
  type: "peers-services" | "https";
56
57
  url: string;
57
58
  }[] | undefined;
59
+ anchorsSignature?: string | undefined;
58
60
  displayName?: string | undefined;
59
61
  }, {
60
62
  publicKey: string;
@@ -64,6 +66,7 @@ export declare const inviteSchema: z.ZodObject<{
64
66
  type: "peers-services" | "https";
65
67
  url: string;
66
68
  }[] | undefined;
69
+ anchorsSignature?: string | undefined;
67
70
  displayName?: string | undefined;
68
71
  }>>;
69
72
  /** Device the counterpart used, when they told us; lets the network layer connect early. */
@@ -95,6 +98,7 @@ export declare const inviteSchema: z.ZodObject<{
95
98
  type: "peers-services" | "https";
96
99
  url: string;
97
100
  }>, "many">>;
101
+ anchorsSignature: z.ZodOptional<z.ZodString>;
98
102
  }, "strict", z.ZodTypeAny, {
99
103
  publicKey: string;
100
104
  userId: string;
@@ -103,6 +107,7 @@ export declare const inviteSchema: z.ZodObject<{
103
107
  type: "peers-services" | "https";
104
108
  url: string;
105
109
  }[] | undefined;
110
+ anchorsSignature?: string | undefined;
106
111
  displayName?: string | undefined;
107
112
  }, {
108
113
  publicKey: string;
@@ -112,6 +117,7 @@ export declare const inviteSchema: z.ZodObject<{
112
117
  type: "peers-services" | "https";
113
118
  url: string;
114
119
  }[] | undefined;
120
+ anchorsSignature?: string | undefined;
115
121
  displayName?: string | undefined;
116
122
  }>;
117
123
  expiresAt: z.ZodString;
@@ -155,6 +161,7 @@ export declare const inviteSchema: z.ZodObject<{
155
161
  type: "peers-services" | "https";
156
162
  url: string;
157
163
  }[] | undefined;
164
+ anchorsSignature?: string | undefined;
158
165
  displayName?: string | undefined;
159
166
  };
160
167
  epoch: number;
@@ -181,6 +188,7 @@ export declare const inviteSchema: z.ZodObject<{
181
188
  type: "peers-services" | "https";
182
189
  url: string;
183
190
  }[] | undefined;
191
+ anchorsSignature?: string | undefined;
184
192
  displayName?: string | undefined;
185
193
  };
186
194
  epoch: number;
@@ -253,6 +261,7 @@ export declare const inviteSchema: z.ZodObject<{
253
261
  type: "peers-services" | "https";
254
262
  url: string;
255
263
  }[] | undefined;
264
+ anchorsSignature?: string | undefined;
256
265
  displayName?: string | undefined;
257
266
  };
258
267
  epoch: number;
@@ -274,6 +283,7 @@ export declare const inviteSchema: z.ZodObject<{
274
283
  type: "peers-services" | "https";
275
284
  url: string;
276
285
  }[] | undefined;
286
+ anchorsSignature?: string | undefined;
277
287
  displayName?: string | undefined;
278
288
  } | undefined;
279
289
  counterpartDeviceId?: string | undefined;
@@ -314,6 +324,7 @@ export declare const inviteSchema: z.ZodObject<{
314
324
  type: "peers-services" | "https";
315
325
  url: string;
316
326
  }[] | undefined;
327
+ anchorsSignature?: string | undefined;
317
328
  displayName?: string | undefined;
318
329
  };
319
330
  epoch: number;
@@ -335,6 +346,7 @@ export declare const inviteSchema: z.ZodObject<{
335
346
  type: "peers-services" | "https";
336
347
  url: string;
337
348
  }[] | undefined;
349
+ anchorsSignature?: string | undefined;
338
350
  displayName?: string | undefined;
339
351
  } | undefined;
340
352
  counterpartDeviceId?: string | undefined;
@@ -383,6 +395,7 @@ export declare function Invites(dataContext?: DataContext): import("..").Table<{
383
395
  type: "peers-services" | "https";
384
396
  url: string;
385
397
  }[] | undefined;
398
+ anchorsSignature?: string | undefined;
386
399
  displayName?: string | undefined;
387
400
  };
388
401
  epoch: number;
@@ -404,6 +417,7 @@ export declare function Invites(dataContext?: DataContext): import("..").Table<{
404
417
  type: "peers-services" | "https";
405
418
  url: string;
406
419
  }[] | undefined;
420
+ anchorsSignature?: string | undefined;
407
421
  displayName?: string | undefined;
408
422
  } | undefined;
409
423
  counterpartDeviceId?: string | undefined;
@@ -14,3 +14,4 @@ export * from "./table-container";
14
14
  export * from "./table-definitions.type";
15
15
  export * from "./table-dependencies";
16
16
  export * from "./types";
17
+ export * from "./write-checks";
@@ -30,3 +30,4 @@ __exportStar(require("./table-container"), exports);
30
30
  __exportStar(require("./table-definitions.type"), exports);
31
31
  __exportStar(require("./table-dependencies"), exports);
32
32
  __exportStar(require("./types"), exports);
33
+ __exportStar(require("./write-checks"), exports);
@@ -96,8 +96,8 @@ export declare class SQLDataSource<T extends {
96
96
  */
97
97
  bulkSave(records: T[]): Promise<T[]>;
98
98
  }
99
- /** Encode a JavaScript field for SQLite parameters or an escaped migration literal. */
100
- export declare function jsValueToSqlValue(value: any, escapeQuote?: boolean): any;
99
+ /** Encode a JavaScript field for SQLite parameters. */
100
+ export declare function jsValueToSqlValue(value: any): any;
101
101
  /** Restore a SQLite field according to its declared Peers type. */
102
102
  export declare function sqlValueToJsValue(value: any, fieldType: FieldType, isArray: boolean): any;
103
103
  /** Default value used when adding a required field to an existing table. */
@@ -136,6 +136,7 @@ class SQLDataSource {
136
136
  // sql query to copy existing data and set new required fields to default values
137
137
  const defaults = this.initRecord();
138
138
  const fieldsInsert = this.metaData.fields.map((f) => `"${f.name}"`).join(", ");
139
+ const params = [];
139
140
  const fieldsSelect = this.metaData.fields
140
141
  .map((f) => {
141
142
  const existingField = existingTableDef.includes(`"${f.name}"`);
@@ -146,18 +147,19 @@ class SQLDataSource {
146
147
  if (defaultValue === undefined) {
147
148
  defaultValue = globalDefaultValues(f);
148
149
  }
149
- defaultValue = jsValueToSqlValue(defaultValue, true);
150
+ if (!f.optional)
151
+ params.push(jsValueToSqlValue(defaultValue));
150
152
  if (existingField && !f.optional) {
151
- return `CASE WHEN "${f.name}" IS NULL THEN '${defaultValue}' ELSE "${f.name}" END as "${f.name}"`;
153
+ return `COALESCE("${f.name}", ?) as "${f.name}"`;
152
154
  }
153
155
  if (!existingField && !f.optional) {
154
- return `'${defaultValue}' as "${f.name}"`;
156
+ return `? as "${f.name}"`;
155
157
  }
156
158
  return `NULL as "${f.name}"`;
157
159
  })
158
160
  .join(", ");
159
161
  const insertSql = `INSERT INTO "${this.tableName}"(${fieldsInsert}) SELECT ${fieldsSelect} FROM "${tempTableName}"`;
160
- await this.db.exec(insertSql, []);
162
+ await this.db.exec(insertSql, params);
161
163
  // drop old table
162
164
  const dropSql = `DROP TABLE IF EXISTS "${tempTableName}"`;
163
165
  await this.db.exec(dropSql, []);
@@ -536,8 +538,8 @@ class SQLDataSource {
536
538
  }
537
539
  }
538
540
  exports.SQLDataSource = SQLDataSource;
539
- /** Encode a JavaScript field for SQLite parameters or an escaped migration literal. */
540
- function jsValueToSqlValue(value, escapeQuote = false) {
541
+ /** Encode a JavaScript field for SQLite parameters. */
542
+ function jsValueToSqlValue(value) {
541
543
  if (value === undefined)
542
544
  return null;
543
545
  if (value instanceof Date) {
@@ -549,9 +551,6 @@ function jsValueToSqlValue(value, escapeQuote = false) {
549
551
  if (typeof value === "boolean") {
550
552
  return value ? 1 : 0;
551
553
  }
552
- if (typeof value === "string" && escapeQuote) {
553
- return value.replace(/'/g, "''");
554
- }
555
554
  return value;
556
555
  }
557
556
  /** Restore a SQLite field according to its declared Peers type. */
@@ -4,6 +4,7 @@ exports.TableContainer = void 0;
4
4
  exports.checkVersionedUpdate = checkVersionedUpdate;
5
5
  const rpc_types_1 = require("../../rpc-types");
6
6
  const utils_1 = require("../../utils");
7
+ const package_table_identity_1 = require("../package-table-identity");
7
8
  const table_1 = require("./table");
8
9
  const table_definitions_system_1 = require("./table-definitions.system");
9
10
  const types_1 = require("./types");
@@ -266,7 +267,8 @@ class TableContainer {
266
267
  const record = await tableDefsTable.findOne({ name: tableName });
267
268
  if (!record || record.deleted)
268
269
  return null;
269
- this.registerFromTableDefinitionRecord(record);
270
+ if (!this.registerFromTableDefinitionRecord(record))
271
+ return null;
270
272
  // Now instantiate via getTableByName which checks fallbackDefinitions
271
273
  return this.getTableByName(tableName);
272
274
  }
@@ -334,22 +336,25 @@ class TableContainer {
334
336
  * External code should not need to call this directly.
335
337
  */
336
338
  registerFromTableDefinitionRecord(record) {
337
- const metaData = record.metaData;
338
- if (!metaData?.tableId || !metaData?.name || !metaData?.fields) {
339
- console.warn(`Invalid table definition record, skipping:`, record);
340
- return;
339
+ try {
340
+ (0, package_table_identity_1.validateTableDefinitionRecord)(record);
341
341
  }
342
+ catch (error) {
343
+ console.warn("Refusing invalid synced table definition", error);
344
+ return false;
345
+ }
346
+ const metaData = record.metaData;
342
347
  const tableName = (0, utils_1.getFullTableName)(metaData);
343
348
  // Deleted tables should not be registered as fallbacks
344
349
  if (record.deleted) {
345
350
  delete this.fallbackDefinitions[tableName];
346
351
  delete this.fallbackInstances[tableName];
347
- return;
352
+ return false;
348
353
  }
349
354
  // Don't overwrite a full (code-registered) definition -- that always takes precedence
350
355
  const fullDef = this.tableDefinitions[tableName] || table_definitions_system_1.systemTableDefinitions[tableName];
351
356
  if (fullDef) {
352
- return; // full definition exists, no need for a fallback
357
+ return true; // full definition exists, no need for a fallback
353
358
  }
354
359
  const existing = this.fallbackDefinitions[tableName];
355
360
  if (existing) {
@@ -357,13 +362,14 @@ class TableContainer {
357
362
  const incomingVersion = record.versionNumber ?? 0;
358
363
  const result = checkVersionedUpdate(existingVersion, incomingVersion, existing.metaData, metaData);
359
364
  if (result === "skip") {
360
- return;
365
+ return true;
361
366
  }
362
367
  // Remove old fallback instance so it gets re-created with the new schema
363
368
  delete this.fallbackInstances[tableName];
364
369
  }
365
370
  const schema = (0, types_1.fieldsToSchema)(metaData.fields);
366
371
  this.fallbackDefinitions[tableName] = { metaData, schema };
372
+ return true;
367
373
  }
368
374
  /**
369
375
  * This ensures all tables have been new'ed up and then returns the entire list of them.
@@ -0,0 +1,89 @@
1
+ import type { DataContext } from "../../context/data-context";
2
+ import type { Table } from "./table";
3
+ /**
4
+ * Prefix of every refusal from a {@link TableWriteCheck}. Sync recovery parks a change refused
5
+ * with it, and checks it again only from local history, when the group's authority changes.
6
+ */
7
+ export declare const WRITE_NOT_AUTHORIZED = "Write not authorized";
8
+ /** Where a checked write lands. */
9
+ export interface ITableWriteCheckContext {
10
+ /** The context the row belongs to. */
11
+ dataContext: DataContext;
12
+ /** The row's primary key, also given for a delete. */
13
+ recordId: string;
14
+ /** Checked snapshots retained by the same atomic recovery transaction. */
15
+ retainedRows?: readonly unknown[];
16
+ /** Records removed by the same atomic recovery transaction. */
17
+ deletedRecordIds?: readonly string[];
18
+ }
19
+ /**
20
+ * A rule a row must meet before it's written, run by the host on every synced change (sync
21
+ * writes rows without going through the table class). Throw an error from
22
+ * {@link writeNotAuthorized} to refuse it.
23
+ *
24
+ * @param existing - The row as stored now, if any.
25
+ * @param candidate - The row about to be stored, or undefined for a delete.
26
+ */
27
+ export type TableWriteCheck<T = any> = (existing: T | undefined, candidate: T | undefined, context: ITableWriteCheckContext) => Promise<void> | void;
28
+ /**
29
+ * Register the {@link TableWriteCheck} for a table name. One per table; a second registration
30
+ * replaces the first.
31
+ *
32
+ * @param tableName - Table the check guards.
33
+ * @param check - The rule.
34
+ */
35
+ export declare function registerTableWriteCheck<T>(tableName: string, check: TableWriteCheck<T>): void;
36
+ /**
37
+ * The {@link TableWriteCheck} registered for a table, if any.
38
+ *
39
+ * @param tableName - Table name.
40
+ */
41
+ export declare function getTableWriteCheck(tableName: string): TableWriteCheck | undefined;
42
+ /**
43
+ * Run `check` on a local save to `table`, as the host runs it on every synced row, so a local
44
+ * write and a synced write are judged by the same code. Nothing is checked in the personal
45
+ * context: only the user's own devices write it.
46
+ *
47
+ * @param table - The table being saved to.
48
+ * @param row - The row about to be stored.
49
+ * @param check - The table's rule.
50
+ */
51
+ export declare function checkLocalSave<T extends {
52
+ [key: string]: any;
53
+ }>(table: Table<T>, row: T, check: TableWriteCheck<T>): Promise<void>;
54
+ /**
55
+ * Tables whose approvals are bound to a context (`signedContextId`) and whose deletes are open,
56
+ * so a deleted row's signing time is retained ({@link deletedSignedAt}).
57
+ */
58
+ export declare const packageApprovalTables: ReadonlySet<string>;
59
+ /**
60
+ * What a table's `signAndSave` (and a re-sign) puts under the signature: `signedAt`, now or
61
+ * just after the row on record when that was signed by a clock running ahead, so the new
62
+ * signature counts as the later one (and, for a package table, after the deleted row's
63
+ * retained stamp); for a package table also the context the approval is for.
64
+ *
65
+ * @param table - The table being signed for.
66
+ * @param row - The row being signed.
67
+ * @param current - A row already read from storage; membership repair may supply another id's row.
68
+ */
69
+ export declare function signingStamp<T extends Record<string, any>>(table: Table<T>, row: T, current?: {
70
+ signedAt?: number;
71
+ }): Promise<{
72
+ signedAt: number;
73
+ signedContextId?: string;
74
+ }>;
75
+ /**
76
+ * Read a deleted authority row's last signing time, optionally retaining the stored row's
77
+ * stamp before deletion. Device-local personal storage survives deletion and log compaction.
78
+ * Only pass `existing` from storage, never an unverified incoming candidate.
79
+ */
80
+ export declare function deletedSignedAt(tableName: string, context: ITableWriteCheckContext, existing?: {
81
+ signedAt?: number;
82
+ }): Promise<number>;
83
+ /**
84
+ * An error refusing a write, carrying the {@link WRITE_NOT_AUTHORIZED} prefix.
85
+ *
86
+ * @param tableName - Table the write was for.
87
+ * @param reason - Why, for logs and the sync recovery queue.
88
+ */
89
+ export declare function writeNotAuthorized(tableName: string, reason: string): Error;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packageApprovalTables = exports.WRITE_NOT_AUTHORIZED = void 0;
4
+ exports.registerTableWriteCheck = registerTableWriteCheck;
5
+ exports.getTableWriteCheck = getTableWriteCheck;
6
+ exports.checkLocalSave = checkLocalSave;
7
+ exports.signingStamp = signingStamp;
8
+ exports.deletedSignedAt = deletedSignedAt;
9
+ exports.writeNotAuthorized = writeNotAuthorized;
10
+ /**
11
+ * Prefix of every refusal from a {@link TableWriteCheck}. Sync recovery parks a change refused
12
+ * with it, and checks it again only from local history, when the group's authority changes.
13
+ */
14
+ exports.WRITE_NOT_AUTHORIZED = "Write not authorized";
15
+ const writeChecks = new Map();
16
+ /**
17
+ * Register the {@link TableWriteCheck} for a table name. One per table; a second registration
18
+ * replaces the first.
19
+ *
20
+ * @param tableName - Table the check guards.
21
+ * @param check - The rule.
22
+ */
23
+ function registerTableWriteCheck(tableName, check) {
24
+ writeChecks.set(tableName, check);
25
+ }
26
+ /**
27
+ * The {@link TableWriteCheck} registered for a table, if any.
28
+ *
29
+ * @param tableName - Table name.
30
+ */
31
+ function getTableWriteCheck(tableName) {
32
+ return writeChecks.get(tableName);
33
+ }
34
+ /**
35
+ * Run `check` on a local save to `table`, as the host runs it on every synced row, so a local
36
+ * write and a synced write are judged by the same code. Nothing is checked in the personal
37
+ * context: only the user's own devices write it.
38
+ *
39
+ * @param table - The table being saved to.
40
+ * @param row - The row about to be stored.
41
+ * @param check - The table's rule.
42
+ */
43
+ async function checkLocalSave(table, row, check) {
44
+ if (!table.groupId)
45
+ return;
46
+ const { getUserContext } = await Promise.resolve().then(() => require("../../context/user-context-singleton"));
47
+ const recordId = row[table.primaryKeyName];
48
+ await check(await table.get(recordId), row, {
49
+ dataContext: (await getUserContext()).getDataContext(table.groupId),
50
+ recordId,
51
+ });
52
+ }
53
+ /**
54
+ * Tables whose approvals are bound to a context (`signedContextId`) and whose deletes are open,
55
+ * so a deleted row's signing time is retained ({@link deletedSignedAt}).
56
+ */
57
+ exports.packageApprovalTables = new Set(["Packages", "PackageVersions"]);
58
+ /**
59
+ * What a table's `signAndSave` (and a re-sign) puts under the signature: `signedAt`, now or
60
+ * just after the row on record when that was signed by a clock running ahead, so the new
61
+ * signature counts as the later one (and, for a package table, after the deleted row's
62
+ * retained stamp); for a package table also the context the approval is for.
63
+ *
64
+ * @param table - The table being signed for.
65
+ * @param row - The row being signed.
66
+ * @param current - A row already read from storage; membership repair may supply another id's row.
67
+ */
68
+ async function signingStamp(table, row, current) {
69
+ const { tableName, groupId, primaryKeyName } = table;
70
+ const recordId = row[primaryKeyName];
71
+ current ??= await table.get(recordId);
72
+ const packageTable = exports.packageApprovalTables.has(tableName);
73
+ let floor = current?.signedAt ?? 0;
74
+ if (packageTable && groupId && !current) {
75
+ const { getUserContext } = await Promise.resolve().then(() => require("../../context/user-context-singleton"));
76
+ const dataContext = (await getUserContext()).getDataContext(groupId);
77
+ floor = await deletedSignedAt(tableName, { dataContext, recordId });
78
+ }
79
+ return {
80
+ signedAt: Math.max(Date.now(), floor + 1),
81
+ ...(packageTable ? { signedContextId: groupId } : {}),
82
+ };
83
+ }
84
+ /**
85
+ * Read a deleted authority row's last signing time, optionally retaining the stored row's
86
+ * stamp before deletion. Device-local personal storage survives deletion and log compaction.
87
+ * Only pass `existing` from storage, never an unverified incoming candidate.
88
+ */
89
+ async function deletedSignedAt(tableName, context, existing) {
90
+ if (!context.dataContext.groupId)
91
+ return 0;
92
+ const { PersistentVars } = await Promise.resolve().then(() => require("../persistent-vars"));
93
+ const vars = PersistentVars(context.dataContext.userContext.userDataContext);
94
+ const name = `authorityOrder:${context.dataContext.groupId}:${tableName}:${context.recordId}`;
95
+ const remembered = await vars.findOne({ name, scope: "device" });
96
+ const floor = Math.max(remembered?.value?.value ?? 0, existing?.signedAt ?? 0);
97
+ if (floor > (remembered?.value?.value ?? 0)) {
98
+ await vars.save({
99
+ persistentVarId: remembered?.persistentVarId ?? "",
100
+ name,
101
+ scope: "device",
102
+ value: { value: floor },
103
+ });
104
+ }
105
+ return floor;
106
+ }
107
+ /**
108
+ * An error refusing a write, carrying the {@link WRITE_NOT_AUTHORIZED} prefix.
109
+ *
110
+ * @param tableName - Table the write was for.
111
+ * @param reason - Why, for logs and the sync recovery queue.
112
+ */
113
+ function writeNotAuthorized(tableName, reason) {
114
+ return new Error(`${exports.WRITE_NOT_AUTHORIZED}: ${tableName}: ${reason}`);
115
+ }
@@ -1,3 +1,4 @@
1
+ import type { ITableDefinitionRecord } from "./table-definitions-table";
1
2
  /**
2
3
  * Validate a guest-supplied isolated-table or field identifier before it can
3
4
  * reach ORM-generated SQL.
@@ -44,3 +45,8 @@ export declare function packageScopedTableName(packageId: string, logicalName: s
44
45
  * @throws When package or table identity is invalid.
45
46
  */
46
47
  export declare function packageScopedTableId(packageId: string, logicalName: string): string;
48
+ /**
49
+ * Validate synced schema metadata before it can name a table, column, or index in SQL.
50
+ * A schema cannot impersonate a system table or supply SQL expressions as identifiers.
51
+ */
52
+ export declare function validateTableDefinitionRecord(record: ITableDefinitionRecord): void;
@@ -4,6 +4,7 @@ exports.validateIsolatedSqlIdentifier = validateIsolatedSqlIdentifier;
4
4
  exports.validatePackageTableName = validatePackageTableName;
5
5
  exports.packageScopedTableName = packageScopedTableName;
6
6
  exports.packageScopedTableId = packageScopedTableId;
7
+ exports.validateTableDefinitionRecord = validateTableDefinitionRecord;
7
8
  const utils_1 = require("../utils");
8
9
  const ISOLATED_SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
10
  /**
@@ -69,3 +70,53 @@ function packageScopedTableName(packageId, logicalName) {
69
70
  function packageScopedTableId(packageId, logicalName) {
70
71
  return (0, utils_1.deterministicTableId)(packageScopedTableName(packageId, logicalName));
71
72
  }
73
+ /**
74
+ * Validate synced schema metadata before it can name a table, column, or index in SQL.
75
+ * A schema cannot impersonate a system table or supply SQL expressions as identifiers.
76
+ */
77
+ function validateTableDefinitionRecord(record) {
78
+ const meta = record.metaData;
79
+ if (!meta ||
80
+ !(0, utils_1.isid)(record.tableId) ||
81
+ meta.tableId !== record.tableId ||
82
+ (meta.packageId !== undefined && !(0, utils_1.isid)(meta.packageId))) {
83
+ throw new Error("Invalid table definition identity");
84
+ }
85
+ validateIsolatedSqlIdentifier(meta.name, "Table name");
86
+ validateIsolatedSqlIdentifier(meta.primaryKeyName, "Primary key");
87
+ if (record.name !== (0, utils_1.getFullTableName)(meta))
88
+ throw new Error("Table definition name mismatch");
89
+ if (!Number.isSafeInteger(record.versionNumber) ||
90
+ record.versionNumber < 0 ||
91
+ record.versionNumber !== (meta.versionNumber ?? 0)) {
92
+ throw new Error("Invalid table definition version");
93
+ }
94
+ if (meta.localOnly !== undefined && typeof meta.localOnly !== "boolean") {
95
+ throw new Error("Invalid table localOnly policy");
96
+ }
97
+ if (!Array.isArray(meta.fields) || !meta.fields.length)
98
+ throw new Error("Missing table fields");
99
+ const names = new Set();
100
+ for (const field of meta.fields) {
101
+ validateIsolatedSqlIdentifier(field.name, "Column name");
102
+ if (names.has(field.name))
103
+ throw new Error("Duplicate table column");
104
+ names.add(field.name);
105
+ }
106
+ if (!names.has(meta.primaryKeyName))
107
+ 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
+ }
122
+ }
@@ -18,6 +18,8 @@ import { type PersistentVar } from "./persistent-vars";
18
18
  export interface IDevicePackagePrefs {
19
19
  /** Which PV this device currently runs */
20
20
  activePackageVersionId?: string;
21
+ /** Tag when this device explicitly selected the version; a later retag cannot opt it into dev. */
22
+ activePackageVersionTag?: string;
21
23
  /** Newer selected PV temporarily bypassed because this host could not run it. */
22
24
  compatibilityFallbackForPackageVersionId?: string;
23
25
  /** If true, never auto-update this package */
@@ -53,6 +55,19 @@ export declare function packagePrefsVar(packageId: string, dataContext?: DataCon
53
55
  * omitted the var tracks the user's default group context.
54
56
  */
55
57
  export declare function updatePackagePrefs(packageId: string, prefs: Partial<IDevicePackagePrefs>, dataContext?: DataContext): Promise<void>;
58
+ /**
59
+ * Check a selected version before evaluating any of its runtime, route or UI bundles.
60
+ * Group releases and shared selections must still have authorized signatures even when
61
+ * already on disk. Dev builds require an explicit device selection made while tagged dev.
62
+ *
63
+ * @param localDev - A host explicitly loading a local development checkout.
64
+ */
65
+ export declare function assertPackageVersionRunnable(pkg: IPackage, pv: IPackageVersion, dataContext: DataContext, localDev?: boolean): Promise<void>;
66
+ /**
67
+ * Verify fetched bundle bytes against the hash in the authorized version. Files rows may be
68
+ * changed by group members, so their own chunk hashes alone cannot authorize executable code.
69
+ */
70
+ export declare function assertPackageBundleIntegrity(pv: IPackageVersion, kind: "package" | "routes" | "ui", contents: string | Uint8Array): void;
56
71
  /**
57
72
  * Effective device-local preferences for a package after merging the device's
58
73
  * raw prefs with the group-level `IPackage` defaults. Every field is resolved