@peers-app/peers-sdk 0.28.11 → 0.28.12

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 (35) hide show
  1. package/dist/contracts/contract-resolvers.js +1 -0
  2. package/dist/contracts/index.d.ts +1 -0
  3. package/dist/contracts/index.js +1 -0
  4. package/dist/contracts/system/group-members-contract.d.ts +25 -0
  5. package/dist/contracts/system/group-members-contract.js +50 -0
  6. package/dist/data/authority-write-checks.d.ts +58 -1
  7. package/dist/data/authority-write-checks.js +176 -1
  8. package/dist/data/group-resign.d.ts +30 -0
  9. package/dist/data/group-resign.js +74 -0
  10. package/dist/data/index.d.ts +1 -0
  11. package/dist/data/index.js +1 -0
  12. package/dist/data/orm/index.d.ts +1 -0
  13. package/dist/data/orm/index.js +1 -0
  14. package/dist/data/orm/signed-package-table.d.ts +126 -0
  15. package/dist/data/orm/signed-package-table.js +293 -0
  16. package/dist/data/orm/table-container.d.ts +22 -2
  17. package/dist/data/orm/table-container.js +49 -14
  18. package/dist/data/orm/types.d.ts +12 -0
  19. package/dist/data/package-table-identity.d.ts +48 -0
  20. package/dist/data/package-table-identity.js +151 -0
  21. package/dist/data/signed-row-id.d.ts +43 -0
  22. package/dist/data/signed-row-id.js +72 -0
  23. package/dist/define/manifest.js +3 -1
  24. package/dist/define/system-contracts.d.ts +99 -0
  25. package/dist/define/system-contracts.js +5 -1
  26. package/dist/define/table.d.ts +32 -0
  27. package/dist/define/table.js +120 -2
  28. package/dist/invites/invite-messages.d.ts +44 -44
  29. package/dist/package-loader/isolated-artifact.d.ts +15 -0
  30. package/dist/package-loader/isolated-artifact.js +12 -1
  31. package/dist/package-loader/isolated-runtime.d.ts +4 -0
  32. package/dist/package-loader/isolated-table-gateway.js +31 -3
  33. package/dist/system-ids.d.ts +2 -0
  34. package/dist/system-ids.js +3 -1
  35. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ import { type ISignedRowIdentity } from "../signed-row-id";
