@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.
- package/dist/data/index.d.ts +2 -1
- package/dist/data/index.js +2 -1
- package/dist/data/package-version-permissions.d.ts +4 -1
- package/dist/data/package-version-permissions.js +7 -3
- package/dist/data/package-version-permissions.test.d.ts +1 -0
- package/dist/data/package-version-permissions.test.js +107 -0
- package/dist/data/package-version-resolver.d.ts +92 -0
- package/dist/data/package-version-resolver.js +208 -0
- package/dist/data/package-version-resolver.test.d.ts +8 -0
- package/dist/data/package-version-resolver.test.js +335 -0
- package/dist/data/package-versions.d.ts +14 -7
- package/dist/data/package-versions.js +12 -12
- package/dist/data/package-versions.test.d.ts +1 -0
- package/dist/data/package-versions.test.js +227 -0
- package/dist/data/packages.d.ts +1 -0
- package/dist/data/packages.js +4 -10
- package/dist/data/workflow-runs.d.ts +2 -2
- package/dist/data/workflows.d.ts +2 -2
- package/dist/package-loader/package-loader.d.ts +1 -0
- package/dist/package-loader/package-loader.js +14 -5
- package/dist/types/workflow.d.ts +4 -4
- package/package.json +1 -1
package/dist/data/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/data/index.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
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 {};
|