@peers-app/peers-sdk 0.18.4 → 0.18.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.
@@ -10,11 +10,12 @@ export * from "./group-members";
10
10
  export * from "./group-permissions";
11
11
  export * from "./group-share";
12
12
  export * from "./groups";
13
- export * from "./peer-types";
14
13
  export * from "./messages";
14
+ export * from "./package-version-resolver";
15
15
  export * from "./package-versions";
16
16
  export * from "./packages";
17
17
  export * from "./packages.utils";
18
+ export * from "./peer-types";
18
19
  export * from "./persistent-vars";
19
20
  export * from "./table-definitions-table";
20
21
  export * from "./tool-tests";
@@ -26,11 +26,12 @@ __exportStar(require("./group-members"), exports);
26
26
  __exportStar(require("./group-permissions"), exports);
27
27
  __exportStar(require("./group-share"), exports);
28
28
  __exportStar(require("./groups"), exports);
29
- __exportStar(require("./peer-types"), exports);
30
29
  __exportStar(require("./messages"), exports);
30
+ __exportStar(require("./package-version-resolver"), exports);
31
31
  __exportStar(require("./package-versions"), exports);
32
32
  __exportStar(require("./packages"), exports);
33
33
  __exportStar(require("./packages.utils"), exports);
34
+ __exportStar(require("./peer-types"), exports);
34
35
  __exportStar(require("./persistent-vars"), exports);
35
36
  __exportStar(require("./table-definitions-table"), exports);
36
37
  __exportStar(require("./tool-tests"), exports);
@@ -1,3 +1,4 @@
1
+ import { GroupMemberRole } from "./group-permissions";
1
2
  import type { IPackageVersion } from "./package-versions";
2
3
  /**
3
4
  * Verifies that a package version update signature is valid.
@@ -6,6 +7,8 @@ import type { IPackageVersion } from "./package-versions";
6
7
  * Beta and stable versions require Admin role.
7
8
  *
8
9
  * @param packageVersion The package version record to verify
10
+ * @param groupId The group context to check roles against
11
+ * @param signerRole Optional pre-resolved role (skips `getUserRoleFromPublicKey` lookup; useful in tests)
9
12
  * @throws Error if signature is invalid or unauthorized
10
13
  */
11
- export declare function verifyPackageVersionSignature(packageVersion: IPackageVersion, groupId: string): Promise<void>;
14
+ export declare function verifyPackageVersionSignature(packageVersion: IPackageVersion, groupId: string, signerRole?: GroupMemberRole): Promise<void>;
@@ -10,12 +10,16 @@ const group_permissions_1 = require("./group-permissions");
10
10
  * Beta and stable versions require Admin role.
11
11
  *
12
12
  * @param packageVersion The package version record to verify
13
+ * @param groupId The group context to check roles against
14
+ * @param signerRole Optional pre-resolved role (skips `getUserRoleFromPublicKey` lookup; useful in tests)
13
15
  * @throws Error if signature is invalid or unauthorized
14
16
  */
15
- async function verifyPackageVersionSignature(packageVersion, groupId) {
17
+ async function verifyPackageVersionSignature(packageVersion, groupId, signerRole) {
16
18
  (0, keys_1.verifyObjectSignature)(packageVersion);
17
- const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageVersion) ?? "";
18
- const signerRole = await (0, group_permissions_1.getUserRoleFromPublicKey)(groupId, signerPublicKey);
19
+ if (signerRole === undefined) {
20
+ const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageVersion) ?? "";
21
+ signerRole = await (0, group_permissions_1.getUserRoleFromPublicKey)(groupId, signerPublicKey);
22
+ }
19
23
  const isDevVersion = packageVersion.versionTag === "dev";
20
24
  const requiredRole = isDevVersion ? group_permissions_1.GroupMemberRole.Writer : group_permissions_1.GroupMemberRole.Admin;
21
25
  if (signerRole < requiredRole) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const keys_1 = require("../keys");