2
+ import type { ISaveOptions } from "./data-query";
3
+ import { Table } from "./table";
4
+ import { type ITableMetaData } from "./types";
5
+ /** A row of a signed table, as stored: the owner's signature and the signing time it covers. */
6
+ export interface ISignedRow {
7
+ /** `publicKey:signature` over the row, from `addSignatureToObject`. */
8
+ signature?: string;
9
+ /** When the owner signed the row; a replacement must not be signed earlier. */
10
+ signedAt?: number;
11
+ /** Context the signature authorizes: the group id, or the personal user's id. */
12
+ signedContextId?: string;
13
+ /** Deterministic package table id the signature authorizes. */
14
+ signedTableId?: string;
15
+ /** In a table without `signedKey`: what the row's id was derived from, with its owner. */
16
+ signedIdSeed?: string;
17
+ }
18
+ /** Signs a row with the signed-in user's key; installed by the host. */
19
+ export type SignedTableSigner = <T extends Record<string, any>>(row: T) => T & {
20
+ signature: string;
21
+ };
22
+ /**
23
+ * Project a signed row onto its stored columns, applying empty-field handling, defaults, and
24
+ * scalar TEXT encoding before signing or verification. Verification must judge these values:
25
+ * a signature over extra columns or a pre-storage value must not authorize a different row.
26
+ *
27
+ * @param metaData - The signed table's stored definition.
28
+ * @param record - The candidate row, including any existing signature.
29
+ * @param schema - Optional code schema used for local saves; synced rows use metadata only.
30
+ * @param existing - Stored row whose omitted required columns an update would preserve.
31
+ * @throws When the row fails validation, including text with unpaired surrogates anywhere in
32
+ * a field (a scalar, or a string or key inside an object or array).
33
+ */
34
+ export declare function signedPackageRowForStorage(metaData: ITableMetaData, record: Record<string, any>, schema?: import("zod").AnyZodObject, existing?: Record<string, any>): Record<string, any>;
35
+ /**
36
+ * The table class of every package table declared with `signedBy`: the host signs each row on
37
+ * save, whoever saves it (package code through the bridge, a UI, the CLI). A row names its owner
38
+ * in the `signedBy` field, and only the signed-in user can save rows they own; the signature and
39
+ * `signedAt` are stamped over the row as it will be stored, so every device can verify it.
40
+ *
41
+ * Peers sets each row's id, derived from its owner ({@link signedRowIdFor}), so no other member
42
+ * can claim it: from the owner and the `signedKey` fields when the table declares them (one row
43
+ * per owner per key), otherwise from the owner and a seed stored in `signedIdSeed`. A new row's
44
+ * seed is the id the caller passed when that's a plain peer id, so saving it again with that id
45
+ * updates the same row; otherwise a fresh one. Callers use the id `save` returns.
46
+ *
47
+ * Rows aren't deleted: a synced delete carries no author, so a package marks a row instead. The
48
+ * group's write check (`signedTableWriteCheck`) judges the row again, on this device and on
49
+ * every device it syncs to. Package code never sees the key: the host installs the signer
50
+ * with {@link SignedPackageTable.enableSignedTableSigning}.
51
+ */
52
+ export declare class SignedPackageTable<T extends {
53
+ [key: string]: any;
54
+ }> extends Table<T> {
55
+ /** The field naming each row's owner. */
56
+ get ownerField(): string;
57
+ /** What derives this table's row ids: its declared name, owner field, and key fields. */
58
+ get rowIdentity(): ISignedRowIdentity;
59
+ /**
60
+ * Sign and store a row the signed-in user owns, as a full snapshot written at its signing
61
+ * time, under the id derived from its owner (see the class). Refuses a row naming anyone else
62
+ * as owner, a change to a row's key fields, and `weakInsert`. In a renderer the host signs:
63
+ * the row is passed through to it.
64
+ *
65
+ * @param record - The row. Its id is replaced by the derived one unless it's already on record.
66
+ * @param opts - Save options; `saveAsSnapshot` and `writtenAt` are set here.
67
+ * @throws When `opts.weakInsert` is set: a weak insert's change time would be near zero while
68
+ * its `signedAt` is current, so sync (by change time) and the write check (by `signedAt`)
69
+ * would order it differently and devices could keep different versions of the row.
70
+ */
71
+ save(record: T, opts?: ISaveOptions): Promise<T>;
72
+ /**
73
+ * Sign a row on record again, unchanged, with `secretKey` instead of the installed signer, at
74
+ * a new signing time. Key rotation calls it for the rows the user signed with the key they
75
+ * rotated away from, since every device verifies a row against its owner's current key, and
76
+ * the running host still signs with the old key until it restarts. Host code only.
77
+ *
78
+ * Static so the instance-method table RPC cannot invoke it with a caller-supplied key.
79
+ *
80
+ * @param table - The host's table instance.
81
+ * @param record - The row as stored.
82
+ * @param secretKey - The owner's new secret key.
83
+ * @throws Like {@link save}: when the row names someone else as owner, or the group's write
84
+ * check refuses the new signature; also when the row on record is no longer `record`, or
85
+ * re-signing would change its content.
86
+ */
87
+ static resign<T extends Record<string, any>>(table: SignedPackageTable<T>, record: T, secretKey: string): Promise<T>;
88
+ /** Store a row {@link signRow} signed, as a full snapshot written at its signing time. */
89
+ private saveSigned;
90
+ /** Same as {@link save}: every write to a signed table is signed. */
91
+ insert(record: T): Promise<T>;
92
+ /** Same as {@link save}: every write to a signed table is signed. */
93
+ update(record: T): Promise<T>;
94
+ /** Refused: a delete can't be signed, so a package marks the row instead. */
95
+ delete(_idOrRecord: string | T): Promise<void>;
96
+ /**
97
+ * The row as it will be stored, signed by the signed-in user: its declared columns only, with
98
+ * empty fields removed and defaults applied as storage does, under the id derived from its
99
+ * owner, then stamped with the signing time and signed with `sign` over all of it.
100
+ *
101
+ * @param record - The row being saved.
102
+ * @param sign - Signs with the signed-in user's key.
103
+ */
104
+ private signRow;
105
+ /**
106
+ * The id `record` is saved under, its seed (in a table without `signedKey`), and the row on
107
+ * record under that id. A row on record keeps its id. Otherwise the id is derived: from the
108
+ * owner and key fields, or from the owner and a seed, which is the id the caller passed when
109
+ * that's a plain peer id (so saving it again updates the same row) or a fresh one.
110
+ *
111
+ * @param record - The row being saved.
112
+ * @param owner - Its owner, the signed-in user.
113
+ */
114
+ private resolveRowId;
115
+ /**
116
+ * Install the signer every signed table uses, for all SDK copies in this JavaScript realm.
117
+ * Replaces any prior signer. The host calls it with the signed-in user's key.
118
+ *
119
+ * @param fn - Signs a row, typically `addSignatureToObject(row, secretKey)`.
120
+ */
121
+ static enableSignedTableSigning(fn: SignedTableSigner): void;
122
+ }
123
+ /** The table class for `metaData`: {@link SignedPackageTable} when it has `signedBy`, else {@link Table}. */
124
+ export declare function defaultTableClass(metaData: {
125
+ signedBy?: string;
126
+ }): typeof Table;
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SignedPackageTable = void 0;
4
+ exports.signedPackageRowForStorage = signedPackageRowForStorage;
5
+ exports.defaultTableClass = defaultTableClass;
6
+ const keys_1 = require("../../keys");
7
+ const rpc_types_1 = require("../../rpc-types");
8
+ const serial_json_1 = require("../../serial-json");
9
+ const field_type_1 = require("../../types/field-type");
10
+ const utils_1 = require("../../utils");
11
+ const package_table_identity_1 = require("../package-table-identity");
12
+ const signed_row_id_1 = require("../signed-row-id");
13
+ const sql_data_source_1 = require("./sql.data-source");
14
+ const table_1 = require("./table");
15
+ const types_1 = require("./types");
16
+ const write_checks_1 = require("./write-checks");
17
+ // Symbol.for keeps the signer shared by separately installed SDK copies, as the system tables do.
18
+ const signingKey = Symbol.for("@peers-app/peers-sdk/signed-table-signing");
19
+ const signingGlobals = globalThis;
20
+ // In Unicode mode this class matches only unpaired surrogates, not valid astral characters.
21
+ const UNPAIRED_SURROGATE = /[\uD800-\uDFFF]/u;
22
+ /**
23
+ * Whether a `toJSON` tree (the form sync encodes) holds an unpaired UTF-16 surrogate in any
24
+ * string or object key, at any depth.
25
+ *
26
+ * @param tree - A value as `toJSON` returns it: no cycles, Dates or Buffers left.
27
+ */
28
+ function hasUnpairedSurrogate(tree) {
29
+ if (typeof tree === "string")
30
+ return UNPAIRED_SURROGATE.test(tree);
31
+ if (Array.isArray(tree))
32
+ return tree.some(hasUnpairedSurrogate);
33
+ if (tree !== null && typeof tree === "object") {
34
+ return Object.entries(tree).some(([key, value]) => UNPAIRED_SURROGATE.test(key) || hasUnpairedSurrogate(value));
35
+ }
36
+ return false;
37
+ }
38
+ /**
39
+ * Project a signed row onto its stored columns, applying empty-field handling, defaults, and
40
+ * scalar TEXT encoding before signing or verification. Verification must judge these values:
41
+ * a signature over extra columns or a pre-storage value must not authorize a different row.
42
+ *
43
+ * @param metaData - The signed table's stored definition.
44
+ * @param record - The candidate row, including any existing signature.
45
+ * @param schema - Optional code schema used for local saves; synced rows use metadata only.
46
+ * @param existing - Stored row whose omitted required columns an update would preserve.
47
+ * @throws When the row fails validation, including text with unpaired surrogates anywhere in
48
+ * a field (a scalar, or a string or key inside an object or array).
49
+ */
50
+ function signedPackageRowForStorage(metaData, record, schema = (0, types_1.fieldsToSchema)(metaData.fields), existing) {
51
+ const columns = Object.fromEntries(metaData.fields
52
+ .filter((field) => record[field.name] !== undefined)
53
+ .map((field) => [field.name, record[field.name]]));
54
+ for (const field of metaData.fields) {
55
+ if (!field.optional &&
56
+ record[field.name] === undefined &&
57
+ existing?.[field.name] !== undefined) {
58
+ columns[field.name] = existing[field.name];
59
+ }
60
+ }
61
+ const row = (0, types_1.withoutEmptyFields)(metaData.fields, columns);
62
+ const parsed = schema.parse(row);
63
+ for (const field of metaData.fields) {
64
+ if (row[field.name] === undefined && parsed[field.name] !== undefined) {
65
+ row[field.name] = parsed[field.name];
66
+ }
67
+ if (row[field.name] !== undefined) {
68
+ // SQLite stores scalar text as UTF-8, and sync sends every string (nested ones and object
69
+ // keys too) as UTF-8: an unpaired UTF-16 surrogate would arrive as different text,
70
+ // invalidating the signature on other devices and possibly the owner-derived id.
71
+ if (hasUnpairedSurrogate((0, serial_json_1.toJSON)(row[field.name]))) {
72
+ throw new Error(`Field "${field.name}" must contain well-formed Unicode`);
73
+ }
74
+ const value = (0, sql_data_source_1.jsValueToSqlValue)(row[field.name]);
75
+ // Untyped/blob scalar columns use TEXT storage and read back without a decoder. Other
76
+ // columns also need their serialization round trip (including nested sentinel values).
77
+ row[field.name] =
78
+ !field.isArray && (field.type === field_type_1.FieldType.any || field.type === field_type_1.FieldType.blob)
79
+ ? value == null
80
+ ? undefined
81
+ : String(value)
82
+ : (0, sql_data_source_1.sqlValueToJsValue)(value, field.type, field.isArray ?? false);
83
+ }
84
+ }
85
+ return row;
86
+ }
87
+ /**
88
+ * The table class of every package table declared with `signedBy`: the host signs each row on
89
+ * save, whoever saves it (package code through the bridge, a UI, the CLI). A row names its owner
90
+ * in the `signedBy` field, and only the signed-in user can save rows they own; the signature and
91
+ * `signedAt` are stamped over the row as it will be stored, so every device can verify it.
92
+ *
93
+ * Peers sets each row's id, derived from its owner ({@link signedRowIdFor}), so no other member
94
+ * can claim it: from the owner and the `signedKey` fields when the table declares them (one row
95
+ * per owner per key), otherwise from the owner and a seed stored in `signedIdSeed`. A new row's
96
+ * seed is the id the caller passed when that's a plain peer id, so saving it again with that id
97
+ * updates the same row; otherwise a fresh one. Callers use the id `save` returns.
98
+ *
99
+ * Rows aren't deleted: a synced delete carries no author, so a package marks a row instead. The
100
+ * group's write check (`signedTableWriteCheck`) judges the row again, on this device and on
101
+ * every device it syncs to. Package code never sees the key: the host installs the signer
102
+ * with {@link SignedPackageTable.enableSignedTableSigning}.
103
+ */
104
+ class SignedPackageTable extends table_1.Table {
105
+ /** The field naming each row's owner. */
106
+ get ownerField() {
107
+ const field = this.metaData.signedBy;
108
+ if (!field)
109
+ throw new Error(`Table ${this.tableName} isn't a signed table`);
110
+ return field;
111
+ }
112
+ /** What derives this table's row ids: its declared name, owner field, and key fields. */
113
+ get rowIdentity() {
114
+ return {
115
+ name: this.metaData.name,
116
+ signedBy: this.ownerField,
117
+ signedKey: this.metaData.signedKey,
118
+ };
119
+ }
120
+ /**
121
+ * Sign and store a row the signed-in user owns, as a full snapshot written at its signing
122
+ * time, under the id derived from its owner (see the class). Refuses a row naming anyone else
123
+ * as owner, a change to a row's key fields, and `weakInsert`. In a renderer the host signs:
124
+ * the row is passed through to it.
125
+ *
126
+ * @param record - The row. Its id is replaced by the derived one unless it's already on record.
127
+ * @param opts - Save options; `saveAsSnapshot` and `writtenAt` are set here.
128
+ * @throws When `opts.weakInsert` is set: a weak insert's change time would be near zero while
129
+ * its `signedAt` is current, so sync (by change time) and the write check (by `signedAt`)
130
+ * would order it differently and devices could keep different versions of the row.
131
+ */
132
+ async save(record, opts) {
133
+ if (opts?.weakInsert) {
134
+ throw new Error(`Signed table ${this.tableName} can't take weakInsert: a row counts from when its owner signs it. Save a row only when there's something to save, and read a missing row as its defaults`);
135
+ }
136
+ if (rpc_types_1.isClient && !(0, rpc_types_1.isSingleProcessClient)())
137
+ return super.save(record, opts);
138
+ return this.saveSigned(await this.signRow(record, installedSigner()), opts);
139
+ }
140
+ /**
141
+ * Sign a row on record again, unchanged, with `secretKey` instead of the installed signer, at
142
+ * a new signing time. Key rotation calls it for the rows the user signed with the key they
143
+ * rotated away from, since every device verifies a row against its owner's current key, and
144
+ * the running host still signs with the old key until it restarts. Host code only.
145
+ *
146
+ * Static so the instance-method table RPC cannot invoke it with a caller-supplied key.
147
+ *
148
+ * @param table - The host's table instance.
149
+ * @param record - The row as stored.
150
+ * @param secretKey - The owner's new secret key.
151
+ * @throws Like {@link save}: when the row names someone else as owner, or the group's write
152
+ * check refuses the new signature; also when the row on record is no longer `record`, or
153
+ * re-signing would change its content.
154
+ */
155
+ static async resign(table, record, secretKey) {
156
+ if (rpc_types_1.isClient && !(0, rpc_types_1.isSingleProcessClient)())
157
+ throw new Error("Re-signing is host-only");
158
+ // A newer version that arrived since `record` was read isn't overwritten by the older one.
159
+ const current = await table.get(record[table.primaryKeyName]);
160
+ if (!current || current.signature !== record.signature) {
161
+ throw new Error(`Row ${record[table.primaryKeyName]} of ${table.tableName} changed before it could be re-signed`);
162
+ }
163
+ const signed = await table.signRow(record, (row) => (0, keys_1.addSignatureToObject)(row, secretKey));
164
+ const content = ({ signature: _signature, signedAt: _signedAt, ...row }) => (0, keys_1.canonicalJSONString)(row);
165
+ if (content(signed) !== content(record)) {
166
+ throw new Error(`Re-signing ${table.tableName} would change the row's content`);
167
+ }
168
+ return table.saveSigned(signed);
169
+ }
170
+ /** Store a row {@link signRow} signed, as a full snapshot written at its signing time. */
171
+ async saveSigned(signed, opts) {
172
+ // Signed tables never have an authorized tombstone. Refused deletes remain in history,
173
+ // so their tombstones must not prevent the owner creating a row on this device later.
174
+ return super.save(signed, {
175
+ ...opts,
176
+ restoreIfDeleted: true,
177
+ saveAsSnapshot: true,
178
+ writtenAt: signed.signedAt,
179
+ });
180
+ }
181
+ /** Same as {@link save}: every write to a signed table is signed. */
182
+ async insert(record) {
183
+ return this.save(record);
184
+ }
185
+ /** Same as {@link save}: every write to a signed table is signed. */
186
+ async update(record) {
187
+ return this.save(record);
188
+ }
189
+ /** Refused: a delete can't be signed, so a package marks the row instead. */
190
+ async delete(_idOrRecord) {
191
+ throw new Error(`Rows in signed table ${this.tableName} aren't deleted; the owner marks them instead (a field such as deleted: true)`);
192
+ }
193
+ /**
194
+ * The row as it will be stored, signed by the signed-in user: its declared columns only, with
195
+ * empty fields removed and defaults applied as storage does, under the id derived from its
196
+ * owner, then stamped with the signing time and signed with `sign` over all of it.
197
+ *
198
+ * @param record - The row being saved.
199
+ * @param sign - Signs with the signed-in user's key.
200
+ */
201
+ async signRow(record, sign) {
202
+ const ownerField = this.ownerField;
203
+ const { getUserContext } = await Promise.resolve().then(() => require("../../context/user-context-singleton"));
204
+ const userId = (await getUserContext()).userId;
205
+ const owner = record[ownerField];
206
+ if (owner !== userId) {
207
+ throw (0, write_checks_1.writeNotAuthorized)(this.tableName, `only the row's owner writes it, and "${ownerField}" names ${owner ?? "nobody"}, not ${userId}`);
208
+ }
209
+ const { recordId, seed, existing } = await this.resolveRowId(record, owner);
210
+ if (existing && existing[ownerField] !== owner) {
211
+ throw (0, write_checks_1.writeNotAuthorized)(this.tableName, "the row on record belongs to another member");
212
+ }
213
+ const columns = {};
214
+ for (const field of this.metaData.fields) {
215
+ if (!package_table_identity_1.SIGNED_TABLE_FIELD_NAMES.has(field.name) && record[field.name] !== undefined) {
216
+ columns[field.name] = record[field.name];
217
+ }
218
+ }
219
+ columns[this.primaryKeyName] = recordId;
220
+ if (seed !== undefined)
221
+ columns.signedIdSeed = seed;
222
+ const row = signedPackageRowForStorage(this.metaData, columns, this.schema, existing);
223
+ // The key fields as stored name the row: a changed key is a different row.
224
+ if ((0, signed_row_id_1.signedRowIdFor)(this.rowIdentity, row, row.signedIdSeed) !== recordId) {
225
+ throw new Error(`Row ${recordId} of ${this.tableName} has other key fields (${(this.metaData.signedKey ?? []).join(", ")}); a row's key can't change, so save a new row instead`);
226
+ }
227
+ const stamped = {
228
+ ...row,
229
+ ...(await (0, write_checks_1.signingStamp)(this, row, existing)),
230
+ signedContextId: this.groupId ?? userId,
231
+ signedTableId: (0, package_table_identity_1.packageScopedTableId)(this.metaData.packageId, this.metaData.name),
232
+ };
233
+ return sign(stamped);
234
+ }
235
+ /**
236
+ * The id `record` is saved under, its seed (in a table without `signedKey`), and the row on
237
+ * record under that id. A row on record keeps its id. Otherwise the id is derived: from the
238
+ * owner and key fields, or from the owner and a seed, which is the id the caller passed when
239
+ * that's a plain peer id (so saving it again updates the same row) or a fresh one.
240
+ *
241
+ * @param record - The row being saved.
242
+ * @param owner - Its owner, the signed-in user.
243
+ */
244
+ async resolveRowId(record, owner) {
245
+ const identity = this.rowIdentity;
246
+ const supplied = record[this.primaryKeyName];
247
+ const onRecord = typeof supplied === "string" && supplied !== "" ? await this.get(supplied) : undefined;
248
+ if (onRecord) {
249
+ return {
250
+ recordId: supplied,
251
+ seed: identity.signedKey ? undefined : onRecord.signedIdSeed,
252
+ existing: onRecord,
253
+ };
254
+ }
255
+ if (identity.signedKey) {
256
+ const recordId = (0, signed_row_id_1.signedRowIdFor)(identity, record);
257
+ if ((0, signed_row_id_1.isSignedRowId)(supplied) && supplied !== recordId) {
258
+ throw new Error(`Row ${supplied} of ${this.tableName} isn't the row its owner and key fields (${identity.signedKey.join(", ")}) name; a row's key can't change, so save a new row instead`);
259
+ }
260
+ return { recordId, existing: await this.get(recordId) };
261
+ }
262
+ if ((0, signed_row_id_1.isSignedRowId)(supplied)) {
263
+ // Derived elsewhere (another device of the owner, not synced here yet, or another signed
264
+ // table's row): its seed is unknown, so saving it would make a second row.
265
+ throw new Error(`Row ${supplied} isn't a row of ${this.tableName} on this device (it may not have synced here yet, or be another signed table's id); a new row's id comes from Peers, so leave it out or pass a new peer id`);
266
+ }
267
+ const seed = typeof supplied === "string" && (0, utils_1.isid)(supplied) ? supplied : (0, utils_1.newid)();
268
+ const recordId = (0, signed_row_id_1.signedRowId)(identity.name, owner, seed);
269
+ return { recordId, seed, existing: await this.get(recordId) };
270
+ }
271
+ /**
272
+ * Install the signer every signed table uses, for all SDK copies in this JavaScript realm.
273
+ * Replaces any prior signer. The host calls it with the signed-in user's key.
274
+ *
275
+ * @param fn - Signs a row, typically `addSignatureToObject(row, secretKey)`.
276
+ */
277
+ static enableSignedTableSigning(fn) {
278
+ signingGlobals[signingKey] = fn;
279
+ }
280
+ }
281
+ exports.SignedPackageTable = SignedPackageTable;
282
+ /** The signer the host installed with {@link SignedPackageTable.enableSignedTableSigning}. */
283
+ function installedSigner() {
284
+ const sign = signingGlobals[signingKey];
285
+ if (!sign) {
286
+ throw new Error("Signed table signing is not enabled. Call SignedPackageTable.enableSignedTableSigning(fn) to enable it.");
287
+ }
288
+ return sign;
289
+ }
290
+ /** The table class for `metaData`: {@link SignedPackageTable} when it has `signedBy`, else {@link Table}. */
291
+ function defaultTableClass(metaData) {
292
+ return metaData.signedBy ? SignedPackageTable : table_1.Table;
293
+ }
@@ -1,6 +1,6 @@
1
1
  import type { z } from "zod";
