@peers-app/peers-sdk 0.27.5 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/user-context.d.ts +7 -1
- package/dist/context/user-context.js +4 -9
- package/dist/contracts/contract-proxy.d.ts +5 -0
- package/dist/contracts/contract-proxy.js +2 -2
- package/dist/contracts/contract-resolvers.d.ts +2 -2
- package/dist/contracts/contract-resolvers.js +14 -15
- package/dist/contracts/system/network-contracts.d.ts +1 -1
- package/dist/contracts/tool-catalog-service.js +1 -1
- package/dist/data/assistants.d.ts +3 -3
- package/dist/data/authority-write-checks.d.ts +42 -0
- package/dist/data/authority-write-checks.js +324 -0
- package/dist/data/change-tracking.d.ts +4 -4
- package/dist/data/create-group.js +1 -0
- package/dist/data/devices.d.ts +27 -4
- package/dist/data/devices.js +31 -0
- package/dist/data/group-authority.d.ts +111 -0
- package/dist/data/group-authority.js +281 -0
- package/dist/data/group-members.d.ts +8 -2
- package/dist/data/group-members.js +41 -18
- package/dist/data/group-permissions.d.ts +15 -31
- package/dist/data/group-permissions.js +25 -95
- package/dist/data/group-resign.d.ts +38 -0
- package/dist/data/group-resign.js +71 -0
- package/dist/data/groups.d.ts +11 -0
- package/dist/data/groups.js +31 -13
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/invites.d.ts +14 -0
- package/dist/data/orm/index.d.ts +1 -0
- package/dist/data/orm/index.js +1 -0
- package/dist/data/orm/sql.data-source.d.ts +2 -2
- package/dist/data/orm/sql.data-source.js +24 -22
- package/dist/data/orm/table-container.js +20 -11
- package/dist/data/orm/write-checks.d.ts +89 -0
- package/dist/data/orm/write-checks.js +115 -0
- package/dist/data/package-table-identity.d.ts +21 -0
- package/dist/data/package-table-identity.js +87 -0
- package/dist/data/package-version-resolver.d.ts +15 -0
- package/dist/data/package-version-resolver.js +70 -3
- package/dist/data/package-versions.d.ts +20 -14
- package/dist/data/package-versions.js +15 -9
- package/dist/data/packages.d.ts +6 -1
- package/dist/data/packages.js +15 -20
- package/dist/data/persistent-vars.js +2 -39
- package/dist/data/sync-recovery-queue.d.ts +4 -4
- package/dist/data/sync-recovery-queue.js +7 -1
- package/dist/data/tool-access-approvals.d.ts +3 -3
- package/dist/data/user-permissions.d.ts +33 -2
- package/dist/data/user-permissions.js +87 -10
- package/dist/data/users.d.ts +49 -12
- package/dist/data/users.js +21 -1
- package/dist/data/workflows.d.ts +1 -1
- package/dist/define/index.d.ts +1 -1
- package/dist/define/index.js +2 -1
- package/dist/define/manifest.js +9 -1
- package/dist/define/table.d.ts +15 -0
- package/dist/device/binary-peer-connection-v2.d.ts +1 -1
- package/dist/device/binary-peer-connection-v2.js +1 -1
- package/dist/device/connection.d.ts +1 -1
- package/dist/device/frame-cipher.d.ts +1 -1
- package/dist/device/frame-cipher.js +1 -1
- package/dist/device/get-trust-level-fn.js +3 -6
- package/dist/device-pairing/types.d.ts +26 -26
- package/dist/identity/user-row-change.d.ts +3 -1
- package/dist/identity/user-row-change.js +33 -18
- package/dist/invites/group-approval.js +23 -24
- package/dist/invites/invite-messages.d.ts +250 -28
- package/dist/invites/invite-token.d.ts +18 -0
- package/dist/invites/invite-token.js +2 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +27 -0
- package/dist/package-installer/package-installer.d.ts +1 -1
- package/dist/package-installer/package-installer.js +8 -3
- package/dist/package-installer/package-propagation.d.ts +2 -0
- package/dist/package-installer/package-propagation.js +20 -38
- package/dist/package-installer/package-remote-checker.js +15 -3
- package/dist/package-loader/isolated-artifact.d.ts +14 -0
- package/dist/package-loader/isolated-artifact.js +3 -3
- package/dist/package-loader/isolated-runtime.d.ts +3 -1
- package/dist/package-loader/isolated-table-gateway.js +4 -1
- package/dist/package-loader/package-loader.js +6 -0
- package/dist/peer-id.d.ts +19 -0
- package/dist/peer-id.js +41 -0
- package/dist/types/peer-device.d.ts +2 -0
- package/dist/types/workflow.d.ts +2 -2
- package/dist/utils.js +13 -32
- package/package.json +1 -1
- package/dist/data/package-permissions.d.ts +0 -14
- package/dist/data/package-permissions.js +0 -26
- package/dist/data/package-version-permissions.d.ts +0 -14
- package/dist/data/package-version-permissions.js +0 -30
package/dist/data/invites.d.ts
CHANGED
|
@@ -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;
|
package/dist/data/orm/index.d.ts
CHANGED
package/dist/data/orm/index.js
CHANGED
|
@@ -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
|
|
100
|
-
export declare function jsValueToSqlValue(value: 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. */
|
|
@@ -10,6 +10,16 @@ const tool_catalog_fields_1 = require("../tool-catalog-fields");
|
|
|
10
10
|
const data_query_1 = require("./data-query");
|
|
11
11
|
const data_query_sqlite_1 = require("./data-query.sqlite");
|
|
12
12
|
const types_1 = require("./types");
|
|
13
|
+
/**
|
|
14
|
+
* The SQLite name {@link SQLDataSource} gives one of a table's indexes.
|
|
15
|
+
*
|
|
16
|
+
* @param tableName - Full (physical) table name.
|
|
17
|
+
* @param index - One entry of `ITableMetaData.indexes`.
|
|
18
|
+
*/
|
|
19
|
+
function sqlIndexName(tableName, index) {
|
|
20
|
+
const fieldNames = index.fields.map((f) => `_${typeof f === "string" ? f : f.name}`).join("");
|
|
21
|
+
return `${tableName}_${fieldNames}_${(0, utils_1.simpleHash)(JSON.stringify(index))}`;
|
|
22
|
+
}
|
|
13
23
|
/** SQLite-backed table storage with additive schema migration and serialized initialization. */
|
|
14
24
|
class SQLDataSource {
|
|
15
25
|
db;
|
|
@@ -79,22 +89,12 @@ class SQLDataSource {
|
|
|
79
89
|
// create indexes if they don't exist
|
|
80
90
|
if (this.metaData.indexes) {
|
|
81
91
|
for (const index of this.metaData.indexes) {
|
|
82
|
-
|
|
92
|
+
// Quoted: a field may be named like an SQL keyword (`order`, `group`).
|
|
83
93
|
const fields = index.fields
|
|
84
|
-
.map((f) => {
|
|
85
|
-
if (typeof f === "string") {
|
|
86
|
-
fieldNames += `_${f}`;
|
|
87
|
-
return f;
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
fieldNames += `_${f.name}`;
|
|
91
|
-
return `${f.name} ${f.order}`;
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
+
.map((f) => (typeof f === "string" ? `"${f}"` : `"${f.name}" ${f.order}`))
|
|
94
95
|
.join(", ");
|
|
95
|
-
const hash = (0, utils_1.simpleHash)(JSON.stringify(index));
|
|
96
96
|
const unique = index.unique ? "UNIQUE" : "";
|
|
97
|
-
const indexName =
|
|
97
|
+
const indexName = sqlIndexName(this.tableName, index);
|
|
98
98
|
indexNames.push(indexName);
|
|
99
99
|
const sql = `CREATE ${unique} INDEX IF NOT EXISTS "${indexName}" ON "${this.tableName}"(${fields});`;
|
|
100
100
|
try {
|
|
@@ -102,6 +102,9 @@ class SQLDataSource {
|
|
|
102
102
|
}
|
|
103
103
|
catch (e) {
|
|
104
104
|
console.error(`Error creating index "${indexName}" on table "${this.tableName}": ${String(e)}`);
|
|
105
|
+
// A package must not accept writes without its declared uniqueness constraint.
|
|
106
|
+
if (this.metaData.packageId && index.unique)
|
|
107
|
+
throw e;
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
}
|
|
@@ -136,6 +139,7 @@ class SQLDataSource {
|
|
|
136
139
|
// sql query to copy existing data and set new required fields to default values
|
|
137
140
|
const defaults = this.initRecord();
|
|
138
141
|
const fieldsInsert = this.metaData.fields.map((f) => `"${f.name}"`).join(", ");
|
|
142
|
+
const params = [];
|
|
139
143
|
const fieldsSelect = this.metaData.fields
|
|
140
144
|
.map((f) => {
|
|
141
145
|
const existingField = existingTableDef.includes(`"${f.name}"`);
|
|
@@ -146,18 +150,19 @@ class SQLDataSource {
|
|
|
146
150
|
if (defaultValue === undefined) {
|
|
147
151
|
defaultValue = globalDefaultValues(f);
|
|
148
152
|
}
|
|
149
|
-
|
|
153
|
+
if (!f.optional)
|
|
154
|
+
params.push(jsValueToSqlValue(defaultValue));
|
|
150
155
|
if (existingField && !f.optional) {
|
|
151
|
-
return `
|
|
156
|
+
return `COALESCE("${f.name}", ?) as "${f.name}"`;
|
|
152
157
|
}
|
|
153
158
|
if (!existingField && !f.optional) {
|
|
154
|
-
return
|
|
159
|
+
return `? as "${f.name}"`;
|
|
155
160
|
}
|
|
156
161
|
return `NULL as "${f.name}"`;
|
|
157
162
|
})
|
|
158
163
|
.join(", ");
|
|
159
164
|
const insertSql = `INSERT INTO "${this.tableName}"(${fieldsInsert}) SELECT ${fieldsSelect} FROM "${tempTableName}"`;
|
|
160
|
-
await this.db.exec(insertSql,
|
|
165
|
+
await this.db.exec(insertSql, params);
|
|
161
166
|
// drop old table
|
|
162
167
|
const dropSql = `DROP TABLE IF EXISTS "${tempTableName}"`;
|
|
163
168
|
await this.db.exec(dropSql, []);
|
|
@@ -536,8 +541,8 @@ class SQLDataSource {
|
|
|
536
541
|
}
|
|
537
542
|
}
|
|
538
543
|
exports.SQLDataSource = SQLDataSource;
|
|
539
|
-
/** Encode a JavaScript field for SQLite parameters
|
|
540
|
-
function jsValueToSqlValue(value
|
|
544
|
+
/** Encode a JavaScript field for SQLite parameters. */
|
|
545
|
+
function jsValueToSqlValue(value) {
|
|
541
546
|
if (value === undefined)
|
|
542
547
|
return null;
|
|
543
548
|
if (value instanceof Date) {
|
|
@@ -549,9 +554,6 @@ function jsValueToSqlValue(value, escapeQuote = false) {
|
|
|
549
554
|
if (typeof value === "boolean") {
|
|
550
555
|
return value ? 1 : 0;
|
|
551
556
|
}
|
|
552
|
-
if (typeof value === "string" && escapeQuote) {
|
|
553
|
-
return value.replace(/'/g, "''");
|
|
554
|
-
}
|
|
555
557
|
return value;
|
|
556
558
|
}
|
|
557
559
|
/** 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");
|
|
@@ -159,9 +160,12 @@ class TableContainer {
|
|
|
159
160
|
this.savingDefinitions = true;
|
|
160
161
|
const tableName = (0, utils_1.getFullTableName)(metaData);
|
|
161
162
|
try {
|
|
162
|
-
// Lazy import to avoid circular dependency at module load time
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
// Lazy import to avoid circular dependency at module load time; it registers the
|
|
164
|
+
// TableDefinitions system table. Use this container's own copy: a definition belongs to
|
|
165
|
+
// the data context the table lives in, not whichever group is active.
|
|
166
|
+
require("../table-definitions-table");
|
|
167
|
+
const definitions = table_definitions_system_1.systemTableDefinitions.TableDefinitions;
|
|
168
|
+
const tableDefsTable = this.getTable(definitions.metaData, definitions.schema);
|
|
165
169
|
const record = {
|
|
166
170
|
tableId: metaData.tableId,
|
|
167
171
|
name: tableName,
|
|
@@ -266,7 +270,8 @@ class TableContainer {
|
|
|
266
270
|
const record = await tableDefsTable.findOne({ name: tableName });
|
|
267
271
|
if (!record || record.deleted)
|
|
268
272
|
return null;
|
|
269
|
-
this.registerFromTableDefinitionRecord(record)
|
|
273
|
+
if (!this.registerFromTableDefinitionRecord(record))
|
|
274
|
+
return null;
|
|
270
275
|
// Now instantiate via getTableByName which checks fallbackDefinitions
|
|
271
276
|
return this.getTableByName(tableName);
|
|
272
277
|
}
|
|
@@ -334,22 +339,25 @@ class TableContainer {
|
|
|
334
339
|
* External code should not need to call this directly.
|
|
335
340
|
*/
|
|
336
341
|
registerFromTableDefinitionRecord(record) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
console.warn(`Invalid table definition record, skipping:`, record);
|
|
340
|
-
return;
|
|
342
|
+
try {
|
|
343
|
+
(0, package_table_identity_1.validateTableDefinitionRecord)(record);
|
|
341
344
|
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
console.warn("Refusing invalid synced table definition", error);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
const metaData = record.metaData;
|
|
342
350
|
const tableName = (0, utils_1.getFullTableName)(metaData);
|
|
343
351
|
// Deleted tables should not be registered as fallbacks
|
|
344
352
|
if (record.deleted) {
|
|
345
353
|
delete this.fallbackDefinitions[tableName];
|
|
346
354
|
delete this.fallbackInstances[tableName];
|
|
347
|
-
return;
|
|
355
|
+
return false;
|
|
348
356
|
}
|
|
349
357
|
// Don't overwrite a full (code-registered) definition -- that always takes precedence
|
|
350
358
|
const fullDef = this.tableDefinitions[tableName] || table_definitions_system_1.systemTableDefinitions[tableName];
|
|
351
359
|
if (fullDef) {
|
|
352
|
-
return; // full definition exists, no need for a fallback
|
|
360
|
+
return true; // full definition exists, no need for a fallback
|
|
353
361
|
}
|
|
354
362
|
const existing = this.fallbackDefinitions[tableName];
|
|
355
363
|
if (existing) {
|
|
@@ -357,13 +365,14 @@ class TableContainer {
|
|
|
357
365
|
const incomingVersion = record.versionNumber ?? 0;
|
|
358
366
|
const result = checkVersionedUpdate(existingVersion, incomingVersion, existing.metaData, metaData);
|
|
359
367
|
if (result === "skip") {
|
|
360
|
-
return;
|
|
368
|
+
return true;
|
|
361
369
|
}
|
|
362
370
|
// Remove old fallback instance so it gets re-created with the new schema
|
|
363
371
|
delete this.fallbackInstances[tableName];
|
|
364
372
|
}
|
|
365
373
|
const schema = (0, types_1.fieldsToSchema)(metaData.fields);
|
|
366
374
|
this.fallbackDefinitions[tableName] = { metaData, schema };
|
|
375
|
+
return true;
|
|
367
376
|
}
|
|
368
377
|
/**
|
|
369
378
|
* 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.
|
|
@@ -12,6 +13,21 @@
|
|
|
12
13
|
* @throws When the identifier is empty or contains SQL syntax.
|
|
13
14
|
*/
|
|
14
15
|
export declare function validateIsolatedSqlIdentifier(identifier: string, label?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Validate a table's index declarations: each lists at least one of the table's fields, in
|
|
18
|
+
* order, by name or as `{ name, order }` with `"ASC"` or `"DESC"`.
|
|
19
|
+
*
|
|
20
|
+
* @param indexes - The declared indexes, if any.
|
|
21
|
+
* @param fieldNames - The table's field names.
|
|
22
|
+
* @param label - Where the indexes come from, for errors.
|
|
23
|
+
* @param allowUnique - Whether an index may be unique. A package may make one unique only on a
|
|
24
|
+
* local-only table: on a synced table two devices can each write a row the other's unique
|
|
25
|
+
* index refuses, and package tables have no conflict resolver. A stored definition may have
|
|
26
|
+
* one anyway (an old-style package's).
|
|
27
|
+
* @throws When an index is malformed, names a field the table lacks, or is unique when not
|
|
28
|
+
* allowed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateTableIndexes(indexes: unknown, fieldNames: ReadonlySet<string>, label: string, allowUnique?: boolean): void;
|
|
15
31
|
/**
|
|
16
32
|
* Validate a package-local table name.
|
|
17
33
|
*
|
|
@@ -44,3 +60,8 @@ export declare function packageScopedTableName(packageId: string, logicalName: s
|
|
|
44
60
|
* @throws When package or table identity is invalid.
|
|
45
61
|
*/
|
|
46
62
|
export declare function packageScopedTableId(packageId: string, logicalName: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Validate synced schema metadata before it can name a table, column, or index in SQL.
|
|
65
|
+
* A schema cannot impersonate a system table or supply SQL expressions as identifiers.
|
|
66
|
+
*/
|
|
67
|
+
export declare function validateTableDefinitionRecord(record: ITableDefinitionRecord): void;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateIsolatedSqlIdentifier = validateIsolatedSqlIdentifier;
|
|
4
|
+
exports.validateTableIndexes = validateTableIndexes;
|
|
4
5
|
exports.validatePackageTableName = validatePackageTableName;
|
|
5
6
|
exports.packageScopedTableName = packageScopedTableName;
|
|
6
7
|
exports.packageScopedTableId = packageScopedTableId;
|
|
8
|
+
exports.validateTableDefinitionRecord = validateTableDefinitionRecord;
|
|
7
9
|
const utils_1 = require("../utils");
|
|
8
10
|
const ISOLATED_SQL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
11
|
/**
|
|
@@ -28,6 +30,53 @@ function validateIsolatedSqlIdentifier(identifier, label = "Isolated SQL identif
|
|
|
28
30
|
}
|
|
29
31
|
return identifier;
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate a table's index declarations: each lists at least one of the table's fields, in
|
|
35
|
+
* order, by name or as `{ name, order }` with `"ASC"` or `"DESC"`.
|
|
36
|
+
*
|
|
37
|
+
* @param indexes - The declared indexes, if any.
|
|
38
|
+
* @param fieldNames - The table's field names.
|
|
39
|
+
* @param label - Where the indexes come from, for errors.
|
|
40
|
+
* @param allowUnique - Whether an index may be unique. A package may make one unique only on a
|
|
41
|
+
* local-only table: on a synced table two devices can each write a row the other's unique
|
|
42
|
+
* index refuses, and package tables have no conflict resolver. A stored definition may have
|
|
43
|
+
* one anyway (an old-style package's).
|
|
44
|
+
* @throws When an index is malformed, names a field the table lacks, or is unique when not
|
|
45
|
+
* allowed.
|
|
46
|
+
*/
|
|
47
|
+
function validateTableIndexes(indexes, fieldNames, label, allowUnique = false) {
|
|
48
|
+
if (indexes === undefined)
|
|
49
|
+
return;
|
|
50
|
+
if (!Array.isArray(indexes))
|
|
51
|
+
throw new Error(`${label} must be an array`);
|
|
52
|
+
for (const [position, index] of indexes.entries()) {
|
|
53
|
+
const at = `${label}[${position}]`;
|
|
54
|
+
const { fields, unique } = (index ?? {});
|
|
55
|
+
if (!index ||
|
|
56
|
+
typeof index !== "object" ||
|
|
57
|
+
Object.keys(index).some((key) => key !== "fields" && key !== "unique") ||
|
|
58
|
+
!Array.isArray(fields) ||
|
|
59
|
+
fields.length === 0) {
|
|
60
|
+
throw new Error(`${at} must be { fields: [...] } with at least one field`);
|
|
61
|
+
}
|
|
62
|
+
if (unique !== undefined && (unique !== true || !allowUnique)) {
|
|
63
|
+
throw new Error(`${at} can be unique only on a local-only table`);
|
|
64
|
+
}
|
|
65
|
+
for (const field of fields) {
|
|
66
|
+
const name = typeof field === "string"
|
|
67
|
+
? field
|
|
68
|
+
: field &&
|
|
69
|
+
typeof field === "object" &&
|
|
70
|
+
Object.keys(field).every((key) => key === "name" || key === "order") &&
|
|
71
|
+
(field.order === "ASC" || field.order === "DESC")
|
|
72
|
+
? field.name
|
|
73
|
+
: undefined;
|
|
74
|
+
if (typeof name !== "string" || !fieldNames.has(name)) {
|
|
75
|
+
throw new Error(`${at} names a field the table doesn't have`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
31
80
|
/**
|
|
32
81
|
* Validate a package-local table name.
|
|
33
82
|
*
|
|
@@ -69,3 +118,41 @@ function packageScopedTableName(packageId, logicalName) {
|
|
|
69
118
|
function packageScopedTableId(packageId, logicalName) {
|
|
70
119
|
return (0, utils_1.deterministicTableId)(packageScopedTableName(packageId, logicalName));
|
|
71
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Validate synced schema metadata before it can name a table, column, or index in SQL.
|
|
123
|
+
* A schema cannot impersonate a system table or supply SQL expressions as identifiers.
|
|
124
|
+
*/
|
|
125
|
+
function validateTableDefinitionRecord(record) {
|
|
126
|
+
const meta = record.metaData;
|
|
127
|
+
if (!meta ||
|
|
128
|
+
!(0, utils_1.isid)(record.tableId) ||
|
|
129
|
+
meta.tableId !== record.tableId ||
|
|
130
|
+
(meta.packageId !== undefined && !(0, utils_1.isid)(meta.packageId))) {
|
|
131
|
+
throw new Error("Invalid table definition identity");
|
|
132
|
+
}
|
|
133
|
+
validateIsolatedSqlIdentifier(meta.name, "Table name");
|
|
134
|
+
validateIsolatedSqlIdentifier(meta.primaryKeyName, "Primary key");
|
|
135
|
+
if (record.name !== (0, utils_1.getFullTableName)(meta))
|
|
136
|
+
throw new Error("Table definition name mismatch");
|
|
137
|
+
if (!Number.isSafeInteger(record.versionNumber) ||
|
|
138
|
+
record.versionNumber < 0 ||
|
|
139
|
+
record.versionNumber !== (meta.versionNumber ?? 0)) {
|
|
140
|
+
throw new Error("Invalid table definition version");
|
|
141
|
+
}
|
|
142
|
+
if (meta.localOnly !== undefined && typeof meta.localOnly !== "boolean") {
|
|
143
|
+
throw new Error("Invalid table localOnly policy");
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(meta.fields) || !meta.fields.length)
|
|
146
|
+
throw new Error("Missing table fields");
|
|
147
|
+
const names = new Set();
|
|
148
|
+
for (const field of meta.fields) {
|
|
149
|
+
validateIsolatedSqlIdentifier(field.name, "Column name");
|
|
150
|
+
if (names.has(field.name))
|
|
151
|
+
throw new Error("Duplicate table column");
|
|
152
|
+
names.add(field.name);
|
|
153
|
+
}
|
|
154
|
+
if (!names.has(meta.primaryKeyName))
|
|
155
|
+
throw new Error("Missing primary key column");
|
|
156
|
+
// Only what reaches SQL here: an old-style package may keep a unique index on a synced table.
|
|
157
|
+
validateTableIndexes(meta.indexes, names, "Table indexes", true);
|
|
158
|
+
}
|