@peers-app/peers-sdk 0.21.0 → 0.21.2

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 (44) hide show
  1. package/dist/context/user-context.d.ts +10 -3
  2. package/dist/context/user-context.js +12 -12
  3. package/dist/contracts/__tests__/builder.test.js +158 -4
  4. package/dist/contracts/__tests__/registry.test.js +45 -303
  5. package/dist/contracts/__tests__/system-contracts.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/system-contracts.test.js +52 -0
  7. package/dist/contracts/builder.d.ts +43 -2
  8. package/dist/contracts/builder.js +79 -5
  9. package/dist/contracts/contract-proxy.d.ts +61 -21
  10. package/dist/contracts/create-local-consumer.d.ts +12 -0
  11. package/dist/contracts/create-local-consumer.js +19 -0
  12. package/dist/contracts/index.d.ts +5 -3
  13. package/dist/contracts/index.js +8 -1
  14. package/dist/contracts/registry.d.ts +17 -0
  15. package/dist/contracts/registry.js +36 -0
  16. package/dist/contracts/system/device-operations-contract.js +7 -0
  17. package/dist/contracts/system/index.d.ts +8 -0
  18. package/dist/contracts/system/index.js +22 -0
  19. package/dist/contracts/system/logs-contract.d.ts +18 -1
  20. package/dist/contracts/system/logs-contract.js +8 -2
  21. package/dist/contracts/system-contracts.d.ts +47 -0
  22. package/dist/contracts/system-contracts.js +69 -0
  23. package/dist/contracts/types.d.ts +44 -0
  24. package/dist/package-installer/index.d.ts +1 -0
  25. package/dist/package-installer/index.js +1 -0
  26. package/dist/package-installer/package-author-signer.d.ts +25 -0
  27. package/dist/package-installer/package-author-signer.js +57 -0
  28. package/dist/package-installer/package-installer.d.ts +27 -0
  29. package/dist/package-installer/package-installer.js +88 -18
  30. package/dist/package-installer/package-installer.test.js +49 -0
  31. package/dist/package-installer/types.d.ts +1 -1
  32. package/dist/package-loader/contract-package-loader.test.d.ts +1 -0
  33. package/dist/package-loader/contract-package-loader.test.js +88 -0
  34. package/dist/package-loader/package-loader.d.ts +11 -3
  35. package/dist/package-loader/package-loader.js +31 -5
  36. package/dist/package-loader/package-loader.test.d.ts +1 -0
  37. package/dist/package-loader/package-loader.test.js +98 -0
  38. package/dist/package-loader/resolve-consumed-contracts.d.ts +13 -0
  39. package/dist/package-loader/resolve-consumed-contracts.js +55 -0
  40. package/dist/package-loader/resolve-consumed-contracts.test.d.ts +1 -0
  41. package/dist/package-loader/resolve-consumed-contracts.test.js +123 -0
  42. package/dist/rpc-types.d.ts +6 -0
  43. package/dist/rpc-types.js +2 -0
  44. package/package.json +1 -1
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSystemContract = registerSystemContract;
4
+ exports.installSystemContracts = installSystemContracts;
5
+ exports.listSystemContracts = listSystemContracts;
6
+ exports._clearSystemContractsForTests = _clearSystemContractsForTests;
7
+ const systemContracts = [];
8
+ function systemContractKey(contractId, version) {
9
+ return `${contractId}@${version}`;
10
+ }
11
+ /**
12
+ * Self-register a built-in system contract.
13
+ *
14
+ * Call once from the contract module (side effect at import). Later
15
+ * {@link installSystemContracts} installs every registered entry into a
16
+ * concrete registry. New system contracts only need this one call.
17
+ */
18
+ function registerSystemContract(registration) {
19
+ if (registration.resolve) {
20
+ if (!registration.contractId || registration.version == null) {
21
+ throw new Error("registerSystemContract: contractId and version are required when resolve is provided");
22
+ }
23
+ // Defer resolver registration so self-registration during SDK barrel / package-loader
24
+ // import cannot re-enter contract-resolvers mid-initialization.
25
+ const { contractId, version, resolve } = registration;
26
+ queueMicrotask(() => {
27
+ const { registerContractResolver } = require("./contract-resolvers");
28
+ registerContractResolver(contractId, version, resolve);
29
+ });
30
+ }
31
+ const key = registration.contractId && registration.version != null
32
+ ? systemContractKey(registration.contractId, registration.version)
33
+ : undefined;
34
+ if (key) {
35
+ const existingIdx = systemContracts.findIndex((entry) => entry.contractId &&
36
+ entry.version != null &&
37
+ systemContractKey(entry.contractId, entry.version) === key);
38
+ if (existingIdx >= 0) {
39
+ systemContracts[existingIdx] = registration;
40
+ return;
41
+ }
42
+ }
43
+ systemContracts.push(registration);
44
+ }
45
+ /**
46
+ * Register every known system contract into `registry` as a provider.
47
+ * Safe to call more than once for the same registry (provider updates in place).
48
+ */
49
+ function installSystemContracts(registry) {
50
+ for (const entry of systemContracts) {
51
+ const definition = entry.getDefinition();
52
+ const result = registry.register(entry.providerPackageId, definition);
53
+ if (!result.valid) {
54
+ console.error(`[SystemContracts] Failed to register ${definition.name} (${definition.contractId}@${definition.version}):`, result.errors);
55
+ }
56
+ }
57
+ }
58
+ /** Snapshot of registered system contracts (for tests and diagnostics). */
59
+ function listSystemContracts() {
60
+ return systemContracts;
61
+ }
62
+ /**
63
+ * Clear the system-contract catalog. Test-only — production code must not call this.
64
+ *
65
+ * @internal
66
+ */
67
+ function _clearSystemContractsForTests() {
68
+ systemContracts.length = 0;
69
+ }
@@ -4,6 +4,7 @@ import type { IToolInstance } from "../data/tools";
4
4
  import type { Event } from "../events";
