@peers-app/peers-sdk 0.26.4 → 0.26.5

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 (47) 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/package-version-resolver.d.ts +8 -2
  22. package/dist/data/package-version-resolver.js +28 -2
  23. package/dist/data/package-version-resolver.test.js +42 -0
  24. package/dist/package-installer/package-author-signer.d.ts +19 -0
  25. package/dist/package-installer/package-author-signer.js +24 -0
  26. package/dist/package-installer/package-creator.d.ts +1 -7
  27. package/dist/package-installer/package-creator.js +0 -12
  28. package/dist/package-installer/package-creator.test.js +0 -16
  29. package/dist/package-installer/package-installer.d.ts +5 -0
  30. package/dist/package-installer/package-installer.js +60 -37
  31. package/dist/package-installer/package-installer.test.js +129 -0
  32. package/dist/package-installer/types.d.ts +5 -11
  33. package/dist/package-loader/index.d.ts +1 -0
  34. package/dist/package-loader/index.js +1 -0
  35. package/dist/package-loader/isolated-runtime.d.ts +18 -0
  36. package/dist/package-loader/package-loader.d.ts +67 -4
  37. package/dist/package-loader/package-loader.js +300 -52
  38. package/dist/package-loader/package-loader.test.js +413 -1
  39. package/dist/package-loader/package-runtime-state.d.ts +112 -0
  40. package/dist/package-loader/package-runtime-state.js +18 -0
  41. package/dist/package-loader/resolve-consumed-contracts.js +5 -4
  42. package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
  43. package/dist/rpc-types.d.ts +0 -121
  44. package/dist/rpc-types.js +0 -39
  45. package/dist/system-ids.d.ts +13 -0
  46. package/dist/system-ids.js +14 -1
  47. package/package.json +1 -1
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PACKAGES_CONTRACT_VERSION = void 0;
4
+ exports.packageVersionPromotionTargets = packageVersionPromotionTargets;
5
+ exports.isPackageRepositoryUrl = isPackageRepositoryUrl;
6
+ exports.getPackagesContractDefinition = getPackagesContractDefinition;
7
+ const system_ids_1 = require("../../system-ids");
8
+ const field_type_1 = require("../../types/field-type");
9
+ const system_contracts_1 = require("../system-contracts");
10
+ /** Version of the built-in System Packages contract. */
11
+ exports.PACKAGES_CONTRACT_VERSION = 1;
12
+ /**
13
+ * Release channels a version may be promoted to from its current tag: `dev` to `beta` or
14
+ * `stable`, and `beta` to `stable`. Missing or empty tags on legacy versions mean `dev`,
15
+ * matching the Versions screen. Promotion never demotes.
16
+ *
17
+ * @param versionTag - The version's current tag.
18
+ */
19
+ function packageVersionPromotionTargets(versionTag) {
20
+ switch (versionTag || "dev") {
21
+ case "dev":
22
+ return ["beta", "stable"];
23
+ case "beta":
24
+ return ["stable"];
25
+ default:
26
+ return [];
27
+ }
28
+ }
29
+ /**
30
+ * Whether user input names a remote repository for `clonePackage` (`http(s)://` or
31
+ * `git@host:`) rather than a local folder or package name. The host still validates the
32
+ * URL; this only routes input in the CLI and UI the same way.
33
+ *
34
+ * @param target - Input as typed.
35
+ */
36
+ function isPackageRepositoryUrl(target) {
37
+ return /^(https?:\/\/|git@[^:/\s]+:)/.test(target.trim());
38
+ }
39
+ const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
40
+ const packageIdField = field("packageId", field_type_1.FieldType.id, "Package id.");
41
+ const operationResultFields = [
42
+ packageIdField,
43
+ field("created", field_type_1.FieldType.boolean, "True when a new package was scaffolded.", {
44
+ optional: true,
45
+ }),
46
+ field("warning", field_type_1.FieldType.string, "Non-fatal problem to show the caller.", { optional: true }),
47
+ field("package", field_type_1.FieldType.object, "The package after the operation."),
48
+ ];
49
+ let definition;
50
+ /**
51
+ * Return the System Packages contract shape.
52
+ *
53
+ * Tools carry no `remoteAccessLevel`, so only callers this device trusts as `Self` (the
54
+ * user's own devices and users explicitly granted Self) can reach them remotely; group
55
+ * members, Admins included, are refused. Package code and workflows are refused
56
+ * everywhere. The provider binds every call to the route's
57
+ * data context; a package's group is the data boundary. Tools the host cannot perform
58
+ * throw `System Packages <tool> is not supported on this host (...)`; `getCapabilities`
59
+ * reports support up front.
60
+ */
61
+ function getPackagesContractDefinition() {
62
+ definition ??= {
63
+ contractId: system_ids_1.packagesContractId,
64
+ version: exports.PACKAGES_CONTRACT_VERSION,
65
+ devTag: "dev",
66
+ name: "System Packages",
67
+ description: "Install, inspect, reload, activate, and uninstall packages, with each device's observed runtime state.",
68
+ tables: [],
69
+ tools: [
70
+ {
71
+ name: "getCapabilities",
72
+ usageDescription: "Report which package operations this host supports.",
73
+ inputFields: [],
74
+ outputFields: [field("capabilities", field_type_1.FieldType.object, "Supported operations.")],
75
+ },
76
+ {
77
+ name: "listPackages",
78
+ usageDescription: "List packages in this data context with their requested version and runtime state on this device.",
79
+ inputFields: [],
80
+ outputFields: [
81
+ field("packages", field_type_1.FieldType.object, "Package summaries.", { isArray: true }),
82
+ ],
83
+ },
84
+ {
85
+ name: "getPackage",
86
+ usageDescription: "Return one package with its versions, local folder, and runtime state.",
87
+ inputFields: [packageIdField],
88
+ outputFields: [field("package", field_type_1.FieldType.object, "Package details.")],
89
+ },
90
+ {
91
+ name: "importPackage",
92
+ usageDescription: "Import an existing package folder on this device and load it.",
93
+ inputFields: [
94
+ field("path", field_type_1.FieldType.string, "Absolute or ~-prefixed folder path on the host."),
95
+ field("name", field_type_1.FieldType.string, "Display name; defaults to package.json.", {
96
+ optional: true,
97
+ }),
98
+ ],
99
+ outputFields: operationResultFields,
100
+ },
101
+ {
102
+ name: "createPackage",
103
+ usageDescription: "Scaffold a new package from the template, or import the folder when it already exists.",
104
+ inputFields: [
105
+ field("name", field_type_1.FieldType.string, "Package display name."),
106
+ field("location", field_type_1.FieldType.string, "Folder; defaults to the packages directory.", {
107
+ optional: true,
108
+ }),
109
+ ],
110
+ outputFields: operationResultFields,
111
+ },
112
+ {
113
+ name: "clonePackage",
114
+ usageDescription: "Clone a package repository, build it, and import it.",
115
+ inputFields: [field("url", field_type_1.FieldType.string, "Git repository URL.")],
116
+ outputFields: operationResultFields,
117
+ },
118
+ {
119
+ name: "installPackage",
120
+ usageDescription: "Re-enable a disabled package, or copy one here from another of your data contexts, then load it.",
121
+ inputFields: [
122
+ packageIdField,
123
+ field("fromDataContextId", field_type_1.FieldType.id, "Context to copy from; searched when omitted.", {
124
+ optional: true,
125
+ }),
126
+ ],
127
+ outputFields: operationResultFields,
128
+ },
129
+ {
130
+ name: "activatePackageVersion",
131
+ usageDescription: "Select a stored version for this device or the whole group, then load it on this device.",
132
+ inputFields: [
133
+ packageIdField,
134
+ field("packageVersionId", field_type_1.FieldType.id, "Version to activate; omit with device scope to follow the group again.", { optional: true }),
135
+ field("scope", field_type_1.FieldType.string, "device (default) or group.", { optional: true }),
136
+ ],
137
+ outputFields: operationResultFields,
138
+ },
139
+ {
140
+ name: "reloadPackage",
141
+ usageDescription: "Rebuild the dev version from the package folder when it changed, then force-reload the package on this device.",
142
+ inputFields: [packageIdField],
143
+ outputFields: operationResultFields,
144
+ },
145
+ {
146
+ name: "reloadAllPackages",
147
+ usageDescription: "Re-read every package folder on this device and reload the packages whose bundles changed.",
148
+ inputFields: [],
149
+ outputFields: [
150
+ field("skipped", field_type_1.FieldType.string, "Why the host skipped the reload.", {
151
+ optional: true,
152
+ }),
153
+ field("results", field_type_1.FieldType.object, "Per-package outcomes.", { isArray: true }),
154
+ ],
155
+ },
156
+ {
157
+ name: "linkRemoteRepo",
158
+ usageDescription: "Create a remote repository for a local package and link it.",
159
+ inputFields: [packageIdField],
160
+ outputFields: operationResultFields,
161
+ },
162
+ {
163
+ name: "uninstallPackage",
164
+ usageDescription: "Remove a package from this data context for every member device and unload it here. Versions, files, and device preferences are kept.",
165
+ inputFields: [packageIdField],
166
+ outputFields: [
167
+ packageIdField,
168
+ field("removed", field_type_1.FieldType.boolean, "True when the package row was removed."),
169
+ ],
170
+ },
171
+ {
172
+ name: "promotePackageVersion",
173
+ usageDescription: "Promote a stored package version to beta or stable (dev to beta or stable, beta to stable). Re-signs it with the package author key when this device has it, records the promotion in its history, and activates it when this device was running it and the group follows the new tag.",
174
+ inputFields: [
175
+ field("packageVersionId", field_type_1.FieldType.id, "Version to promote."),
176
+ field("versionTag", field_type_1.FieldType.string, "New release channel: beta or stable."),
177
+ ],
178
+ outputFields: [
179
+ ...operationResultFields,
180
+ field("packageVersionId", field_type_1.FieldType.id, "Promoted version."),
181
+ field("version", field_type_1.FieldType.string, "Its version number."),
182
+ field("versionTag", field_type_1.FieldType.string, "Its new release channel."),
183
+ field("signed", field_type_1.FieldType.boolean, "True when this device signed the promoted version."),
184
+ field("activated", field_type_1.FieldType.boolean, "True when the promotion also activated the version for the group or this device."),
185
+ ],
186
+ },
187
+ {
188
+ name: "installDefaultPackages",
189
+ usageDescription: "Install the default packages into this data context.",
190
+ inputFields: [],
191
+ outputFields: [
192
+ field("installed", field_type_1.FieldType.boolean, "True when anything was installed or updated."),
193
+ ],
194
+ },
195
+ ],
196
+ observables: [
197
+ {
198
+ name: "runtimeStates",
199
+ description: "Runtime state of this context's packages on this device, keyed by package id.",
200
+ valueType: field_type_1.FieldType.object,
201
+ writable: false,
202
+ },
203
+ ],
204
+ events: [],
205
+ };
206
+ return definition;
207
+ }
208
+ (0, system_contracts_1.registerSystemContract)({
209
+ providerPackageId: system_ids_1.peersSystemPackageId,
210
+ contractId: system_ids_1.packagesContractId,
211
+ version: exports.PACKAGES_CONTRACT_VERSION,
212
+ getDefinition: getPackagesContractDefinition,
213
+ });
@@ -19,6 +19,6 @@ export declare const GROUP_NAME_MAX_LENGTH = 100;
19
19
  *
