@peers-app/peers-sdk 0.28.11 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/contract-resolvers.js +1 -0
- package/dist/contracts/index.d.ts +2 -1
- package/dist/contracts/index.js +5 -2
- package/dist/contracts/remote-contract-transport.d.ts +27 -0
- package/dist/contracts/remote-contract-transport.js +27 -0
- package/dist/contracts/system/group-members-contract.d.ts +25 -0
- package/dist/contracts/system/group-members-contract.js +50 -0
- package/dist/contracts/system/network-contracts.js +1 -1
- package/dist/data/authority-write-checks.d.ts +58 -1
- package/dist/data/authority-write-checks.js +176 -1
- package/dist/data/group-resign.d.ts +30 -0
- package/dist/data/group-resign.js +74 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/orm/index.d.ts +1 -0
- package/dist/data/orm/index.js +1 -0
- package/dist/data/orm/signed-package-table.d.ts +126 -0
- package/dist/data/orm/signed-package-table.js +293 -0
- package/dist/data/orm/table-container.d.ts +22 -2
- package/dist/data/orm/table-container.js +49 -14
- package/dist/data/orm/types.d.ts +12 -0
- package/dist/data/package-table-identity.d.ts +48 -0
- package/dist/data/package-table-identity.js +151 -0
- package/dist/data/signed-row-id.d.ts +43 -0
- package/dist/data/signed-row-id.js +72 -0
- package/dist/define/context.d.ts +9 -0
- package/dist/define/manifest.js +3 -1
- package/dist/define/system-contracts.d.ts +99 -0
- package/dist/define/system-contracts.js +5 -1
- package/dist/define/table.d.ts +32 -0
- package/dist/define/table.js +120 -2
- package/dist/device/binary-peer-connection-v2.d.ts +2 -2
- package/dist/device/binary-peer-connection-v2.js +51 -13
- package/dist/device/tx-encoding.d.ts +22 -1
- package/dist/device/tx-encoding.js +63 -26
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/invites/invite-messages.d.ts +44 -44
- package/dist/keys.d.ts +7 -2
- package/dist/keys.js +61 -12
- package/dist/native-codecs.d.ts +34 -0
- package/dist/native-codecs.js +16 -0
- package/dist/package-loader/isolated-artifact.d.ts +17 -0
- package/dist/package-loader/isolated-artifact.js +15 -2
- package/dist/package-loader/isolated-runtime.d.ts +10 -0
- package/dist/package-loader/isolated-table-gateway.js +31 -3
- package/dist/package-loader/package-loader.js +1 -0
- package/dist/serial-json.d.ts +7 -0
- package/dist/serial-json.js +16 -2
- package/dist/system-ids.d.ts +2 -0
- package/dist/system-ids.js +3 -1
- package/dist/types/peer-device.d.ts +6 -0
- package/dist/types/peer-device.js +7 -1
- package/dist/utils.js +2 -2
- package/package.json +1 -1
package/dist/data/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export * from "./packages.utils";
|
|
|
27
27
|
export * from "./peer-types";
|
|
28
28
|
export * from "./peers-services-registration.pvar";
|
|
29
29
|
export * from "./persistent-vars";
|
|
30
|
+
export * from "./signed-row-id";
|
|
30
31
|
export * from "./sync-recovery-queue";
|
|
31
32
|
export * from "./table-definitions-table";
|
|
32
33
|
export * from "./tool-access";
|
package/dist/data/index.js
CHANGED
|
@@ -43,6 +43,7 @@ __exportStar(require("./packages.utils"), exports);
|
|
|
43
43
|
__exportStar(require("./peer-types"), exports);
|
|
44
44
|
__exportStar(require("./peers-services-registration.pvar"), exports);
|
|
45
45
|
__exportStar(require("./persistent-vars"), exports);
|
|
46
|
+
__exportStar(require("./signed-row-id"), exports);
|
|
46
47
|
__exportStar(require("./sync-recovery-queue"), exports);
|
|
47
48
|
__exportStar(require("./table-definitions-table"), exports);
|
|
48
49
|
__exportStar(require("./tool-access"), exports);
|
package/dist/data/orm/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./doc";
|
|
|
6
6
|
export * from "./event-registry";
|
|
7
7
|
export * from "./factory";
|
|
8
8
|
export * from "./multi-cursors";
|
|
9
|
+
export * from "./signed-package-table";
|
|
9
10
|
export * from "./sql.data-source";
|
|
10
11
|
export * from "./sql-db";
|
|
11
12
|
export * from "./subscribable.data-source";
|
package/dist/data/orm/index.js
CHANGED
|
@@ -22,6 +22,7 @@ __exportStar(require("./doc"), exports);
|
|
|
22
22
|
__exportStar(require("./event-registry"), exports);
|
|
23
23
|
__exportStar(require("./factory"), exports);
|
|
24
24
|
__exportStar(require("./multi-cursors"), exports);
|
|
25
|
+
__exportStar(require("./signed-package-table"), exports);
|
|
25
26
|
__exportStar(require("./sql.data-source"), exports);
|
|
26
27
|
__exportStar(require("./sql-db"), exports);
|
|
27
28
|
__exportStar(require("./subscribable.data-source"), exports);
|
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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 || (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|