@peers-app/peers-sdk 0.18.5 → 0.18.8

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);
@@ -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 {};
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ /**
3
+ * Regression tests for device-local package version resolution.
4
+ *
5
+ * Several cases document known gaps from the device-local package versions review.
6
+ * They are expected to fail until the resolver honors group-level pinned, legacy
7
+ * deviceVersionTag, and pinned devices are not overridden before resolve.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ const SQLiteDB = require("better-sqlite3");
11
+ const user_context_1 = require("../context/user-context");
12
+ const user_context_singleton_1 = require("../context/user-context-singleton");
13
+ const utils_1 = require("../utils");
14
+ const assistants_1 = require("./assistants");
15
+ const sql_data_source_1 = require("./orm/sql.data-source");
16
+ const package_version_resolver_1 = require("./package-version-resolver");
17
+ const package_versions_1 = require("./package-versions");
18
+ const packages_1 = require("./packages");
19
+ const persistent_vars_1 = require("./persistent-vars");
20
+ const workflows_1 = require("./workflows");
21
+ class DBHarness {
22
+ _db = null;
23
+ get db() {
24
+ if (!this._db) {
25
+ this._db = new SQLiteDB(":memory:");
26
+ this._db.pragma("journal_mode = WAL");
27
+ }
28
+ return this._db;
29
+ }
30
+ async get(sql, params = []) {
31
+ return this.db.prepare(sql).get(params);
32
+ }
33
+ async all(sql, params = []) {
34
+ return this.db.prepare(sql).all(params);
35
+ }
36
+ async exec(sql, params = []) {
37
+ const result = this.db.prepare(sql).run(params);
38
+ return { changes: result.changes };
39
+ }
40
+ async close() {
41
+ this._db?.close();
42
+ this._db = null;
43
+ }
44
+ }
45
+ const testUserId = (0, utils_1.newid)();
46
+ const testGroupId = (0, utils_1.newid)();
47
+ let userContext;
48
+ let db;
49
+ function groupDc() {
50
+ const dc = userContext.getDataContext(testGroupId);
51
+ dc.setAsDefault();
52
+ return dc;
53
+ }
54
+ function makePV(packageId, overrides = {}) {
55
+ return {
56
+ packageVersionId: (0, utils_1.newid)(),
57
+ packageId,
58
+ version: "1.0.0",
59
+ versionTag: "stable",
60
+ packageVersionHash: "hash",
61
+ packageBundleFileId: (0, utils_1.newid)(),
62
+ packageBundleFileHash: "bundlehash",
63
+ signature: "",
64
+ createdBy: testUserId,
65
+ createdAt: new Date().toISOString(),
66
+ ...overrides,
67
+ };
68
+ }
69
+ function makePkg(overrides = {}) {
70
+ return {
71
+ packageId: (0, utils_1.newid)(),
72
+ name: "test-package",
73
+ description: "test",
74
+ createdBy: testUserId,
75
+ signature: "",
76
+ ...overrides,
77
+ };
78
+ }
79
+ beforeAll(async () => {
80
+ db = new DBHarness();
81
+ const dataSourceFactory = (metaData, schema) => new sql_data_source_1.SQLDataSource(db, metaData, schema);
82
+ userContext = new user_context_1.UserContext(testUserId, dataSourceFactory, true);
83
+ await userContext.loadingPromise;
84
+ userContext.deviceId((0, utils_1.newid)());
85
+ (0, user_context_singleton_1.setUserContext)(userContext);
86
+ packages_1.PackagesTable.isPassthrough = true;
87
+ package_versions_1.PackageVersionsTable.isPassthrough = true;
88
+ });
89
+ afterAll(async () => {
90
+ packages_1.PackagesTable.isPassthrough = false;
91
+ package_versions_1.PackageVersionsTable.isPassthrough = false;
92
+ userContext?.dispose();
93
+ await db?.close();
94
+ });
95
+ afterEach(() => {
96
+ jest.restoreAllMocks();
97
+ });
98
+ describe("resolveDevicePackageVersion — known gaps (expected to fail until fixed)", () => {
99
+ it("does not auto-upgrade when IPackage.versionFollowRange is pinned (no device prefs)", async () => {
100
+ const dc = groupDc();
101
+ const packageId = (0, utils_1.newid)();
102
+ const stableV1 = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
103
+ const stableV2 = makePV(packageId, { version: "2.0.0", versionTag: "stable" });
104
+ await (0, package_versions_1.PackageVersions)(dc).save(stableV1);
105
+ await (0, package_versions_1.PackageVersions)(dc).save(stableV2);
106
+ const pkg = makePkg({
107
+ packageId,
108
+ activePackageVersionId: stableV1.packageVersionId,
109
+ versionFollowRange: "pinned",
110
+ });
111
+ await (0, packages_1.Packages)(dc).save(pkg);
112
+ const loadedPvIds = [];
113
+ jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
114
+ if (opts?.packageVersionId)
115
+ loadedPvIds.push(opts.packageVersionId);
116
+ return { packageId };
117
+ });
118
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
119
+ expect(result.upgraded).toBe(false);
120
+ expect(loadedPvIds).toContain(stableV1.packageVersionId);
121
+ expect(loadedPvIds).not.toContain(stableV2.packageVersionId);
122
+ });
123
+ it("pinned device keeps stable PV after activePackageVersionId is overwritten to dev (updatePackageBundle sequence)", async () => {
124
+ const dc = groupDc();
125
+ const packageId = (0, utils_1.newid)();
126
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
127
+ const devPv = makePV(packageId, { version: "1.0.1", versionTag: "dev" });
128
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
129
+ await (0, package_versions_1.PackageVersions)(dc).save(devPv);
130
+ const pkg = makePkg({
131
+ packageId,
132
+ activePackageVersionId: stablePv.packageVersionId,
133
+ });
134
+ await (0, packages_1.Packages)(dc).save(pkg);
135
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { pinned: true, activePackageVersionId: stablePv.packageVersionId }, dc);
136
+ // Mirrors updatePackageBundle: unconditionally merges dev active id before resolve
137
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: devPv.packageVersionId }, dc);
138
+ const loadedPvIds = [];
139
+ jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
140
+ if (opts?.packageVersionId)
141
+ loadedPvIds.push(opts.packageVersionId);
142
+ return { packageId };
143
+ });
144
+ await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
145
+ expect(loadedPvIds[loadedPvIds.length - 1]).toBe(stablePv.packageVersionId);
146
+ });
147
+ it("honors legacy deviceVersionTag pvar when choosing beta auto-upgrade", async () => {
148
+ const dc = groupDc();
149
+ const packageId = (0, utils_1.newid)();
150
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
151
+ const betaPv = makePV(packageId, { version: "2.0.0", versionTag: "beta" });
152
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
153
+ await (0, package_versions_1.PackageVersions)(dc).save(betaPv);
154
+ const legacyVarName = `deviceVersionTag_${dc.dataContextId}`;
155
+ await (0, persistent_vars_1.PersistentVars)(userContext.userDataContext).save({
156
+ persistentVarId: (0, utils_1.deterministicPvarId)(legacyVarName),
157
+ name: legacyVarName,
158
+ scope: "groupDevice",
159
+ value: { value: "beta" },
160
+ });
161
+ const pkg = makePkg({
162
+ packageId,
163
+ activePackageVersionId: stablePv.packageVersionId,
164
+ versionFollowRange: "latest",
165
+ });
166
+ await (0, packages_1.Packages)(dc).save(pkg);
167
+ const loadedPvIds = [];
168
+ jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
169
+ if (opts?.packageVersionId)
170
+ loadedPvIds.push(opts.packageVersionId);
171
+ return { packageId };
172
+ });
173
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
174
+ expect(result.upgraded).toBe(true);
175
+ expect(loadedPvIds).toContain(betaPv.packageVersionId);
176
+ });
177
+ });
178
+ describe("resolveDevicePackageVersion — expected behavior (control)", () => {
179
+ it("does not auto-activate dev when device has no prefs and group default is stable", async () => {
180
+ const dc = groupDc();
181
+ const packageId = (0, utils_1.newid)();
182
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
183
+ const devPv = makePV(packageId, { version: "9.9.9", versionTag: "dev" });
184
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
185
+ await (0, package_versions_1.PackageVersions)(dc).save(devPv);
186
+ const pkg = makePkg({
187
+ packageId,
188
+ activePackageVersionId: stablePv.packageVersionId,
189
+ versionFollowRange: "latest",
190
+ });
191
+ await (0, packages_1.Packages)(dc).save(pkg);
192
+ const pvar = (0, package_version_resolver_1.packagePrefsVar)(packageId, dc);
193
+ await pvar.loadingPromise;
194
+ expect(pvar()).toEqual({});
195
+ const loadedPvIds = [];
196
+ jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
197
+ if (opts?.packageVersionId)
198
+ loadedPvIds.push(opts.packageVersionId);
199
+ return { packageId };
200
+ });
201
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
202
+ expect(result.upgraded).toBe(false);
203
+ expect(loadedPvIds).toContain(stablePv.packageVersionId);
204
+ expect(loadedPvIds).not.toContain(devPv.packageVersionId);
205
+ });
206
+ it("installs assistants and workflows into the data context on auto-upgrade", async () => {
207
+ const dc = groupDc();
208
+ const packageId = (0, utils_1.newid)();
209
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
210
+ const stableV2 = makePV(packageId, { version: "2.0.0", versionTag: "stable" });
211
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
212
+ await (0, package_versions_1.PackageVersions)(dc).save(stableV2);
213
+ const pkg = makePkg({
214
+ packageId,
215
+ activePackageVersionId: stablePv.packageVersionId,
216
+ versionFollowRange: "latest",
217
+ });
218
+ await (0, packages_1.Packages)(dc).save(pkg);
219
+ // Explicitly set followTags so the legacy-pvar migration from earlier tests
220
+ // doesn't interfere (it would write "beta" followTags otherwise).
221
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { followTags: "stable" }, dc);
222
+ const testAssistantId = (0, utils_1.newid)();
223
+ const testWorkflowId = (0, utils_1.newid)();
224
+ const testAssistant = {
225
+ assistantId: testAssistantId,
226
+ name: "Upgrade Test Assistant",
227
+ packageId,
228
+ createdAt: new Date(),
229
+ assistantRunnerToolId: "000peers0tool00000runner1",
230
+ assistantRunnerConfig: {},
231
+ toolsToInclude: "",
232
+ toolInclusionStrategy: "Linked",
233
+ };
234
+ const testWorkflow = {
235
+ workflowId: testWorkflowId,
236
+ name: "Upgrade Test Workflow",
237
+ description: "test",
238
+ defaultAssistantId: testAssistantId,
239
+ createdBy: testUserId,
240
+ createdAt: new Date(),
241
+ updatedAt: new Date(),
242
+ instructions: [],
243
+ packageId,
244
+ };
245
+ jest.spyOn(dc.packageLoader, "loadPackage").mockResolvedValue({
246
+ packageId,
247
+ assistants: [testAssistant],
248
+ workflows: [testWorkflow],
249
+ });
250
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
251
+ expect(result.upgraded).toBe(true);
252
+ expect(result.pv?.packageVersionId).toBe(stableV2.packageVersionId);
253
+ const savedAssistant = await (0, assistants_1.Assistants)(dc).get(testAssistantId);
254
+ expect(savedAssistant).toBeDefined();
255
+ expect(savedAssistant?.name).toBe("Upgrade Test Assistant");
256
+ const savedWorkflow = await (0, workflows_1.Workflows)(dc).get(testWorkflowId);
257
+ expect(savedWorkflow).toBeDefined();
258
+ expect(savedWorkflow?.name).toBe("Upgrade Test Workflow");
259
+ });
260
+ });
261
+ describe("getEffectivePackagePrefs", () => {
262
+ it("uses group defaults when device prefs are absent", () => {
263
+ const pkg = makePkg({
264
+ activePackageVersionId: "pv-group",
265
+ followVersionTags: "stable,beta",
266
+ versionFollowRange: "minor",
267
+ });
268
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, undefined)).toEqual({
269
+ activePackageVersionId: "pv-group",
270
+ isPinned: false,
271
+ followRange: "minor",
272
+ followTags: "stable,beta",
273
+ });
274
+ });
275
+ it("honors group-level pinned when there are no device prefs", () => {
276
+ const pkg = makePkg({
277
+ activePackageVersionId: "pv-pin",
278
+ versionFollowRange: "pinned",
279
+ });
280
+ const eff = (0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, undefined);
281
+ expect(eff.isPinned).toBe(true);
282
+ expect(eff.activePackageVersionId).toBe("pv-pin");
283
+ });
284
+ it("prefers device active id over group default", () => {
285
+ const pkg = makePkg({ activePackageVersionId: "pv-group" });
286
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, {
287
+ activePackageVersionId: "pv-device",
288
+ })).toMatchObject({
289
+ activePackageVersionId: "pv-device",
290
+ isPinned: false,
291
+ });
292
+ });
293
+ it("device pinned overrides group not pinned", () => {
294
+ const pkg = makePkg({
295
+ activePackageVersionId: "pv1",
296
+ versionFollowRange: "latest",
297
+ });
298
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, { pinned: true }).isPinned).toBe(true);
299
+ });
300
+ it("empty device prefs falls back to group pinned (matches groupDeviceVar default)", () => {
301
+ const pkg = makePkg({
302
+ activePackageVersionId: "pv1",
303
+ versionFollowRange: "pinned",
304
+ });
305
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, {}).isPinned).toBe(true);
306
+ });
307
+ it("device explicitly unpinned overrides group pinned", () => {
308
+ const pkg = makePkg({
309
+ activePackageVersionId: "pv1",
310
+ versionFollowRange: "pinned",
311
+ });
312
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, { pinned: false }).isPinned).toBe(false);
313
+ });
314
+ it("device followRange overrides group default", () => {
315
+ const pkg = makePkg({ versionFollowRange: "latest" });
316
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, { followRange: "patch" }).followRange).toBe("patch");
317
+ });
318
+ it("device followTags overrides group default", () => {
319
+ const pkg = makePkg({ followVersionTags: "stable" });
320
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, { followTags: "*" }).followTags).toBe("*");
321
+ });
322
+ it("falls back to 'latest' followRange when group has no setting", () => {
323
+ const pkg = makePkg({});
324
+ expect((0, package_version_resolver_1.getEffectivePackagePrefs)(pkg, undefined).followRange).toBe("latest");
325
+ });
326
+ });
327
+ describe("packagePrefsVar", () => {
328
+ it("returns the same observable instance for repeated calls with the same args", () => {
329
+ const dc = groupDc();
330
+ const id = (0, utils_1.newid)();
331
+ const a = (0, package_version_resolver_1.packagePrefsVar)(id, dc);
332
+ const b = (0, package_version_resolver_1.packagePrefsVar)(id, dc);
333
+ expect(a).toBe(b);
334
+ });
335
+ });
@@ -53,12 +53,12 @@ declare const schema: z.ZodObject<{
53
53
  version: string;
54
54
  signature: string;
55
55
  packageId: string;
56
+ createdBy: string;
57
+ createdAt: string;
56
58
  packageVersionId: string;
57
59
  packageVersionHash: string;
58
60
  packageBundleFileId: string;
59
61
  packageBundleFileHash: string;
60
- createdBy: string;
61
- createdAt: string;
62
62
  history?: {
63
63
  at: string;
64
64
  action: string;
@@ -80,12 +80,12 @@ declare const schema: z.ZodObject<{
80
80
  version: string;
81
81
  signature: string;
82
82
  packageId: string;
83
+ createdBy: string;
84
+ createdAt: string;
83
85
  packageVersionId: string;
84
86
  packageVersionHash: string;
85
87
  packageBundleFileId: string;
86
88
  packageBundleFileHash: string;
87
- createdBy: string;
88
- createdAt: string;
89
89
  history?: {
90
90
  at: string;
91
91
  action: string;
@@ -120,10 +120,17 @@ export declare function PackageVersions(dataContext?: DataContext): PackageVersi
120
120
  * content only — `versionTag` is intentionally excluded so the same code
121
121
  * produces the same hash regardless of its promotion level (dev/beta/stable).
122
122
  *
123
- * @deprecated The `versionTag` parameter is accepted for backward compatibility
124
- * but ignored. It will be removed in a future release.
123
+ * @param version - Semver version string.
124
+ * @param _versionTag - **Deprecated.** Accepted for backward compatibility
125
+ * but ignored. Will be removed in a future release.
126
+ * @param packageBundleFileHash - Hash of the main package bundle.
127
+ * @param routesBundleFileHash - Hash of the routes bundle, if any.
128
+ * @param uiBundleFileHash - Hash of the UI bundle, if any.
129
+ * @returns A content-addressable hash string.
125
130
  */
