@peers-app/peers-sdk 0.19.0 → 0.19.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.
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Device-local package version resolver.
3
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.
4
+ * Each device resolves which package version to run from the shared group
5
+ * package settings plus optional device-local prefs. When an admin device
6
+ * upgrades a non-dev version while following group settings, the shared
7
+ * `IPackage.activePackageVersionId` advances so the group stays aligned.
8
8
  */
9
9
  import type { DataContext } from "../context/data-context";
10
10
  import { type IPackageVersion } from "./package-versions";
11
- import type { IPackage } from "./packages";
11
+ import { type IPackage } from "./packages";
12
12
  import { type PersistentVar } from "./persistent-vars";
13
13
  /**
14
14
  * Device-local preferences for a single package. Stored in a `groupDeviceVar`
@@ -68,6 +68,7 @@ export interface IEffectivePackagePrefs {
68
68
  * resolver and by UI components that need to reflect device behavior.
69
69
  */
70
70
  export declare function getEffectivePackagePrefs(pkg: IPackage, devicePrefs: IDevicePackagePrefs | undefined): IEffectivePackagePrefs;
71
+ /** Result of a device-level package version resolution attempt. */
71
72
  export interface IResolveResult {
72
73
  /** Whether a package was successfully loaded */
73
74
  loaded: boolean;
@@ -90,3 +91,10 @@ export declare function resolveDevicePackageVersion(pkg: IPackage, dataContext:
90
91
  force?: boolean;
91
92
  localPath?: string;
92
93
  }): Promise<IResolveResult>;
