@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.
Files changed (59) hide show
  1. package/dist/contracts/__tests__/contract-resolvers.test.js +24 -0
  2. package/dist/contracts/__tests__/legacy-removal.test.js +38 -0
  3. package/dist/contracts/__tests__/remote-contract-transport.test.js +23 -32
  4. package/dist/contracts/__tests__/system-contracts.test.js +80 -0
  5. package/dist/contracts/contract-resolvers.d.ts +1 -1
  6. package/dist/contracts/contract-resolvers.js +3 -2
  7. package/dist/contracts/create-local-consumer.d.ts +2 -1
  8. package/dist/contracts/create-local-consumer.js +1 -1
  9. package/dist/contracts/index.d.ts +2 -0
  10. package/dist/contracts/index.js +2 -0
  11. package/dist/contracts/remote-contract-transport.d.ts +3 -3
  12. package/dist/contracts/remote-contract-transport.js +19 -17
  13. package/dist/contracts/system/host-shell-contract.d.ts +191 -0
  14. package/dist/contracts/system/host-shell-contract.js +190 -0
  15. package/dist/contracts/system/index.d.ts +4 -0
  16. package/dist/contracts/system/index.js +4 -0
  17. package/dist/contracts/system/packages-contract.d.ts +257 -0
  18. package/dist/contracts/system/packages-contract.js +213 -0
  19. package/dist/data/create-group.d.ts +1 -1
  20. package/dist/data/create-group.js +1 -1
  21. package/dist/data/group-members.d.ts +6 -1
  22. package/dist/data/group-members.js +12 -4
  23. package/dist/data/groups.d.ts +6 -1
  24. package/dist/data/groups.js +12 -4
  25. package/dist/data/package-version-resolver.d.ts +8 -2
  26. package/dist/data/package-version-resolver.js +28 -2
  27. package/dist/data/package-version-resolver.test.js +42 -0
  28. package/dist/data/package-versions.d.ts +5 -1
  29. package/dist/data/package-versions.js +11 -4
  30. package/dist/data/packages.d.ts +7 -1
  31. package/dist/data/packages.js +13 -4
  32. package/dist/data/table-signing.test.d.ts +1 -0
  33. package/dist/data/table-signing.test.js +118 -0
  34. package/dist/data/users.d.ts +1 -2
  35. package/dist/data/users.js +8 -5
  36. package/dist/package-installer/package-author-signer.d.ts +19 -0
  37. package/dist/package-installer/package-author-signer.js +24 -0
  38. package/dist/package-installer/package-creator.d.ts +1 -7
  39. package/dist/package-installer/package-creator.js +0 -12
  40. package/dist/package-installer/package-creator.test.js +0 -16
  41. package/dist/package-installer/package-installer.d.ts +5 -0
  42. package/dist/package-installer/package-installer.js +60 -37
  43. package/dist/package-installer/package-installer.test.js +129 -0
  44. package/dist/package-installer/types.d.ts +5 -11
  45. package/dist/package-loader/index.d.ts +1 -0
  46. package/dist/package-loader/index.js +1 -0
  47. package/dist/package-loader/isolated-runtime.d.ts +18 -0
  48. package/dist/package-loader/package-loader.d.ts +67 -4
  49. package/dist/package-loader/package-loader.js +300 -52
  50. package/dist/package-loader/package-loader.test.js +413 -1
  51. package/dist/package-loader/package-runtime-state.d.ts +112 -0
  52. package/dist/package-loader/package-runtime-state.js +18 -0
  53. package/dist/package-loader/resolve-consumed-contracts.js +5 -4
  54. package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
  55. package/dist/rpc-types.d.ts +0 -121
  56. package/dist/rpc-types.js +0 -39
  57. package/dist/system-ids.d.ts +13 -0
  58. package/dist/system-ids.js +14 -1
  59. 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 `seedBundledPeersCore`) is the caller's responsibility.
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 `seedBundledPeersCore`) is the caller's responsibility.
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
- private static addSignatureToGroupMember;
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
- if (!GroupMembersTable.addSignatureToGroupMember) {
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 = GroupMembersTable.addSignatureToGroupMember(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
- static addSignatureToGroupMember = undefined;
128
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
122
129
  static enableGroupMemberSigning(fn) {
123
- GroupMembersTable.addSignatureToGroupMember = fn;
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
  }
@@ -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
- private static addSignatureToGroup;
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;
@@ -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
- if (!GroupsTable.addSignatureToGroup) {
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 = GroupsTable.addSignatureToGroup(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
- static addSignatureToGroup = undefined;
127
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
121
128
  static enableGroupSigning(fn) {
122
- GroupsTable.addSignatureToGroup = fn;
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. Never auto-activates dev versions
85
- * unless the device explicitly follows "dev".
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. Never auto-activates dev versions
89
- * unless the device explicitly follows "dev".
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
- private static addSignatureToPackageVersion;
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).