@peers-app/peers-sdk 0.26.4 → 0.26.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/contracts/__tests__/contract-resolvers.test.js +24 -0
  2. package/dist/contracts/__tests__/legacy-removal.test.js +38 -0
  3. package/dist/contracts/__tests__/remote-contract-transport.test.js +23 -32
  4. package/dist/contracts/__tests__/system-contracts.test.js +80 -0
  5. package/dist/contracts/contract-resolvers.d.ts +1 -1
  6. package/dist/contracts/contract-resolvers.js +3 -2
  7. package/dist/contracts/create-local-consumer.d.ts +2 -1
  8. package/dist/contracts/create-local-consumer.js +1 -1
  9. package/dist/contracts/index.d.ts +2 -0
  10. package/dist/contracts/index.js +2 -0
  11. package/dist/contracts/remote-contract-transport.d.ts +3 -3
  12. package/dist/contracts/remote-contract-transport.js +19 -17
  13. package/dist/contracts/system/host-shell-contract.d.ts +191 -0
  14. package/dist/contracts/system/host-shell-contract.js +190 -0
  15. package/dist/contracts/system/index.d.ts +4 -0
  16. package/dist/contracts/system/index.js +4 -0
  17. package/dist/contracts/system/packages-contract.d.ts +257 -0
  18. package/dist/contracts/system/packages-contract.js +213 -0
  19. package/dist/data/create-group.d.ts +1 -1
  20. package/dist/data/create-group.js +1 -1
  21. package/dist/data/group-members.d.ts +6 -1
  22. package/dist/data/group-members.js +12 -4
  23. package/dist/data/groups.d.ts +6 -1
  24. package/dist/data/groups.js +12 -4
  25. package/dist/data/package-version-resolver.d.ts +8 -2
  26. package/dist/data/package-version-resolver.js +28 -2
  27. package/dist/data/package-version-resolver.test.js +42 -0
  28. package/dist/data/package-versions.d.ts +5 -1
  29. package/dist/data/package-versions.js +11 -4
  30. package/dist/data/packages.d.ts +7 -1
  31. package/dist/data/packages.js +13 -4
  32. package/dist/data/table-signing.test.d.ts +1 -0
  33. package/dist/data/table-signing.test.js +118 -0
  34. package/dist/data/users.d.ts +1 -2
  35. package/dist/data/users.js +8 -5
  36. package/dist/package-installer/package-author-signer.d.ts +19 -0
  37. package/dist/package-installer/package-author-signer.js +24 -0
  38. package/dist/package-installer/package-creator.d.ts +1 -7
  39. package/dist/package-installer/package-creator.js +0 -12
  40. package/dist/package-installer/package-creator.test.js +0 -16
  41. package/dist/package-installer/package-installer.d.ts +5 -0
  42. package/dist/package-installer/package-installer.js +60 -37
  43. package/dist/package-installer/package-installer.test.js +129 -0
  44. package/dist/package-installer/types.d.ts +5 -11
  45. package/dist/package-loader/index.d.ts +1 -0
  46. package/dist/package-loader/index.js +1 -0
  47. package/dist/package-loader/isolated-runtime.d.ts +18 -0
  48. package/dist/package-loader/package-loader.d.ts +67 -4
  49. package/dist/package-loader/package-loader.js +300 -52
  50. package/dist/package-loader/package-loader.test.js +413 -1
  51. package/dist/package-loader/package-runtime-state.d.ts +112 -0
  52. package/dist/package-loader/package-runtime-state.js +18 -0
  53. package/dist/package-loader/resolve-consumed-contracts.js +5 -4
  54. package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
  55. package/dist/rpc-types.d.ts +0 -121
  56. package/dist/rpc-types.js +0 -39
  57. package/dist/system-ids.d.ts +13 -0
  58. package/dist/system-ids.js +14 -1
  59. package/package.json +1 -1
@@ -92,6 +92,10 @@ const metaData = {
92
92
  fields: (0, types_1.schemaToFields)(schema),
93
93
  indexes: [{ fields: ["packageId"] }],
94
94
  };