94
+ /**
95
+ * Returns true when the device has an explicit follow-policy override
96
+ * (`followRange`, `followTags`, or `pinned`). Used by the UI to drive the
97
+ * "Override on this device" toggle and by the resolver to decide whether
98
+ * to advance the group-level active version on upgrade.
99
+ */
100
+ export declare function hasDeviceFollowOverride(prefs: IDevicePackagePrefs | undefined): boolean;
@@ -2,19 +2,23 @@
2
2
  /**
3
3
  * Device-local package version resolver.
4
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.
5
+ * Each device resolves which package version to run from the shared group
6
+ * package settings plus optional device-local prefs. When an admin device
7
+ * upgrades a non-dev version while following group settings, the shared
8
+ * `IPackage.activePackageVersionId` advances so the group stays aligned.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.packagePrefsVar = packagePrefsVar;
12
12
  exports.updatePackagePrefs = updatePackagePrefs;
13
13
  exports.getEffectivePackagePrefs = getEffectivePackagePrefs;
14
14
  exports.resolveDevicePackageVersion = resolveDevicePackageVersion;
15
+ exports.hasDeviceFollowOverride = hasDeviceFollowOverride;
15
16
  const user_context_singleton_1 = require("../context/user-context-singleton");
16
17
  const assistants_1 = require("./assistants");
18
+ const group_member_roles_1 = require("./group-member-roles");
19
+ const group_permissions_1 = require("./group-permissions");
17
20
  const package_versions_1 = require("./package-versions");
21
+ const packages_1 = require("./packages");
18
22
  const persistent_vars_1 = require("./persistent-vars");
19
23
  const workflows_1 = require("./workflows");
20
24
  /**
@@ -129,6 +133,17 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
129
133
  .get(effectiveActivePvId)
130
134
  .catch(() => undefined);
131
135
  }
136
+ // Dev versions are intentionally local working versions. If a device is
137
+ // currently running dev, keep loading that exact PV until the user manually
138
+ // activates a non-dev version.
139
+ if (activePv?.versionTag === "dev") {
140
+ const instance = await dataContext.packageLoader.loadPackage(pkg, {
141
+ force: opts?.force,
142
+ localPath: opts?.localPath,
143
+ packageVersionId: effectiveActivePvId,
144
+ });
145
+ return { loaded: !!instance, pv: activePv, upgraded: false };
146
+ }
132
147
  // Evaluate all available PVs to find the best one
133
148
  const allVersions = await (0, package_versions_1.PackageVersions)(dataContext).list({ packageId: pkg.packageId });
134
149
  let bestPv = activePv;
@@ -178,9 +193,41 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
178
193
  }
179
194
  return { loaded: false, upgraded: false };
180
195
  }
181
- // Load succeeded - install assistants/workflows, then update device prefs
196
+ // Load succeeded - install assistants/workflows, then persist the activation
197
+ // either as a group-level active version or as device-local state.
182
198
  await installPackageContents(dataContext, pkg, instance);
183
- await updatePackagePrefs(pkg.packageId, { activePackageVersionId: bestPv.packageVersionId }, dataContext);
199
+ // When the device is following group settings (no device-level follow
200
+ // override) and the upgraded version is non-dev, also advance the group's
201
+ // activePackageVersionId so other devices stay in sync. Only admin+ users
202
+ // are allowed to write to the Packages record.
203
+ let advancedGroupActiveVersion = false;
204
+ if (bestPv.versionTag !== "dev" && !hasDeviceFollowOverride(rawPrefs)) {
205
+ try {
206
+ const groupId = dataContext.groupId;
207
+ if (groupId) {
208
+ const userCtx = await (0, user_context_singleton_1.getUserContext)();
209
+ const role = await (0, group_permissions_1.getUserRole)(groupId, userCtx.userId);
210
+ if (role >= group_member_roles_1.GroupMemberRole.Admin) {
211
+ const freshPkg = await (0, packages_1.Packages)(dataContext).get(pkg.packageId);
212
+ if (freshPkg) {
213
+ if (freshPkg.activePackageVersionId !== bestPv.packageVersionId) {
214
+ freshPkg.activePackageVersionId = bestPv.packageVersionId;
215
+ await (0, packages_1.Packages)(dataContext).signAndSave(freshPkg);
216
+ }
217
+ advancedGroupActiveVersion = true;
218
+ }
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ // Non-critical: personal contexts have no group, signature checks may
224
+ // fail for non-admins. Swallow and continue — the device upgrade
225
+ // already succeeded.
226
+ }
227
+ }
228
+ await updatePackagePrefs(pkg.packageId, {
229
+ activePackageVersionId: advancedGroupActiveVersion ? undefined : bestPv.packageVersionId,
230
+ }, dataContext);
184
231
  return { loaded: true, pv: bestPv, upgraded: true };
185
232
  }
186
233
  /**
@@ -197,6 +244,17 @@ async function installPackageContents(dataContext, pkg, instance) {
197
244
  ];
198
245
  await Promise.all(saves);
199
246
  }
247
+ /**
248
+ * Returns true when the device has an explicit follow-policy override
249
+ * (`followRange`, `followTags`, or `pinned`). Used by the UI to drive the
250
+ * "Override on this device" toggle and by the resolver to decide whether
251
+ * to advance the group-level active version on upgrade.
252
+ */
253
+ function hasDeviceFollowOverride(prefs) {
254
+ if (!prefs)
255
+ return false;
256
+ return prefs.followRange != null || prefs.followTags != null || prefs.pinned != null;
257
+ }
200
258
  /** Convert `IPackage.versionFollowRange` to the resolver's range type. */
