@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
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PackageLoader = void 0;
4
4
  exports.setDefaultRequire = setDefaultRequire;
5
+ const registry_1 = require("../contracts/registry");
6
+ const system_contracts_1 = require("../contracts/system-contracts");
5
7
  const files_1 = require("../data/files");
6
8
  const package_version_resolver_1 = require("../data/package-version-resolver");
7
9
  const package_versions_1 = require("../data/package-versions");
@@ -9,6 +11,9 @@ const packages_1 = require("../data/packages");
9
11
  const tools_1 = require("../data/tools");
10
12
  const tools_2 = require("../tools");
11
13
  const contract_package_loader_1 = require("./contract-package-loader");
14
+ const resolve_consumed_contracts_1 = require("./resolve-consumed-contracts");
15
+ // Ensure built-in system contracts are cataloged before any PackageLoader installs them.
16
+ require("../contracts/system");
12
17
  class PackageLoader {
13
18
  dataContext;
14
19
  static PeersSDK;
@@ -16,9 +21,17 @@ class PackageLoader {
16
21
  packageInstances = {};
17
22
  /** Packages that were installed via the contract-based path (tools/assistants/tables already saved with packageId). */
18
23
  contractInstalledPackages = new Set();
24
+ /**
25
+ * In-memory contract providers for this data context. Seeded with system
26
+ * contracts at construction; package providers are added via
27
+ * {@link installContractPackage}.
28
+ */
29
+ contractRegistry;
19
30
  require = undefined;
20
31
  constructor(dataContext) {
21
32
  this.dataContext = dataContext;
33
+ this.contractRegistry = new registry_1.ContractRegistry();
34
+ (0, system_contracts_1.installSystemContracts)(this.contractRegistry);
22
35
  }
23
36
  /** Returns true if a package was installed via the contract-based definePackage() path. */
24
37
  isContractInstalled(packageId) {
@@ -104,9 +117,9 @@ class PackageLoader {
104
117
  }
105
118
  /**
106
119
  * Post-correction: if the package definition carries a version string,
107
- * update the active IPackageVersion record to match. This fixes cases where
108
- * the PV record was created with stale or placeholder values (e.g. "0.0.1")
109
- * before the bundle was evaluated.
120
+ * update the active IPackageVersion record to match — but only when that
121
+ * active version is the mutable `dev` working copy. Never rewrite
122
+ * beta/stable version labels from a local package.json bump.
110
123
  */
111
124
  correctPackageVersion(pkg, def) {
112
125
  if (!pkg.activePackageVersionId)
@@ -119,8 +132,18 @@ class PackageLoader {
119
132
  .then((pv) => {
120
133
  if (!pv)
121
134
  return;
135
+ if (pv.versionTag !== "dev")
136
+ return;
122
137
  if (def.version && pv.version !== def.version) {
123
- return pvTable.signAndSave({ ...pv, version: def.version });
138
+ const packageVersionHash = (0, package_versions_1.computePackageVersionHash)(def.version, pv.versionTag || "dev", pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
139
+ // Version is part of both the content hash and the author-signed
140
+ // payload — clear any prior author signature so it cannot go stale.
141
+ const { packageAuthorSignature: _staleSig, ...rest } = pv;
142
+ return pvTable.signAndSave({
143
+ ...rest,
144
+ version: def.version,
145
+ packageVersionHash,
146
+ });
124
147
  }
125
148
  })
126
149
  .catch((err) => {
@@ -198,7 +221,10 @@ class PackageLoader {
198
221
  if (packageDefinition.packageId !== pkg.packageId) {
199
222
  console.warn(`[PackageLoader] packageId mismatch: definition has "${packageDefinition.packageId}" but DB record has "${pkg.packageId}"`);
200
223
  }
201
- await (0, contract_package_loader_1.installContractPackage)(this.dataContext, packageDefinition);
224
+ await (0, contract_package_loader_1.installContractPackage)(this.dataContext, packageDefinition, {
225
+ registry: this.contractRegistry,
226
+ });
227
+ await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(this.dataContext, this.contractRegistry, packageDefinition);
202
228
  this.contractInstalledPackages.add(packageDefinition.packageId);
203
229
  this.correctPackageVersion(pkg, packageDefinition);
204
230
  // Build a proper IPeersPackage from the contract definition so callers
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mockGet = jest.fn();
4
+ const mockSignAndSave = jest.fn();
5
+ const mockComputePackageVersionHash = jest.fn((version, _tag, packageBundleFileHash, _routesBundleFileHash, _uiBundleFileHash) => `hash-${version}-${packageBundleFileHash}`);
6
+ jest.mock("../data/package-versions", () => ({
7
+ PackageVersions: () => ({
8
+ get: (...args) => mockGet(...args),
9
+ signAndSave: (...args) => mockSignAndSave(...args),
10
+ }),
11
+ computePackageVersionHash: (version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) => mockComputePackageVersionHash(version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash),
12
+ }));
13
+ const registry_1 = require("../contracts/registry");
14
+ const system_ids_1 = require("../system-ids");
15
+ const package_loader_1 = require("./package-loader");
16
+ describe("PackageLoader.correctPackageVersion", () => {
17
+ const activePvId = "pv-active";
18
+ const pkg = {
19
+ packageId: "pkg-1",
20
+ name: "Test Package",
21
+ activePackageVersionId: activePvId,
22
+ };
23
+ const def = { version: "0.3.6" };
24
+ let loader;
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ mockSignAndSave.mockResolvedValue(undefined);
28
+ loader = new package_loader_1.PackageLoader({ dataContextId: "ctx" });
29
+ });
30
+ async function correctPackageVersion() {
31
+ loader.correctPackageVersion(pkg, def);
32
+ // Flush the fire-and-forget promise chain
33
+ await new Promise((r) => setImmediate(r));
34
+ }
35
+ it("updates the active PV hash and clears a stale author signature", async () => {
36
+ mockGet.mockResolvedValue({
37
+ packageVersionId: activePvId,
38
+ version: "0.3.1",
39
+ versionTag: "dev",
40
+ packageBundleFileHash: "bundle-hash",
41
+ routesBundleFileHash: "routes-hash",
42
+ uiBundleFileHash: "ui-hash",
43
+ packageAuthorSignature: "stale-sig",
44
+ packageVersionHash: "old-hash",
45
+ });
46
+ await correctPackageVersion();
47
+ expect(mockComputePackageVersionHash).toHaveBeenCalledWith("0.3.6", "dev", "bundle-hash", "routes-hash", "ui-hash");
48
+ expect(mockSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
49
+ packageVersionId: activePvId,
50
+ version: "0.3.6",
51
+ versionTag: "dev",
52
+ packageVersionHash: "hash-0.3.6-bundle-hash",
53
+ }));
54
+ expect(mockSignAndSave.mock.calls[0][0].packageAuthorSignature).toBeUndefined();
55
+ });
56
+ it("does not update the active PV when it is stable", async () => {
57
+ mockGet.mockResolvedValue({
58
+ packageVersionId: activePvId,
59
+ version: "0.3.5",
60
+ versionTag: "stable",
61
+ });
62
+ await correctPackageVersion();
63
+ expect(mockSignAndSave).not.toHaveBeenCalled();
64
+ });
65
+ it("does not update the active PV when it is beta", async () => {
66
+ mockGet.mockResolvedValue({
67
+ packageVersionId: activePvId,
68
+ version: "0.3.5",
69
+ versionTag: "beta",
70
+ });
71
+ await correctPackageVersion();
72
+ expect(mockSignAndSave).not.toHaveBeenCalled();
73
+ });
74
+ it("does nothing when the active PV version already matches", async () => {
75
+ mockGet.mockResolvedValue({
76
+ packageVersionId: activePvId,
77
+ version: "0.3.6",
78
+ versionTag: "dev",
79
+ });
80
+ await correctPackageVersion();
81
+ expect(mockSignAndSave).not.toHaveBeenCalled();
82
+ });
83
+ });
84
+ describe("PackageLoader.contractRegistry", () => {
85
+ it("owns an in-memory ContractRegistry per loader instance", () => {
86
+ const loaderA = new package_loader_1.PackageLoader({ dataContextId: "ctx-a" });
87
+ const loaderB = new package_loader_1.PackageLoader({ dataContextId: "ctx-b" });
88
+ expect(loaderA.contractRegistry).toBeInstanceOf(registry_1.ContractRegistry);
89
+ expect(loaderB.contractRegistry).toBeInstanceOf(registry_1.ContractRegistry);
90
+ expect(loaderA.contractRegistry).not.toBe(loaderB.contractRegistry);
91
+ });
92
+ it("installs system contracts into each new registry", () => {
93
+ const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-system" });
94
+ expect(loader.contractRegistry.getProviderPackageId(system_ids_1.logsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
95
+ expect(loader.contractRegistry.resolve(system_ids_1.logsContractId, 1)?.name).toBe("System Logs");
96
+ expect(loader.contractRegistry.getProviderPackageId(system_ids_1.deviceOperationsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
97
+ });
98
+ });
@@ -0,0 +1,13 @@
1
+ import type { DataContext } from "../context/data-context";
2
+ import type { ContractRegistry } from "../contracts/registry";
3
+ import type { IPackageDefinitionResult } from "../contracts/types";
4
+ /**
5
+ * Settle each deferred `consumes` handle on a package definition after its
6
+ * providers have been registered.
7
+ *
8
+ * A consume succeeds only when {@link ContractRegistry.selectProvider} finds a
9
+ * matching provider **and** a live {@link getContractResolver|ContractResolver}
10
+ * exists for that contract version. Missing optional deps settle to `undefined`;
11
+ * missing required deps reject the handle.
12
+ */
13
+ export declare function resolveConsumedContracts(dataContext: DataContext, registry: ContractRegistry, packageDefinition: IPackageDefinitionResult): Promise<void>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveConsumedContracts = resolveConsumedContracts;
4
+ const builder_1 = require("../contracts/builder");
5
+ const contract_resolvers_1 = require("../contracts/contract-resolvers");
6
+ const create_local_consumer_1 = require("../contracts/create-local-consumer");
7
+ /**
8
+ * Settle each deferred `consumes` handle on a package definition after its
9
+ * providers have been registered.
10
+ *
11
+ * A consume succeeds only when {@link ContractRegistry.selectProvider} finds a
12
+ * matching provider **and** a live {@link getContractResolver|ContractResolver}
13
+ * exists for that contract version. Missing optional deps settle to `undefined`;
14
+ * missing required deps reject the handle.
15
+ */
16
+ async function resolveConsumedContracts(dataContext, registry, packageDefinition) {
17
+ for (const dep of packageDefinition.consumes) {
18
+ const handle = dep.handle;
19
+ if (!handle) {
20
+ continue;
21
+ }
22
+ try {
23
+ const selected = registry.selectProvider(dep);
24
+ if (!selected) {
25
+ settleMissing(dep.optional === true, handle, dep.contractId, dep.version, "no matching provider");
26
+ continue;
27
+ }
28
+ const resolver = (0, contract_resolvers_1.getContractResolver)(dep.contractId, dep.version);
29
+ if (!resolver) {
30
+ settleMissing(dep.optional === true, handle, dep.contractId, dep.version, `provider ${selected.providerPackageId} has no live resolver`);
31
+ continue;
32
+ }
33
+ const resolution = await resolver({ dataContext });
34
+ const consumer = (0, create_local_consumer_1.createLocalContractConsumer)(selected.definition, resolution, {
35
+ dataContextId: dataContext.dataContextId,
36
+ });
37
+ (0, builder_1.settleConsumedContractHandle)(handle, consumer);
38
+ }
39
+ catch (err) {
40
+ if (dep.optional === true) {
41
+ (0, builder_1.settleConsumedContractHandle)(handle, undefined);
42
+ }
43
+ else {
44
+ (0, builder_1.rejectConsumedContractHandle)(handle, err);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ function settleMissing(optional, handle, contractId, version, reason) {
50
+ if (optional) {
51
+ (0, builder_1.settleConsumedContractHandle)(handle, undefined);
52
+ return;
53
+ }
54
+ (0, builder_1.rejectConsumedContractHandle)(handle, new Error(`Required contract ${contractId}@${version} could not be resolved: ${reason}`));
55
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const builder_1 = require("../contracts/builder");
4
+ const contract_resolvers_1 = require("../contracts/contract-resolvers");
5
+ const registry_1 = require("../contracts/registry");
6
+ const utils_1 = require("../utils");
7
+ const resolve_consumed_contracts_1 = require("./resolve-consumed-contracts");
8
+ describe("resolveConsumedContracts", () => {
9
+ const contractId = (0, utils_1.newid)();
10
+ const providerPackageId = (0, utils_1.newid)();
11
+ function mockDataContext() {
12
+ return { dataContextId: "test-ctx" };
13
+ }
14
+ function makeProvidingRegistry() {
15
+ const registry = new registry_1.ContractRegistry();
16
+ registry.register(providerPackageId, {
17
+ contractId,
18
+ version: 1,
19
+ devTag: "dev",
20
+ name: "Fake Contract",
21
+ description: "test",
22
+ tables: [
23
+ {
24
+ name: "Items",
25
+ description: "Items table",
26
+ primaryKeyName: "itemId",
27
+ fields: [],
28
+ },
29
+ ],
30
+ tools: [],
31
+ observables: [],
32
+ events: [],
33
+ });
34
+ return registry;
35
+ }
36
+ beforeEach(() => {
37
+ const resolution = {
38
+ contractId,
39
+ version: 1,
40
+ tables: {
41
+ Items: {
42
+ list: jest.fn().mockResolvedValue([{ itemId: "1" }]),
43
+ count: jest.fn().mockResolvedValue(1),
44
+ },
45
+ },
46
+ };
47
+ (0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => resolution);
48
+ });
49
+ it("settles a required consume with a live local consumer proxy", async () => {
50
+ const registry = makeProvidingRegistry();
51
+ let consumeHandle;
52
+ const packageDefinition = (0, builder_1.definePackage)((pkg) => {
53
+ pkg.packageId = (0, utils_1.newid)();
54
+ pkg.version = "1.0.0";
55
+ consumeHandle = pkg.consumes(contractId, 1);
56
+ });
57
+ await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(mockDataContext(), registry, packageDefinition);
58
+ if (!consumeHandle) {
59
+ throw new Error("expected consume handle");
60
+ }
61
+ const consumer = (await consumeHandle.consumer);
62
+ expect(consumer).toBeDefined();
63
+ expect(consumer?.contractId).toBe(contractId);
64
+ expect(consumer?.version).toBe(1);
65
+ await expect(consumer?.tables.Items.list()).resolves.toEqual([{ itemId: "1" }]);
66
+ await expect(consumer?.tables.Items.count()).resolves.toBe(1);
67
+ });
68
+ it("settles optional missing consumes to undefined", async () => {
69
+ const registry = new registry_1.ContractRegistry();
70
+ let consumeHandle;
71
+ const packageDefinition = (0, builder_1.definePackage)((pkg) => {
72
+ pkg.packageId = (0, utils_1.newid)();
73
+ pkg.version = "1.0.0";
74
+ consumeHandle = pkg.consumes(contractId, 1, { optional: true });
75
+ });
76
+ await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(mockDataContext(), registry, packageDefinition);
77
+ if (!consumeHandle) {
78
+ throw new Error("expected consume handle");
79
+ }
80
+ await expect(consumeHandle.consumer).resolves.toBeUndefined();
81
+ });
82
+ it("rejects required consumes when no provider is available", async () => {
83
+ const registry = new registry_1.ContractRegistry();
84
+ let consumeHandle;
85
+ const packageDefinition = (0, builder_1.definePackage)((pkg) => {
86
+ pkg.packageId = (0, utils_1.newid)();
87
+ pkg.version = "1.0.0";
88
+ consumeHandle = pkg.consumes(contractId, 1);
89
+ });
90
+ await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(mockDataContext(), registry, packageDefinition);
91
+ if (!consumeHandle) {
92
+ throw new Error("expected consume handle");
93
+ }
94
+ await expect(consumeHandle.consumer).rejects.toThrow(/could not be resolved/);
95
+ });
96
+ it("treats a registered provider without a live resolver as missing", async () => {
97
+ const noResolverContractId = (0, utils_1.newid)();
98
+ const registry = new registry_1.ContractRegistry();
99
+ registry.register(providerPackageId, {
100
+ contractId: noResolverContractId,
101
+ version: 1,
102
+ devTag: "dev",
103
+ name: "No Resolver",
104
+ description: "test",
105
+ tables: [],
106
+ tools: [],
107
+ observables: [],
108
+ events: [],
109
+ });
110
+ expect((0, contract_resolvers_1.getContractResolver)(noResolverContractId, 1)).toBeUndefined();
111
+ let consumeHandle;
112
+ const packageDefinition = (0, builder_1.definePackage)((pkg) => {
113
+ pkg.packageId = (0, utils_1.newid)();
114
+ pkg.version = "1.0.0";
115
+ consumeHandle = pkg.consumes(noResolverContractId, 1, { optional: true });
116
+ });
117
+ await (0, resolve_consumed_contracts_1.resolveConsumedContracts)(mockDataContext(), registry, packageDefinition);
118
+ if (!consumeHandle) {
119
+ throw new Error("expected consume handle");
120
+ }
121
+ await expect(consumeHandle.consumer).resolves.toBeUndefined();
122
+ });
123
+ });
@@ -2,6 +2,7 @@ import type { IDeviceDatabaseQueryResult, IDeviceOperationStatus, IDeviceToolRun
2
2
  import type { ITool } from "./data/tools";
3
3
  import type { DevicePairingCeremonyAction, DevicePairingStartDestinationInput, DevicePairingStartSourceResult, DevicePairingState } from "./device-pairing";
4
4
  import type { IConsoleLog } from "./logging/console-logs.table";
5
+ import type { IAppNav } from "./types/app-nav";
5
6
  /** Named event payload sent across the frontend RPC bridge. */
6
7
  export interface IEventData<T = any> {
7
8
  name: string;
@@ -115,6 +116,8 @@ export declare const rpcServerCalls: {
115
116
  /**
116
117
  * Sign a package version with the package author's key (server-side only — key never leaves the server).
117
118
  * Returns the base64url detached signature, or undefined if the signing key isn't available.
119
+ * Optional fields (appNavs, createdBy, createdAt) must match the PV record when present so
120
+ * propagation verification of the full record succeeds.
118
121
  */
119
122
  signPackageAuthorVersion: (pvFields: {
120
123
  packageId: string;
@@ -124,6 +127,9 @@ export declare const rpcServerCalls: {
124
127
  packageBundleFileHash: string;
125
128
  routesBundleFileHash?: string;
126
129
  uiBundleFileHash?: string;
130
+ appNavs?: IAppNav[];
131
+ createdBy?: string;
132
+ createdAt?: string;
127
133
  }) => Promise<string | undefined>;
128
134
  registerWithPeersServices: () => Promise<string>;
129
135
  appQuit: () => Promise<void>;
package/dist/rpc-types.js CHANGED
@@ -58,6 +58,8 @@ exports.rpcServerCalls = {
58
58
  /**
59
59
  * Sign a package version with the package author's key (server-side only — key never leaves the server).
60
60
  * Returns the base64url detached signature, or undefined if the signing key isn't available.
61
+ * Optional fields (appNavs, createdBy, createdAt) must match the PV record when present so
62
+ * propagation verification of the full record succeeds.
61
63
  */
62
64
  signPackageAuthorVersion: rpcStub("signPackageAuthorVersion"),
63
65
  registerWithPeersServices: rpcStub("registerWithPeersServices"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.21.0",
3
+ "version": "0.21.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"