@peers-app/peers-sdk 0.26.4 → 0.26.6
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/__tests__/contract-resolvers.test.js +24 -0
- package/dist/contracts/__tests__/legacy-removal.test.js +38 -0
- package/dist/contracts/__tests__/remote-contract-transport.test.js +23 -32
- package/dist/contracts/__tests__/system-contracts.test.js +80 -0
- package/dist/contracts/contract-resolvers.d.ts +1 -1
- package/dist/contracts/contract-resolvers.js +3 -2
- package/dist/contracts/create-local-consumer.d.ts +2 -1
- package/dist/contracts/create-local-consumer.js +1 -1
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/remote-contract-transport.d.ts +3 -3
- package/dist/contracts/remote-contract-transport.js +19 -17
- package/dist/contracts/system/host-shell-contract.d.ts +191 -0
- package/dist/contracts/system/host-shell-contract.js +190 -0
- package/dist/contracts/system/index.d.ts +4 -0
- package/dist/contracts/system/index.js +4 -0
- package/dist/contracts/system/packages-contract.d.ts +257 -0
- package/dist/contracts/system/packages-contract.js +213 -0
- package/dist/data/create-group.d.ts +1 -1
- package/dist/data/create-group.js +1 -1
- package/dist/data/group-members.d.ts +6 -1
- package/dist/data/group-members.js +12 -4
- package/dist/data/groups.d.ts +6 -1
- package/dist/data/groups.js +12 -4
- package/dist/data/package-version-resolver.d.ts +8 -2
- package/dist/data/package-version-resolver.js +28 -2
- package/dist/data/package-version-resolver.test.js +42 -0
- package/dist/data/package-versions.d.ts +5 -1
- package/dist/data/package-versions.js +11 -4
- package/dist/data/packages.d.ts +7 -1
- package/dist/data/packages.js +13 -4
- package/dist/data/table-signing.test.d.ts +1 -0
- package/dist/data/table-signing.test.js +118 -0
- package/dist/data/users.d.ts +1 -2
- package/dist/data/users.js +8 -5
- package/dist/package-installer/package-author-signer.d.ts +19 -0
- package/dist/package-installer/package-author-signer.js +24 -0
- package/dist/package-installer/package-creator.d.ts +1 -7
- package/dist/package-installer/package-creator.js +0 -12
- package/dist/package-installer/package-creator.test.js +0 -16
- package/dist/package-installer/package-installer.d.ts +5 -0
- package/dist/package-installer/package-installer.js +60 -37
- package/dist/package-installer/package-installer.test.js +129 -0
- package/dist/package-installer/types.d.ts +5 -11
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-runtime.d.ts +18 -0
- package/dist/package-loader/package-loader.d.ts +67 -4
- package/dist/package-loader/package-loader.js +300 -52
- package/dist/package-loader/package-loader.test.js +413 -1
- package/dist/package-loader/package-runtime-state.d.ts +112 -0
- package/dist/package-loader/package-runtime-state.js +18 -0
- package/dist/package-loader/resolve-consumed-contracts.js +5 -4
- package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
- package/dist/rpc-types.d.ts +0 -121
- package/dist/rpc-types.js +0 -39
- package/dist/system-ids.d.ts +13 -0
- package/dist/system-ids.js +14 -1
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PACKAGES_CONTRACT_VERSION = void 0;
|
|
4
|
+
exports.packageVersionPromotionTargets = packageVersionPromotionTargets;
|
|
5
|
+
exports.isPackageRepositoryUrl = isPackageRepositoryUrl;
|
|
6
|
+
exports.getPackagesContractDefinition = getPackagesContractDefinition;
|
|
7
|
+
const system_ids_1 = require("../../system-ids");
|
|
8
|
+
const field_type_1 = require("../../types/field-type");
|
|
9
|
+
const system_contracts_1 = require("../system-contracts");
|
|
10
|
+
/** Version of the built-in System Packages contract. */
|
|
11
|
+
exports.PACKAGES_CONTRACT_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Release channels a version may be promoted to from its current tag: `dev` to `beta` or
|
|
14
|
+
* `stable`, and `beta` to `stable`. Missing or empty tags on legacy versions mean `dev`,
|
|
15
|
+
* matching the Versions screen. Promotion never demotes.
|
|
16
|
+
*
|
|
17
|
+
* @param versionTag - The version's current tag.
|
|
18
|
+
*/
|
|
19
|
+
function packageVersionPromotionTargets(versionTag) {
|
|
20
|
+
switch (versionTag || "dev") {
|
|
21
|
+
case "dev":
|
|
22
|
+
return ["beta", "stable"];
|
|
23
|
+
case "beta":
|
|
24
|
+
return ["stable"];
|
|
25
|
+
default:
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether user input names a remote repository for `clonePackage` (`http(s)://` or
|
|
31
|
+
* `git@host:`) rather than a local folder or package name. The host still validates the
|
|
32
|
+
* URL; this only routes input in the CLI and UI the same way.
|
|
33
|
+
*
|
|
34
|
+
* @param target - Input as typed.
|
|
35
|
+
*/
|
|
36
|
+
function isPackageRepositoryUrl(target) {
|
|
37
|
+
return /^(https?:\/\/|git@[^:/\s]+:)/.test(target.trim());
|
|
38
|
+
}
|
|
39
|
+
const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
|
|
40
|
+
const packageIdField = field("packageId", field_type_1.FieldType.id, "Package id.");
|
|
41
|
+
const operationResultFields = [
|
|
42
|
+
packageIdField,
|
|
43
|
+
field("created", field_type_1.FieldType.boolean, "True when a new package was scaffolded.", {
|
|
44
|
+
optional: true,
|
|
45
|
+
}),
|
|
46
|
+
field("warning", field_type_1.FieldType.string, "Non-fatal problem to show the caller.", { optional: true }),
|
|
47
|
+
field("package", field_type_1.FieldType.object, "The package after the operation."),
|
|
48
|
+
];
|
|
49
|
+
let definition;
|
|
50
|
+
/**
|
|
51
|
+
* Return the System Packages contract shape.
|
|
52
|
+
*
|
|
53
|
+
* Tools carry no `remoteAccessLevel`, so only callers this device trusts as `Self` (the
|
|
54
|
+
* user's own devices and users explicitly granted Self) can reach them remotely; group
|
|
55
|
+
* members, Admins included, are refused. Package code and workflows are refused
|
|
56
|
+
* everywhere. The provider binds every call to the route's
|
|
57
|
+
* data context; a package's group is the data boundary. Tools the host cannot perform
|
|
58
|
+
* throw `System Packages <tool> is not supported on this host (...)`; `getCapabilities`
|
|
59
|
+
* reports support up front.
|
|
60
|
+
*/
|
|
61
|
+
function getPackagesContractDefinition() {
|
|
62
|
+
definition ??= {
|
|
63
|
+
contractId: system_ids_1.packagesContractId,
|
|
64
|
+
version: exports.PACKAGES_CONTRACT_VERSION,
|
|
65
|
+
devTag: "dev",
|
|
66
|
+
name: "System Packages",
|
|
67
|
+
description: "Install, inspect, reload, activate, and uninstall packages, with each device's observed runtime state.",
|
|
68
|
+
tables: [],
|
|
69
|
+
tools: [
|
|
70
|
+
{
|
|
71
|
+
name: "getCapabilities",
|
|
72
|
+
usageDescription: "Report which package operations this host supports.",
|
|
73
|
+
inputFields: [],
|
|
74
|
+
outputFields: [field("capabilities", field_type_1.FieldType.object, "Supported operations.")],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "listPackages",
|
|
78
|
+
usageDescription: "List packages in this data context with their requested version and runtime state on this device.",
|
|
79
|
+
inputFields: [],
|
|
80
|
+
outputFields: [
|
|
81
|
+
field("packages", field_type_1.FieldType.object, "Package summaries.", { isArray: true }),
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "getPackage",
|
|
86
|
+
usageDescription: "Return one package with its versions, local folder, and runtime state.",
|
|
87
|
+
inputFields: [packageIdField],
|
|
88
|
+
outputFields: [field("package", field_type_1.FieldType.object, "Package details.")],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "importPackage",
|
|
92
|
+
usageDescription: "Import an existing package folder on this device and load it.",
|
|
93
|
+
inputFields: [
|
|
94
|
+
field("path", field_type_1.FieldType.string, "Absolute or ~-prefixed folder path on the host."),
|
|
95
|
+
field("name", field_type_1.FieldType.string, "Display name; defaults to package.json.", {
|
|
96
|
+
optional: true,
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
outputFields: operationResultFields,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "createPackage",
|
|
103
|
+
usageDescription: "Scaffold a new package from the template, or import the folder when it already exists.",
|
|
104
|
+
inputFields: [
|
|
105
|
+
field("name", field_type_1.FieldType.string, "Package display name."),
|
|
106
|
+
field("location", field_type_1.FieldType.string, "Folder; defaults to the packages directory.", {
|
|
107
|
+
optional: true,
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
outputFields: operationResultFields,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "clonePackage",
|
|
114
|
+
usageDescription: "Clone a package repository, build it, and import it.",
|
|
115
|
+
inputFields: [field("url", field_type_1.FieldType.string, "Git repository URL.")],
|
|
116
|
+
outputFields: operationResultFields,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "installPackage",
|
|
120
|
+
usageDescription: "Re-enable a disabled package, or copy one here from another of your data contexts, then load it.",
|
|
121
|
+
inputFields: [
|
|
122
|
+
packageIdField,
|
|
123
|
+
field("fromDataContextId", field_type_1.FieldType.id, "Context to copy from; searched when omitted.", {
|
|
124
|
+
optional: true,
|
|
125
|
+
}),
|
|
126
|
+
],
|
|
127
|
+
outputFields: operationResultFields,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "activatePackageVersion",
|
|
131
|
+
usageDescription: "Select a stored version for this device or the whole group, then load it on this device.",
|
|
132
|
+
inputFields: [
|
|
133
|
+
packageIdField,
|
|
134
|
+
field("packageVersionId", field_type_1.FieldType.id, "Version to activate; omit with device scope to follow the group again.", { optional: true }),
|
|
135
|
+
field("scope", field_type_1.FieldType.string, "device (default) or group.", { optional: true }),
|
|
136
|
+
],
|
|
137
|
+
outputFields: operationResultFields,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "reloadPackage",
|
|
141
|
+
usageDescription: "Rebuild the dev version from the package folder when it changed, then force-reload the package on this device.",
|
|
142
|
+
inputFields: [packageIdField],
|
|
143
|
+
outputFields: operationResultFields,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "reloadAllPackages",
|
|
147
|
+
usageDescription: "Re-read every package folder on this device and reload the packages whose bundles changed.",
|
|
148
|
+
inputFields: [],
|
|
149
|
+
outputFields: [
|
|
150
|
+
field("skipped", field_type_1.FieldType.string, "Why the host skipped the reload.", {
|
|
151
|
+
optional: true,
|
|
152
|
+
}),
|
|
153
|
+
field("results", field_type_1.FieldType.object, "Per-package outcomes.", { isArray: true }),
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "linkRemoteRepo",
|
|
158
|
+
usageDescription: "Create a remote repository for a local package and link it.",
|
|
159
|
+
inputFields: [packageIdField],
|
|
160
|
+
outputFields: operationResultFields,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "uninstallPackage",
|
|
164
|
+
usageDescription: "Remove a package from this data context for every member device and unload it here. Versions, files, and device preferences are kept.",
|
|
165
|
+
inputFields: [packageIdField],
|
|
166
|
+
outputFields: [
|
|
167
|
+
packageIdField,
|
|
168
|
+
field("removed", field_type_1.FieldType.boolean, "True when the package row was removed."),
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "promotePackageVersion",
|
|
173
|
+
usageDescription: "Promote a stored package version to beta or stable (dev to beta or stable, beta to stable). Re-signs it with the package author key when this device has it, records the promotion in its history, and activates it when this device was running it and the group follows the new tag.",
|
|
174
|
+
inputFields: [
|
|
175
|
+
field("packageVersionId", field_type_1.FieldType.id, "Version to promote."),
|
|
176
|
+
field("versionTag", field_type_1.FieldType.string, "New release channel: beta or stable."),
|
|
177
|
+
],
|
|
178
|
+
outputFields: [
|
|
179
|
+
...operationResultFields,
|
|
180
|
+
field("packageVersionId", field_type_1.FieldType.id, "Promoted version."),
|
|
181
|
+
field("version", field_type_1.FieldType.string, "Its version number."),
|
|
182
|
+
field("versionTag", field_type_1.FieldType.string, "Its new release channel."),
|
|
183
|
+
field("signed", field_type_1.FieldType.boolean, "True when this device signed the promoted version."),
|
|
184
|
+
field("activated", field_type_1.FieldType.boolean, "True when the promotion also activated the version for the group or this device."),
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "installDefaultPackages",
|
|
189
|
+
usageDescription: "Install the default packages into this data context.",
|
|
190
|
+
inputFields: [],
|
|
191
|
+
outputFields: [
|
|
192
|
+
field("installed", field_type_1.FieldType.boolean, "True when anything was installed or updated."),
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
observables: [
|
|
197
|
+
{
|
|
198
|
+
name: "runtimeStates",
|
|
199
|
+
description: "Runtime state of this context's packages on this device, keyed by package id.",
|
|
200
|
+
valueType: field_type_1.FieldType.object,
|
|
201
|
+
writable: false,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
events: [],
|
|
205
|
+
};
|
|
206
|
+
return definition;
|
|
207
|
+
}
|
|
208
|
+
(0, system_contracts_1.registerSystemContract)({
|
|
209
|
+
providerPackageId: system_ids_1.peersSystemPackageId,
|
|
210
|
+
contractId: system_ids_1.packagesContractId,
|
|
211
|
+
version: exports.PACKAGES_CONTRACT_VERSION,
|
|
212
|
+
getDefinition: getPackagesContractDefinition,
|
|
213
|
+
});
|
|
@@ -19,6 +19,6 @@ export declare const GROUP_NAME_MAX_LENGTH = 100;
|
|
|
19
19
|
*
|
|
20
20
|
* Runs on a host with signing enabled (`GroupsTable.enableGroupSigning`); UIs reach it
|
|
21
21
|
* through the host rather than duplicating the steps. Package seeding for the new context
|
|
22
|
-
* (for example `
|
|
22
|
+
* (for example System Packages `installDefaultPackages`) is the caller's responsibility.
|
|
23
23
|
*/
|
|
24
24
|
export declare function createGroup(userContext: UserContext, input: CreateGroupInput): Promise<IGroup>;
|
|
@@ -20,7 +20,7 @@ exports.GROUP_NAME_MAX_LENGTH = 100;
|
|
|
20
20
|
*
|
|
21
21
|
* Runs on a host with signing enabled (`GroupsTable.enableGroupSigning`); UIs reach it
|
|
22
22
|
* through the host rather than duplicating the steps. Package seeding for the new context
|
|
23
|
-
* (for example `
|
|
23
|
+
* (for example System Packages `installDefaultPackages`) is the caller's responsibility.
|
|
24
24
|
*/
|
|
25
25
|
async function createGroup(userContext, input) {
|
|
26
26
|
const name = input.name.trim();
|
|
@@ -3,6 +3,7 @@ import type { DataContext } from "../context/data-context";
|
|
|
3
3
|
import { GroupMemberRole } from "./group-member-roles";
|
|
4
4
|
import type { ISaveOptions } from "./orm/data-query";
|
|
5
5
|
import { Table } from "./orm/table";
|
|
6
|
+
/** Schema for a signed group membership, including its role and soft deletion state. */
|
|
6
7
|
export declare const groupMemberSchema: z.ZodObject<{
|
|
7
8
|
groupMemberId: z.ZodEffects<z.ZodString, string, string>;
|
|
8
9
|
groupId: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -25,18 +26,22 @@ export declare const groupMemberSchema: z.ZodObject<{
|
|
|
25
26
|
groupMemberId: string;
|
|
26
27
|
deleted?: boolean | undefined;
|
|
27
28
|
}>;
|
|
29
|
+
/** A signed association between a user and a group with an assigned role. */
|
|
28
30
|
export type IGroupMember = z.infer<typeof groupMemberSchema>;
|
|
31
|
+
/** System table name for group membership records. */
|
|
29
32
|
export declare const groupMembersTableName = "GroupMembers";
|
|
33
|
+
/** Group membership table with signature checks and signed soft deletion. */
|
|
30
34
|
export declare class GroupMembersTable extends Table<IGroupMember> {
|
|
31
35
|
static isPassthrough: boolean;
|
|
32
36
|
save(groupMember: IGroupMember, opts?: ISaveOptions): Promise<IGroupMember>;
|
|
33
37
|
signAndSave(groupMember: IGroupMember, opts?: ISaveOptions): Promise<IGroupMember>;
|
|
34
38
|
delete(groupMemberId: string): Promise<void>;
|
|
35
|
-
|
|
39
|
+
/** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
|
|
36
40
|
static enableGroupMemberSigning(fn: (groupMember: IGroupMember) => IGroupMember): void;
|
|
37
41
|
/** @deprecated Forbidden on GroupsTable; use save() */
|
|
38
42
|
insert(..._args: Parameters<Table<IGroupMember>["insert"]>): never;
|
|
39
43
|
/** @deprecated Forbidden on GroupsTable; use save() */
|
|
40
44
|
update(..._args: Parameters<Table<IGroupMember>["update"]>): never;
|
|
41
45
|
}
|
|
46
|
+
/** Returns the GroupMembers table for the given context or the current default context. */
|
|
42
47
|
export declare function GroupMembers(dataContext?: DataContext): GroupMembersTable;
|
|
@@ -46,6 +46,7 @@ const table_1 = require("./orm/table");
|
|
|
46
46
|
const table_definitions_system_1 = require("./orm/table-definitions.system");
|
|
47
47
|
const types_1 = require("./orm/types");
|
|
48
48
|
const users_1 = require("./users");
|
|
49
|
+
/** Schema for a signed group membership, including its role and soft deletion state. */
|
|
49
50
|
exports.groupMemberSchema = zod_1.z.object({
|
|
50
51
|
groupMemberId: zod_types_1.zodPeerId,
|
|
51
52
|
groupId: zod_types_1.zodPeerId.describe("The id of the group"),
|
|
@@ -59,6 +60,7 @@ exports.groupMemberSchema = zod_1.z.object({
|
|
|
59
60
|
.optional()
|
|
60
61
|
.describe("Whether this membership has been deleted (soft delete)"),
|
|
61
62
|
});
|
|
63
|
+
/** System table name for group membership records. */
|
|
62
64
|
exports.groupMembersTableName = "GroupMembers";
|
|
63
65
|
const metaData = {
|
|
64
66
|
name: exports.groupMembersTableName,
|
|
@@ -67,6 +69,10 @@ const metaData = {
|
|
|
67
69
|
fields: (0, types_1.schemaToFields)(exports.groupMemberSchema),
|
|
68
70
|
indexes: [{ fields: ["groupId", "userId"], unique: true }],
|
|
69
71
|
};
|
|
72
|
+
// Symbol.for keeps signer registration shared by separately installed SDK copies.
|
|
73
|
+
const signingKey = Symbol.for("@peers-app/peers-sdk/group-member-signing");
|
|
74
|
+
const signingGlobals = globalThis;
|
|
75
|
+
/** Group membership table with signature checks and signed soft deletion. */
|
|
70
76
|
let GroupMembersTable = (() => {
|
|
71
77
|
let _classSuper = table_1.Table;
|
|
72
78
|
let _instanceExtraInitializers = [];
|
|
@@ -96,10 +102,11 @@ let GroupMembersTable = (() => {
|
|
|
96
102
|
throw new Error("Group member signature is not valid");
|
|
97
103
|
}
|
|
98
104
|
async signAndSave(groupMember, opts) {
|
|
99
|
-
|
|
105
|
+
const sign = signingGlobals[signingKey];
|
|
106
|
+
if (!sign) {
|
|
100
107
|
throw new Error("Group member signing must be enabled to sign and save group members. Call GroupMembersTable.enableGroupMemberSigning(fn) to enable it.");
|
|
101
108
|
}
|
|
102
|
-
groupMember =
|
|
109
|
+
groupMember = sign.call(GroupMembersTable, groupMember);
|
|
103
110
|
return this.save(groupMember, {
|
|
104
111
|
saveAsSnapshot: true,
|
|
105
112
|
restoreIfDeleted: true,
|
|
@@ -118,9 +125,9 @@ let GroupMembersTable = (() => {
|
|
|
118
125
|
await this.signAndSave(groupMember);
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
|
-
|
|
128
|
+
/** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
|
|
122
129
|
static enableGroupMemberSigning(fn) {
|
|
123
|
-
|
|
130
|
+
signingGlobals[signingKey] = fn;
|
|
124
131
|
}
|
|
125
132
|
/** @deprecated Forbidden on GroupsTable; use save() */
|
|
126
133
|
insert(..._args) {
|
|
@@ -138,6 +145,7 @@ let GroupMembersTable = (() => {
|
|
|
138
145
|
})();
|
|
139
146
|
exports.GroupMembersTable = GroupMembersTable;
|
|
140
147
|
(0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.groupMemberSchema, GroupMembersTable);
|
|
148
|
+
/** Returns the GroupMembers table for the given context or the current default context. */
|
|
141
149
|
function GroupMembers(dataContext) {
|
|
142
150
|
return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.groupMemberSchema, GroupMembersTable);
|
|
143
151
|
}
|
package/dist/data/groups.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { DataContext } from "../context/data-context";
|
|
|
3
3
|
import { GroupMemberRole } from "./group-member-roles";
|
|
4
4
|
import type { ISaveOptions } from "./orm/data-query";
|
|
5
5
|
import { Table } from "./orm/table";
|
|
6
|
+
/** Schema for a signed group record, including membership policy and group public keys. */
|
|
6
7
|
export declare const groupSchema: z.ZodObject<{
|
|
7
8
|
groupId: z.ZodEffects<z.ZodString, string, string>;
|
|
8
9
|
name: z.ZodString;
|
|
@@ -37,18 +38,22 @@ export declare const groupSchema: z.ZodObject<{
|
|
|
37
38
|
iconClassName?: string | undefined;
|
|
38
39
|
publicRole?: GroupMemberRole | undefined;
|
|
39
40
|
}>;
|
|
41
|
+
/** A signed group record shared between personal and group contexts. */
|
|
40
42
|
export type IGroup = z.infer<typeof groupSchema>;
|
|
43
|
+
/** System table name for group records. */
|
|
41
44
|
export declare const groupsTableName = "Groups";
|
|
45
|
+
/** Group table with signature enforcement outside the personal context. */
|
|
42
46
|
export declare class GroupsTable extends Table<IGroup> {
|
|
43
47
|
static isPassthrough: boolean;
|
|
44
48
|
save(group: IGroup, opts?: ISaveOptions): Promise<IGroup>;
|
|
45
49
|
signAndSave(group: IGroup, opts?: ISaveOptions): Promise<IGroup>;
|
|
46
50
|
delete(groupId: string): Promise<void>;
|
|
47
|
-
|
|
51
|
+
/** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
|
|
48
52
|
static enableGroupSigning(fn: (group: IGroup) => IGroup): void;
|
|
49
53
|
/** @deprecated Direct inserts forbidden; use save() */
|
|
50
54
|
insert(..._args: Parameters<Table<IGroup>["insert"]>): never;
|
|
51
55
|
/** @deprecated Direct updates forbidden; use save() */
|
|
52
56
|
update(..._args: Parameters<Table<IGroup>["update"]>): never;
|
|
53
57
|
}
|
|
58
|
+
/** Returns the Groups table for the given context or the current default context. */
|
|
54
59
|
export declare function Groups(dataContext?: DataContext): GroupsTable;
|
package/dist/data/groups.js
CHANGED
|
@@ -45,6 +45,7 @@ const decorators_1 = require("./orm/decorators");
|
|
|
45
45
|
const table_1 = require("./orm/table");
|
|
46
46
|
const table_definitions_system_1 = require("./orm/table-definitions.system");
|
|
47
47
|
const types_1 = require("./orm/types");
|
|
48
|
+
/** Schema for a signed group record, including membership policy and group public keys. */
|
|
48
49
|
exports.groupSchema = zod_1.z.object({
|
|
49
50
|
groupId: zod_types_1.zodPeerId,
|
|
50
51
|
name: zod_1.z.string(),
|
|
@@ -67,6 +68,7 @@ exports.groupSchema = zod_1.z.object({
|
|
|
67
68
|
.string()
|
|
68
69
|
.describe("The public key used to encrypt data that only group members with writer permissions or higher can decrypt"),
|
|
69
70
|
});
|
|
71
|
+
/** System table name for group records. */
|
|
70
72
|
exports.groupsTableName = "Groups";
|
|
71
73
|
const metaData = {
|
|
72
74
|
name: exports.groupsTableName,
|
|
@@ -76,6 +78,10 @@ const metaData = {
|
|
|
76
78
|
iconClassName: "bi bi-people-fill",
|
|
77
79
|
indexes: [{ fields: ["name"] }],
|
|
78
80
|
};
|
|
81
|
+
// Symbol.for keeps signer registration shared by separately installed SDK copies.
|
|
82
|
+
const signingKey = Symbol.for("@peers-app/peers-sdk/group-signing");
|
|
83
|
+
const signingGlobals = globalThis;
|
|
84
|
+
/** Group table with signature enforcement outside the personal context. */
|
|
79
85
|
let GroupsTable = (() => {
|
|
80
86
|
let _classSuper = table_1.Table;
|
|
81
87
|
let _instanceExtraInitializers = [];
|
|
@@ -103,10 +109,11 @@ let GroupsTable = (() => {
|
|
|
103
109
|
return super.save(group, opts);
|
|
104
110
|
}
|
|
105
111
|
async signAndSave(group, opts) {
|
|
106
|
-
|
|
112
|
+
const sign = signingGlobals[signingKey];
|
|
113
|
+
if (!sign) {
|
|
107
114
|
throw new Error("Group signing must be enabled to sign and save groups. Call GroupsTable.enableGroupSigning(fn) to enable it.");
|
|
108
115
|
}
|
|
109
|
-
group =
|
|
116
|
+
group = sign.call(GroupsTable, group);
|
|
110
117
|
return this.save(group, opts);
|
|
111
118
|
}
|
|
112
119
|
async delete(groupId) {
|
|
@@ -117,9 +124,9 @@ let GroupsTable = (() => {
|
|
|
117
124
|
}
|
|
118
125
|
throw new Error("Only deleting groups from the user context Groups table is allowed");
|
|
119
126
|
}
|
|
120
|
-
|
|
127
|
+
/** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
|
|
121
128
|
static enableGroupSigning(fn) {
|
|
122
|
-
|
|
129
|
+
signingGlobals[signingKey] = fn;
|
|
123
130
|
}
|
|
124
131
|
/** @deprecated Direct inserts forbidden; use save() */
|
|
125
132
|
insert(..._args) {
|
|
@@ -137,6 +144,7 @@ let GroupsTable = (() => {
|
|
|
137
144
|
})();
|
|
138
145
|
exports.GroupsTable = GroupsTable;
|
|
139
146
|
(0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.groupSchema, GroupsTable);
|
|
147
|
+
/** Returns the Groups table for the given context or the current default context. */
|
|
140
148
|
function Groups(dataContext) {
|
|
141
149
|
return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.groupSchema, GroupsTable);
|
|
142
150
|
}
|
|
@@ -81,17 +81,23 @@ export interface IResolveResult {
|
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
83
|
* Core resolver: evaluates available package versions against the device's
|
|
84
|
-
* follow policy and loads the best one.
|
|
85
|
-
*
|
|
84
|
+
* follow policy and loads the best one. A dev version runs only on a device whose own
|
|
85
|
+
* selection points at it; a group selection of a dev version is left unloaded here.
|
|
86
86
|
*
|
|
87
87
|
* Callers:
|
|
88
88
|
* - `PackagesTrackedDataSource.applyChanges` (on sync)
|
|
89
89
|
* - `PackageLoader.loadAllPackages` (startup)
|
|
90
90
|
* - `package-installer.ts` (after saving new bundles)
|
|
91
|
+
*
|
|
92
|
+
* @param opts.force - Reload even when the package is already loaded.
|
|
93
|
+
* @param opts.localPath - Package folder whose rebuilt dev bundle wins over the stored one.
|
|
94
|
+
* @param opts.onlyIfChanged - With `force`, keep a running package whose selected version and
|
|
95
|
+
* bundle are unchanged instead of restarting it (sync reconciliation).
|
|
91
96
|
*/
|
|
92
97
|
export declare function resolveDevicePackageVersion(pkg: IPackage, dataContext: DataContext, opts?: {
|
|
93
98
|
force?: boolean;
|
|
94
99
|
localPath?: string;
|
|
100
|
+
onlyIfChanged?: boolean;
|
|
95
101
|
}): Promise<IResolveResult>;
|
|
96
102
|
/**
|
|
97
103
|
* Returns true when the device has an explicit follow-policy override
|
|
@@ -85,13 +85,18 @@ function getEffectivePackagePrefs(pkg, devicePrefs) {
|
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
87
|
* Core resolver: evaluates available package versions against the device's
|
|
88
|
-
* follow policy and loads the best one.
|
|
89
|
-
*
|
|
88
|
+
* follow policy and loads the best one. A dev version runs only on a device whose own
|
|
89
|
+
* selection points at it; a group selection of a dev version is left unloaded here.
|
|
90
90
|
*
|
|
91
91
|
* Callers:
|
|
92
92
|
* - `PackagesTrackedDataSource.applyChanges` (on sync)
|
|
93
93
|
* - `PackageLoader.loadAllPackages` (startup)
|
|
94
94
|
* - `package-installer.ts` (after saving new bundles)
|
|
95
|
+
*
|
|
96
|
+
* @param opts.force - Reload even when the package is already loaded.
|
|
97
|
+
* @param opts.localPath - Package folder whose rebuilt dev bundle wins over the stored one.
|
|
98
|
+
* @param opts.onlyIfChanged - With `force`, keep a running package whose selected version and
|
|
99
|
+
* bundle are unchanged instead of restarting it (sync reconciliation).
|
|
95
100
|
*/
|
|
96
101
|
async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
97
102
|
const pvar = packagePrefsVar(pkg.packageId, dataContext);
|
|
@@ -118,6 +123,21 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
|
118
123
|
if (!effectiveActivePvId) {
|
|
119
124
|
return { loaded: false, upgraded: false };
|
|
120
125
|
}
|
|
126
|
+
// A dev version runs only on a device that activated it. The group's selection can be a
|
|
127
|
+
// dev version (a new package, or one copied from another context); other devices leave it
|
|
128
|
+
// unloaded until they activate it themselves. Once activated here, rebuilds of that same
|
|
129
|
+
// version row keep loading through the dev branch below.
|
|
130
|
+
if (rawPrefs.activePackageVersionId !== effectiveActivePvId) {
|
|
131
|
+
const groupSelection = await (0, package_versions_1.PackageVersions)(dataContext)
|
|
132
|
+
.get(effectiveActivePvId)
|
|
133
|
+
.catch(() => undefined);
|
|
134
|
+
if (groupSelection?.versionTag === "dev") {
|
|
135
|
+
await dataContext.packageLoader.unloadPackage(pkg.packageId, {
|
|
136
|
+
reason: `Only a dev version (v${groupSelection.version}) is selected; activate it on this device to run it`,
|
|
137
|
+
});
|
|
138
|
+
return { loaded: false, pv: groupSelection, upgraded: false };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
121
141
|
const localPath = opts?.localPath ??
|
|
122
142
|
(await readStoredPackageLocalPath(pkg.packageId, dataContext).catch(() => undefined));
|
|
123
143
|
const compatibilityTargetId = rawPrefs.compatibilityFallbackForPackageVersionId;
|
|
@@ -130,6 +150,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
|
130
150
|
if (compatibilityTarget) {
|
|
131
151
|
try {
|
|
132
152
|
const restored = await dataContext.packageLoader.loadPackage(pkg, {
|
|
153
|
+
onlyIfChanged: opts?.onlyIfChanged,
|
|
133
154
|
force: true,
|
|
134
155
|
localPath,
|
|
135
156
|
packageVersionId: compatibilityTargetId,
|
|
@@ -158,6 +179,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
|
158
179
|
if (!pv)
|
|
159
180
|
return { loaded: false, pv: undefined, upgraded: false };
|
|
160
181
|
const instance = await dataContext.packageLoader.loadPackage(pkg, {
|
|
182
|
+
onlyIfChanged: opts?.onlyIfChanged,
|
|
161
183
|
force: opts?.force,
|
|
162
184
|
localPath,
|
|
163
185
|
packageVersionId: effectiveActivePvId,
|
|
@@ -182,6 +204,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
|
182
204
|
// activates a non-dev version.
|
|
183
205
|
if (activePv?.versionTag === "dev") {
|
|
184
206
|
const instance = await dataContext.packageLoader.loadPackage(pkg, {
|
|
207
|
+
onlyIfChanged: opts?.onlyIfChanged,
|
|
185
208
|
force: opts?.force,
|
|
186
209
|
localPath,
|
|
187
210
|
packageVersionId: effectiveActivePvId,
|
|
@@ -216,6 +239,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
|
216
239
|
if (!bestPv || bestPv.packageVersionId === effectiveActivePvId) {
|
|
217
240
|
if (activePv) {
|
|
218
241
|
const instance = await dataContext.packageLoader.loadPackage(pkg, {
|
|
242
|
+
onlyIfChanged: opts?.onlyIfChanged,
|
|
219
243
|
force: opts?.force,
|
|
220
244
|
localPath,
|
|
221
245
|
packageVersionId: effectiveActivePvId,
|
|
@@ -233,6 +257,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
|
233
257
|
// A better PV was found - attempt to load it (getFileContents will
|
|
234
258
|
// auto-download missing chunks from peers)
|
|
235
259
|
const instance = await dataContext.packageLoader.loadPackage(pkg, {
|
|
260
|
+
onlyIfChanged: opts?.onlyIfChanged,
|
|
236
261
|
force: true,
|
|
237
262
|
localPath,
|
|
238
263
|
packageVersionId: bestPv.packageVersionId,
|
|
@@ -241,6 +266,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
|
|
|
241
266
|
// Bundle unavailable even after download attempt - keep current version
|
|
242
267
|
if (activePv) {
|
|
243
268
|
const fallbackInstance = await dataContext.packageLoader.loadPackage(pkg, {
|
|
269
|
+
onlyIfChanged: opts?.onlyIfChanged,
|
|
244
270
|
force: opts?.force,
|
|
245
271
|
localPath,
|
|
246
272
|
packageVersionId: effectiveActivePvId,
|
|
@@ -368,6 +368,48 @@ describe("resolveDevicePackageVersion — expected behavior (control)", () => {
|
|
|
368
368
|
expect(loadedPvIds).toContain(stablePv.packageVersionId);
|
|
369
369
|
expect(loadedPvIds).not.toContain(devPv.packageVersionId);
|
|
370
370
|
});
|
|
371
|
+
it.each([
|
|
372
|
+
["no prefs", false],
|
|
373
|
+
["a pin", true],
|
|
374
|
+
])("leaves a group-selected dev version unloaded on a device with %s", async (_label, pinned) => {
|
|
375
|
+
const dc = freshGroupDc();
|
|
376
|
+
const packageId = (0, utils_1.newid)();
|
|
377
|
+
const devPv = makePV(packageId, { version: "0.1.0", versionTag: "dev" });
|
|
378
|
+
await (0, package_versions_1.PackageVersions)(dc).save(devPv);
|
|
379
|
+
const pkg = makePkg({ packageId, activePackageVersionId: devPv.packageVersionId });
|
|
380
|
+
await (0, packages_1.Packages)(dc).save(pkg);
|
|
381
|
+
if (pinned)
|
|
382
|
+
await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { pinned: true }, dc);
|
|
383
|
+
const load = jest.spyOn(dc.packageLoader, "loadPackage");
|
|
384
|
+
const unload = jest
|
|
385
|
+
.spyOn(dc.packageLoader, "unloadPackage")
|
|
386
|
+
.mockImplementation(async () => undefined);
|
|
387
|
+
const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
|
|
388
|
+
expect(result).toMatchObject({ loaded: false, upgraded: false });
|
|
389
|
+
expect(result.pv?.packageVersionId).toBe(devPv.packageVersionId);
|
|
390
|
+
expect(load).not.toHaveBeenCalled();
|
|
391
|
+
expect(unload).toHaveBeenCalledWith(packageId, {
|
|
392
|
+
reason: "Only a dev version (v0.1.0) is selected; activate it on this device to run it",
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
it("runs a group-selected dev version once this device activates it", async () => {
|
|
396
|
+
const dc = freshGroupDc();
|
|
397
|
+
const packageId = (0, utils_1.newid)();
|
|
398
|
+
const devPv = makePV(packageId, { version: "0.1.0", versionTag: "dev" });
|
|
399
|
+
await (0, package_versions_1.PackageVersions)(dc).save(devPv);
|
|
400
|
+
const pkg = makePkg({ packageId, activePackageVersionId: devPv.packageVersionId });
|
|
401
|
+
await (0, packages_1.Packages)(dc).save(pkg);
|
|
402
|
+
await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: devPv.packageVersionId }, dc);
|
|
403
|
+
const loadedPvIds = [];
|
|
404
|
+
jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
|
|
405
|
+
if (opts?.packageVersionId)
|
|
406
|
+
loadedPvIds.push(opts.packageVersionId);
|
|
407
|
+
return { packageId };
|
|
408
|
+
});
|
|
409
|
+
const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
|
|
410
|
+
expect(result.loaded).toBe(true);
|
|
411
|
+
expect(loadedPvIds).toEqual([devPv.packageVersionId]);
|
|
412
|
+
});
|
|
371
413
|
it("installs assistants and workflows into the data context on auto-upgrade", async () => {
|
|
372
414
|
const dc = groupDc();
|
|
373
415
|
const packageId = (0, utils_1.newid)();
|
|
@@ -107,16 +107,19 @@ declare const schema: z.ZodObject<{
|
|
|
107
107
|
uiBundleFileHash?: string | undefined;
|
|
108
108
|
packageAuthorSignature?: string | undefined;
|
|
109
109
|
}>;
|
|
110
|
+
/** A signed package build and its bundle hashes, tag, and lifecycle history. */
|
|
110
111
|
export type IPackageVersion = z.infer<typeof schema>;
|
|
112
|
+
/** Package version table with signature and group promotion permission checks. */
|
|
111
113
|
export declare class PackageVersionsTable extends Table<IPackageVersion> {
|
|
112
114
|
static isPassthrough: boolean;
|
|
113
115
|
insert(packageVersion: IPackageVersion, opts?: ISaveOptions): Promise<IPackageVersion>;
|
|
114
116
|
update(packageVersion: IPackageVersion, opts?: ISaveOptions): Promise<IPackageVersion>;
|
|
115
117
|
save(packageVersion: IPackageVersion, opts?: ISaveOptions): Promise<IPackageVersion>;
|
|
116
118
|
signAndSave(packageVersion: IPackageVersion, opts?: ISaveOptions): Promise<IPackageVersion>;
|
|
117
|
-
|
|
119
|
+
/** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
|
|
118
120
|
static enablePackageVersionSigning(fn: (packageVersion: IPackageVersion) => IPackageVersion): void;
|
|
119
121
|
}
|
|
122
|
+
/** Returns the PackageVersions table for the given context or the current default context. */
|
|
120
123
|
export declare function PackageVersions(dataContext?: DataContext): PackageVersionsTable;
|
|
121
124
|
/**
|
|
122
125
|
* Compute a content hash from bundle file hashes. The hash represents code
|
|
@@ -134,6 +137,7 @@ export declare function PackageVersions(dataContext?: DataContext): PackageVersi
|
|
|
134
137
|
export declare function computePackageVersionHash(version: string,
|
|
135
138
|
/** @deprecated Ignored — will be removed in a future release. */
|
|
136
139
|
_versionTag: string, packageBundleFileHash: string, routesBundleFileHash?: string, uiBundleFileHash?: string): string;
|
|
140
|
+
/** Checks whether an incoming version falls within the selected automatic update range. */
|
|
137
141
|
export declare function isVersionInRange(activeVersion: string, incomingVersion: string, range: "pinned" | "patch" | "minor" | "latest"): boolean;
|
|
138
142
|
/**
|
|
139
143
|
* Returns true if incomingVersion is strictly newer than activeVersion (semver comparison).
|