20
20
  * Runs on a host with signing enabled (`GroupsTable.enableGroupSigning`); UIs reach it
21
21
  * through the host rather than duplicating the steps. Package seeding for the new context
22
- * (for example `seedBundledPeersCore`) is the caller's responsibility.
22
+ * (for example System Packages `installDefaultPackages`) is the caller's responsibility.
23
23
  */
24
24
  export declare function createGroup(userContext: UserContext, input: CreateGroupInput): Promise<IGroup>;
@@ -20,7 +20,7 @@ exports.GROUP_NAME_MAX_LENGTH = 100;
20
20
  *
21
21
  * Runs on a host with signing enabled (`GroupsTable.enableGroupSigning`); UIs reach it
22
22
  * through the host rather than duplicating the steps. Package seeding for the new context
23
- * (for example `seedBundledPeersCore`) is the caller's responsibility.
23
+ * (for example System Packages `installDefaultPackages`) is the caller's responsibility.
24
24
  */
25
25
  async function createGroup(userContext, input) {
26
26
  const name = input.name.trim();
@@ -81,17 +81,23 @@ export interface IResolveResult {
81
81
  }
82
82
  /**
83
83
  * Core resolver: evaluates available package versions against the device's
84
- * follow policy and loads the best one. Never auto-activates dev versions
85
- * unless the device explicitly follows "dev".
84
+ * follow policy and loads the best one. A dev version runs only on a device whose own
85
+ * selection points at it; a group selection of a dev version is left unloaded here.
86
86
  *
87
87
  * Callers:
88
88
  * - `PackagesTrackedDataSource.applyChanges` (on sync)
89
89
  * - `PackageLoader.loadAllPackages` (startup)
90
90
  * - `package-installer.ts` (after saving new bundles)
91
+ *
92
+ * @param opts.force - Reload even when the package is already loaded.
93
+ * @param opts.localPath - Package folder whose rebuilt dev bundle wins over the stored one.
94
+ * @param opts.onlyIfChanged - With `force`, keep a running package whose selected version and
95
+ * bundle are unchanged instead of restarting it (sync reconciliation).
91
96
  */
92
97
  export declare function resolveDevicePackageVersion(pkg: IPackage, dataContext: DataContext, opts?: {
93
98
  force?: boolean;
94
99
  localPath?: string;
100
+ onlyIfChanged?: boolean;
95
101
  }): Promise<IResolveResult>;
96
102
  /**
97
103
  * Returns true when the device has an explicit follow-policy override
@@ -85,13 +85,18 @@ function getEffectivePackagePrefs(pkg, devicePrefs) {
85
85
  }
86
86
  /**
87
87
  * Core resolver: evaluates available package versions against the device's
88
- * follow policy and loads the best one. Never auto-activates dev versions
89
- * unless the device explicitly follows "dev".
88
+ * follow policy and loads the best one. A dev version runs only on a device whose own
89
+ * selection points at it; a group selection of a dev version is left unloaded here.
90
90
  *
91
91
  * Callers:
92
92
  * - `PackagesTrackedDataSource.applyChanges` (on sync)
93
93
  * - `PackageLoader.loadAllPackages` (startup)
94
94
  * - `package-installer.ts` (after saving new bundles)
95
+ *
96
+ * @param opts.force - Reload even when the package is already loaded.
97
+ * @param opts.localPath - Package folder whose rebuilt dev bundle wins over the stored one.
98
+ * @param opts.onlyIfChanged - With `force`, keep a running package whose selected version and
99
+ * bundle are unchanged instead of restarting it (sync reconciliation).
95
100
  */
96
101
  async function resolveDevicePackageVersion(pkg, dataContext, opts) {
97
102
  const pvar = packagePrefsVar(pkg.packageId, dataContext);
@@ -118,6 +123,21 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
118
123
  if (!effectiveActivePvId) {
119
124
  return { loaded: false, upgraded: false };
120
125
  }
126
+ // A dev version runs only on a device that activated it. The group's selection can be a
127
+ // dev version (a new package, or one copied from another context); other devices leave it
128
+ // unloaded until they activate it themselves. Once activated here, rebuilds of that same
129
+ // version row keep loading through the dev branch below.
130
+ if (rawPrefs.activePackageVersionId !== effectiveActivePvId) {
131
+ const groupSelection = await (0, package_versions_1.PackageVersions)(dataContext)
132
+ .get(effectiveActivePvId)
133
+ .catch(() => undefined);
134
+ if (groupSelection?.versionTag === "dev") {
135
+ await dataContext.packageLoader.unloadPackage(pkg.packageId, {
136
+ reason: `Only a dev version (v${groupSelection.version}) is selected; activate it on this device to run it`,
137
+ });
138
+ return { loaded: false, pv: groupSelection, upgraded: false };
139
+ }
140
+ }
121
141
  const localPath = opts?.localPath ??
122
142
  (await readStoredPackageLocalPath(pkg.packageId, dataContext).catch(() => undefined));
123
143
  const compatibilityTargetId = rawPrefs.compatibilityFallbackForPackageVersionId;
@@ -130,6 +150,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
130
150
  if (compatibilityTarget) {
131
151
  try {
132
152
  const restored = await dataContext.packageLoader.loadPackage(pkg, {
153
+ onlyIfChanged: opts?.onlyIfChanged,
133
154
  force: true,
134
155
  localPath,
135
156
  packageVersionId: compatibilityTargetId,
@@ -158,6 +179,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
158
179
  if (!pv)
159
180
  return { loaded: false, pv: undefined, upgraded: false };
160
181
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
182
+ onlyIfChanged: opts?.onlyIfChanged,
161
183
  force: opts?.force,
162
184
  localPath,
163
185
  packageVersionId: effectiveActivePvId,
@@ -182,6 +204,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
182
204
  // activates a non-dev version.
183
205
  if (activePv?.versionTag === "dev") {
184
206
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
207
+ onlyIfChanged: opts?.onlyIfChanged,
185
208
  force: opts?.force,
186
209
  localPath,
187
210
  packageVersionId: effectiveActivePvId,
@@ -216,6 +239,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
216
239
  if (!bestPv || bestPv.packageVersionId === effectiveActivePvId) {
217
240
  if (activePv) {
218
241
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
242
+ onlyIfChanged: opts?.onlyIfChanged,
219
243
  force: opts?.force,
220
244
  localPath,
221
245
  packageVersionId: effectiveActivePvId,
@@ -233,6 +257,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
233
257
  // A better PV was found - attempt to load it (getFileContents will
234
258
  // auto-download missing chunks from peers)
235
259
  const instance = await dataContext.packageLoader.loadPackage(pkg, {
260
+ onlyIfChanged: opts?.onlyIfChanged,
236
261
  force: true,
237
262
  localPath,
238
263
  packageVersionId: bestPv.packageVersionId,
@@ -241,6 +266,7 @@ async function resolveDevicePackageVersion(pkg, dataContext, opts) {
241
266
  // Bundle unavailable even after download attempt - keep current version
242
267
  if (activePv) {
243
268
  const fallbackInstance = await dataContext.packageLoader.loadPackage(pkg, {
269
+ onlyIfChanged: opts?.onlyIfChanged,
244
270
  force: opts?.force,
245
271
  localPath,
246
272
  packageVersionId: effectiveActivePvId,
@@ -368,6 +368,48 @@ describe("resolveDevicePackageVersion — expected behavior (control)", () => {
368
368
  expect(loadedPvIds).toContain(stablePv.packageVersionId);
369
369
  expect(loadedPvIds).not.toContain(devPv.packageVersionId);
370
370
  });
371
+ it.each([
372
+ ["no prefs", false],
373
+ ["a pin", true],
374
+ ])("leaves a group-selected dev version unloaded on a device with %s", async (_label, pinned) => {
375
+ const dc = freshGroupDc();
376
+ const packageId = (0, utils_1.newid)();
377
+ const devPv = makePV(packageId, { version: "0.1.0", versionTag: "dev" });
378
+ await (0, package_versions_1.PackageVersions)(dc).save(devPv);
379
+ const pkg = makePkg({ packageId, activePackageVersionId: devPv.packageVersionId });
380
+ await (0, packages_1.Packages)(dc).save(pkg);
381
+ if (pinned)
382
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { pinned: true }, dc);
383
+ const load = jest.spyOn(dc.packageLoader, "loadPackage");
384
+ const unload = jest
385
+ .spyOn(dc.packageLoader, "unloadPackage")
386
+ .mockImplementation(async () => undefined);
387
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
388
+ expect(result).toMatchObject({ loaded: false, upgraded: false });
389
+ expect(result.pv?.packageVersionId).toBe(devPv.packageVersionId);
390
+ expect(load).not.toHaveBeenCalled();
391
+ expect(unload).toHaveBeenCalledWith(packageId, {
392
+ reason: "Only a dev version (v0.1.0) is selected; activate it on this device to run it",
393
+ });
394
+ });
395
+ it("runs a group-selected dev version once this device activates it", async () => {
396
+ const dc = freshGroupDc();
397
+ const packageId = (0, utils_1.newid)();
398
+ const devPv = makePV(packageId, { version: "0.1.0", versionTag: "dev" });
399
+ await (0, package_versions_1.PackageVersions)(dc).save(devPv);
400
+ const pkg = makePkg({ packageId, activePackageVersionId: devPv.packageVersionId });
401
+ await (0, packages_1.Packages)(dc).save(pkg);
402
+ await (0, package_version_resolver_1.updatePackagePrefs)(packageId, { activePackageVersionId: devPv.packageVersionId }, dc);
403
+ const loadedPvIds = [];
404
+ jest.spyOn(dc.packageLoader, "loadPackage").mockImplementation(async (_pkg, opts) => {
405
+ if (opts?.packageVersionId)
406
+ loadedPvIds.push(opts.packageVersionId);
407
+ return { packageId };
408
+ });
409
+ const result = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dc, { force: true });
410
+ expect(result.loaded).toBe(true);
411
+ expect(loadedPvIds).toEqual([devPv.packageVersionId]);
412
+ });
371
413
  it("installs assistants and workflows into the data context on auto-upgrade", async () => {
372
414
  const dc = groupDc();
373
415
  const packageId = (0, utils_1.newid)();
@@ -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
  /**