95
+ // Symbol.for keeps signer registration shared by separately installed SDK copies.
96
+ const signingKey = Symbol.for("@peers-app/peers-sdk/package-version-signing");
97
+ const signingGlobals = globalThis;
98
+ /** Package version table with signature and group promotion permission checks. */
95
99
  let PackageVersionsTable = (() => {
96
100
  let _classSuper = table_1.Table;
97
101
  let _instanceExtraInitializers = [];
@@ -122,15 +126,16 @@ let PackageVersionsTable = (() => {
122
126
  return super.save(packageVersion, opts);
123
127
  }
124
128
  async signAndSave(packageVersion, opts) {
125
- if (!PackageVersionsTable.addSignatureToPackageVersion) {
129
+ const sign = signingGlobals[signingKey];
130
+ if (!sign) {
126
131
  throw new Error("Package version signing is not enabled. Call PackageVersionsTable.enablePackageVersionSigning(fn) to enable it.");
127
132
  }
128
- packageVersion = await PackageVersionsTable.addSignatureToPackageVersion(packageVersion);
133
+ packageVersion = await sign.call(PackageVersionsTable, packageVersion);
129
134
  return this.save(packageVersion, opts);
130
135
  }
131
- static addSignatureToPackageVersion = undefined;
136
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
132
137
  static enablePackageVersionSigning(fn) {
133
- PackageVersionsTable.addSignatureToPackageVersion = fn;
138
+ signingGlobals[signingKey] = fn;
134
139
  }
135
140
  constructor() {
136
141
  super(...arguments);
@@ -140,6 +145,7 @@ let PackageVersionsTable = (() => {
140
145
  })();
141
146
  exports.PackageVersionsTable = PackageVersionsTable;
142
147
  (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, schema, PackageVersionsTable);
148
+ /** Returns the PackageVersions table for the given context or the current default context. */
143
149
  function PackageVersions(dataContext) {
144
150
  return (0, context_1.getTableContainer)(dataContext).getTable(metaData, schema, PackageVersionsTable);
145
151
  }
@@ -161,6 +167,7 @@ function computePackageVersionHash(version,
161
167
  _versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) {
162
168
  return (0, keys_1.hashValue)([version, packageBundleFileHash, routesBundleFileHash ?? "", uiBundleFileHash ?? ""].join(":"));
163
169
  }
170
+ /** Checks whether an incoming version falls within the selected automatic update range. */
164
171
  function isVersionInRange(activeVersion, incomingVersion, range) {
165
172
  if (range === "pinned")
166
173
  return false;
@@ -42,19 +42,25 @@ declare const schema: z.ZodObject<{
42
42
  followVersionTags?: string | undefined;
43
43
  updateUrl?: string | undefined;
44
44
  }>;
45
+ /** A signed package record with publisher identity and version selection policy. */
45
46
  export type IPackage = z.infer<typeof schema>;
47
+ /** Package table with signature enforcement outside personal contexts. */
46
48
  export declare class PackagesTable extends Table<IPackage> {
47
49
  static isPassthrough: boolean;
48
50
  insert(packageObj: IPackage, opts?: ISaveOptions): Promise<IPackage>;
49
51
  update(packageObj: IPackage, opts?: ISaveOptions): Promise<IPackage>;
50
52
  save(packageObj: IPackage, opts?: ISaveOptions): Promise<IPackage>;
51
53
  signAndSave(packageObj: IPackage, opts?: ISaveOptions): Promise<IPackage>;
52
- private static addSignatureToPackage;
54
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
53
55
  static enablePackageSigning(fn: (packageObj: IPackage) => IPackage): void;
54
56
  delete(packageId: string): Promise<void>;
55
57
  }
58
+ /** Returns the Packages table for the given context or the current default context. */
56
59
  export declare function Packages(dataContext?: DataContext): PackagesTable;
60
+ /** Default directory for locally stored package files. */
57
61
  export declare const packagesRootDir = "~/peers/packages";
62
+ /** Device preference controlling package reloads when the UI refreshes. */
58
63
  export declare const reloadPackagesOnPageRefresh: import("./persistent-vars").PersistentVar<boolean>;
64
+ /** Device preference enabling automatic peers-core updates. */
59
65
  export declare const autoUpdatePeersCore: import("./persistent-vars").PersistentVar<boolean>;
60
66
  export {};
@@ -83,6 +83,10 @@ const metaData = {
83
83
  iconClassName: "bi bi-box-fill",
84
84
  fields: (0, types_1.schemaToFields)(schema),
85
85
  };
86
+ // Symbol.for keeps signer registration shared by separately installed SDK copies.
87
+ const signingKey = Symbol.for("@peers-app/peers-sdk/package-signing");
88
+ const signingGlobals = globalThis;
89
+ /** Package table with signature enforcement outside personal contexts. */
86
90
  let PackagesTable = (() => {
87
91
  let _classSuper = table_1.Table;
88
92
  let _instanceExtraInitializers = [];
@@ -113,15 +117,16 @@ let PackagesTable = (() => {
113
117
  return super.save(packageObj, opts);
114
118
  }
115
119
  async signAndSave(packageObj, opts) {
116
- if (!PackagesTable.addSignatureToPackage) {
120
+ const sign = signingGlobals[signingKey];
121
+ if (!sign) {
117
122
  throw new Error("Package signing is not enabled. Call PackagesTable.enablePackageSigning(fn) to enable it.");
118
123
  }
119
- packageObj = await PackagesTable.addSignatureToPackage(packageObj);
124
+ packageObj = await sign.call(PackagesTable, packageObj);
120
125
  return this.save(packageObj, opts);
121
126
  }
122
- static addSignatureToPackage = undefined;
127
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
123
128
  static enablePackageSigning(fn) {
124
- PackagesTable.addSignatureToPackage = fn;
129
+ signingGlobals[signingKey] = fn;
125
130
  }
126
131
  async delete(packageId) {
127
132
  // anyone (with write permissions) can delete packages since that isn't a security risk (unless something else is relying on a package to be present?)
@@ -141,11 +146,15 @@ let PackagesTable = (() => {
141
146
  })();
142
147
  exports.PackagesTable = PackagesTable;
143
148
  (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, schema, PackagesTable);
149
+ /** Returns the Packages table for the given context or the current default context. */
144
150
  function Packages(dataContext) {
145
151
  return (0, context_1.getTableContainer)(dataContext).getTable(metaData, schema, PackagesTable);
146
152
  }
153
+ /** Default directory for locally stored package files. */
147
154
  exports.packagesRootDir = "~/peers/packages";
155
+ /** Device preference controlling package reloads when the UI refreshes. */
148
156
  exports.reloadPackagesOnPageRefresh = (0, persistent_vars_1.deviceVar)("reloadPackagesOnPageRefresh", {
149
157
  defaultValue: true,
150
158
  });
159
+ /** Device preference enabling automatic peers-core updates. */
151
160
  exports.autoUpdatePeersCore = (0, persistent_vars_1.deviceVar)("autoUpdatePeersCore", { defaultValue: true });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tableNames = ["package-version", "package", "group", "group-member", "user"];
4
+ // These key strings are the contract between installed SDK versions; do not rename them.
5
+ const signingKey = (name) => Symbol.for(`@peers-app/peers-sdk/${name}-signing`);
6
+ // Restore process-global registrations so these tests never leave test signers installed.
7
+ const priorSigners = tableNames.map((name) => {
8
+ const key = signingKey(name);
9
+ return { key, descriptor: Object.getOwnPropertyDescriptor(globalThis, key) };
10
+ });
11
+ beforeEach(() => {
12
+ for (const { key } of priorSigners) {
13
+ Reflect.deleteProperty(globalThis, key);
14
+ }
15
+ });
16
+ afterEach(() => {
17
+ for (const { key, descriptor } of priorSigners) {
18
+ if (descriptor) {
19
+ Object.defineProperty(globalThis, key, descriptor);
20
+ }
21
+ else {
22
+ Reflect.deleteProperty(globalThis, key);
23
+ }
24
+ }
25
+ });
26
+ function loadSigningTables() {
27
+ let tables;
28
+ jest.isolateModules(() => {
29
+ const { PackageVersionsTable } = require("./package-versions");
30
+ const { PackagesTable } = require("./packages");
31
+ const { GroupsTable } = require("./groups");
32
+ const { GroupMembersTable } = require("./group-members");
33
+ const { UsersTable } = require("./users");
34
+ tables = {
35
+ "package-version": {
36
+ prototype: PackageVersionsTable.prototype,
37
+ enable: PackageVersionsTable.enablePackageVersionSigning,
38
+ },
39
+ package: { prototype: PackagesTable.prototype, enable: PackagesTable.enablePackageSigning },
40
+ group: { prototype: GroupsTable.prototype, enable: GroupsTable.enableGroupSigning },
41
+ "group-member": {
42
+ prototype: GroupMembersTable.prototype,
43
+ enable: GroupMembersTable.enableGroupMemberSigning,
44
+ },
45
+ user: { prototype: UsersTable.prototype, enable: UsersTable.enableUserSigning },
46
+ };
47
+ });
48
+ return tables;
49
+ }
50
+ describe("table signing across SDK module instances", () => {
51
+ it.each(tableNames)("shares and replaces the %s signer across copies", async (name) => {
52
+ const first = loadSigningTables()[name];
53
+ const second = loadSigningTables()[name];
54
+ expect(first.prototype).not.toBe(second.prototype);
55
+ // Stub persistence only: exercise the real signAndSave and registration methods.
56
+ const receiver = { save: jest.fn(async (record) => record) };
57
+ const row = { name: "Example", signature: "" };
58
+ const opts = { saveAsSnapshot: false };
59
+ await expect(second.prototype.signAndSave.call(receiver, row, opts)).rejects.toThrow(/signing.*enabled/i);
60
+ expect(receiver.save).not.toHaveBeenCalled();
61
+ const sign = jest.fn((record) => ({ ...record, signature: "first" }));
62
+ first.enable(sign);
63
+ expect(Reflect.get(globalThis, signingKey(name))).toBe(sign);
64
+ await expect(second.prototype.signAndSave.call(receiver, row, opts)).resolves.toEqual({
65
+ ...row,
66
+ signature: "first",
67
+ });
68
+ expect(sign).toHaveBeenCalledWith(row);
69
+ expect(sign.mock.contexts.at(-1)).toBe(second.prototype.constructor);
70
+ expect(receiver.save).toHaveBeenLastCalledWith({ ...row, signature: "first" }, name === "group-member" ? { ...opts, restoreIfDeleted: true } : opts);
71
+ // Loading another copy must preserve a signer that is already registered.
72
+ const third = loadSigningTables()[name];
73
+ expect(third.prototype).not.toBe(first.prototype);
74
+ await expect(third.prototype.signAndSave.call(receiver, row)).resolves.toEqual({
75
+ ...row,
76
+ signature: "first",
77
+ });
78
+ expect(sign.mock.contexts.at(-1)).toBe(third.prototype.constructor);
79
+ second.enable((record) => ({ ...record, signature: "replacement" }));
80
+ for (const copy of [first, third]) {
81
+ await expect(copy.prototype.signAndSave.call(receiver, row)).resolves.toEqual({
82
+ ...row,
83
+ signature: "replacement",
84
+ });
85
+ }
86
+ receiver.save.mockClear();
87
+ const failsOnce = jest
88
+ .fn((record) => ({ ...record, signature: "recovered" }))
89
+ .mockImplementationOnce(() => {
90
+ throw new Error("signing failed");
91
+ });
92
+ first.enable(failsOnce);
93
+ await expect(second.prototype.signAndSave.call(receiver, row)).rejects.toThrow("signing failed");
94
+ expect(receiver.save).not.toHaveBeenCalled();
95
+ await expect(third.prototype.signAndSave.call(receiver, row)).resolves.toEqual({
96
+ ...row,
97
+ signature: "recovered",
98
+ });
99
+ expect(failsOnce).toHaveBeenCalledTimes(2);
100
+ receiver.save.mockRejectedValueOnce(new Error("save failed"));
101
+ await expect(second.prototype.signAndSave.call(receiver, row)).rejects.toThrow("save failed");
102
+ await expect(first.prototype.signAndSave.call(receiver, row)).resolves.toEqual({
103
+ ...row,
104
+ signature: "recovered",
105
+ });
106
+ });
107
+ it("keeps the five signer registrations independent", async () => {
108
+ const first = loadSigningTables();
109
+ const second = loadSigningTables();
110
+ for (const name of tableNames) {
111
+ first[name].enable((record) => ({ ...record, signature: name }));
112
+ }
113
+ const receiver = { save: jest.fn(async (record) => record) };
114
+ for (const name of tableNames) {
115
+ await expect(second[name].prototype.signAndSave.call(receiver, { name: "Example", signature: "" })).resolves.toEqual({ name: "Example", signature: name });
116
+ }
117
+ });
118
+ });
@@ -84,8 +84,7 @@ export declare class UsersTable extends Table<IUser> {
84
84
  */
85
85
  save(user: IUser, opts?: ISaveOptions): Promise<IUser>;
86
86
  signAndSave(user: IUser, opts?: ISaveOptions): Promise<IUser>;
87
- private static addSignatureToUser;
88
- /** Enables signing for proxied `signAndSave` calls. */
87
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
89
88
  static enableUserSigning(fn: (user: IUser) => IUser): void;
90
89
  /** @deprecated Forbidden on UsersTable; use save() */
91
90
  insert(..._args: Parameters<Table<IUser>["insert"]>): never;
@@ -102,6 +102,9 @@ let userRowChangeGuard = refuseIdentityChanges;
102
102
  function setUserRowChangeGuard(guard) {
103
103
  userRowChangeGuard = guard ?? refuseIdentityChanges;
104
104
  }
105
+ // Symbol.for keeps signer registration shared by separately installed SDK copies.
106
+ const signingKey = Symbol.for("@peers-app/peers-sdk/user-signing");
107
+ const signingGlobals = globalThis;
105
108
  /** User table with owner-signature enforcement outside the personal context. */
106
109
  let UsersTable = (() => {
107
110
  let _classSuper = table_1.Table;
@@ -170,20 +173,20 @@ let UsersTable = (() => {
170
173
  return super.save(verdict.row, opts);
171
174
  }
172
175
  async signAndSave(user, opts) {
173
- if (!UsersTable.addSignatureToUser) {
176
+ const sign = signingGlobals[signingKey];
177
+ if (!sign) {
174
178
  throw new Error("User signing must be enabled to sign and save users. Call UsersTable.enableUserSigning(fn) to enable it.");
175
179
  }
176
180
  user = {
177
181
  ...user,
178
182
  name: user.name.slice(0, devices_1.DISPLAY_NAME_MAX_LENGTH),
179
183
  };
180
- user = UsersTable.addSignatureToUser(user);
184
+ user = sign.call(UsersTable, user);
181
185
  return this.save(user, opts);
182
186
  }
183
- static addSignatureToUser = undefined;
184
- /** Enables signing for proxied `signAndSave` calls. */
187
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
185
188
  static enableUserSigning(fn) {
186
- UsersTable.addSignatureToUser = fn;
189
+ signingGlobals[signingKey] = fn;
187
190
  }
188
191
  /** @deprecated Forbidden on UsersTable; use save() */
189
192
  insert(..._args) {
@@ -23,3 +23,22 @@ export declare function loadPackageSigningKey(packageId: string, getUserSecretKe
23
23
  * @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
24
24
  */
25
25
  export declare function trySignPackageAuthorVersion(pvFields: AuthorSignableFields, getUserSecretKey: () => Promise<string>): Promise<string | undefined>;
26
+ /** Host-installed signer for PackageVersion author signatures. */
27
+ export type PackageAuthorVersionSigner = (pvFields: AuthorSignableFields) => Promise<string | undefined>;
28
+ /**
29
+ * Install (or clear) the host's PackageVersion author signer.
30
+ *
31
+ * `peers-device` installs one at startup over the signed-in user's secret key, so Runtime
32
+ * code (the installer, System Packages) can re-sign versions without a host RPC.
33
+ *
34
+ * @param signer - Signer to use, or `undefined` to clear it.
35
+ */
36
+ export declare function setPackageAuthorVersionSigner(signer: PackageAuthorVersionSigner | undefined): void;
37
+ /**
38
+ * Sign PackageVersion fields with the installed host signer.
39
+ *
40
+ * @param pvFields - Author-signable PackageVersion fields.
41
+ * @returns The detached signature, or `undefined` when no signer is installed, the package
42
+ * signing key is unavailable, or signing fails.
43
+ */
44
+ export declare function signPackageAuthorVersionOnHost(pvFields: AuthorSignableFields): Promise<string | undefined>;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadPackageSigningKey = loadPackageSigningKey;
4
4
  exports.trySignPackageAuthorVersion = trySignPackageAuthorVersion;
5
+ exports.setPackageAuthorVersionSigner = setPackageAuthorVersionSigner;
6
+ exports.signPackageAuthorVersionOnHost = signPackageAuthorVersionOnHost;
5
7
  const user_context_singleton_1 = require("../context/user-context-singleton");
6
8
  const persistent_vars_1 = require("../data/persistent-vars");
7
9
  const keys_1 = require("../keys");
@@ -55,3 +57,25 @@ async function trySignPackageAuthorVersion(pvFields, getUserSecretKey) {
55
57
  return undefined;
56
58
  }
57
59
  }
60
+ let installedSigner;
61
+ /**
62
+ * Install (or clear) the host's PackageVersion author signer.
63
+ *
64
+ * `peers-device` installs one at startup over the signed-in user's secret key, so Runtime
65
+ * code (the installer, System Packages) can re-sign versions without a host RPC.
66
+ *
67
+ * @param signer - Signer to use, or `undefined` to clear it.
68
+ */
69
+ function setPackageAuthorVersionSigner(signer) {
70
+ installedSigner = signer;
71
+ }
72
+ /**
73
+ * Sign PackageVersion fields with the installed host signer.
74
+ *
75
+ * @param pvFields - Author-signable PackageVersion fields.
76
+ * @returns The detached signature, or `undefined` when no signer is installed, the package
77
+ * signing key is unavailable, or signing fails.
78
+ */
79
+ async function signPackageAuthorVersionOnHost(pvFields) {
80
+ return installedSigner?.(pvFields);
81
+ }
@@ -1,11 +1,5 @@
1
1
  import type { DataContext } from "../context/data-context";
2
- import type { IAddOrUpdatePackageResult, ICreatePackageOpts, ICreatePackageResult, IPackageInstallerDeps, ITemplateCacheResult, TemplateCacheRefreshFailure } from "./types";
3
- /**
4
- * Normalize `addOrUpdatePackage` RPC results.
5
- * Older hosts return a bare packageId string; current hosts return
6
- * {@link IAddOrUpdatePackageResult}.
7
- */
8
- export declare function unwrapAddOrUpdatePackageResult(result: string | IAddOrUpdatePackageResult): IAddOrUpdatePackageResult;
2
+ import type { ICreatePackageOpts, ICreatePackageResult, IPackageInstallerDeps, ITemplateCacheResult, TemplateCacheRefreshFailure } from "./types";
9
3
  /**
10
4
  * Convert a human-readable package name to a filesystem-safe folder slug.
11
5
  * "My Widget" → "my-widget", "camelCase Thing" → "camel-case-thing"
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.unwrapAddOrUpdatePackageResult = unwrapAddOrUpdatePackageResult;
4
3
  exports.nameToFolderSlug = nameToFolderSlug;
5
4
  exports.classifyTemplatePullFailure = classifyTemplatePullFailure;
6
5
  exports.ensureTemplateCache = ensureTemplateCache;
@@ -24,17 +23,6 @@ function patchTemplateTokens(contents, values) {
24
23
  .replaceAll("<app-screen-id>", values.appScreenId)
25
24
  .replaceAll("<contract-id>", values.contractId);
26
25
  }
27
- /**
28
- * Normalize `addOrUpdatePackage` RPC results.
29
- * Older hosts return a bare packageId string; current hosts return
30
- * {@link IAddOrUpdatePackageResult}.
31
- */
32
- function unwrapAddOrUpdatePackageResult(result) {
33
- if (typeof result === "string") {
34
- return { packageId: result };
35
- }
36
- return result;
37
- }
38
26
  /**
39
27
  * Convert a human-readable package name to a filesystem-safe folder slug.
40
28
  * "My Widget" → "my-widget", "camelCase Thing" → "camel-case-thing"
@@ -87,22 +87,6 @@ beforeEach(() => {
87
87
  mockNewKeys.mockReturnValue(generatedKeys);
88
88
  mockPersistentVarsSave.mockReset();
89
89
  });
90
- describe("unwrapAddOrUpdatePackageResult", () => {
91
- it("accepts a legacy packageId string", () => {
92
- expect((0, package_creator_1.unwrapAddOrUpdatePackageResult)("00mnkoyo7mizav6tseubjzr3a")).toEqual({
93
- packageId: "00mnkoyo7mizav6tseubjzr3a",
94
- });
95
- });
96
- it("passes through a structured result including a template warning", () => {
97
- expect((0, package_creator_1.unwrapAddOrUpdatePackageResult)({
98
- packageId: "00mnkoyo7mizav6tseubjzr3a",
99
- warning: "stale template",
100
- })).toEqual({
101
- packageId: "00mnkoyo7mizav6tseubjzr3a",
102
- warning: "stale template",
103
- });
104
- });
105
- });
106
90
  describe("nameToFolderSlug", () => {
107
91
  it("converts a simple name", () => {
108
92
  expect((0, package_creator_1.nameToFolderSlug)("My Widget")).toBe("my-widget");
@@ -8,6 +8,11 @@ import type { IInstallOpts, IInstallResult, IPackageInfo, IPackageInstallerDeps
8
8
  * bundles from `dist/`, hashes them for deduplication, saves File records
9
9
  * only when content has changed, upserts the PackageVersion, and optionally
10
10
  * triggers the package loader and device version resolution.
11
+ *
12
+ * When the bundle hash matches the existing PackageVersion, no files or PV are
13
+ * rewritten. The Packages row still gets a missing `activePackageVersionId`
14
+ * (and description), and when the id was missing the version is activated on
15
+ * this device unless `skipResolve`.
11
16
  */
12
17
  export declare function installPackageFromBundles(dataContext: DataContext, deps: IPackageInstallerDeps, packageId: string, opts?: IInstallOpts): Promise<IInstallResult>;
13
18
  /**
@@ -12,8 +12,8 @@ const package_version_resolver_1 = require("../data/package-version-resolver");
12
12
  const package_versions_1 = require("../data/package-versions");
13
13
  const packages_1 = require("../data/packages");
14
14
  const persistent_vars_1 = require("../data/persistent-vars");
15
- const rpc_types_1 = require("../rpc-types");
16
15
  const utils_1 = require("../utils");
16
+ const package_author_signer_1 = require("./package-author-signer");
17
17
  /**
18
18
  * Install or update a dev PackageVersion from locally-built bundle files.
19
19
  *
@@ -21,6 +21,11 @@ const utils_1 = require("../utils");
21
21
  * bundles from `dist/`, hashes them for deduplication, saves File records
22
22
  * only when content has changed, upserts the PackageVersion, and optionally
23
23
  * triggers the package loader and device version resolution.
24
+ *
25
+ * When the bundle hash matches the existing PackageVersion, no files or PV are
26
+ * rewritten. The Packages row still gets a missing `activePackageVersionId`
27
+ * (and description), and when the id was missing the version is activated on
28
+ * this device unless `skipResolve`.
24
29
  */
25
30
  async function installPackageFromBundles(dataContext, deps, packageId, opts) {
26
31
  const versionTag = opts?.versionTag ?? "dev";
@@ -53,23 +58,24 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
53
58
  const uiHash = uiBundle ? (0, file_types_1.computeFileHash)(uiBundle) : undefined;
54
59
  // 5. Compute PV hash
55
60
  const pvHash = (0, package_versions_1.computePackageVersionHash)(version, "", packageHash, routesHash, uiHash);
56
- // 6. Early exit if unchanged (still repair missing appNavs — see syncDevBundles skipLoad)
61
+ // 6. Early exit if unchanged. Still repair missing appNavs (see syncDevBundles
62
+ // skipLoad), and still fill an empty activePackageVersionId (e.g. reimport after
63
+ // the Packages row was deleted). If it was empty, activate on the device unless
64
+ // skipResolve.
57
65
  const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
58
66
  const existingPv = await pvTable.findOne({ packageId, versionTag });
59
67
  if (existingPv && existingPv.packageVersionHash === pvHash) {
60
- if (existingPv.appNavs?.length) {
61
- return {
62
- packageVersion: existingPv,
63
- loaded: undefined,
64
- unchanged: true,
65
- };
68
+ let loaded;
69
+ let unchanged = true;
70
+ if (!existingPv.appNavs?.length) {
71
+ loaded = await loadAndPersistAppNavs(dataContext, packageId, existingPv, localPath);
72
+ unchanged = !loaded?.appNavs?.length;
66
73
  }
67
- const loaded = await loadAndPersistAppNavs(dataContext, packageId, existingPv, localPath);
68
- return {
69
- packageVersion: existingPv,
70
- loaded,
71
- unchanged: !loaded?.appNavs?.length,
72
- };
74
+ await activateInstalledPackageVersion(dataContext, packageId, existingPv, info, localPath, {
75
+ skipResolve: opts?.skipResolve,
76
+ resolveOnlyWhenActivated: true,
77
+ });
78
+ return { packageVersion: existingPv, loaded, unchanged };
73
79
  }
74
80
  // 7. Save bundle files (deduped by hash)
75
81
  const packageFile = await saveBundleFile(dataContext, packageBundle, `${packageId}-package.bundle.js`, packageId);
@@ -110,31 +116,48 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
110
116
  if (!opts?.skipLoad || !savedPv.appNavs?.length) {
111
117
  loaded = await loadAndPersistAppNavs(dataContext, packageId, savedPv, localPath);
112
118
  }
113
- // 10. Update package record if needed (description, first-install activation)
119
+ // 10–11. Update package record and activate on device (unless skipResolve)
120
+ await activateInstalledPackageVersion(dataContext, packageId, savedPv, info, localPath, {
121
+ skipResolve: opts?.skipResolve,
122
+ });
123
+ return { packageVersion: savedPv, loaded, unchanged: false };
124
+ }
125
+ /**
126
+ * Sync the Packages row with an installed PackageVersion, then activate it on
127
+ * this device.
128
+ *
129
+ * Updates the description when it differs, and sets `activePackageVersionId`
130
+ * when it is empty (first install, or reimport after the row was deleted).
131
+ * Unless `skipResolve`, writes device prefs and force-resolves the version.
132
+ * With `resolveOnlyWhenActivated`, the device step runs only when this call
133
+ * filled an empty `activePackageVersionId`.
134
+ */
135
+ async function activateInstalledPackageVersion(dataContext, packageId, packageVersion, info, localPath, opts) {
136
+ // Update package record if needed (description, first-install activation)
114
137
  const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
115
- if (pkg) {
116
- let needsSave = false;
117
- if (info.description && info.description !== pkg.description) {
118
- pkg.description = info.description;
119
- needsSave = true;
120
- }
121
- if (!pkg.activePackageVersionId) {
122
- pkg.activePackageVersionId = savedPv.packageVersionId;
123
- needsSave = true;
124
- }
125
- if (needsSave) {
126
- await (0, packages_1.Packages)(dataContext).signAndSave(pkg);
127
- }
138
+ if (!pkg)
139
+ return;
140
+ let needsSave = false;
141
+ if (info.description && info.description !== pkg.description) {
142
+ pkg.description = info.description;
143
+ needsSave = true;
128
144
  }
129
- // 11. Activate on device (unless skipResolve)
130
- if (!opts?.skipResolve && pkg) {
131
- await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: savedPv.packageVersionId }, dataContext);
132
- await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dataContext, {
133
- force: true,
134
- localPath,
135
- });
145
+ const activated = !pkg.activePackageVersionId;
146
+ if (activated) {
147
+ pkg.activePackageVersionId = packageVersion.packageVersionId;
148
+ needsSave = true;
136
149
  }
137
- return { packageVersion: savedPv, loaded, unchanged: false };
150
+ if (needsSave) {
151
+ await (0, packages_1.Packages)(dataContext).signAndSave(pkg);
152
+ }
153
+ // Activate on device
154
+ if (opts.skipResolve || (opts.resolveOnlyWhenActivated && !activated))
155
+ return;
156
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: packageVersion.packageVersionId }, dataContext);
157
+ await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dataContext, {
158
+ force: true,
159
+ localPath,
160
+ });
138
161
  }
139
162
  /**
140
163
  * Load a package bundle and persist `appNavs` onto the PackageVersion when the
@@ -159,7 +182,7 @@ async function loadAndPersistAppNavs(dataContext, packageId, pv, localPath) {
159
182
  return loaded;
160
183
  pv.appNavs = loaded.appNavs;
161
184
  if (pv.packageAuthorSignature) {
162
- const authorSig = await rpc_types_1.rpcServerCalls.signPackageAuthorVersion({
185
+ const authorSig = await (0, package_author_signer_1.signPackageAuthorVersionOnHost)({
163
186
  packageId: pv.packageId,
164
187
  packageVersionId: pv.packageVersionId,
165
188
  version: pv.version,