126
- export declare function computePackageVersionHash(version: string, _versionTag: string, packageBundleFileHash: string, routesBundleFileHash?: string, uiBundleFileHash?: string): string;
131
+ export declare function computePackageVersionHash(version: string,
132
+ /** @deprecated Ignored — will be removed in a future release. */
133
+ _versionTag: string, packageBundleFileHash: string, routesBundleFileHash?: string, uiBundleFileHash?: string): string;
127
134
  export declare function isVersionInRange(activeVersion: string, incomingVersion: string, range: "pinned" | "patch" | "minor" | "latest"): boolean;
128
135
  /**
129
136
  * Returns true if incomingVersion is strictly newer than activeVersion (semver comparison).
@@ -143,10 +143,17 @@ function PackageVersions(dataContext) {
143
143
  * content only — `versionTag` is intentionally excluded so the same code
144
144
  * produces the same hash regardless of its promotion level (dev/beta/stable).
145
145
  *
146
- * @deprecated The `versionTag` parameter is accepted for backward compatibility
147
- * but ignored. It will be removed in a future release.
146
+ * @param version - Semver version string.
147
+ * @param _versionTag - **Deprecated.** Accepted for backward compatibility
148
+ * but ignored. Will be removed in a future release.
149
+ * @param packageBundleFileHash - Hash of the main package bundle.
150
+ * @param routesBundleFileHash - Hash of the routes bundle, if any.
151
+ * @param uiBundleFileHash - Hash of the UI bundle, if any.
152
+ * @returns A content-addressable hash string.
148
153
  */