5
5
  import type { Observable } from "../observable";
6
6
  import type { FieldType, IField } from "../types/field-type";
7
+ import type { IContractConsumer, IContractConsumerBase } from "./contract-proxy";
7
8
  /**
8
9
  * A versioned interface definition declaring the shape of tables, tools,
9
10
  * observables, and events that a package provides. Pure data — no executable code.
@@ -58,6 +59,43 @@ export interface IContractEvent {
58
59
  /** Shape of the payload delivered to subscribers when the event fires. */
59
60
  payloadFields: IField[];
60
61
  }
62
+ /**
63
+ * Options for {@link PackageBuilder.consumes}.
64
+ *
65
+ * Provider selection (`providerPackageIds`, `fallbackToDefaultProvider`) is
66
+ * recorded here for the package-loader; wiring that resolves the deferred
67
+ * handle is a follow-up step.
68
+ */
69
+ export interface IConsumedContractOptions {
70
+ /**
71
+ * When true, a missing provider settles the handle with `undefined` instead
72
+ * of rejecting. Defaults to false (required).
73
+ */
74
+ optional?: boolean;
75
+ /**
76
+ * Ordered preference of provider package IDs. The loader tries these in
77
+ * order before considering the active/default provider.
78
+ */
79
+ providerPackageIds?: string[];
80
+ /**
81
+ * When true and none of {@link providerPackageIds} is available, fall back
82
+ * to the active/default provider for the contract. Ignored when
83
+ * `providerPackageIds` is omitted (default provider is used directly).
84
+ */
85
+ fallbackToDefaultProvider?: boolean;
86
+ }
87
+ /**
88
+ * Deferred handle returned by {@link PackageBuilder.consumes}.
89
+ *
90
+ * The handle itself is not thenable — await {@link consumer} from runtime
91
+ * code after `definePackage` finishes, not inside the define callback.
92
+ * Settlement is owned by the SDK (package-loader); package code cannot
93
+ * resolve or reject the promise.
94
+ */
95
+ export interface IConsumedContractHandle<T extends IContractConsumerBase | undefined = IContractConsumer> {
96
+ /** Resolves to the live consumer proxy, or `undefined` when optional and unmatched. */
97
+ readonly consumer: Promise<T>;
98
+ }
61
99
  /**
62
100
  * A reference to a contract that a package wants to consume.
63
101
  * Recorded during `definePackage` and checked at install time.
@@ -66,6 +104,12 @@ export interface IConsumedContract {
66
104
  contractId: string;
67
105
  version: number;
68
106
  optional?: boolean;
107
+ /** Ordered preferred provider package IDs, if any. */
108
+ providerPackageIds?: string[];
109
+ /** Whether to fall back to the default provider when preferred ones are missing. */
110
+ fallbackToDefaultProvider?: boolean;
111
+ /** Deferred handle returned by `consumes()` for this dependency. */
112
+ handle?: IConsumedContractHandle<IContractConsumerBase | undefined>;
69
113
  }