2
2
  import type { ITableDefinitionRecord } from "../table-definitions-table";
3
- import { Table } from "./table";
3
+ import type { Table } from "./table";
4
4
  import type { ITableDefinition, TableConstructor, TableFactory } from "./table-definitions.type";
5
5
  import { type ITableMetaData } from "./types";
6
6
  /**
@@ -19,7 +19,20 @@ export declare class TableContainer {
19
19
  private readonly fallbackDefinitions;
20
20
  private fallbackInstances;
21
21
  private fallbackSubscribed;
22
+ private readonly definitionListeners;
22
23
  constructor(tableFactory: TableFactory, groupId?: string | undefined);
24
+ /**
25
+ * Call `listener` whenever this container takes a table definition it didn't have: a code
26
+ * definition registered (a package loading, or replacing its tables on an update), or a synced
27
+ * definition for a table no package defines, first or newer. Sync uses it to recheck rows an
28
+ * older definition refused. Runs synchronously, after the new definition is in place, so a
29
+ * table looked up from the listener (or after it) uses it.
30
+ *
31
+ * @param listener - Receives the new definition's metadata.
32
+ * @returns A function that removes the listener.
33
+ */
34
+ onDefinitionChanged(listener: (metaData: ITableMetaData) => void): () => void;
35
+ private definitionChanged;
23
36
  /** Register a code-owned table, preserving an existing binding unless overwrite is set. */