149
- function computePackageVersionHash(version, _versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) {
154
+ function computePackageVersionHash(version,
155
+ /** @deprecated Ignored — will be removed in a future release. */
156
+ _versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) {
150
157
  return (0, keys_1.hashValue)([version, packageBundleFileHash, routesBundleFileHash ?? "", uiBundleFileHash ?? ""].join(":"));
151
158
  }
152
159
  function isVersionInRange(activeVersion, incomingVersion, range) {
@@ -9,6 +9,7 @@ declare const schema: z.ZodObject<{
9
9
  createdBy: z.ZodEffects<z.ZodString, string, string>;
10
10
  disabled: z.ZodOptional<z.ZodBoolean>;
11
11
  remoteRepo: z.ZodOptional<z.ZodString>;
12
+ /** @deprecated Read appNavs from the active IPackageVersion record instead. */
12
13
  appNavs: z.ZodOptional<z.ZodArray<z.ZodObject<{
13
14
  name: z.ZodString;
14
15
  displayName: z.ZodOptional<z.ZodString>;
@@ -56,10 +56,11 @@ const schema = zod_1.z.object({
56
56
  .optional()
57
57
  .describe("Whether the package's components should be loaded and included in the app runtime"),
58
58
  remoteRepo: zod_1.z.string().optional().describe("The remote repository where the package is stored"),
59
+ /** @deprecated Read appNavs from the active IPackageVersion record instead. */
59
60
  appNavs: app_nav_1.appNavSchema
60
61
  .array()
61
62
  .optional()
62
- .describe("The app navigation items that this package provides"),
63
+ .describe("DEPRECATED: Read appNavs from the active IPackageVersion record instead"),
63
64
  activePackageVersionId: zod_types_1.zodPeerId
64
65
  .optional()
65
66
  .describe("FK to PackageVersions — the currently active version"),
@@ -32,7 +32,6 @@ export declare const workflowRunSchema: z.ZodObject<{
32
32
  parentWorkflowRunId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
33
33
  defaultAssistantId: z.ZodEffects<z.ZodString, string, string>;
34
34
  }, "strip", z.ZodTypeAny, {
35
- createdAt: Date;
36
35
  defaultAssistantId: string;
37
36
  instructions: {
38
37
  markdown?: string | undefined;
@@ -40,6 +39,7 @@ export declare const workflowRunSchema: z.ZodObject<{
40
39
  directCallToolId?: string | undefined;
41
40
  subWorkflowId?: string | undefined;
42
41
  }[];
42
+ createdAt: Date;
43
43
  workflowRunId: string;
44
44
  parentMessageId: string;
45
45
  currentInstructionIndex: number;
@@ -57,7 +57,6 @@ export declare const workflowRunSchema: z.ZodObject<{
57
57
  defaultAssistantId: string;
58
58
  workflowRunId: string;
59
59
  parentMessageId: string;
60
- createdAt?: Date | undefined;
61
60
  workflowId?: string | undefined;
62
61
  instructions?: {
63
62
  markdown?: string | undefined;
@@ -65,6 +64,7 @@ export declare const workflowRunSchema: z.ZodObject<{
65
64
  directCallToolId?: string | undefined;
66
65
  subWorkflowId?: string | undefined;
67
66
  }[] | undefined;
67
+ createdAt?: Date | undefined;
68
68
  scheduleDT?: Date | undefined;
69
69
  currentInstructionIndex?: number | undefined;
70
70
  instructionResults?: any[] | undefined;
@@ -2,8 +2,6 @@ import type { DataContext } from "../context/data-context";
2
2
  export declare function Workflows(dataContext?: DataContext): import("./orm").Table<{
3
3
  name: string;
4
4
  description: string;
5
- createdBy: string;
6
- createdAt: Date;
7
5
  workflowId: string;
8
6
  defaultAssistantId: string;
9
7
  instructions: {
@@ -12,6 +10,8 @@ export declare function Workflows(dataContext?: DataContext): import("./orm").Ta
12
10
  directCallToolId?: string | undefined;
13
11
  subWorkflowId?: string | undefined;
14
12
  }[];
13
+ createdBy: string;
14
+ createdAt: Date;
15
15
  updatedAt: Date;
16
16
  packageId?: string | undefined;
17
17
  }>;
@@ -18,6 +18,7 @@ export declare class PackageLoader {
18
18
  loadPackage(pkg: IPackage, opts?: {
19
19
  force?: boolean;
20
20
  localPath?: string;
21
+ packageVersionId?: string;
21
22
  }): Promise<IPeersPackage | undefined>;
22
23
  private _readLocalBundle;
23
24
  /**
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PackageLoader = void 0;
4
4
  exports.setDefaultRequire = setDefaultRequire;
5
5
  const files_1 = require("../data/files");
6
+ const package_version_resolver_1 = require("../data/package-version-resolver");
6
7
  const package_versions_1 = require("../data/package-versions");
7
8
  const packages_1 = require("../data/packages");
8
9
  const tools_1 = require("../data/tools");
@@ -25,7 +26,9 @@ class PackageLoader {
25
26
  }
26
27
  async loadAllPackages(opts) {
27
28
  const packages = await (0, packages_1.Packages)(this.dataContext).list();
28
- await Promise.all(packages.filter((pkg) => !pkg.disabled).map((pkg) => this.loadPackage(pkg, opts)));
29
+ await Promise.all(packages
30
+ .filter((pkg) => !pkg.disabled)
31
+ .map((pkg) => (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, this.dataContext, opts)));
29
32
  }
30
33
  async loadPackage(pkg, opts) {
31
34
  if (this.packageInstances[pkg.packageId] && !opts?.force) {
@@ -35,9 +38,10 @@ class PackageLoader {
35
38
  try {
36
39
  let bundleCode = "";
37
40
  let bundleFileId;
38
- if (pkg.activePackageVersionId) {
41
+ const pvId = opts?.packageVersionId ?? pkg.activePackageVersionId;
42
+ if (pvId) {
39
43
  try {
40
- const pv = await (0, package_versions_1.PackageVersions)(this.dataContext).get(pkg.activePackageVersionId);
44
+ const pv = await (0, package_versions_1.PackageVersions)(this.dataContext).get(pvId);
41
45
  if (pv) {
42
46
  bundleFileId = pv.packageBundleFileId;
43
47
  }
@@ -110,12 +114,17 @@ class PackageLoader {
110
114
  if (!def.version)
111
115
  return;
112
116
  const pvTable = (0, package_versions_1.PackageVersions)(this.dataContext);
113
- pvTable.get(pkg.activePackageVersionId).then((pv) => {
117
+ pvTable
118
+ .get(pkg.activePackageVersionId)
119
+ .then((pv) => {
114
120
  if (!pv)
115
121
  return;
116
122
  if (def.version && pv.version !== def.version) {
117
- pvTable.save({ ...pv, version: def.version });
123
+ return pvTable.signAndSave({ ...pv, version: def.version });
118
124
  }
125
+ })
126
+ .catch((err) => {
127
+ console.warn(`[PackageLoader] Failed to correct version for ${pkg.name}:`, err.message);
119
128
  });
120
129
  }
121
130
  async _evaluateBundle(pkg, bundleCode) {
@@ -44,8 +44,6 @@ export declare const workflowSchema: z.ZodObject<{
44
44
  }, "strip", z.ZodTypeAny, {
45
45
  name: string;
46
46
  description: string;
47
- createdBy: string;
48
- createdAt: Date;
49
47
  workflowId: string;
50
48
  defaultAssistantId: string;
51
49
  instructions: {
@@ -54,22 +52,24 @@ export declare const workflowSchema: z.ZodObject<{
54
52
  directCallToolId?: string | undefined;
55
53
  subWorkflowId?: string | undefined;
56
54
  }[];
55
+ createdBy: string;
56
+ createdAt: Date;
57
57
  updatedAt: Date;
58
58
  packageId?: string | undefined;
59
59
  }, {
60
60
  name: string;
61
61
  description: string;
62
- createdBy: string;
63
62
  workflowId: string;
64
63
  defaultAssistantId: string;
64
+ createdBy: string;
65
65
  packageId?: string | undefined;
66
- createdAt?: Date | undefined;
67
66
  instructions?: {
68
67
  markdown?: string | undefined;
69
68
  onError?: string | undefined;
70
69
  directCallToolId?: string | undefined;
71
70
  subWorkflowId?: string | undefined;
72
71
  }[] | undefined;
72
+ createdAt?: Date | undefined;
73
73
  updatedAt?: Date | undefined;
74
74
  }>;
75
75
  export type IWorkflow = z.infer<typeof workflowSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.18.5",
3
+ "version": "0.18.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"