@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.
- package/dist/contracts/contract-resolvers.js +1 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/system/group-members-contract.d.ts +25 -0
- package/dist/contracts/system/group-members-contract.js +50 -0
- 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/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/invites/invite-messages.d.ts +44 -44
- package/dist/package-loader/isolated-artifact.d.ts +15 -0
- package/dist/package-loader/isolated-artifact.js +12 -1
- package/dist/package-loader/isolated-runtime.d.ts +4 -0
- package/dist/package-loader/isolated-table-gateway.js +31 -3
- package/dist/system-ids.d.ts +2 -0
- package/dist/system-ids.js +3 -1
- package/package.json +1 -1
|
@@ -211,6 +211,7 @@ async function isSelfTrustedContractCaller(userContext, remoteIdentity) {
|
|
|
211
211
|
}
|
|
212
212
|
const permissionDenied = (call, reason) => new Error(`Permission denied: ${call.member} '${call.name}'.${call.operation} on contract ${call.contractId}${reason ? ` (${reason})` : ""}`);
|
|
213
213
|
const localOnlyContractIds = new Set([
|
|
214
|
+
system_ids_1.groupMembersContractId,
|
|
214
215
|
system_ids_1.hostToolsContractId,
|
|
215
216
|
system_ids_1.desktopToolsContractId,
|
|
216
217
|
system_ids_1.contractCatalogContractId,
|
|
@@ -19,6 +19,7 @@ export * from "./system/app-updates-contract";
|
|
|
19
19
|
export { CONTRACT_CATALOG_VERSION, contractCatalogProviderPackageId, getContractCatalogDefinition, type IContractCatalogConsumer, type IContractDefinitionWithPolicy, type IContractProviderInfo, type IContractToolPolicy, type IExposedContractInfo, type IExposedToolInfo, type IGetContractDefinitionInput, type IListContractProvidersInput, } from "./system/contract-catalog-contract";
|
|
20
20
|
export * from "./system/desktop-tools-contract";
|
|
21
21
|
export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./system/device-operations-contract";
|
|
22
|
+
export * from "./system/group-members-contract";
|
|
22
23
|
export * from "./system/host-shell-contract";
|
|
23
24
|
export * from "./system/host-tools-contract";
|
|
24
25
|
export { getHttpContractDefinition, HTTP_CONTRACT_VERSION, HTTP_DEFAULT_TIMEOUT_MS, HTTP_MAX_BODY_BYTES, HTTP_MAX_TIMEOUT_MS, httpProviderPackageId, type IHttpCancelInput, type IHttpConsumer, type IHttpCredentialInfo, type IHttpMultipartPart, type IHttpPolicyInfo, type IHttpRequestInput, type IHttpRequestResult, } from "./system/http-contract";
|
package/dist/contracts/index.js
CHANGED
|
@@ -103,6 +103,7 @@ Object.defineProperty(exports, "DEVICE_OPERATIONS_MAX_TOOLS", { enumerable: true
|
|
|
103
103
|
Object.defineProperty(exports, "deviceOperationsProviderPackageId", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsProviderPackageId; } });
|
|
104
104
|
Object.defineProperty(exports, "deviceOperationsToolIds", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsToolIds; } });
|
|
105
105
|
Object.defineProperty(exports, "getDeviceOperationsContractDefinition", { enumerable: true, get: function () { return device_operations_contract_1.getDeviceOperationsContractDefinition; } });
|
|
106
|
+
__exportStar(require("./system/group-members-contract"), exports);
|
|
106
107
|
__exportStar(require("./system/host-shell-contract"), exports);
|
|
107
108
|
__exportStar(require("./system/host-tools-contract"), exports);
|
|
108
109
|
var http_contract_1 = require("./system/http-contract");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IContractConsumerBase } from "../contract-proxy";
|
|
2
|
+
import type { IContractDefinition } from "../types";
|
|
3
|
+
/** Version of the local-only System Group Members contract. */
|
|
4
|
+
export declare const GROUP_MEMBERS_CONTRACT_VERSION = 1;
|
|
5
|
+
/** A current member with at least Reader access; names come from signed user profiles. */
|
|
6
|
+
export interface IGroupMemberInfo {
|
|
7
|
+
userId: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
role: string;
|
|
10
|
+
accessLevel: number;
|
|
11
|
+
}
|
|
12
|
+
/** Membership of the caller's data context. Personal contexts contain only their owner. */
|
|
13
|
+
export interface IGroupMembersResult {
|
|
14
|
+
dataContextId: string;
|
|
15
|
+
personal: boolean;
|
|
16
|
+
members: IGroupMemberInfo[];
|
|
17
|
+
}
|
|
18
|
+
/** Package-safe, read-only membership consumer. No caller-supplied group id is accepted. */
|
|
19
|
+
export interface IGroupMembersConsumer extends IContractConsumerBase {
|
|
20
|
+
tools: {
|
|
21
|
+
getMembers(): Promise<IGroupMembersResult>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Return the System Group Members v1 declaration. Tool identities are derived by the catalog. */
|
|
25
|
+
export declare function getGroupMembersContractDefinition(): IContractDefinition;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GROUP_MEMBERS_CONTRACT_VERSION = void 0;
|
|
4
|
+
exports.getGroupMembersContractDefinition = getGroupMembersContractDefinition;
|
|
5
|
+
const system_ids_1 = require("../../system-ids");
|
|
6
|
+
const field_type_1 = require("../../types/field-type");
|
|
7
|
+
const system_contracts_1 = require("../system-contracts");
|
|
8
|
+
/** Version of the local-only System Group Members contract. */
|
|
9
|
+
exports.GROUP_MEMBERS_CONTRACT_VERSION = 1;
|
|
10
|
+
/** Return the System Group Members v1 declaration. Tool identities are derived by the catalog. */
|
|
11
|
+
function getGroupMembersContractDefinition() {
|
|
12
|
+
return {
|
|
13
|
+
contractId: system_ids_1.groupMembersContractId,
|
|
14
|
+
version: exports.GROUP_MEMBERS_CONTRACT_VERSION,
|
|
15
|
+
devTag: "dev",
|
|
16
|
+
name: "System Group Members",
|
|
17
|
+
description: "Current verified members of the caller's own data context.",
|
|
18
|
+
tables: [],
|
|
19
|
+
observables: [],
|
|
20
|
+
events: [],
|
|
21
|
+
tools: [
|
|
22
|
+
{
|
|
23
|
+
name: "getMembers",
|
|
24
|
+
usageDescription: "Read current members, including the founder and Readers, in this context.",
|
|
25
|
+
inputFields: [],
|
|
26
|
+
outputFields: [
|
|
27
|
+
{ name: "dataContextId", type: field_type_1.FieldType.id },
|
|
28
|
+
{ name: "personal", type: field_type_1.FieldType.boolean },
|
|
29
|
+
{
|
|
30
|
+
name: "members",
|
|
31
|
+
type: field_type_1.FieldType.object,
|
|
32
|
+
isArray: true,
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: "userId", type: field_type_1.FieldType.id },
|
|
35
|
+
{ name: "name", type: field_type_1.FieldType.string, optional: true },
|
|
36
|
+
{ name: "role", type: field_type_1.FieldType.string },
|
|
37
|
+
{ name: "accessLevel", type: field_type_1.FieldType.number },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
(0, system_contracts_1.registerSystemContract)({
|
|
46
|
+
providerPackageId: system_ids_1.peersSystemPackageId,
|
|
47
|
+
contractId: system_ids_1.groupMembersContractId,
|
|
48
|
+
version: exports.GROUP_MEMBERS_CONTRACT_VERSION,
|
|
49
|
+
getDefinition: getGroupMembersContractDefinition,
|
|
50
|
+
});
|
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import type { IGroupMember } from "./group-members";
|
|
2
2
|
import type { IGroup } from "./groups";
|
|
3
|
-
import { type
|
|
3
|
+
import { type ISignedRow } from "./orm/signed-package-table";
|
|
4
|
+
import type { ITableMetaData } from "./orm/types";
|
|
5
|
+
import { type ITableWriteCheckContext, type TableWriteCheck } from "./orm/write-checks";
|
|
4
6
|
import type { IPackageVersion } from "./package-versions";
|
|
5
7
|
import type { IPackage } from "./packages";
|
|
8
|
+
/**
|
|
9
|
+
* Check the signed stamp: package approvals belong to this context, and a replacement must
|
|
10
|
+
* not have been signed before the row on record. The
|
|
11
|
+
* tables' `signAndSave` puts `signedAt` under the signature, so a member can't restore a revoked
|
|
12
|
+
* grant, an earlier group setting or an earlier release selection by serving a copy of an older,
|
|
13
|
+
* validly signed row as a new change. Remember the stored stamp in device-local personal
|
|
14
|
+
* storage before it can be deleted: an unsigned package delete must not erase revocations.
|
|
15
|
+
* A row signed before the field existed counts as oldest.
|
|
16
|
+
*
|
|
17
|
+
* @param tableName - The table, for the refusal.
|
|
18
|
+
* @param existing - The row on record, if any.
|
|
19
|
+
* @param candidate - The row about to be stored, or undefined for a delete.
|
|
20
|
+
* @param context - Where the write lands.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkSignedOrder(tableName: string, existing: {
|
|
23
|
+
signedAt?: number;
|
|
24
|
+
signature?: string;
|
|
25
|
+
} | undefined, candidate: {
|
|
26
|
+
signedAt?: number;
|
|
27
|
+
signature?: string;
|
|
28
|
+
signedContextId?: string;
|
|
29
|
+
} | undefined, context: ITableWriteCheckContext): Promise<void>;
|
|
6
30
|
/**
|
|
7
31
|
* A membership must be signed by a member who counts as at least Admin, at least the role
|
|
8
32
|
* granted, and at least the member's current role. Only the founder writes the founder's row.
|
|
@@ -40,3 +64,36 @@ export declare function isPackageRowAdminSigned(pkg: IPackage, dataContext: ITab
|
|
|
40
64
|
* accepted from anyone, since every device installs the bundled release.
|
|
41
65
|
*/
|
|
42
66
|
export declare function checkPackageVersion(existing: IPackageVersion | undefined, candidate: IPackageVersion | undefined, context: ITableWriteCheckContext): Promise<void>;
|
|
67
|
+
/** A row of a signed package table: its owner in the `signedBy` field, plus the signature. */
|
|
68
|
+
type SignedTableRow = Record<string, any> & ISignedRow;
|
|
69
|
+
/**
|
|
70
|
+
* The write check for a package table declared with `signedBy`, keyed by its physical name
|
|
71
|
+
* (`<name>_<packageId>`). In a group, a row (synced or saved locally) is stored only when:
|
|
72
|
+
* - its id is the one derived from its owner, the user its `signedBy` field names, and its key
|
|
73
|
+
* ({@link signedRowIdFor}), so no member can claim another member's row id, even on a device
|
|
74
|
+
* that doesn't have that row yet;
|
|
75
|
+
* - its owner is a Writer or above whose key validly signed it, for this group and table;
|
|
76
|
+
* - it wasn't signed before the row on record ({@link checkSignedOrder}, so a replay of an older
|
|
77
|
+
* signed row can't roll it back on a device that has the newer one).
|
|
78
|
+
*
|
|
79
|
+
* A delete carries no author, so a row is never deleted: a package marks it instead. Nothing is
|
|
80
|
+
* checked in the personal context, which only the user's own devices write.
|
|
81
|
+
*
|
|
82
|
+
* @param metaData - The table's metadata, with `signedBy` set.
|
|
83
|
+
*/
|
|
84
|
+
export declare function signedTableWriteCheck(metaData: ITableMetaData): TableWriteCheck<SignedTableRow>;
|
|
85
|
+
/**
|
|
86
|
+
* The write check the host runs on every write to a table in a group context: the check
|
|
87
|
+
* registered under the table's physical name, or, for a signed package table, its
|
|
88
|
+
* {@link signedTableWriteCheck}, registered under that name on first use so the same check
|
|
89
|
+
* serves later lookups. The host calls this when it builds a table's data source, and the
|
|
90
|
+
* isolated table gateway when it registers a package's tables, so a signed table is checked
|
|
91
|
+
* whether its definition arrived with the package or through sync. Generated checks are reused
|
|
92
|
+
* only for the same primary key, owner field, key fields, and fields; unsigned metadata never
|
|
93
|
+
* inherits another group's check.
|
|
94
|
+
*
|
|
95
|
+
* @param metaData - The table's metadata.
|
|
96
|
+
* @returns The check, or undefined for a table without one.
|
|
97
|
+
*/
|
|
98
|
+
export declare function tableWriteCheckFor(metaData: ITableMetaData): TableWriteCheck | undefined;
|
|
99
|
+
export {};
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkSignedOrder = checkSignedOrder;
|
|
3
4
|
exports.checkGroupMember = checkGroupMember;
|
|
4
5
|
exports.checkGroup = checkGroup;
|
|
5
6
|
exports.checkPackageWrite = checkPackageWrite;
|
|
6
7
|
exports.isPackageRowAdminSigned = isPackageRowAdminSigned;
|
|
7
8
|
exports.checkPackageVersion = checkPackageVersion;
|
|
9
|
+
exports.signedTableWriteCheck = signedTableWriteCheck;
|
|
10
|
+
exports.tableWriteCheckFor = tableWriteCheckFor;
|
|
8
11
|
const lodash_1 = require("lodash");
|
|
12
|
+
const keys_1 = require("../keys");
|
|
9
13
|
const package_author_signing_1 = require("../package-installer/package-author-signing");
|
|
10
14
|
const system_ids_1 = require("../system-ids");
|
|
15
|
+
const utils_1 = require("../utils");
|
|
11
16
|
const devices_1 = require("./devices");
|
|
12
17
|
const group_authority_1 = require("./group-authority");
|
|
13
18
|
const group_member_roles_1 = require("./group-member-roles");
|
|
19
|
+
const signed_package_table_1 = require("./orm/signed-package-table");
|
|
14
20
|
const write_checks_1 = require("./orm/write-checks");
|
|
15
21
|
const package_table_identity_1 = require("./package-table-identity");
|
|
16
22
|
const persistent_vars_1 = require("./persistent-vars");
|
|
23
|
+
const signed_row_id_1 = require("./signed-row-id");
|
|
17
24
|
const user_permissions_1 = require("./user-permissions");
|
|
18
25
|
/**
|
|
19
26
|
* Write checks for the rows that decide authority in a group: memberships, the group row, user
|
|
@@ -46,6 +53,11 @@ async function requireSigner(tableName, row, context) {
|
|
|
46
53
|
* validly signed row as a new change. Remember the stored stamp in device-local personal
|
|
47
54
|
* storage before it can be deleted: an unsigned package delete must not erase revocations.
|
|
48
55
|
* A row signed before the field existed counts as oldest.
|
|
56
|
+
*
|
|
57
|
+
* @param tableName - The table, for the refusal.
|
|
58
|
+
* @param existing - The row on record, if any.
|
|
59
|
+
* @param candidate - The row about to be stored, or undefined for a delete.
|
|
60
|
+
* @param context - Where the write lands.
|
|
49
61
|
*/
|
|
50
62
|
async function checkSignedOrder(tableName, existing, candidate, context) {
|
|
51
63
|
if (candidate &&
|
|
@@ -312,13 +324,176 @@ async function checkDevice(existing, candidate, context) {
|
|
|
312
324
|
throw (0, write_checks_1.writeNotAuthorized)("PersistentVars", "personal and device settings do not belong in a group");
|
|
313
325
|
}
|
|
314
326
|
});
|
|
315
|
-
(0, write_checks_1.registerTableWriteCheck)("TableDefinitions", (
|
|
327
|
+
(0, write_checks_1.registerTableWriteCheck)("TableDefinitions", async (existing, candidate, context) => {
|
|
328
|
+
const signedBy = (record) => record?.metaData?.signedBy;
|
|
329
|
+
const signedKey = (record) => record?.metaData?.signedKey;
|
|
330
|
+
// Keep the security declaration even after package unload/restart. A hard delete would
|
|
331
|
+
// allow a fresh unsigned definition; a soft delete disables fallback sync and loses history.
|
|
332
|
+
if (signedBy(existing) && (!candidate || candidate.deleted)) {
|
|
333
|
+
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", "a signed table's definition can't be deleted");
|
|
334
|
+
}
|
|
316
335
|
if (!candidate)
|
|
317
336
|
return;
|
|
337
|
+
if (signedBy(existing) && existing?.name !== candidate.name) {
|
|
338
|
+
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", "a signed table's identity can't change");
|
|
339
|
+
}
|
|
318
340
|
try {
|
|
319
341
|
(0, package_table_identity_1.validateTableDefinitionRecord)(candidate);
|
|
320
342
|
}
|
|
321
343
|
catch (error) {
|
|
322
344
|
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", String(error));
|
|
323
345
|
}
|
|
346
|
+
// A member can't turn a signed table's checks off (or on) by serving a changed definition.
|
|
347
|
+
// Compare SQLite's case-insensitive physical identity too: an alternate definition id or
|
|
348
|
+
// casing must not open an unchecked source over the same storage. Even a signed alias is
|
|
349
|
+
// refused: its separate schema initialization could rebuild the real table before checks.
|
|
350
|
+
const { TableDefinitions } = await Promise.resolve().then(() => require("./table-definitions-table"));
|
|
351
|
+
const physicalName = candidate.name.toLowerCase();
|
|
352
|
+
const peers = (await TableDefinitions(context.dataContext).list()).filter((record) => record.name.toLowerCase() === physicalName);
|
|
353
|
+
for (const row of context.retainedRows ?? []) {
|
|
354
|
+
const record = row;
|
|
355
|
+
if (record?.name?.toLowerCase() === physicalName)
|
|
356
|
+
peers.push(record);
|
|
357
|
+
}
|
|
358
|
+
if (peers.some((record) => record.name !== candidate.name && (signedBy(record) || signedBy(candidate)))) {
|
|
359
|
+
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", "a signed table can't have a case alias");
|
|
360
|
+
}
|
|
361
|
+
if ([...(existing ? [existing] : []), ...peers].some((record) => signedBy(record) !== signedBy(candidate) && (signedBy(record) || signedBy(candidate)))) {
|
|
362
|
+
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", "a table is signed from its first version or never; signedBy can't change");
|
|
363
|
+
}
|
|
364
|
+
// Row ids are derived from the key fields, so changing them would orphan every row.
|
|
365
|
+
if ([...(existing ? [existing] : []), ...peers].some((record) => signedBy(record) && !(0, lodash_1.isEqual)(signedKey(record), signedKey(candidate)))) {
|
|
366
|
+
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", "a signed table's signedKey can't change");
|
|
367
|
+
}
|
|
368
|
+
// The derived ids live in the primary key column: another column would hold none of them.
|
|
369
|
+
const primaryKeyName = (record) => record?.metaData?.primaryKeyName;
|
|
370
|
+
if ([...(existing ? [existing] : []), ...peers].some((record) => signedBy(record) && primaryKeyName(record) !== primaryKeyName(candidate))) {
|
|
371
|
+
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", "a signed table's primary key can't change");
|
|
372
|
+
}
|
|
373
|
+
if (signedBy(candidate) && candidate.deleted) {
|
|
374
|
+
throw (0, write_checks_1.writeNotAuthorized)("TableDefinitions", "a signed table's definition can't be deleted");
|
|
375
|
+
}
|
|
324
376
|
});
|
|
377
|
+
// Generated checks can be replaced for a different context's declaration. Existing sources
|
|
378
|
+
// keep their own closure, and unsigned tables never inherit a generated signed-table check.
|
|
379
|
+
const signedCheckMetadata = new WeakMap();
|
|
380
|
+
/**
|
|
381
|
+
* The write check for a package table declared with `signedBy`, keyed by its physical name
|
|
382
|
+
* (`<name>_<packageId>`). In a group, a row (synced or saved locally) is stored only when:
|
|
383
|
+
* - its id is the one derived from its owner, the user its `signedBy` field names, and its key
|
|
384
|
+
* ({@link signedRowIdFor}), so no member can claim another member's row id, even on a device
|
|
385
|
+
* that doesn't have that row yet;
|
|
386
|
+
* - its owner is a Writer or above whose key validly signed it, for this group and table;
|
|
387
|
+
* - it wasn't signed before the row on record ({@link checkSignedOrder}, so a replay of an older
|
|
388
|
+
* signed row can't roll it back on a device that has the newer one).
|
|
389
|
+
*
|
|
390
|
+
* A delete carries no author, so a row is never deleted: a package marks it instead. Nothing is
|
|
391
|
+
* checked in the personal context, which only the user's own devices write.
|
|
392
|
+
*
|
|
393
|
+
* @param metaData - The table's metadata, with `signedBy` set.
|
|
394
|
+
*/
|
|
395
|
+
function signedTableWriteCheck(metaData) {
|
|
396
|
+
const tableName = (0, utils_1.getFullTableName)(metaData);
|
|
397
|
+
const ownerField = metaData.signedBy;
|
|
398
|
+
if (!ownerField)
|
|
399
|
+
throw new Error(`Table ${tableName} isn't a signed table`);
|
|
400
|
+
const identity = { name: metaData.name, signedBy: ownerField, signedKey: metaData.signedKey };
|
|
401
|
+
return async (existing, candidate, context) => {
|
|
402
|
+
if (!context.dataContext.groupId)
|
|
403
|
+
return;
|
|
404
|
+
if (!candidate) {
|
|
405
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, "rows in a signed table aren't deleted");
|
|
406
|
+
}
|
|
407
|
+
const owner = candidate[ownerField];
|
|
408
|
+
if (typeof owner !== "string" || !(0, utils_1.isid)(owner)) {
|
|
409
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, `"${ownerField}" must name the row's owner`);
|
|
410
|
+
}
|
|
411
|
+
if (candidate[metaData.primaryKeyName] !== context.recordId) {
|
|
412
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, "the row's id isn't the record's");
|
|
413
|
+
}
|
|
414
|
+
// Ids are derived from the owner (checked below), so only a hash collision reaches this.
|
|
415
|
+
if (existing && existing[ownerField] !== owner) {
|
|
416
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, "the row on record belongs to another member");
|
|
417
|
+
}
|
|
418
|
+
if (!Number.isSafeInteger(candidate.signedAt)) {
|
|
419
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, "the row has no signing time");
|
|
420
|
+
}
|
|
421
|
+
if (candidate.signedContextId !== context.dataContext.groupId ||
|
|
422
|
+
candidate.signedTableId !== (0, package_table_identity_1.packageScopedTableId)(metaData.packageId, metaData.name)) {
|
|
423
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, "signature is for another context or table");
|
|
424
|
+
}
|
|
425
|
+
await checkSignedOrder(tableName, existing, candidate, context);
|
|
426
|
+
let stored;
|
|
427
|
+
let matchesStoredForm;
|
|
428
|
+
try {
|
|
429
|
+
stored = (0, signed_package_table_1.signedPackageRowForStorage)(metaData, candidate, undefined, existing);
|
|
430
|
+
matchesStoredForm = (0, keys_1.canonicalJSONString)(stored) === (0, keys_1.canonicalJSONString)(candidate);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
// Schema, serialization, and canonicalisation failures are all authorization refusals.
|
|
434
|
+
// A cyclic extra field can fail even after projection succeeds. Sync must still retain
|
|
435
|
+
// the owner's last authorized version when such a forgery is served after it.
|
|
436
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, `the row doesn't fit the table: ${String(error)}`);
|
|
437
|
+
}
|
|
438
|
+
if (!matchesStoredForm) {
|
|
439
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, "the signed row differs from its stored form");
|
|
440
|
+
}
|
|
441
|
+
// Bound to its owner: another member's row under this id would have to name this owner,
|
|
442
|
+
// and only this owner's key signs that.
|
|
443
|
+
let ownersId;
|
|
444
|
+
try {
|
|
445
|
+
if (metaData.signedKey && stored.signedIdSeed !== undefined) {
|
|
446
|
+
throw new Error("a row with key fields has no id seed");
|
|
447
|
+
}
|
|
448
|
+
ownersId = (0, signed_row_id_1.signedRowIdFor)(identity, stored, stored.signedIdSeed);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, `the row's id can't be checked: ${String(error)}`);
|
|
452
|
+
}
|
|
453
|
+
if (stored[metaData.primaryKeyName] !== ownersId) {
|
|
454
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, "the row's id isn't derived from its owner and key");
|
|
455
|
+
}
|
|
456
|
+
const { signer, signerRole } = await requireSigner(tableName, stored, context);
|
|
457
|
+
if (signer !== owner) {
|
|
458
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, `signed by ${signer}, not by its owner ${owner}`);
|
|
459
|
+
}
|
|
460
|
+
// Members write through their devices only as Writers; a Reader's row relayed by a
|
|
461
|
+
// Writer's device is refused too.
|
|
462
|
+
if (signerRole < group_member_roles_1.GroupMemberRole.Writer) {
|
|
463
|
+
throw (0, write_checks_1.writeNotAuthorized)(tableName, `${owner} isn't a Writer in the group`);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* The write check the host runs on every write to a table in a group context: the check
|
|
469
|
+
* registered under the table's physical name, or, for a signed package table, its
|
|
470
|
+
* {@link signedTableWriteCheck}, registered under that name on first use so the same check
|
|
471
|
+
* serves later lookups. The host calls this when it builds a table's data source, and the
|
|
472
|
+
* isolated table gateway when it registers a package's tables, so a signed table is checked
|
|
473
|
+
* whether its definition arrived with the package or through sync. Generated checks are reused
|
|
474
|
+
* only for the same primary key, owner field, key fields, and fields; unsigned metadata never
|
|
475
|
+
* inherits another group's check.
|
|
476
|
+
*
|
|
477
|
+
* @param metaData - The table's metadata.
|
|
478
|
+
* @returns The check, or undefined for a table without one.
|
|
479
|
+
*/
|
|
480
|
+
function tableWriteCheckFor(metaData) {
|
|
481
|
+
const tableName = (0, utils_1.getFullTableName)(metaData);
|
|
482
|
+
const registered = (0, write_checks_1.getTableWriteCheck)(tableName);
|
|
483
|
+
if (registered && !signedCheckMetadata.has(registered))
|
|
484
|
+
return registered;
|
|
485
|
+
if (!metaData.signedBy)
|
|
486
|
+
return undefined;
|
|
487
|
+
const previous = registered && signedCheckMetadata.get(registered);
|
|
488
|
+
if (previous &&
|
|
489
|
+
previous.primaryKeyName === metaData.primaryKeyName &&
|
|
490
|
+
previous.signedBy === metaData.signedBy &&
|
|
491
|
+
(0, lodash_1.isEqual)(previous.signedKey, metaData.signedKey) &&
|
|
492
|
+
(0, lodash_1.isEqual)(previous.fields, metaData.fields)) {
|
|
493
|
+
return registered;
|
|
494
|
+
}
|
|
495
|
+
const check = signedTableWriteCheck(metaData);
|
|
496
|
+
signedCheckMetadata.set(check, metaData);
|
|
497
|
+
(0, write_checks_1.registerTableWriteCheck)(tableName, check);
|
|
498
|
+
return check;
|
|
499
|
+
}
|
|
@@ -36,3 +36,33 @@ export interface IResignGroupAuthorityOptions {
|
|
|
36
36
|
* @param opts - Limit to rows signed by one key.
|
|
37
37
|
*/
|
|
38
38
|
export declare function resignGroupAuthority(dataContext: DataContext, opts?: IResignGroupAuthorityOptions): Promise<IResignGroupAuthorityResult>;
|
|
39
|
+
/** What {@link resignSignedPackageRows} did. */
|
|
40
|
+
export interface IResignSignedPackageRowsResult {
|
|
41
|
+
/** Rows re-signed, by the table's physical name (`<name>_<packageId>`). */
|
|
42
|
+
resigned: Record<string, number>;
|
|
43
|
+
/** Rows left as they were, with why; no `recordId` when the whole table couldn't be opened. */
|
|
44
|
+
refused: {
|
|
45
|
+
table: string;
|
|
46
|
+
recordId?: string;
|
|
47
|
+
error: string;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Re-sign this user's rows in a group's signed package tables (`defineTable({ signedBy })`)
|
|
52
|
+
* that `signedByKey` signed, with `secretKey`. Key rotation calls it with the retired key and
|
|
53
|
+
* the new one: every device verifies a signed row against its owner's current key, so rows
|
|
54
|
+
* signed only by a retired key would stop verifying on devices that don't have them yet.
|
|
55
|
+
*
|
|
56
|
+
* Each row is signed again unchanged, at a new signing time, through its table's own save
|
|
57
|
+
* ({@link SignedPackageTable.resign}), so the group's write check judges the new signature.
|
|
58
|
+
* Tables come from the group's stored definitions, so a package that isn't loaded is covered.
|
|
59
|
+
* Rows a table refuses are left as they were and reported.
|
|
60
|
+
*
|
|
61
|
+
* @param dataContext - The group's context (not the personal context, where nothing is checked).
|
|
62
|
+
* @param opts.signedByKey - The retired public key: only rows it validly signed are re-signed.
|
|
63
|
+
* @param opts.secretKey - The new secret key.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resignSignedPackageRows(dataContext: DataContext, opts: {
|
|
66
|
+
signedByKey: string;
|
|
67
|
+
secretKey: string;
|
|
68
|
+
}): Promise<IResignSignedPackageRowsResult>;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resignGroupAuthority = resignGroupAuthority;
|
|
4
|
+
exports.resignSignedPackageRows = resignSignedPackageRows;
|
|
4
5
|
const keys_1 = require("../keys");
|
|
6
|
+
const signed_package_table_1 = require("./orm/signed-package-table");
|
|
5
7
|
const write_checks_1 = require("./orm/write-checks");
|
|
8
|
+
const package_table_identity_1 = require("./package-table-identity");
|
|
6
9
|
/**
|
|
7
10
|
* Re-sign the rows that carry a group's authority (the group row, memberships, packages and
|
|
8
11
|
* package versions) with this device's current key, so they count under the group's verified
|
|
@@ -72,3 +75,74 @@ async function resignGroupAuthority(dataContext, opts = {}) {
|
|
|
72
75
|
await resign(versions, await versions.list());
|
|
73
76
|
return result;
|
|
74
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Re-sign this user's rows in a group's signed package tables (`defineTable({ signedBy })`)
|
|
80
|
+
* that `signedByKey` signed, with `secretKey`. Key rotation calls it with the retired key and
|
|
81
|
+
* the new one: every device verifies a signed row against its owner's current key, so rows
|
|
82
|
+
* signed only by a retired key would stop verifying on devices that don't have them yet.
|
|
83
|
+
*
|
|
84
|
+
* Each row is signed again unchanged, at a new signing time, through its table's own save
|
|
85
|
+
* ({@link SignedPackageTable.resign}), so the group's write check judges the new signature.
|
|
86
|
+
* Tables come from the group's stored definitions, so a package that isn't loaded is covered.
|
|
87
|
+
* Rows a table refuses are left as they were and reported.
|
|
88
|
+
*
|
|
89
|
+
* @param dataContext - The group's context (not the personal context, where nothing is checked).
|
|
90
|
+
* @param opts.signedByKey - The retired public key: only rows it validly signed are re-signed.
|
|
91
|
+
* @param opts.secretKey - The new secret key.
|
|
92
|
+
*/
|
|
93
|
+
async function resignSignedPackageRows(dataContext, opts) {
|
|
94
|
+
if (!dataContext.groupId) {
|
|
95
|
+
throw new Error("resignSignedPackageRows needs a group context");
|
|
96
|
+
}
|
|
97
|
+
const { TableDefinitions } = await Promise.resolve().then(() => require("./table-definitions-table"));
|
|
98
|
+
const userId = dataContext.userContext.userId;
|
|
99
|
+
const result = { resigned: {}, refused: [] };
|
|
100
|
+
const definitions = (await TableDefinitions(dataContext).list()).filter((record) => !record.deleted && record.metaData?.signedBy);
|
|
101
|
+
for (const definition of definitions) {
|
|
102
|
+
const { name } = definition;
|
|
103
|
+
let table;
|
|
104
|
+
let rows;
|
|
105
|
+
try {
|
|
106
|
+
table = await openTable(dataContext, definition);
|
|
107
|
+
if (!(table instanceof signed_package_table_1.SignedPackageTable)) {
|
|
108
|
+
throw new Error("not a signed table on this device");
|
|
109
|
+
}
|
|
110
|
+
rows = await table.list({ [table.ownerField]: userId });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
result.refused.push({ table: name, error: String(error) });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Only rows that key really signed: a row can name any key in its signature field.
|
|
117
|
+
for (const row of rows.filter((row) => (0, keys_1.signedByKey)(row) === opts.signedByKey)) {
|
|
118
|
+
try {
|
|
119
|
+
await signed_package_table_1.SignedPackageTable.resign(table, row, opts.secretKey);
|
|
120
|
+
result.resigned[name] = (result.resigned[name] ?? 0) + 1;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
result.refused.push({
|
|
124
|
+
table: name,
|
|
125
|
+
recordId: row[table.primaryKeyName],
|
|
126
|
+
error: String(error),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The table this device stores the definition's rows through: the loaded package's, else the
|
|
135
|
+
* fallback over the stored definition. Always the live binding, even when a newer definition is
|
|
136
|
+
* stored: its rows are stored in its form (an older table refuses rows with newer columns), and
|
|
137
|
+
* a second source over the newer definition would rebuild the SQL table under the live one and
|
|
138
|
+
* write outside the mutation queue sync uses.
|
|
139
|
+
*/
|
|
140
|
+
async function openTable(dataContext, definition) {
|
|
141
|
+
(0, package_table_identity_1.validateTableDefinitionRecord)(definition);
|
|
142
|
+
try {
|
|
143
|
+
return dataContext.tableContainer.getTableByName(definition.name);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return dataContext.tableContainer.getFallbackTable(definition.name);
|
|
147
|
+
}
|
|
148
|
+
}
|
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);
|