24
37
  registerTableDefinition<T extends {
25
38
  [key: string]: any;
@@ -29,11 +42,13 @@ export declare class TableContainer {
29
42
  /**
30
43
  * Validate and stage the entire batch before changing any live table binding.
31
44
  * An invalid later definition leaves earlier definitions and instances intact.
32
- * No table factories or schema persistence run during registration.
45
+ * Staging runs no table factories or schema persistence. Listeners are notified only after
46
+ * the entire batch is published, and may then resolve tables against those bindings.
33
47
  */
34
48
  registerTableDefinitions(definitions: readonly ITableDefinition<any>[], opts?: {
35
49
  overwrite?: boolean;
36
50
  }): void;
51
+ /** Stage one definition on a candidate container; true when it took the definition. */
37
52
  private stageTableDefinition;
38
53
  /**
39
54
  * This shouldn't be used unless you have a good reason to bypass the normal table creation process.
@@ -60,6 +75,11 @@ export declare class TableContainer {
60
75
  getTableByName<T extends {
61
76
  [key: string]: any;
62
77
  }>(tableName: string): Table<T>;
78
+ /**
79
+ * A table over a synced definition: no package code, so the class is the generic one, or the
80
+ * signed one when the definition is signed, so a local write still can't skip signing.
81
+ */
82
+ private createFallbackInstance;
63
83
  /**
64
84
  * Lazily look up a fallback table by name from the TableDefinitions system table.
65
85
  * On first call, subscribes to TableDefinitions.dataChanged so future synced
@@ -5,7 +5,7 @@ exports.checkVersionedUpdate = checkVersionedUpdate;
5
5
  const rpc_types_1 = require("../../rpc-types");
6
6
  const utils_1 = require("../../utils");
7
7
  const package_table_identity_1 = require("../package-table-identity");
8
- const table_1 = require("./table");
8
+ const signed_package_table_1 = require("./signed-package-table");
9
9
  const table_definitions_system_1 = require("./table-definitions.system");
10
10
  const types_1 = require("./types");
11
11
  /**
@@ -63,10 +63,37 @@ class TableContainer {
63
63
  fallbackDefinitions = {};
64
64
  fallbackInstances = {};
65
65
  fallbackSubscribed = false;
66
+ definitionListeners = new Set();
66
67
  constructor(tableFactory, groupId) {
67
68
  this.tableFactory = tableFactory;
68
69
  this.groupId = groupId;
69
70
  }
71
+ /**
72
+ * Call `listener` whenever this container takes a table definition it didn't have: a code
73
+ * definition registered (a package loading, or replacing its tables on an update), or a synced
74
+ * definition for a table no package defines, first or newer. Sync uses it to recheck rows an
75
+ * older definition refused. Runs synchronously, after the new definition is in place, so a
76
+ * table looked up from the listener (or after it) uses it.
77
+ *
78
+ * @param listener - Receives the new definition's metadata.
79
+ * @returns A function that removes the listener.
80
+ */
81
+ onDefinitionChanged(listener) {
82
+ this.definitionListeners.add(listener);
83
+ return () => {
84
+ this.definitionListeners.delete(listener);
85
+ };
86
+ }
87
+ definitionChanged(metaData) {
88
+ for (const listener of this.definitionListeners) {
89
+ try {
90
+ listener(metaData);
91
+ }
92
+ catch (error) {
93
+ console.warn(`Table definition listener failed for ${(0, utils_1.getFullTableName)(metaData)}`, error);
94
+ }
95
+ }
96
+ }
70
97
  /** Register a code-owned table, preserving an existing binding unless overwrite is set. */
71
98
  registerTableDefinition(tableDefinition, opts) {
72
99
  this.registerTableDefinitions([tableDefinition], opts);
@@ -74,17 +101,20 @@ class TableContainer {
74
101
  /**
75
102
  * Validate and stage the entire batch before changing any live table binding.
76
103
  * An invalid later definition leaves earlier definitions and instances intact.
77
- * No table factories or schema persistence run during registration.
104
+ * Staging runs no table factories or schema persistence. Listeners are notified only after
105
+ * the entire batch is published, and may then resolve tables against those bindings.
78
106
  */
79
107
  registerTableDefinitions(definitions, opts) {
80
108
  const candidate = new TableContainer(this.tableFactory, this.groupId);
81
109
  Object.assign(candidate.tableDefinitions, this.tableDefinitions);
82
110
  Object.assign(candidate.tableInstances, this.tableInstances);
83
- for (const definition of definitions)
84
- candidate.stageTableDefinition(definition, opts);
111
+ const staged = definitions.filter((definition) => candidate.stageTableDefinition(definition, opts));
85
112
  Object.assign(this.tableDefinitions, candidate.tableDefinitions);
86
113
  this.tableInstances = candidate.tableInstances;
114
+ for (const definition of staged)
115
+ this.definitionChanged(definition.metaData);
87
116
  }
117
+ /** Stage one definition on a candidate container; true when it took the definition. */
88
118
  stageTableDefinition(tableDefinition, opts) {
89
119
  const tableId = tableDefinition.metaData.tableId;
90
120
  if (!(0, utils_1.isid)(tableId)) {
@@ -94,18 +124,19 @@ class TableContainer {
94
124
  const existing = this.tableInstances[tableName] || this.tableDefinitions[tableName];
95
125
  if (existing && !opts?.overwrite) {
96
126
  console.warn(`Table ${tableName} is already registered.`);
97
- return;
127
+ return false;
98
128
  }
99
129
  if (existing && opts?.overwrite) {
100
130
  const existingDef = this.tableDefinitions[tableName];
101
131
  if (existingDef &&
102
132
  (0, utils_1.simpleObjectHash)(existingDef.metaData) === (0, utils_1.simpleObjectHash)(tableDefinition.metaData)) {
103
- return; // definition hasn't changed, no need to overwrite
133
+ return false; // definition hasn't changed, no need to overwrite
104
134
  }
105
135
  console.warn(`Overwriting table definition for ${tableName}.`);
106
136
  delete this.tableInstances[tableName];
107
137
  }
108
138
  this.tableDefinitions[tableName] = tableDefinition;
139
+ return true;
109
140
  }
110
141
  /**
111
142
  * This shouldn't be used unless you have a good reason to bypass the normal table creation process.
@@ -136,7 +167,7 @@ class TableContainer {
136
167
  schema ??= (0, types_1.fieldsToSchema)(metaData.fields);
137
168
  // Check if there's a registered custom constructor
138
169
  const registeredDef = this.tableDefinitions[tableName] || table_definitions_system_1.systemTableDefinitions[tableName];
139
- const TableClass = tableConstructor || registeredDef?.tableConstructor || (table_1.Table);
170
+ const TableClass = tableConstructor || registeredDef?.tableConstructor || (0, signed_package_table_1.defaultTableClass)(metaData);
140
171
  const table = this.tableFactory(metaData, schema, TableClass, this.groupId);
141
172
  this.tableInstances[tableName] = table;
142
173
  this.saveTableDefinition(metaData);
@@ -224,13 +255,19 @@ class TableContainer {
224
255
  // 4. Check for a fallback definition and create a fallback instance
225
256
  const fallbackDef = this.fallbackDefinitions[tableName];
226
257
  if (fallbackDef) {
227
- const { metaData, schema } = fallbackDef;
228
- const table = this.tableFactory(metaData, schema ?? (0, types_1.fieldsToSchema)(metaData.fields), table_1.Table, this.groupId);
258
+ const table = this.createFallbackInstance(fallbackDef);
229
259
  this.fallbackInstances[tableName] = table;
230
260
  return table;
231
261
  }
232
262
  throw new Error(`There is no instantiated table or registered definition for ${tableName}`);
233
263
  }
264
+ /**
265
+ * A table over a synced definition: no package code, so the class is the generic one, or the
266
+ * signed one when the definition is signed, so a local write still can't skip signing.
267
+ */
268
+ createFallbackInstance({ metaData, schema }) {
269
+ return this.tableFactory(metaData, schema ?? (0, types_1.fieldsToSchema)(metaData.fields), (0, signed_package_table_1.defaultTableClass)(metaData), this.groupId);
270
+ }
234
271
  /**
235
272
  * Lazily look up a fallback table by name from the TableDefinitions system table.
236
273
  * On first call, subscribes to TableDefinitions.dataChanged so future synced
@@ -242,8 +279,7 @@ class TableContainer {
242
279
  if (this.fallbackInstances[tableName])
243
280
  return this.fallbackInstances[tableName];
244
281
  if (this.fallbackDefinitions[tableName]) {
245
- const { metaData, schema } = this.fallbackDefinitions[tableName];
246
- const table = this.tableFactory(metaData, schema ?? (0, types_1.fieldsToSchema)(metaData.fields), table_1.Table, this.groupId);
282
+ const table = this.createFallbackInstance(this.fallbackDefinitions[tableName]);
247
283
  this.fallbackInstances[tableName] = table;
248
284
  return table;
249
285
  }
@@ -372,6 +408,7 @@ class TableContainer {
372
408
  }
373
409
  const schema = (0, types_1.fieldsToSchema)(metaData.fields);
374
410
  this.fallbackDefinitions[tableName] = { metaData, schema };
411
+ this.definitionChanged(metaData);
375
412
  return true;
376
413
  }
377
414
  /**
@@ -394,9 +431,7 @@ class TableContainer {
394
431
  // 2. Instantiate fallback definitions that don't already have a full instance
395
432
  for (const [tableName, fallbackDef] of Object.entries(this.fallbackDefinitions)) {
396
433
  if (!this.tableInstances[tableName] && !this.fallbackInstances[tableName]) {
397
- const { metaData, schema } = fallbackDef;
398
- const table = this.tableFactory(metaData, schema ?? (0, types_1.fieldsToSchema)(metaData.fields), table_1.Table, this.groupId);
399
- this.fallbackInstances[tableName] = table;
434
+ this.fallbackInstances[tableName] = this.createFallbackInstance(fallbackDef);
400
435
  }
401
436
  }
402
437
  // 3. Merge: full instances take precedence, then add fallbacks for tables not in full
@@ -24,6 +24,18 @@ export interface ITableMetaData {
24
24
  iconClassName?: string;
25
25
  localOnly?: boolean;
26
26
  versionNumber?: number;
27
+ /**
28
+ * The `id` field naming the user who owns each row. The host signs a row with the signed-in
29
+ * user's key when it's saved, and a group refuses a row that isn't validly signed by its
30
+ * owner. Set from `defineTable({ signedBy })`; a table has it from its first version or never.
31
+ */
32
+ signedBy?: string;
33
+ /**
34
+ * In a signed table, the fields that with the owner name a row: each owner has one row per
35
+ * key, and its id is derived from the owner and the key. Without it, a row's id is derived
36
+ * from its owner and a host-chosen seed. Set from `defineTable({ signedKey })`; it can't change.
37
+ */
38
+ signedKey?: string[];
27
39
  }
28
40
  /**
29
41
  * Convert field metadata to the runtime Zod schema the Runtime validates table rows with.