4
+ const utils_1 = require("../utils");
5
+ const group_member_roles_1 = require("./group-member-roles");
6
+ const package_version_permissions_1 = require("./package-version-permissions");
7
+ describe("Package Version Permissions", () => {
8
+ const writerKeys = (0, keys_1.newKeys)();
9
+ const adminKeys = (0, keys_1.newKeys)();
10
+ function makePV(overrides = {}) {
11
+ return {
12
+ packageVersionId: (0, utils_1.newid)(),
13
+ packageId: (0, utils_1.newid)(),
14
+ version: "1.0.0",
15
+ versionTag: "dev",
16
+ packageVersionHash: "testhash",
17
+ packageBundleFileId: (0, utils_1.newid)(),
18
+ packageBundleFileHash: "bundlehash",
19
+ signature: "",
20
+ createdBy: (0, utils_1.newid)(),
21
+ createdAt: new Date().toISOString(),
22
+ ...overrides,
23
+ };
24
+ }
25
+ describe("dev versions", () => {
26
+ it("allows Writer to sign a dev version", async () => {
27
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
28
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Writer)).resolves.toBeUndefined();
29
+ });
30
+ it("allows Admin to sign a dev version", async () => {
31
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), adminKeys.secretKey);
32
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
33
+ });
34
+ it("rejects Reader signing a dev version", async () => {
35
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
36
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Reader)).rejects.toThrow("Only group writers or above");
37
+ });
38
+ });
39
+ describe("beta versions", () => {
40
+ it("allows Admin to sign a beta version", async () => {
41
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "beta" }), adminKeys.secretKey);
42
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
43
+ });
44
+ it("allows Owner to sign a beta version", async () => {
45
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "beta" }), adminKeys.secretKey);
46
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Owner)).resolves.toBeUndefined();
47
+ });
48
+ it("rejects Writer signing a beta version", async () => {
49
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "beta" }), writerKeys.secretKey);
50
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
51
+ });
52
+ });
53
+ describe("stable versions", () => {
54
+ it("allows Admin to sign a stable version", async () => {
55
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "stable" }), adminKeys.secretKey);
56
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
57
+ });
58
+ it("rejects Writer signing a stable version", async () => {
59
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "stable" }), writerKeys.secretKey);
60
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
61
+ });
62
+ });
63
+ describe("promotion scenarios (dev → beta → stable)", () => {
64
+ it("rejects Writer promoting dev to beta", async () => {
65
+ const devPV = makePV({ versionTag: "dev" });
66
+ const promoted = { ...devPV, versionTag: "beta" };
67
+ const signed = (0, keys_1.addSignatureToObject)(promoted, writerKeys.secretKey);
68
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
69
+ });
70
+ it("rejects Writer promoting dev to stable", async () => {
71
+ const devPV = makePV({ versionTag: "dev" });
72
+ const promoted = { ...devPV, versionTag: "stable" };
73
+ const signed = (0, keys_1.addSignatureToObject)(promoted, writerKeys.secretKey);
74
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
75
+ });
76
+ it("rejects Writer promoting beta to stable", async () => {
77
+ const betaPV = makePV({ versionTag: "beta" });
78
+ const promoted = { ...betaPV, versionTag: "stable" };
79
+ const signed = (0, keys_1.addSignatureToObject)(promoted, writerKeys.secretKey);
80
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
81
+ });
82
+ it("allows Admin promoting dev to beta", async () => {
83
+ const devPV = makePV({ versionTag: "dev" });
84
+ const promoted = { ...devPV, versionTag: "beta" };
85
+ const signed = (0, keys_1.addSignatureToObject)(promoted, adminKeys.secretKey);
86
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
87
+ });
88
+ it("allows Admin promoting beta to stable", async () => {
89
+ const betaPV = makePV({ versionTag: "beta" });
90
+ const promoted = { ...betaPV, versionTag: "stable" };
91
+ const signed = (0, keys_1.addSignatureToObject)(promoted, adminKeys.secretKey);
92
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
93
+ });
94
+ });
95
+ describe("signature tampering", () => {
96
+ it("rejects a tampered package version", async () => {
97
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
98
+ pv.version = "9.9.9";
99
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).rejects.toThrow();
100
+ });
101
+ it("rejects a version with tag changed after signing", async () => {
102
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
103
+ pv.versionTag = "stable";
104
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).rejects.toThrow();
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Device-local package version resolver.
3
+ *
4
+ * Each device independently decides which package version to run based on a
5
+ * per-package `groupDeviceVar` containing the active PV ID and follow
6
+ * preferences. The shared `IPackage.activePackageVersionId` serves only as
7
+ * the group-level default for new devices.
8
+ */
9
+ import type { DataContext } from "../context/data-context";
10
+ import { type IPackageVersion } from "./package-versions";
11
+ import type { IPackage } from "./packages";
12
+ import { type PersistentVar } from "./persistent-vars";
13
+ /**
14
+ * Device-local preferences for a single package. Stored in a `groupDeviceVar`
15
+ * keyed by `packagePrefs_${packageId}`. When undefined or empty, the device
16
+ * falls back to the shared `IPackage` fields (zero-migration backward compat).
17
+ */
18
+ export interface IDevicePackagePrefs {
19
+ /** Which PV this device currently runs */
20
+ activePackageVersionId?: string;
21
+ /** If true, never auto-update this package */
22
+ pinned?: boolean;
23
+ /** Auto-update range: patch | minor | latest */
24
+ followRange?: "patch" | "minor" | "latest";
25
+ /** Tag policy: "stable" | "stable,beta" | "*" */
26
+ followTags?: string;
27
+ }
28
+ /**
29
+ * Reactive, cached observable holding device-local preferences for a single
30
+ * package. Backed by a `groupDeviceVar` so the UI can subscribe via
31
+ * `useObservable` and get automatic updates when prefs change (including
32
+ * cross-process DB writes and group context switches).
33
+ *
34
+ * DB record name: `packagePrefs_${packageId}_${dataContextId}` -- identical to
35
+ * the previous manual naming convention, so existing records load without
36
+ * migration.
37
+ *
38
+ * @param dataContext - Pinned data context for server-side callers. When
39
+ * omitted the var tracks the user's default group context.
40
+ */
41
+ export declare function packagePrefsVar(packageId: string, dataContext?: DataContext): PersistentVar<IDevicePackagePrefs>;
42
+ /**
43
+ * Merge partial updates into device-local package preferences. Preserves
44
+ * existing fields that are not overridden.
45
+ *
46
+ * Guard: when the device is already pinned, callers cannot silently overwrite
47
+ * `activePackageVersionId`. Include `pinned` in the same update to signal
48
+ * intent (e.g. unpinning + activating in one step).
49
+ *
50
+ * @param dataContext - Pinned data context for server-side callers. When
51
+ * omitted the var tracks the user's default group context.
52
+ */
53
+ export declare function updatePackagePrefs(packageId: string, prefs: Partial<IDevicePackagePrefs>, dataContext?: DataContext): Promise<void>;
54
+ /**
55
+ * Effective device-local preferences for a package after merging the device's
56
+ * raw prefs with the group-level `IPackage` defaults. Every field is resolved
57
+ * -- no `undefined` means "use the IPackage field" ambiguity remains.
58
+ */
59
+ export interface IEffectivePackagePrefs {
60
+ activePackageVersionId: string | undefined;
61
+ isPinned: boolean;
62
+ followRange: "patch" | "minor" | "latest";
63
+ followTags: string | undefined;
64
+ }
65
+ /**
66
+ * Pure function: merge raw device prefs with the group-level `IPackage`
67
+ * defaults to produce fully-resolved effective preferences. Used by the
68
+ * resolver and by UI components that need to reflect device behavior.
69
+ */
70
+ export declare function getEffectivePackagePrefs(pkg: IPackage, devicePrefs: IDevicePackagePrefs | undefined): IEffectivePackagePrefs;
71
+ export interface IResolveResult {
72
+ /** Whether a package was successfully loaded */
73
+ loaded: boolean;
74
+ /** The PV that was resolved (may be the current one if no upgrade found) */
75
+ pv?: IPackageVersion;
76
+ /** Whether the active PV changed */
77
+ upgraded: boolean;
78
+ }
79
+ /**
80
+ * Core resolver: evaluates available package versions against the device's
81
+ * follow policy and loads the best one. Never auto-activates dev versions
82
+ * unless the device explicitly follows "dev".
83
+ *
84
+ * Callers:
85
+ * - `PackagesTrackedDataSource.applyChanges` (on sync)
86
+ * - `PackageLoader.loadAllPackages` (startup)
87
+ * - `package-installer.ts` (after saving new bundles)
88
+ */
89
+ export declare function resolveDevicePackageVersion(pkg: IPackage, dataContext: DataContext, opts?: {
90
+ force?: boolean;
91
+ localPath?: string;
92
+ }): Promise<IResolveResult>;
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ /**
3
+ * Device-local package version resolver.
4
+ *
5
+ * Each device independently decides which package version to run based on a
6
+ * per-package `groupDeviceVar` containing the active PV ID and follow
7
+ * preferences. The shared `IPackage.activePackageVersionId` serves only as
8
+ * the group-level default for new devices.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.packagePrefsVar = packagePrefsVar;
12
+ exports.updatePackagePrefs = updatePackagePrefs;
13
+ exports.getEffectivePackagePrefs = getEffectivePackagePrefs;
14
+ exports.resolveDevicePackageVersion = resolveDevicePackageVersion;
15
+ const user_context_singleton_1 = require("../context/user-context-singleton");
16
+ const assistants_1 = require("./assistants");
17
+ const package_versions_1 = require("./package-versions");
18
+ const persistent_vars_1 = require("./persistent-vars");
19
+ const workflows_1 = require("./workflows");
20
+ /**
21
+ * Reactive, cached observable holding device-local preferences for a single
22
+ * package. Backed by a `groupDeviceVar` so the UI can subscribe via
23
+ * `useObservable` and get automatic updates when prefs change (including
24
+ * cross-process DB writes and group context switches).
25
+ *
26
+ * DB record name: `packagePrefs_${packageId}_${dataContextId}` -- identical to
27
+ * the previous manual naming convention, so existing records load without
28
+ * migration.
29
+ *
30
+ * @param dataContext - Pinned data context for server-side callers. When
31
+ * omitted the var tracks the user's default group context.
32
+ */
33
+ function packagePrefsVar(packageId, dataContext) {
34
+ return (0, persistent_vars_1.groupDeviceVar)(`packagePrefs_${packageId}`, {
35
+ defaultValue: {},
36
+ dataContext,
37
+ });
38
+ }
39
+ /**
40
+ * Merge partial updates into device-local package preferences. Preserves
41
+ * existing fields that are not overridden.
42
+ *
43
+ * Guard: when the device is already pinned, callers cannot silently overwrite
44
+ * `activePackageVersionId`. Include `pinned` in the same update to signal
45
+ * intent (e.g. unpinning + activating in one step).
46
+ *
47
+ * @param dataContext - Pinned data context for server-side callers. When
48
+ * omitted the var tracks the user's default group context.
49
+ */
50
+ async function updatePackagePrefs(packageId, prefs, dataContext) {
51
+ const pvar = packagePrefsVar(packageId, dataContext);
52
+ await pvar.loadingPromise;
53
+ const existing = pvar() ?? {};
54
+ let effectivePrefs = prefs;
55
+ if (existing.pinned && !("pinned" in prefs) && "activePackageVersionId" in prefs) {
56
+ const { activePackageVersionId: _, ...rest } = prefs;
57
+ effectivePrefs = rest;
58
+ }
59
+ pvar({ ...existing, ...effectivePrefs });
60
+ }
61
+ /**
62
+ * Pure function: merge raw device prefs with the group-level `IPackage`
63
+ * defaults to produce fully-resolved effective preferences. Used by the
64
+ * resolver and by UI components that need to reflect device behavior.
65
+ */
66
+ function getEffectivePackagePrefs(pkg, devicePrefs) {
67
+ const hasDevicePrefs = devicePrefs != null && Object.keys(devicePrefs).length > 0;
68
+ return {
69
+ activePackageVersionId: devicePrefs?.activePackageVersionId ?? pkg.activePackageVersionId,
70
+ isPinned: !!devicePrefs?.pinned || (!hasDevicePrefs && pkg.versionFollowRange === "pinned"),
71
+ followRange: devicePrefs?.followRange ?? rangeFromPkg(pkg),
72
+ followTags: devicePrefs?.followTags ?? pkg.followVersionTags,
73
+ };
74
+ }
75
+ /**
76
+ * Core resolver: evaluates available package versions against the device's
77
+ * follow policy and loads the best one. Never auto-activates dev versions
78
+ * unless the device explicitly follows "dev".
79
+ *
80
+ * Callers:
81
+ * - `PackagesTrackedDataSource.applyChanges` (on sync)
82
+ * - `PackageLoader.loadAllPackages` (startup)
83
+ * - `package-installer.ts` (after saving new bundles)
84
+ */
85
+ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
86
+ const pvar = packagePrefsVar(pkg.packageId, dataContext);
87
+ await pvar.loadingPromise;
88
+ // Migrate legacy per-group deviceVersionTag pvar into per-package followTags.
89
+ // The old system stored a single tag preference per group; the new system is
90
+ // per-package. We copy the value on first resolution so future calls skip
91
+ // this path (the legacy pvar stays around harmlessly for other packages that
92
+ // haven't resolved yet).
93
+ let rawPrefs = pvar() ?? {};
94
+ if (!rawPrefs.followTags) {
95
+ const legacyVarName = `deviceVersionTag_${dataContext.dataContextId}`;
96
+ const userCtx = await (0, user_context_singleton_1.getUserContext)();
97
+ const legacyRec = await (0, persistent_vars_1.PersistentVars)(userCtx.userDataContext).findOne({
98
+ name: legacyVarName,
99
+ });
100
+ const legacyTag = legacyRec?.value?.value;
101
+ if (legacyTag) {
102
+ await updatePackagePrefs(pkg.packageId, { followTags: legacyTag }, dataContext);
103
+ rawPrefs = pvar() ?? {};
104
+ }
105
+ }
106
+ const { activePackageVersionId: effectiveActivePvId, isPinned, followRange, followTags, } = getEffectivePackagePrefs(pkg, rawPrefs);
107
+ if (!effectiveActivePvId) {
108
+ return { loaded: false, upgraded: false };
109
+ }
110
+ // If the device (or the group-level default) has pinned this package, just load
111
+ // the current version — never auto-upgrade.
112
+ if (isPinned) {
113
+ const pv = await (0, package_versions_1.PackageVersions)(dataContext)
114
+ .get(effectiveActivePvId)
115
+ .catch(() => undefined);
116
+ if (!pv)
117
+ return { loaded: false, pv: undefined, upgraded: false };
118
+ const instance = await dataContext.packageLoader.loadPackage(pkg, {
119
+ force: opts?.force,
120
+ localPath: opts?.localPath,
121
+ packageVersionId: effectiveActivePvId,
122
+ });
123
+ return { loaded: !!instance, pv, upgraded: false };
124
+ }
125
+ // Load the currently active PV for comparison
126
+ let activePv;
127
+ if (effectiveActivePvId) {
128
+ activePv = await (0, package_versions_1.PackageVersions)(dataContext)
129
+ .get(effectiveActivePvId)
130
+ .catch(() => undefined);
131
+ }
132
+ // Evaluate all available PVs to find the best one
133
+ const allVersions = await (0, package_versions_1.PackageVersions)(dataContext).list({ packageId: pkg.packageId });
134
+ let bestPv = activePv;
135
+ for (const candidate of allVersions) {
136
+ if (candidate.packageVersionId === effectiveActivePvId)
137
+ continue;
138
+ const tagOk = (0, package_versions_1.doesTagMatch)(activePv?.versionTag, candidate.versionTag, followTags, undefined);
139
+ if (!tagOk)
140
+ continue;
141
+ const activeVersion = bestPv?.version ?? "0.0.0";
142
+ const inRange = (0, package_versions_1.isVersionInRange)(activeVersion, candidate.version, followRange);
143
+ if (!inRange)
144
+ continue;
145
+ const newer = (0, package_versions_1.isNewerVersion)(activeVersion, candidate.version);
146
+ if (!newer)
147
+ continue;
148
+ bestPv = candidate;
149
+ }
150
+ // If the best candidate is the same as what we already have, just load it
151
+ if (!bestPv || bestPv.packageVersionId === effectiveActivePvId) {
152
+ if (activePv) {
153
+ const instance = await dataContext.packageLoader.loadPackage(pkg, {
154
+ force: opts?.force,
155
+ localPath: opts?.localPath,
156
+ packageVersionId: effectiveActivePvId,
157
+ });
158
+ return { loaded: !!instance, pv: activePv, upgraded: false };
159
+ }
160
+ return { loaded: false, upgraded: false };
161
+ }
162
+ // A better PV was found - attempt to load it (getFileContents will
163
+ // auto-download missing chunks from peers)
164
+ const instance = await dataContext.packageLoader.loadPackage(pkg, {
165
+ force: true,
166
+ localPath: opts?.localPath,
167
+ packageVersionId: bestPv.packageVersionId,
168
+ });
169
+ if (!instance) {
170
+ // Bundle unavailable even after download attempt - keep current version
171
+ if (activePv) {
172
+ const fallbackInstance = await dataContext.packageLoader.loadPackage(pkg, {
173
+ force: opts?.force,
174
+ localPath: opts?.localPath,
175
+ packageVersionId: effectiveActivePvId,
176
+ });
177
+ return { loaded: !!fallbackInstance, pv: activePv, upgraded: false };
178
+ }
179
+ return { loaded: false, upgraded: false };
180
+ }
181
+ // Load succeeded - install assistants/workflows, then update device prefs
182
+ await installPackageContents(dataContext, pkg, instance);
183
+ await updatePackagePrefs(pkg.packageId, { activePackageVersionId: bestPv.packageVersionId }, dataContext);
184
+ return { loaded: true, pv: bestPv, upgraded: true };
185
+ }
186
+ /**
187
+ * Save a loaded package's assistants and workflows into the data context.
188
+ * Contract-installed packages are skipped because their content is already
189
+ * managed by the contract loader.
190
+ */
191
+ async function installPackageContents(dataContext, pkg, instance) {
192
+ if (dataContext.packageLoader.isContractInstalled(pkg.packageId))
193
+ return;
194
+ const saves = [
195
+ ...(instance.assistants?.map((a) => (0, assistants_1.Assistants)(dataContext).save(a)) ?? []),
196
+ ...(instance.workflows?.map((w) => (0, workflows_1.Workflows)(dataContext).save(w)) ?? []),
197
+ ];
198
+ await Promise.all(saves);
199
+ }
200
+ /** Convert `IPackage.versionFollowRange` to the resolver's range type. */
201
+ function rangeFromPkg(pkg) {
202
+ const r = pkg.versionFollowRange;
203
+ if (r === "pinned")
204
+ return "latest"; // unreachable — isPinned check returns early
205
+ if (r === "patch" || r === "minor")
206
+ return r;
207
+ return "latest";
208
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Regression tests for device-local package version resolution.
3
+ *
4
+ * Several cases document known gaps from the device-local package versions review.
5
+ * They are expected to fail until the resolver honors group-level pinned, legacy
6
+ * deviceVersionTag, and pinned devices are not overridden before resolve.
7
+ */
8
+ export {};