201
259
  function rangeFromPkg(pkg) {
202
260
  const r = pkg.versionFollowRange;
@@ -1,8 +1,4 @@
1
1
  /**
2
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
3
  */
8
4
  export {};
@@ -1,10 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
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
4
  */
9
5
  Object.defineProperty(exports, "__esModule", { value: true });
10
6
  const SQLiteDB = require("better-sqlite3");
@@ -12,6 +8,8 @@ const user_context_1 = require("../context/user-context");
12
8
  const user_context_singleton_1 = require("../context/user-context-singleton");
13
9
  const utils_1 = require("../utils");
14
10
  const assistants_1 = require("./assistants");
11
+ const group_member_roles_1 = require("./group-member-roles");
12
+ const groups_1 = require("./groups");
15
13
  const sql_data_source_1 = require("./orm/sql.data-source");
16
14
  const package_version_resolver_1 = require("./package-version-resolver");
17
15
  const package_versions_1 = require("./package-versions");
@@ -51,6 +49,11 @@ function groupDc() {
51
49
  dc.setAsDefault();
52
50
  return dc;
53
51
  }
52
+ function freshGroupDc() {
53
+ const dc = userContext.getDataContext((0, utils_1.newid)());
54
+ dc.setAsDefault();
55
+ return dc;
56
+ }
54
57
  function makePV(packageId, overrides = {}) {
55
58
  return {
56
59
  packageVersionId: (0, utils_1.newid)(),
@@ -85,6 +88,7 @@ beforeAll(async () => {
85
88
  userContext.deviceId((0, utils_1.newid)());
86
89
  (0, user_context_singleton_1.setUserContext)(userContext);
87
90
  packages_1.PackagesTable.isPassthrough = true;
91
+ packages_1.PackagesTable.enablePackageSigning((pkg) => pkg);
88
92
  package_versions_1.PackageVersionsTable.isPassthrough = true;
89
93
  });
90
94
  afterAll(async () => {
@@ -96,7 +100,7 @@ afterAll(async () => {
96
100
  afterEach(() => {
97
101
  jest.restoreAllMocks();
98
102
  });
99
- describe("resolveDevicePackageVersion — known gaps (expected to fail until fixed)", () => {
103
+ describe("resolveDevicePackageVersion — regression coverage", () => {
100
104
  it("does not auto-upgrade when IPackage.versionFollowRange is pinned (no device prefs)", async () => {
101
105
  const dc = groupDc();
102
106
  const packageId = (0, utils_1.newid)();
@@ -177,6 +181,123 @@ describe("resolveDevicePackageVersion — known gaps (expected to fail until fix
177
181
  });
178
182
  });
179
183
  describe("resolveDevicePackageVersion — expected behavior (control)", () => {
184
+ it("advances the group active version when an admin auto-upgrades without device overrides", async () => {
185
+ const dc = freshGroupDc();
186
+ const packageId = (0, utils_1.newid)();
187
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
188
+ const stableV2 = makePV(packageId, { version: "2.0.0", versionTag: "stable" });
189
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
190
+ await (0, package_versions_1.PackageVersions)(dc).save(stableV2);
191
+ const pkg = makePkg({
192
+ packageId,
193
+ activePackageVersionId: stablePv.packageVersionId,
194
+ versionFollowRange: "latest",
195
+ followVersionTags: "stable",
196
+ });
197
+ await (0, packages_1.Packages)(dc).save(pkg);
198
+ jest.spyOn(dc.packageLoader, "loadPackage").mockResolvedValue({ packageId });
199
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
200
+ expect(result.upgraded).toBe(true);
201
+ expect(result.pv?.packageVersionId).toBe(stableV2.packageVersionId);
202
+ const savedPkg = await (0, packages_1.Packages)(dc).get(packageId);
203
+ expect(savedPkg?.activePackageVersionId).toBe(stableV2.packageVersionId);
204
+ const pvar = (0, package_version_resolver_1.packagePrefsVar)(packageId, dc);
205
+ await pvar.loadingPromise;
206
+ expect(pvar()?.activePackageVersionId).toBeUndefined();
207
+ });
208
+ it("keeps an auto-upgrade device-local when device follow overrides are enabled", async () => {
209
+ const dc = freshGroupDc();
210
+ const packageId = (0, utils_1.newid)();
211
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
212
+ const stableV2 = makePV(packageId, { version: "2.0.0", versionTag: "stable" });
213
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
214
+ await (0, package_versions_1.PackageVersions)(dc).save(stableV2);
215
+ const pkg = makePkg({
216
+ packageId,
217
+ activePackageVersionId: stablePv.packageVersionId,
218
+ versionFollowRange: "latest",
219
+ followVersionTags: "stable",
220
+ });
221
+ await (0, packages_1.Packages)(dc).save(pkg);
222
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { followRange: "latest", followTags: "stable" }, dc);
223
+ jest.spyOn(dc.packageLoader, "loadPackage").mockResolvedValue({ packageId });
224
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
225
+ expect(result.upgraded).toBe(true);
226
+ expect(result.pv?.packageVersionId).toBe(stableV2.packageVersionId);
227
+ const savedPkg = await (0, packages_1.Packages)(dc).get(packageId);
228
+ expect(savedPkg?.activePackageVersionId).toBe(stablePv.packageVersionId);
229
+ const pvar = (0, package_version_resolver_1.packagePrefsVar)(packageId, dc);
230
+ await pvar.loadingPromise;
231
+ expect(pvar()?.activePackageVersionId).toBe(stableV2.packageVersionId);
232
+ });
233
+ it("keeps an auto-upgrade device-local when the device user is not a group admin", async () => {
234
+ const dc = freshGroupDc();
235
+ const packageId = (0, utils_1.newid)();
236
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
237
+ const stableV2 = makePV(packageId, { version: "2.0.0", versionTag: "stable" });
238
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
239
+ await (0, package_versions_1.PackageVersions)(dc).save(stableV2);
240
+ await (0, groups_1.Groups)(userContext.userDataContext).save({
241
+ groupId: dc.dataContextId,
242
+ name: "Non-admin resolver test",
243
+ description: "test",
244
+ founderUserId: (0, utils_1.newid)(),
245
+ publicRole: group_member_roles_1.GroupMemberRole.Writer,
246
+ publicKey: "",
247
+ publicBoxKey: "",
248
+ signature: "",
249
+ });
250
+ const pkg = makePkg({
251
+ packageId,
252
+ activePackageVersionId: stablePv.packageVersionId,
253
+ versionFollowRange: "latest",
254
+ followVersionTags: "stable",
255
+ });
256
+ await (0, packages_1.Packages)(dc).save(pkg);
257
+ jest.spyOn(dc.packageLoader, "loadPackage").mockResolvedValue({ packageId });
258
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
259
+ expect(result.upgraded).toBe(true);
260
+ expect(result.pv?.packageVersionId).toBe(stableV2.packageVersionId);
261
+ const savedPkg = await (0, packages_1.Packages)(dc).get(packageId);
262
+ expect(savedPkg?.activePackageVersionId).toBe(stablePv.packageVersionId);
263
+ const pvar = (0, package_version_resolver_1.packagePrefsVar)(packageId, dc);
264
+ await pvar.loadingPromise;
265
+ expect(pvar()?.activePackageVersionId).toBe(stableV2.packageVersionId);
266
+ });
267
+ it("keeps a local dev activation on this device without turning override on", async () => {
268
+ const dc = freshGroupDc();
269
+ const packageId = (0, utils_1.newid)();
270
+ const stablePv = makePV(packageId, { version: "1.0.0", versionTag: "stable" });
271
+ const devPv = makePV(packageId, { version: "1.2.0", versionTag: "dev" });
272
+ const stableV2 = makePV(packageId, { version: "2.0.0", versionTag: "stable" });
273
+ await (0, package_versions_1.PackageVersions)(dc).save(stablePv);
274
+ await (0, package_versions_1.PackageVersions)(dc).save(devPv);
275
+ await (0, package_versions_1.PackageVersions)(dc).save(stableV2);
276
+ const pkg = makePkg({
277
+ packageId,
278
+ activePackageVersionId: stablePv.packageVersionId,
279
+ versionFollowRange: "latest",
280
+ followVersionTags: "stable",
281
+ });
282
+ await (0, packages_1.Packages)(dc).save(pkg);
283
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: devPv.packageVersionId }, dc);
284
+ const pvar = (0, package_version_resolver_1.packagePrefsVar)(packageId, dc);
285
+ await pvar.loadingPromise;
286
+ expect((0, package_version_resolver_1.hasDeviceFollowOverride)(pvar())).toBe(false);
287
+ const loadedPvIds = [];
288
+ jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
289
+ if (opts?.packageVersionId)
290
+ loadedPvIds.push(opts.packageVersionId);
291
+ return { packageId };
292
+ });
293
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
294
+ expect(result.upgraded).toBe(false);
295
+ expect(result.pv?.packageVersionId).toBe(devPv.packageVersionId);
296
+ expect(loadedPvIds).toEqual([devPv.packageVersionId]);
297
+ const savedPkg = await (0, packages_1.Packages)(dc).get(packageId);
298
+ expect(savedPkg?.activePackageVersionId).toBe(stablePv.packageVersionId);
299
+ expect(pvar()?.activePackageVersionId).toBe(devPv.packageVersionId);
300
+ });
180
301
  it("does not auto-activate dev when device has no prefs and group default is stable", async () => {
181
302
  const dc = groupDc();
182
303
  const packageId = (0, utils_1.newid)();
@@ -9,23 +9,6 @@ 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. */
13
- appNavs: z.ZodOptional<z.ZodArray<z.ZodObject<{
14
- name: z.ZodString;
15
- displayName: z.ZodOptional<z.ZodString>;
16
- iconClassName: z.ZodString;
17
- navigationPath: z.ZodString;
18
- }, "strip", z.ZodTypeAny, {
19
- name: string;
20
- iconClassName: string;
21
- navigationPath: string;
22
- displayName?: string | undefined;
23
- }, {
24
- name: string;
25
- iconClassName: string;
26
- navigationPath: string;
27
- displayName?: string | undefined;
28
- }>, "many">>;
29
12
  activePackageVersionId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
30
13
  versionFollowRange: z.ZodOptional<z.ZodEnum<["pinned", "patch", "minor", "latest"]>>;
31
14
  followVersionTags: z.ZodOptional<z.ZodString>;
@@ -40,12 +23,6 @@ declare const schema: z.ZodObject<{
40
23
  createdBy: string;
41
24
  publishPublicKey: string;
42
25
  disabled?: boolean | undefined;
43
- appNavs?: {
44
- name: string;
45
- iconClassName: string;
46
- navigationPath: string;
47
- displayName?: string | undefined;
48
- }[] | undefined;
49
26
  remoteRepo?: string | undefined;
50
27
  activePackageVersionId?: string | undefined;
51
28
  versionFollowRange?: "pinned" | "patch" | "minor" | "latest" | undefined;
@@ -59,12 +36,6 @@ declare const schema: z.ZodObject<{
59
36
  createdBy: string;
60
37
  publishPublicKey: string;
61
38
  disabled?: boolean | undefined;
62
- appNavs?: {
63
- name: string;
64
- iconClassName: string;
65
- navigationPath: string;
66
- displayName?: string | undefined;
67
- }[] | undefined;
68
39
  remoteRepo?: string | undefined;
69
40
  activePackageVersionId?: string | undefined;
70
41
  versionFollowRange?: "pinned" | "patch" | "minor" | "latest" | undefined;
@@ -38,7 +38,6 @@ exports.autoUpdatePeersCore = exports.reloadPackagesOnPageRefresh = exports.pack
38
38
  exports.Packages = Packages;
39
39
  const zod_1 = require("zod");
40
40
  const context_1 = require("../context");
41
- const app_nav_1 = require("../types/app-nav");
42
41
  const zod_types_1 = require("../types/zod-types");
43
42
  const decorators_1 = require("./orm/decorators");
44
43
  const table_1 = require("./orm/table");
@@ -56,11 +55,6 @@ const schema = zod_1.z.object({
56
55
  .optional()
57
56
  .describe("Whether the package's components should be loaded and included in the app runtime"),
58
57
  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. */
60
- appNavs: app_nav_1.appNavSchema
61
- .array()
62
- .optional()
63
- .describe("DEPRECATED: Read appNavs from the active IPackageVersion record instead"),
64
58
  activePackageVersionId: zod_types_1.zodPeerId
65
59
  .optional()
66
60
  .describe("FK to PackageVersions — the currently active version"),
@@ -1,4 +1,5 @@
1
1
  import type { IPackageVersion } from "../data/package-versions";
2
+ import type { IAppNav } from "../types/app-nav";
2
3
  /** Prefix for the persistent variable name that stores a package's signing secret key. */
3
4
  export declare const PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
4
5
  /**
@@ -14,18 +15,23 @@ export interface IAuthorSignedPayload {
14
15
  routesBundleFileHash?: string;
15
16
  uiBundleFileHash?: string;
16
17
  publicKey: string;
18
+ appNavs?: IAppNav[];
19
+ createdBy?: string;
20
+ createdAt?: string;
17
21
  }
22
+ /** PV fields required by {@link buildAuthorSignedPayload} and related signing/verification functions. */
23
+ export type AuthorSignableFields = Pick<IPackageVersion, "packageId" | "packageVersionId" | "version" | "versionTag" | "packageBundleFileHash" | "routesBundleFileHash" | "uiBundleFileHash" | "appNavs"> & Partial<Pick<IPackageVersion, "createdBy" | "createdAt">>;
18
24
  /**
19
25
  * Extracts the signable payload from a PackageVersion record.
20
- * Only includes optional hash fields when they have a value (undefined fields are omitted
26
+ * Only includes optional fields when they have a value (undefined fields are omitted
21
27
  * entirely so `stableStringify` produces a consistent output).
22
28
  */
23
- export declare function buildAuthorSignedPayload(pv: Pick<IPackageVersion, "packageId" | "packageVersionId" | "version" | "versionTag" | "packageBundleFileHash" | "routesBundleFileHash" | "uiBundleFileHash">, publicKey: string): IAuthorSignedPayload;
29
+ export declare function buildAuthorSignedPayload(pv: AuthorSignableFields, publicKey: string): IAuthorSignedPayload;
24
30
  /**
25
31
  * Signs a PackageVersion with the publisher's Ed25519 secret key.
26
32
  * @returns A base64url-encoded detached Ed25519 signature over `stableStringify(payload)`.
27
33
  */
28
- export declare function signPackageAuthor(pv: Pick<IPackageVersion, "packageId" | "packageVersionId" | "version" | "versionTag" | "packageBundleFileHash" | "routesBundleFileHash" | "uiBundleFileHash">, secretKey: string): string;
34
+ export declare function signPackageAuthor(pv: AuthorSignableFields, secretKey: string): string;
29
35
  /**
30
36
  * Result of verifying a packageAuthorSignature.
31
37
  * `publicKey` is the key embedded in the payload (extracted from the signature verification process).
@@ -45,4 +51,4 @@ export interface IVerifyAuthorSignatureResult {
45
51
  * @param expectedPublicKey - The public key to verify against (from Package.publishPublicKey)
46
52
  * @returns `{ valid, publicKey }` — valid is true if signature verifies and key matches
47
53
  */
48
- export declare function verifyPackageAuthorSignature(pv: Pick<IPackageVersion, "packageId" | "packageVersionId" | "version" | "versionTag" | "packageBundleFileHash" | "routesBundleFileHash" | "uiBundleFileHash" | "packageAuthorSignature">, expectedPublicKey: string): IVerifyAuthorSignatureResult;
54
+ export declare function verifyPackageAuthorSignature(pv: AuthorSignableFields & Pick<IPackageVersion, "packageAuthorSignature">, expectedPublicKey: string): IVerifyAuthorSignatureResult;
@@ -11,7 +11,7 @@ const keys_1 = require("../keys");
11
11
  exports.PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
12
12
  /**
13
13
  * Extracts the signable payload from a PackageVersion record.
14
- * Only includes optional hash fields when they have a value (undefined fields are omitted
14
+ * Only includes optional fields when they have a value (undefined fields are omitted
15
15
  * entirely so `stableStringify` produces a consistent output).
16
16
  */
17
17
  function buildAuthorSignedPayload(pv, publicKey) {
@@ -29,6 +29,15 @@ function buildAuthorSignedPayload(pv, publicKey) {
29
29
  if (pv.uiBundleFileHash) {
30
30
  payload.uiBundleFileHash = pv.uiBundleFileHash;
31
31
  }
32
+ if (pv.appNavs) {
33
+ payload.appNavs = pv.appNavs;
34
+ }
35
+ if (pv.createdBy) {
36
+ payload.createdBy = pv.createdBy;
37
+ }
38
+ if (pv.createdAt) {
39
+ payload.createdAt = pv.createdAt;
40
+ }
32
41
  return payload;
33
42
  }
34
43
  /**
@@ -342,6 +342,7 @@ function buildPropagatedPv(pv, files) {
342
342
  routesBundleFileHash: routesBundle?.expectedHash,
343
343
  uiBundleFileId: uiBundle?.file.fileId,
344
344
  uiBundleFileHash: uiBundle?.expectedHash,
345
+ appNavs: pv.appNavs,
345
346
  packageAuthorSignature: pv.packageAuthorSignature,
346
347
  signature: "",
347
348
  createdBy: pv.createdBy,
@@ -6,6 +6,7 @@
6
6
  * I/O — callers (the system tool, tests) provide bundle strings and receive
7
7
  * artifacts in memory.
8
8
  */
9
+ import type { IAppNav } from "../types/app-nav";
9
10
  import { type IAuthorSignedPayload } from "./package-author-signing";
10
11
  import type { ILatestPointer } from "./package-remote-checker";
11
12
  import { type ITarballPayload } from "./package-tarball";
@@ -30,6 +31,10 @@ export interface IBuildPublishArtifactsOpts {
30
31
  publicKey: string;
31
32
  /** Pre-generated packageVersionId. If omitted, a new one is created via {@link newid}. */
32
33
  packageVersionId?: string;
34
+ /** App navigation items to include in the tarball metadata. */
35
+ appNavs?: IAppNav[];
36
+ /** Peer ID of the publisher. Recorded as `createdBy` on the PV. */
37
+ createdBy?: string;
33
38
  }
34
39
  /** Artifacts produced by {@link buildPublishArtifacts}. */
35
40
  export interface IPublishArtifacts {
@@ -32,6 +32,7 @@ async function buildPublishArtifacts(opts) {
32
32
  ? (0, file_types_1.computeFileHash)(bundles.routesBundle)
33
33
  : undefined;
34
34
  const uiBundleFileHash = bundles.uiBundle ? (0, file_types_1.computeFileHash)(bundles.uiBundle) : undefined;
35
+ const now = new Date().toISOString();
35
36
  const pvFields = {
36
37
  packageId,
37
38
  packageVersionId,
@@ -40,6 +41,9 @@ async function buildPublishArtifacts(opts) {
40
41
  packageBundleFileHash,
41
42
  routesBundleFileHash,
42
43
  uiBundleFileHash,
44
+ appNavs: opts.appNavs,
45
+ createdBy: opts.createdBy,
46
+ createdAt: now,
43
47
  };
44
48
  const payload = (0, package_author_signing_1.buildAuthorSignedPayload)(pvFields, publicKey);
45
49
  const packageAuthorSignature = await sign(payload);
@@ -14,6 +14,7 @@ const file_types_1 = require("../data/files/file.types");
14
14
  const package_versions_1 = require("../data/package-versions");
15
15
  const packages_1 = require("../data/packages");
16
16
  const keys_1 = require("../keys");
17
+ const package_version_resolver_1 = require("../data/package-version-resolver");
17
18
  const package_author_signing_1 = require("./package-author-signing");
18
19
  const package_installer_1 = require("./package-installer");
19
20
  const package_propagation_1 = require("./package-propagation");
@@ -84,6 +85,9 @@ async function doRemoteCheck(packageId, updateUrl, tag, http) {
84
85
  packageBundleFileHash: unpacked.payload.packageBundleFileHash,
85
86
  routesBundleFileHash: unpacked.payload.routesBundleFileHash,
86
87
  uiBundleFileHash: unpacked.payload.uiBundleFileHash,
88
+ appNavs: unpacked.payload.appNavs,
89
+ createdBy: unpacked.payload.createdBy,
90
+ createdAt: unpacked.payload.createdAt,
87
91
  packageAuthorSignature: unpacked.payload.packageAuthorSignature,
88
92
  }, unpacked.payload.publicKey);
89
93
  if (!sigResult.valid) {
@@ -160,10 +164,11 @@ async function installRemotePackageVersion(dataContext, result) {
160
164
  routesBundleFileHash: routesFile?.fileHash,
161
165
  uiBundleFileId: uiFile?.fileId,
162
166
  uiBundleFileHash: uiFile?.fileHash,
167
+ appNavs: unpacked.payload.appNavs,
163
168
  packageAuthorSignature: unpacked.payload.packageAuthorSignature,
164
169
  signature: "",
165
- createdBy: packageId,
166
- createdAt: new Date().toISOString(),
170
+ createdBy: unpacked.payload.createdBy ?? packageId,
171
+ createdAt: unpacked.payload.createdAt ?? new Date().toISOString(),
167
172
  };
168
173
  const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
169
174
  const savedPv = await pvTable.signAndSave(pv);
@@ -173,8 +178,11 @@ async function installRemotePackageVersion(dataContext, result) {
173
178
  await (0, packages_1.Packages)(dataContext).signAndSave(pkg);
174
179
  }
175
180
  const activationResult = await (0, package_propagation_1.activateBestEligibleVersion)(dataContext, pkg);
181
+ // Also run the device-level resolver so this device picks up the new version
182
+ // (activateBestEligibleVersion only updates the group-level default).
183
+ const resolveResult = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dataContext, { force: true });
176
184
  return {
177
- activated: activationResult?.activated ?? false,
185
+ activated: activationResult?.activated || resolveResult.upgraded,
178
186
  packageVersion: savedPv,
179
187
  };
180
188
  }
@@ -28,6 +28,10 @@ const mockActivateBestEligibleVersion = jest.fn();
28
28
  jest.mock("./package-propagation", () => ({
29
29
  activateBestEligibleVersion: (...args) => mockActivateBestEligibleVersion(...args),
30
30
  }));
31
+ const mockResolveDevicePackageVersion = jest.fn();
32
+ jest.mock("../data/package-version-resolver", () => ({
33
+ resolveDevicePackageVersion: (...args) => mockResolveDevicePackageVersion(...args),
34
+ }));
31
35
  const file_types_1 = require("../data/files/file.types");
32
36
  const keys_1 = require("../keys");
33
37
  const package_author_signing_1 = require("./package-author-signing");
@@ -96,6 +100,7 @@ describe("package-remote-checker", () => {
96
100
  activated: true,
97
101
  activePackageVersionId: PV_ID,
98
102
  });
103
+ mockResolveDevicePackageVersion.mockResolvedValue({ loaded: true, upgraded: true });
99
104
  });
100
105
  describe("checkPackageRemoteForNewVersion", () => {
101
106
  it("downloads and verifies tarball when new version available", async () => {
@@ -258,6 +263,7 @@ describe("package-remote-checker", () => {
258
263
  expect(installed.activated).toBe(true);
259
264
  expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ publishPublicKey: keys.publicKey }));
260
265
  expect(mockPackageVersionsSignAndSave).toHaveBeenCalled();
266
+ expect(mockResolveDevicePackageVersion).toHaveBeenCalledWith(expect.objectContaining({ packageId: PKG_ID }), expect.anything(), { force: true });
261
267
  });
262
268
  });
263
269
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.19.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"