70
114
  /**
71
115
  * Declares that the owning contract's implementation also satisfies another
@@ -1,3 +1,4 @@
1
+ export * from "./package-author-signer";
1
2
  export * from "./package-author-signing";
2
3
  export * from "./package-cloner";
3
4
  export * from "./package-creator";
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./package-author-signer"), exports);
17
18
  __exportStar(require("./package-author-signing"), exports);
18
19
  __exportStar(require("./package-cloner"), exports);
19
20
  __exportStar(require("./package-creator"), exports);
@@ -0,0 +1,25 @@
1
+ import { type AuthorSignableFields } from "./package-author-signing";
2
+ /**
3
+ * Load and decrypt the package author signing secret key from the user's personal group.
4
+ * Returns `undefined` when the key is missing or cannot be decrypted.
5
+ *
6
+ * @param packageId - Package whose `packageSigningKey_<id>` secret pvar to load
7
+ * @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
8
+ */
9
+ export declare function loadPackageSigningKey(packageId: string, getUserSecretKey: () => Promise<string>): Promise<string | undefined>;
10
+ /**
11
+ * Sign a package version with the package author's key.
12
+ *
13
+ * Safe to call from Electron (main process) or PWA (single-process client). The
14
+ * signing key is loaded from a secret persistent variable and never returned to
15
+ * callers — only the detached signature is.
16
+ *
17
+ * Returns the base64url detached signature, or `undefined` if the signing key
18
+ * isn't available or signing fails. Callers that require a signature (e.g.
19
+ * remote publish) should treat `undefined` as an error; create/promote may leave
20
+ * `packageAuthorSignature` unset.
21
+ *
22
+ * @param pvFields - Author-signable PackageVersion fields
23
+ * @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
24
+ */
25
+ export declare function trySignPackageAuthorVersion(pvFields: AuthorSignableFields, getUserSecretKey: () => Promise<string>): Promise<string | undefined>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadPackageSigningKey = loadPackageSigningKey;
4
+ exports.trySignPackageAuthorVersion = trySignPackageAuthorVersion;
5
+ const user_context_singleton_1 = require("../context/user-context-singleton");
6
+ const persistent_vars_1 = require("../data/persistent-vars");
7
+ const keys_1 = require("../keys");
8
+ const package_author_signing_1 = require("./package-author-signing");
9
+ /**
10
+ * Load and decrypt the package author signing secret key from the user's personal group.
11
+ * Returns `undefined` when the key is missing or cannot be decrypted.
12
+ *
13
+ * @param packageId - Package whose `packageSigningKey_<id>` secret pvar to load
14
+ * @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
15
+ */
16
+ async function loadPackageSigningKey(packageId, getUserSecretKey) {
17
+ try {
18
+ const userContext = await (0, user_context_singleton_1.getUserContext)();
19
+ const secretKey = await getUserSecretKey();
20
+ const keyName = `${package_author_signing_1.PACKAGE_SIGNING_KEY_PREFIX}${packageId}`;
21
+ const keyPvar = await (0, persistent_vars_1.PersistentVars)(userContext.userDataContext).findOne({ name: keyName });
22
+ if (!keyPvar?.value?.value || !keyPvar.isSecret) {
23
+ return undefined;
24
+ }
25
+ return (0, keys_1.decryptData)(keyPvar.value.value, secretKey) || undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ /**
32
+ * Sign a package version with the package author's key.
33
+ *
34
+ * Safe to call from Electron (main process) or PWA (single-process client). The
35
+ * signing key is loaded from a secret persistent variable and never returned to
36
+ * callers — only the detached signature is.
37
+ *
38
+ * Returns the base64url detached signature, or `undefined` if the signing key
39
+ * isn't available or signing fails. Callers that require a signature (e.g.
40
+ * remote publish) should treat `undefined` as an error; create/promote may leave
41
+ * `packageAuthorSignature` unset.
42
+ *
43
+ * @param pvFields - Author-signable PackageVersion fields
44
+ * @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
45
+ */
46
+ async function trySignPackageAuthorVersion(pvFields, getUserSecretKey) {
47
+ try {
48
+ const signingKey = await loadPackageSigningKey(pvFields.packageId, getUserSecretKey);
49
+ if (!signingKey) {
50
+ return undefined;
51
+ }
52
+ return (0, package_author_signing_1.signPackageAuthor)(pvFields, signingKey);
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
@@ -1,4 +1,5 @@
1
1
  import type { DataContext } from "../context/data-context";
2
+ import type { IPackageVersion } from "../data/package-versions";
2
3
  import type { IInstallOpts, IInstallResult, IPackageInfo, IPackageInstallerDeps } from "./types";
3
4
  /**
4
5
  * Install or update a dev PackageVersion from locally-built bundle files.
@@ -9,6 +10,20 @@ import type { IInstallOpts, IInstallResult, IPackageInfo, IPackageInstallerDeps
9
10
  * triggers the package loader and device version resolution.
10
11
  */
11
12
  export declare function installPackageFromBundles(dataContext: DataContext, deps: IPackageInstallerDeps, packageId: string, opts?: IInstallOpts): Promise<IInstallResult>;
13
+ /**
14
+ * Ensure a PackageVersion has `appNavs` from its package definition.
15
+ * No-ops when appNavs are already present or the package cannot be loaded.
16
+ *
17
+ * Used to repair PVs that were created with `skipLoad` (startup sync while the
18
+ * group was not on `dev`) and later activated/promoted without nav metadata.
19
+ *
20
+ * @param dataContext - Group or personal data context holding the PV
21
+ * @param deps - Filesystem / path helpers
22
+ * @param packageId - Package that owns the version
23
+ * @param packageVersionId - Specific PV to repair
24
+ * @returns The PV after any update, or `undefined` if not found
25
+ */
26
+ export declare function ensurePackageVersionAppNavs(dataContext: DataContext, deps: IPackageInstallerDeps, packageId: string, packageVersionId: string): Promise<IPackageVersion | undefined>;
12
27
  /**
13
28
  * Install bundles for all non-disabled packages in a data context.
14
29
  * Errors are caught per-package so one failure doesn't block others.
@@ -30,3 +45,15 @@ export declare function saveBundleFile(dataContext: DataContext, content: string
30
45
  * Uses `deps.fs.readJson` instead of `require()` — no cache issues.
31
46
  */
32
47
  export declare function getPackageInfo(deps: IPackageInstallerDeps, localPath: string): Promise<IPackageInfo>;
48
+ /**
49
+ * Resolve the local filesystem path for a package based on device vars.
50
+ * Awaits the loading promise to ensure the DB-stored value is available
51
+ * before reading. Falls back to paths from other data contexts on this device,
52
+ * then to `{packagesRootDir}/{name-slug}` when that directory exists.
53
+ *
54
+ * @param packageId - Package to resolve
55
+ * @param dataContext - Preferred data context for the `packageLocalPath_*` device var
56
+ * @param deps - Filesystem / path helpers used by the package installer
57
+ * @returns Absolute (or deps-resolved) path to the package directory
58
+ */
59
+ export declare function getPackageLocalPath(packageId: string, dataContext: DataContext, deps: IPackageInstallerDeps): Promise<string>;
@@ -1,15 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.installPackageFromBundles = installPackageFromBundles;
4
+ exports.ensurePackageVersionAppNavs = ensurePackageVersionAppNavs;
4
5
  exports.installAllPackageBundles = installAllPackageBundles;
5
6
  exports.saveBundleFile = saveBundleFile;
6
7
  exports.getPackageInfo = getPackageInfo;
8
+ exports.getPackageLocalPath = getPackageLocalPath;
7
9
  const file_types_1 = require("../data/files/file.types");
8
10
  const files_1 = require("../data/files/files");
9
11
  const package_version_resolver_1 = require("../data/package-version-resolver");
10
12
  const package_versions_1 = require("../data/package-versions");
11
13
  const packages_1 = require("../data/packages");
12
14
  const persistent_vars_1 = require("../data/persistent-vars");
15
+ const rpc_types_1 = require("../rpc-types");
13
16
  const utils_1 = require("../utils");
14
17
  /**
15
18
  * Install or update a dev PackageVersion from locally-built bundle files.
@@ -50,14 +53,22 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
50
53
  const uiHash = uiBundle ? (0, file_types_1.computeFileHash)(uiBundle) : undefined;
51
54
  // 5. Compute PV hash
52
55
  const pvHash = (0, package_versions_1.computePackageVersionHash)(version, "", packageHash, routesHash, uiHash);
53
- // 6. Early exit if unchanged
56
+ // 6. Early exit if unchanged (still repair missing appNavs — see syncDevBundles skipLoad)
54
57
  const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
55
58
  const existingPv = await pvTable.findOne({ packageId, versionTag });
56
59
  if (existingPv && existingPv.packageVersionHash === pvHash) {
60
+ if (existingPv.appNavs?.length) {
61
+ return {
62
+ packageVersion: existingPv,
63
+ loaded: undefined,
64
+ unchanged: true,
65
+ };
66
+ }
67
+ const loaded = await loadAndPersistAppNavs(dataContext, packageId, existingPv, localPath);
57
68
  return {
58
69
  packageVersion: existingPv,
59
- loaded: undefined,
60
- unchanged: true,
70
+ loaded,
71
+ unchanged: !loaded?.appNavs?.length,
61
72
  };
62
73
  }
63
74
  // 7. Save bundle files (deduped by hash)
@@ -91,22 +102,13 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
91
102
  createdAt: now,
92
103
  };
93
104
  const savedPv = await pvTable.signAndSave(pv);
94
- // 9. Load package (unless skipLoad)
105
+ // 9. Load package for in-memory use and/or appNavs backfill.
106
+ // `skipLoad` is used by startup sync when the group is not on `dev` (avoid
107
+ // activating), but we must still persist appNavs or the apps list stays empty
108
+ // after that PV is later activated/promoted.
95
109
  let loaded;
96
- if (!opts?.skipLoad) {
97
- const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
98
- if (pkg) {
99
- loaded = await dataContext.packageLoader.loadPackage(pkg, {
100
- force: true,
101
- localPath,
102
- packageVersionId: savedPv.packageVersionId,
103
- });
104
- // Extract appNavs from loaded instance onto the PV
105
- if (loaded?.appNavs && JSON.stringify(loaded.appNavs) !== JSON.stringify(savedPv.appNavs)) {
106
- savedPv.appNavs = loaded.appNavs;
107
- await pvTable.signAndSave(savedPv);
108
- }
109
- }
110
+ if (!opts?.skipLoad || !savedPv.appNavs?.length) {
111
+ loaded = await loadAndPersistAppNavs(dataContext, packageId, savedPv, localPath);
110
112
  }
111
113
  // 10. Update package record if needed (description, first-install activation)
112
114
  const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
@@ -134,6 +136,69 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
134
136
  }
135
137
  return { packageVersion: savedPv, loaded, unchanged: false };
136
138
  }
139
+ /**
140
+ * Load a package bundle and persist `appNavs` onto the PackageVersion when the
141
+ * definition provides them.
142
+ *
143
+ * If the PV already has a `packageAuthorSignature`, refresh it (or clear it when
144
+ * the signing key is unavailable) because `appNavs` is part of the author-signed
145
+ * payload.
146
+ */
147
+ async function loadAndPersistAppNavs(dataContext, packageId, pv, localPath) {
148
+ const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
149
+ if (!pkg)
150
+ return undefined;
151
+ const loaded = await dataContext.packageLoader.loadPackage(pkg, {
152
+ force: true,
153
+ localPath,
154
+ packageVersionId: pv.packageVersionId,
155
+ });
156
+ if (!loaded?.appNavs?.length)
157
+ return loaded;
158
+ if (JSON.stringify(loaded.appNavs) === JSON.stringify(pv.appNavs))
159
+ return loaded;
160
+ pv.appNavs = loaded.appNavs;
161
+ if (pv.packageAuthorSignature) {
162
+ const authorSig = await rpc_types_1.rpcServerCalls.signPackageAuthorVersion({
163
+ packageId: pv.packageId,
164
+ packageVersionId: pv.packageVersionId,
165
+ version: pv.version,
166
+ versionTag: pv.versionTag ?? "",
167
+ packageBundleFileHash: pv.packageBundleFileHash,
168
+ routesBundleFileHash: pv.routesBundleFileHash,
169
+ uiBundleFileHash: pv.uiBundleFileHash,
170
+ appNavs: pv.appNavs,
171
+ createdBy: pv.createdBy,
172
+ createdAt: pv.createdAt,
173
+ });
174
+ pv.packageAuthorSignature = authorSig;
175
+ }
176
+ await (0, package_versions_1.PackageVersions)(dataContext).signAndSave(pv);
177
+ return loaded;
178
+ }
179
+ /**
180
+ * Ensure a PackageVersion has `appNavs` from its package definition.
181
+ * No-ops when appNavs are already present or the package cannot be loaded.
182
+ *
183
+ * Used to repair PVs that were created with `skipLoad` (startup sync while the
184
+ * group was not on `dev`) and later activated/promoted without nav metadata.
185
+ *
186
+ * @param dataContext - Group or personal data context holding the PV
187
+ * @param deps - Filesystem / path helpers
188
+ * @param packageId - Package that owns the version
189
+ * @param packageVersionId - Specific PV to repair
190
+ * @returns The PV after any update, or `undefined` if not found
191
+ */
192
+ async function ensurePackageVersionAppNavs(dataContext, deps, packageId, packageVersionId) {
193
+ const pv = await (0, package_versions_1.PackageVersions)(dataContext).get(packageVersionId);
194
+ if (!pv)
195
+ return undefined;
196
+ if (pv.appNavs?.length)
197
+ return pv;
198
+ const localPath = await getPackageLocalPath(packageId, dataContext, deps);
199
+ await loadAndPersistAppNavs(dataContext, packageId, pv, localPath);
200
+ return pv;
201
+ }
137
202
  /**
138
203
  * Install bundles for all non-disabled packages in a data context.
139
204
  * Errors are caught per-package so one failure doesn't block others.
@@ -247,6 +312,11 @@ function packageNameToFolderSlug(name) {
247
312
  * Awaits the loading promise to ensure the DB-stored value is available
248
313
  * before reading. Falls back to paths from other data contexts on this device,
249
314
  * then to `{packagesRootDir}/{name-slug}` when that directory exists.
315
+ *
316
+ * @param packageId - Package to resolve
317
+ * @param dataContext - Preferred data context for the `packageLocalPath_*` device var
318
+ * @param deps - Filesystem / path helpers used by the package installer
319
+ * @returns Absolute (or deps-resolved) path to the package directory
250
320
  */
251
321
  async function getPackageLocalPath(packageId, dataContext, deps) {
252
322
  const candidates = [];
@@ -440,6 +440,55 @@ describe("installPackageFromBundles", () => {
440
440
  const secondCall = mockSignAndSavePV.mock.calls[1][0];
441
441
  expect(secondCall.appNavs).toEqual(appNavs);
442
442
  });
443
+ it("backfills appNavs even when skipLoad is true", async () => {
444
+ const { deps } = setupBasicInstall();
445
+ const appNavs = [{ name: "App", iconClassName: "bi bi-app", navigationPath: "app" }];
446
+ const mockLoadPackage = jest.fn().mockResolvedValue({ packageId, appNavs });
447
+ const dc = mockDataContext({
448
+ packageLoader: { loadPackage: mockLoadPackage },
449
+ });
450
+ await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, {
451
+ skipLoad: true,
452
+ skipResolve: true,
453
+ });
454
+ expect(mockLoadPackage).toHaveBeenCalled();
455
+ expect(mockSignAndSavePV).toHaveBeenCalledTimes(2);
456
+ expect(mockSignAndSavePV.mock.calls[1][0].appNavs).toEqual(appNavs);
457
+ });
458
+ it("repairs missing appNavs on unchanged early-return", async () => {
459
+ const { deps } = setupBasicInstall();
460
+ const packageHash = (0, file_types_1.computeFileHash)(packageBundle);
461
+ const routesHash = (0, file_types_1.computeFileHash)(routesBundle);
462
+ const uiHash = (0, file_types_1.computeFileHash)(uiBundle);
463
+ const { computePackageVersionHash } = jest.requireActual("../data/package-versions");
464
+ const expectedPvHash = computePackageVersionHash("1.2.3", "", packageHash, routesHash, uiHash);
465
+ const appNavs = [{ name: "App", iconClassName: "bi bi-app", navigationPath: "app" }];
466
+ mockFindOnePV.mockResolvedValue({
467
+ packageVersionId: (0, utils_1.newid)(),
468
+ packageId,
469
+ version: "1.2.3",
470
+ versionTag: "dev",
471
+ packageVersionHash: expectedPvHash,
472
+ packageBundleFileHash: packageHash,
473
+ routesBundleFileHash: routesHash,
474
+ uiBundleFileHash: uiHash,
475
+ appNavs: undefined,
476
+ signature: "sig",
477
+ createdBy: "user1",
478
+ createdAt: "2024-01-01T00:00:00.000Z",
479
+ });
480
+ const mockLoadPackage = jest.fn().mockResolvedValue({ packageId, appNavs });
481
+ const dc = mockDataContext({
482
+ packageLoader: { loadPackage: mockLoadPackage },
483
+ });
484
+ const result = await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, {
485
+ skipLoad: true,
486
+ skipResolve: true,
487
+ });
488
+ expect(mockLoadPackage).toHaveBeenCalled();
489
+ expect(result.packageVersion.appNavs).toEqual(appNavs);
490
+ expect(mockSignAndSavePV).toHaveBeenCalledTimes(1);
491
+ });
443
492
  it("calls updatePackagePrefs and resolveDevicePackageVersion when skipResolve is false", async () => {
444
493
  const { deps } = setupBasicInstall();
445
494
  const dc = mockDataContext();
@@ -47,7 +47,7 @@ export interface IInstallOpts {
47
47
  localPath?: string;
48
48
  /** Version tag for the created PackageVersion. Defaults to `"dev"`. */
49
49
  versionTag?: string;
50
- /** Skip `packageLoader.loadPackage` (useful for batch installs). */
50
+ /** Skip keeping the package in the loader cache for activation. AppNavs are still backfilled when missing. */
51
51
  skipLoad?: boolean;
52
52
  /** Skip `resolveDevicePackageVersion` after install. */
53
53
  skipResolve?: boolean;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ jest.mock("../data/tools", () => ({
4
+ Tools: () => ({
5
+ save: jest.fn().mockResolvedValue(undefined),
6
+ }),
7
+ }));
8
+ jest.mock("../tools", () => ({
9
+ registerTool: jest.fn(),
10
+ }));
11
+ jest.mock("../data/assistants", () => ({
12
+ Assistants: () => ({
13
+ save: jest.fn().mockResolvedValue(undefined),
14
+ }),
15
+ }));
16
+ const registry_1 = require("../contracts/registry");
17
+ const utils_1 = require("../utils");
18
+ const contract_package_loader_1 = require("./contract-package-loader");
19
+ describe("installContractPackage registry wiring", () => {
20
+ const packageId = (0, utils_1.newid)();
21
+ const contractId = (0, utils_1.newid)();
22
+ function makeDefinition(overrides = {}) {
23
+ return {
24
+ packageId,
25
+ version: "1.0.0",
26
+ contracts: [
27
+ {
28
+ contractId,
29
+ version: 1,
30
+ devTag: "dev",
31
+ name: "Test Contract",
32
+ description: "For registry install tests",
33
+ tables: [],
34
+ tools: [],
35
+ observables: [],
36
+ events: [],
37
+ },
38
+ ],
39
+ consumes: [],
40
+ alsoImplements: new Map(),
41
+ assistants: [],
42
+ appNavs: [],
43
+ toolInstances: [],
44
+ tableDefinitions: [],
45
+ ...overrides,
46
+ };
47
+ }
48
+ function mockDataContext() {
49
+ return {
50
+ dataContextId: "test-ctx",
51
+ tableContainer: {
52
+ registerTableDefinition: jest.fn(),
53
+ },
54
+ };
55
+ }
56
+ it("registers contract providers into the provided ContractRegistry", async () => {
57
+ const registry = new registry_1.ContractRegistry();
58
+ const dataContext = mockDataContext();
59
+ const definition = makeDefinition();
60
+ await (0, contract_package_loader_1.installContractPackage)(dataContext, definition, { registry });
61
+ expect(registry.resolve(contractId, 1)?.name).toBe("Test Contract");
62
+ expect(registry.getProviderPackageId(contractId, 1)).toBe(packageId);
63
+ expect(registry.getContractKeysForPackage(packageId)).toEqual([`${contractId}@1`]);
64
+ });
65
+ it("does not register providers when no registry is provided", async () => {
66
+ const dataContext = mockDataContext();
67
+ await expect((0, contract_package_loader_1.installContractPackage)(dataContext, makeDefinition())).resolves.toBeUndefined();
68
+ });
69
+ });
70
+ describe("extractPackageDefinition", () => {
71
+ it("returns a package definition when packageId and contracts are present", () => {
72
+ const packageId = (0, utils_1.newid)();
73
+ const def = {
74
+ packageId,
75
+ contracts: [],
76
+ consumes: [],
77
+ alsoImplements: new Map(),
78
+ assistants: [],
79
+ appNavs: [],
80
+ toolInstances: [],
81
+ tableDefinitions: [],
82
+ };
83
+ expect((0, contract_package_loader_1.extractPackageDefinition)({ packageDefinition: def })).toBe(def);
84
+ });
85
+ it("returns undefined when packageDefinition is missing", () => {
86
+ expect((0, contract_package_loader_1.extractPackageDefinition)({})).toBeUndefined();
87
+ });
88
+ });
@@ -1,6 +1,8 @@
1
1
  import type { DataContext } from "../context/data-context";
2
+ import { ContractRegistry } from "../contracts/registry";
2
3
  import { type IPackage } from "../data/packages";
3
4
  import type { IPeersPackage } from "../types/peers-package";
5
+ import "../contracts/system";
4
6
  export declare class PackageLoader {
5
7
  readonly dataContext: DataContext;
6
8
  static PeersSDK: any;
@@ -8,6 +10,12 @@ export declare class PackageLoader {
8
10
  private packageInstances;
9
11
  /** Packages that were installed via the contract-based path (tools/assistants/tables already saved with packageId). */
10
12
  private contractInstalledPackages;
13
+ /**
14
+ * In-memory contract providers for this data context. Seeded with system
15
+ * contracts at construction; package providers are added via
16
+ * {@link installContractPackage}.
17
+ */
18
+ readonly contractRegistry: ContractRegistry;
11
19
  require: (<T>(module: string) => T) | undefined;
12
20
  constructor(dataContext: DataContext);
13
21
  /** Returns true if a package was installed via the contract-based definePackage() path. */
@@ -23,9 +31,9 @@ export declare class PackageLoader {
23
31
  private _readLocalBundle;
24
32
  /**
25
33
  * Post-correction: if the package definition carries a version string,
26
- * update the active IPackageVersion record to match. This fixes cases where
27
- * the PV record was created with stale or placeholder values (e.g. "0.0.1")
28
- * before the bundle was evaluated.
34
+ * update the active IPackageVersion record to match — but only when that
35
+ * active version is the mutable `dev` working copy. Never rewrite
36
+ * beta/stable version labels from a local package.json bump.
29
37
  */
30
38
  private correctPackageVersion;
31
39
  private _evaluateBundle;