@peers-app/peers-sdk 0.26.0 → 0.26.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.
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const system_ids_1 = require("../../system-ids");
3
4
  const index_1 = require("../index");
4
5
  const makeRequest = (overrides = {}) => ({
5
6
  contractId: "contract-a",
@@ -49,11 +50,49 @@ describe("remote contract endpoint", () => {
49
50
  await expect(endpoint.handleCall(toolCall, {})).rejects.toThrow("disposed");
50
51
  expect(emit).not.toHaveBeenCalled();
51
52
  });
53
+ it("refuses package callers for contracts whose providers refuse packages", async () => {
54
+ const { transport, emit } = makeTransport();
55
+ const packageContext = { callerPackageId: "package-a" };
56
+ expect(() => (0, index_1.createRemoteContractEndpoint)(makeRequest({ contractId: system_ids_1.appUpdatesContractId, context: packageContext }), () => transport)).toThrow("packages cannot call contract");
57
+ const operator = (0, index_1.createRemoteContractEndpoint)(makeRequest({ contractId: system_ids_1.appUpdatesContractId }), () => transport);
58
+ await expect(operator.handleCall({ ...toolCall, contractId: system_ids_1.appUpdatesContractId }, {})).resolves.toBe("pong");
59
+ const packageTool = (0, index_1.createRemoteContractEndpoint)(makeRequest({ context: packageContext }), () => transport);
60
+ await expect(packageTool.handleCall(toolCall, packageContext)).resolves.toBe("pong");
61
+ expect(emit).toHaveBeenCalledTimes(2);
62
+ });
52
63
  it("fails clearly when no transport factory is registered or the route is incomplete", () => {
53
64
  expect(() => (0, index_1.createRemoteContractEndpoint)(makeRequest())).toThrow("no remote contract transport is registered");
54
65
  expect(() => (0, index_1.createRemoteContractEndpoint)(makeRequest({ dataContextId: undefined }), () => makeTransport().transport)).toThrow("data context");
55
66
  expect(() => (0, index_1.createRemoteContractEndpoint)(makeRequest({ targetDeviceId: undefined }), () => makeTransport().transport)).toThrow("target device");
56
67
  });
68
+ it("refuses package runTool forwarding that could invoke App Updates indirectly", async () => {
69
+ const { transport, emit } = makeTransport();
70
+ const packageContext = { callerPackageId: "package-a" };
71
+ const endpoint = (0, index_1.createRemoteContractEndpoint)(makeRequest({ contractId: system_ids_1.deviceOperationsContractId, context: packageContext }), () => transport);
72
+ for (const toolName of ["checkForUpdates", "installUpdateAndRestart"]) {
73
+ const call = {
74
+ ...toolCall,
75
+ contractId: system_ids_1.deviceOperationsContractId,
76
+ name: "runTool",
77
+ args: [
78
+ {
79
+ toolIdOrName: (0, index_1.deriveToolCatalogId)({
80
+ contractId: system_ids_1.appUpdatesContractId,
81
+ version: index_1.APP_UPDATES_CONTRACT_VERSION,
82
+ toolName,
83
+ }),
84
+ },
85
+ ],
86
+ };
87
+ await expect(endpoint.handleCall(call, packageContext)).rejects.toThrow("packages cannot call Device Operations runTool");
88
+ }
89
+ expect(emit).not.toHaveBeenCalled();
90
+ // Read-only tools on the same cached package endpoint still work.
91
+ await expect(endpoint.handleCall({ ...toolCall, contractId: system_ids_1.deviceOperationsContractId, name: "getDeviceStatus" }, packageContext)).resolves.toBe("pong");
92
+ const operator = (0, index_1.createRemoteContractEndpoint)(makeRequest({ contractId: system_ids_1.deviceOperationsContractId }), () => transport);
93
+ await expect(operator.handleCall({ ...toolCall, contractId: system_ids_1.deviceOperationsContractId, name: "runTool" }, {})).resolves.toBe("pong");
94
+ expect(emit).toHaveBeenCalledTimes(2);
95
+ });
57
96
  });
58
97
  describe("resolveRemoteTargetEndpoint", () => {
59
98
  it("returns undefined for local routes and for the local device itself", () => {
@@ -15,6 +15,7 @@ describe("system contract catalog", () => {
15
15
  .map((entry) => entry.contractId)
16
16
  .filter(Boolean);
17
17
  expect(ids).toEqual(expect.arrayContaining([
18
+ system_ids_1.appUpdatesContractId,
18
19
  system_ids_1.logsContractId,
19
20
  system_ids_1.deviceOperationsContractId,
20
21
  system_ids_1.identityContractId,
@@ -7,6 +7,7 @@ const field_type_1 = require("../../types/field-type");
7
7
  const zod_types_1 = require("../../types/zod-types");
8
8
  const utils_1 = require("../../utils");
9
9
  const registry_1 = require("../registry");
10
+ const tool_catalog_contract_1 = require("../system/tool-catalog-contract");
10
11
  const systemContracts = require("../system-contracts");
11
12
  const tool_catalog_1 = require("../tool-catalog");
12
13
  const tool_catalog_identity_1 = require("../tool-catalog-identity");
@@ -570,6 +571,280 @@ test("untargeted deterministic IDs and quarantined rows cannot be claimed by a p
570
571
  expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...row, catalogState: "quarantined" }, { ...row, catalogState: "resolved" })).toThrow(/Quarantined/);
571
572
  await f.catalog.dispose();
572
573
  });
574
+ describe("bare deterministic row repair", () => {
575
+ const target = { contractId, version: 1, toolName: "read" };
576
+ const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)(target);
577
+ const bare = {
578
+ toolId,
579
+ name: "read",
580
+ usageDescription: "stale",
581
+ code: "",
582
+ inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
583
+ outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
584
+ catalogState: "quarantined",
585
+ catalogReason: `Error: Missing tool catalog target: ${toolId}`,
586
+ };
587
+ const restored = {
588
+ ...bare,
589
+ contractId,
590
+ contractVersion: 1,
591
+ toolName: "read",
592
+ catalogIdentity: "deterministic",
593
+ catalogState: "resolved",
594
+ catalogReason: undefined,
595
+ };
596
+ test.each([undefined, "deterministic"].flatMap((catalogIdentity) => [undefined, "unresolved", "quarantined"].map((catalogState) => ({
597
+ catalogIdentity,
598
+ catalogState,
599
+ }))))("publication restores identity=$catalogIdentity state=$catalogState and preserves current policy", async (state) => {
600
+ const old = {
601
+ ...bare,
602
+ ...state,
603
+ packageId: (0, utils_1.newid)(),
604
+ codeRepository: "historical repository",
605
+ accessLevel: 70,
606
+ remoteAccessLevel: 60,
607
+ authorityPropagation: "cap",
608
+ isAssistantRunner: true,
609
+ detailedDescription: "saved details",
610
+ configSchema: { type: tools_1.IOSchemaType.none, fields: [] },
611
+ };
612
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(old, restored, toolId, true)).not.toThrow();
613
+ const f = fixture(new Map([[toolId, old]]));
614
+ const update = required(f.table.update.getMockImplementation());
615
+ f.table.update.mockImplementation(async (patch) => {
616
+ if (patch.toolId === toolId && patch.catalogState === "resolved") {
617
+ expect((0, tools_1.isToolCatalogPatch)(patch)).toBe(true);
618
+ expect(patch).toEqual({
619
+ toolId,
620
+ contractId,
621
+ contractVersion: 1,
622
+ toolName: "read",
623
+ catalogIdentity: "deterministic",
624
+ catalogState: "resolved",
625
+ catalogReason: undefined,
626
+ });
627
+ f.rows.set(toolId, {
628
+ ...required(f.rows.get(toolId)),
629
+ accessLevel: 90,
630
+ remoteAccessLevel: 80,
631
+ authorityPropagation: "preserve",
632
+ });
633
+ }
634
+ return update(patch);
635
+ });
636
+ try {
637
+ await f.publish();
638
+ expect(f.rows.get(toolId)).toEqual({
639
+ ...old,
640
+ ...restored,
641
+ accessLevel: 90,
642
+ remoteAccessLevel: 80,
643
+ authorityPropagation: "preserve",
644
+ });
645
+ expect(f.registry.resolve(contractId, 1)?.name).toBe("Test");
646
+ }
647
+ finally {
648
+ await f.catalog.dispose();
649
+ }
650
+ });
651
+ test.each([
652
+ ["executable source", { code: "existing" }],
653
+ ["whitespace source", { code: " " }],
654
+ ["missing source", { code: undefined }],
655
+ ["another identity", { catalogIdentity: "legacy-alias" }],
656
+ ["invalid empty identity", { catalogIdentity: "" }],
657
+ ["different name", { name: "write" }],
658
+ ["different name case", { name: "Read" }],
659
+ ["matching stored target", { contractId, contractVersion: 1, toolName: "read" }],
660
+ ["conflicting stored target", { contractId, contractVersion: 1, toolName: "write" }],
661
+ ["partial contract ID", { contractId }],
662
+ ["partial version", { contractVersion: 1 }],
663
+ ["partial member name", { toolName: "read" }],
664
+ ])("rejects %s even for an explicit catalog patch", (_name, fields) => {
665
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...bare, ...fields }, restored, toolId, true)).toThrow(/Quarantined/);
666
+ // Exercise the cannot-be-claimed branch without a quarantine early-out too.
667
+ // A matching established target is already a valid mapping in this case.
668
+ if (_name !== "matching stored target")
669
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...bare, ...fields, catalogState: undefined }, restored, toolId, true)).toThrow();
670
+ });
671
+ test.each([
672
+ ["missing target", { contractId: undefined, contractVersion: undefined, toolName: undefined }],
673
+ ["partial target", { contractVersion: undefined }],
674
+ ["invalid target", { contractVersion: 0 }],
675
+ ["nonmatching deterministic ID", { contractVersion: 2 }],
676
+ ["different member", { toolName: "write" }],
677
+ // A repair must resolve in the same write; it cannot retarget quarantine evidence.
678
+ ["quarantined state", { catalogState: "quarantined" }],
679
+ ["unset state", { catalogState: undefined }],
680
+ ["unset identity kind", { catalogIdentity: undefined }],
681
+ ])("rejects a candidate with %s", (_name, fields) => {
682
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, { ...restored, ...fields }, toolId, true)).toThrow();
683
+ });
684
+ test("ordinary saves and replay cannot clear quarantine, even with the canonical target", () => {
685
+ for (const candidate of [
686
+ restored,
687
+ { ...bare, catalogState: "resolved" },
688
+ { ...bare, catalogState: undefined, usageDescription: "metadata" },
689
+ ])
690
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, candidate)).toThrow(/Quarantined/);
691
+ const metadata = (0, tool_catalog_1.retainToolCatalogFields)(bare, {
692
+ ...legacy(toolId),
693
+ code: "",
694
+ name: "read",
695
+ usageDescription: "older peer metadata",
696
+ });
697
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, metadata)).not.toThrow();
698
+ expect(metadata.catalogState).toBe("quarantined");
699
+ });
700
+ test("repair requires exactly one selected ID even when the first ID matches", async () => {
701
+ const ids = jest
702
+ .spyOn(await Promise.resolve().then(() => require("../tool-catalog-migrations")), "toolCatalogIds")
703
+ .mockReturnValue([toolId, (0, utils_1.newid)()]);
704
+ try {
705
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, restored, toolId, true)).toThrow(/Quarantined/);
706
+ }
707
+ finally {
708
+ ids.mockRestore();
709
+ }
710
+ });
711
+ test.each([
712
+ { contractId, version: 1, toolName: "new-task" },
713
+ { contractId: "00muxgugn6xvs8f2r80ru205t", version: 1, toolName: "invoke" },
714
+ ])("aliases suppress deterministic repair for $toolName", async (aliasedTarget) => {
715
+ const deterministicId = (0, tool_catalog_identity_1.deriveToolCatalogId)(aliasedTarget);
716
+ const aliases = (0, tool_catalog_migrations_1.toolCatalogIds)(aliasedTarget);
717
+ const old = { ...bare, toolId: deterministicId, name: aliasedTarget.toolName };
718
+ const candidate = {
719
+ ...restored,
720
+ toolId: deterministicId,
721
+ contractId: aliasedTarget.contractId,
722
+ toolName: aliasedTarget.toolName,
723
+ };
724
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(old, candidate, deterministicId, true)).toThrow();
725
+ for (const alias of aliases)
726
+ expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...old, toolId: alias }, { ...candidate, toolId: alias, catalogIdentity: "legacy-alias" }, alias, true)).toThrow(/Quarantined/);
727
+ const f = fixture(new Map([[deterministicId, old]]));
728
+ try {
729
+ await f.publish({
730
+ ...definition([aliasedTarget.toolName]),
731
+ contractId: aliasedTarget.contractId,
732
+ });
733
+ expect(f.rows.get(deterministicId)).toEqual(old);
734
+ for (const alias of aliases)
735
+ expect(f.rows.get(alias)?.catalogIdentity).toBe("legacy-alias");
736
+ expect(f.table.save.mock.calls.some(([row]) => row.toolId === deterministicId)).toBe(false);
737
+ }
738
+ finally {
739
+ await f.catalog.dispose();
740
+ }
741
+ });
742
+ test.each([
743
+ "collision",
744
+ "quarantine",
745
+ ])("a later %s prevents all repair writes in the batch", async (failure) => {
746
+ const f = fixture();
747
+ await f.catalog.ready();
748
+ const hash = failure === "collision"
749
+ ? jest.spyOn(nacl, "hash").mockReturnValue(new Uint8Array(64))
750
+ : undefined;
751
+ const firstId = (0, tool_catalog_identity_1.deriveToolCatalogId)(target);
752
+ const old = { ...bare, toolId: firstId };
753
+ f.rows.set(firstId, old);
754
+ if (failure === "quarantine") {
755
+ const secondId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ ...target, toolName: "write" });
756
+ f.rows.set(secondId, {
757
+ ...restored,
758
+ toolId: secondId,
759
+ toolName: "write",
760
+ catalogState: "quarantined",
761
+ });
762
+ }
763
+ f.table.update.mockClear();
764
+ f.table.save.mockClear();
765
+ try {
766
+ await expect(f.publish(definition(["read", "write"]))).rejects.toThrow(/collision|Quarantined/);
767
+ expect(f.rows.get(firstId)).toEqual(old);
768
+ expect(f.table.update).not.toHaveBeenCalled();
769
+ expect(f.table.save).not.toHaveBeenCalled();
770
+ expect(f.registry.resolve(contractId, 1)).toBeUndefined();
771
+ }
772
+ finally {
773
+ hash?.mockRestore();
774
+ await f.catalog.dispose();
775
+ }
776
+ });
777
+ });
778
+ test.each([
779
+ false,
780
+ true,
781
+ ])("system readiness restores the startup listTools row (quarantined=%s)", async (quarantined) => {
782
+ const definition = (0, tool_catalog_contract_1.getToolCatalogContractDefinition)();
783
+ const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({
784
+ contractId: definition.contractId,
785
+ version: definition.version,
786
+ toolName: "listTools",
787
+ });
788
+ expect(toolId).toBe("000tool0ggg5rilyp6kbxevip");
789
+ const bare = {
790
+ ...legacy(toolId),
791
+ name: "listTools",
792
+ code: "",
793
+ catalogState: quarantined ? "quarantined" : undefined,
794
+ catalogReason: quarantined ? `Error: Missing tool catalog target: ${toolId}` : undefined,
795
+ };
796
+ const f = fixture(new Map([[toolId, bare]]));
797
+ try {
798
+ await f.catalog.ready();
799
+ expect(f.rows.get(toolId)).toMatchObject({
800
+ contractId: definition.contractId,
801
+ contractVersion: definition.version,
802
+ toolName: "listTools",
803
+ catalogIdentity: "deterministic",
804
+ catalogState: "resolved",
805
+ code: "",
806
+ accessLevel: 70,
807
+ });
808
+ expect(f.rows.get(toolId)?.catalogReason).toBeUndefined();
809
+ expect(f.registry.getProviderPackageId(definition.contractId, definition.version)).toBeDefined();
810
+ const writes = f.table.update.mock.calls.length;
811
+ await f.catalog.ready();
812
+ expect(f.table.update).toHaveBeenCalledTimes(writes);
813
+ }
814
+ finally {
815
+ await f.catalog.dispose();
816
+ }
817
+ });
818
+ test.each([
819
+ ["executable source", { code: "existing" }],
820
+ ["a different name", { name: "list-tools" }],
821
+ ["another identity", { catalogIdentity: "legacy-alias" }],
822
+ ])("system readiness still aborts on a real listTools quarantine with %s", async (_name, fields) => {
823
+ const definition = (0, tool_catalog_contract_1.getToolCatalogContractDefinition)();
824
+ const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({
825
+ contractId: definition.contractId,
826
+ version: definition.version,
827
+ toolName: "listTools",
828
+ });
829
+ const quarantined = {
830
+ ...legacy(toolId),
831
+ name: "listTools",
832
+ code: "",
833
+ ...fields,
834
+ catalogState: "quarantined",
835
+ catalogReason: `Error: Missing tool catalog target: ${toolId}`,
836
+ };
837
+ const f = fixture(new Map([[toolId, quarantined]]));
838
+ const publish = jest.spyOn(f.registry, "replaceBatch");
839
+ try {
840
+ await expect(f.catalog.ready()).rejects.toThrow(`Quarantined tool identity: ${toolId}`);
841
+ expect(f.rows.get(toolId)).toEqual(quarantined);
842
+ expect(publish).not.toHaveBeenCalled();
843
+ }
844
+ finally {
845
+ await f.catalog.dispose();
846
+ }
847
+ });
573
848
  test("host removal waits for publication and disposal cancels queued activation", async () => {
574
849
  const f = fixture();
575
850
  const provider = (0, utils_1.newid)();
@@ -13,6 +13,7 @@ export { getHostContractTransport, type HostContractTransportListener, registerH
13
13
  export { PersistentContractRegistry } from "./persistent-registry";
14
14
  export { ContractRegistry, type IContractProviderRegistration } from "./registry";
15
15
  export { createRemoteContractEndpoint, getRemoteContractTransportFactory, type IRemoteContractHost, type IRemoteContractTarget, type RemoteContractTransportFactory, resolveRemoteTargetEndpoint, setRemoteContractTransportFactory, } from "./remote-contract-transport";
16
+ export * from "./system/app-updates-contract";
16
17
  export { CONTRACT_CATALOG_VERSION, contractCatalogProviderPackageId, getContractCatalogDefinition, type IContractCatalogConsumer, type IContractProviderInfo, type IListContractProvidersInput, } from "./system/contract-catalog-contract";
17
18
  export * from "./system/desktop-tools-contract";
18
19
  export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./system/device-operations-contract";
@@ -83,6 +83,7 @@ Object.defineProperty(exports, "createRemoteContractEndpoint", { enumerable: tru
83
83
  Object.defineProperty(exports, "getRemoteContractTransportFactory", { enumerable: true, get: function () { return remote_contract_transport_1.getRemoteContractTransportFactory; } });
84
84
  Object.defineProperty(exports, "resolveRemoteTargetEndpoint", { enumerable: true, get: function () { return remote_contract_transport_1.resolveRemoteTargetEndpoint; } });
85
85
  Object.defineProperty(exports, "setRemoteContractTransportFactory", { enumerable: true, get: function () { return remote_contract_transport_1.setRemoteContractTransportFactory; } });
86
+ __exportStar(require("./system/app-updates-contract"), exports);
86
87
  var contract_catalog_contract_1 = require("./system/contract-catalog-contract");
87
88
  Object.defineProperty(exports, "CONTRACT_CATALOG_VERSION", { enumerable: true, get: function () { return contract_catalog_contract_1.CONTRACT_CATALOG_VERSION; } });
88
89
  Object.defineProperty(exports, "contractCatalogProviderPackageId", { enumerable: true, get: function () { return contract_catalog_contract_1.contractCatalogProviderPackageId; } });
@@ -60,10 +60,13 @@ export declare function resolveRemoteTargetEndpoint(request: IContractProviderEn
60
60
  * *local* caller for the route. It strips {@link IContractCall.targetDeviceId} before
61
61
  * emitting so the receiving device sees an ordinary call addressed to itself and the
62
62
  * message is signed with this device's own identity. Only tool runs are forwarded; the
63
- * router rejects other members before reaching this endpoint.
63
+ * router rejects other members before reaching this endpoint. Package callers cannot
64
+ * reach contracts whose providers refuse packages, such as System App Updates, or the
65
+ * Device Operations `runTool` wrapper that could invoke them indirectly.
64
66
  *
65
67
  * @param request Authorized route whose `targetDeviceId` and `dataContextId` are set.
66
68
  * @param factory Transport factory; defaults to the process-wide registration.
67
- * @throws If no factory is available or the route lacks a target device or data context.
69
+ * @throws If no factory is available, the route lacks a target device or data context, or
70
+ * a package targets a contract that refuses package callers.
68
71
  */
69
72
  export declare function createRemoteContractEndpoint(request: IContractProviderEndpointRequest, factory?: RemoteContractTransportFactory | undefined): IContractProviderEndpoint;
@@ -4,7 +4,14 @@ exports.setRemoteContractTransportFactory = setRemoteContractTransportFactory;
4
4
  exports.getRemoteContractTransportFactory = getRemoteContractTransportFactory;
5
5
  exports.resolveRemoteTargetEndpoint = resolveRemoteTargetEndpoint;
6
6
  exports.createRemoteContractEndpoint = createRemoteContractEndpoint;
7
+ const system_ids_1 = require("../system-ids");
7
8
  const contract_transport_1 = require("./contract-transport");
9
+ /**
10
+ * Contracts whose providers refuse package callers. A forwarded call reaches the target
11
+ * signed by this device alone, so the target cannot tell that a package made it; the
12
+ * forwarding host refuses instead.
13
+ */
14
+ const packageRefusedRemoteContractIds = new Set([system_ids_1.appUpdatesContractId]);
8
15
  const factoriesByUserContext = new WeakMap();
9
16
  let defaultFactory;
10
17
  /**
@@ -58,17 +65,23 @@ function resolveRemoteTargetEndpoint(request, host) {
58
65
  * *local* caller for the route. It strips {@link IContractCall.targetDeviceId} before
59
66
  * emitting so the receiving device sees an ordinary call addressed to itself and the
60
67
  * message is signed with this device's own identity. Only tool runs are forwarded; the
61
- * router rejects other members before reaching this endpoint.
68
+ * router rejects other members before reaching this endpoint. Package callers cannot
69
+ * reach contracts whose providers refuse packages, such as System App Updates, or the
70
+ * Device Operations `runTool` wrapper that could invoke them indirectly.
62
71
  *
63
72
  * @param request Authorized route whose `targetDeviceId` and `dataContextId` are set.
64
73
  * @param factory Transport factory; defaults to the process-wide registration.
65
- * @throws If no factory is available or the route lacks a target device or data context.
74
+ * @throws If no factory is available, the route lacks a target device or data context, or
75
+ * a package targets a contract that refuses package callers.
66
76
  */
67
77
  function createRemoteContractEndpoint(request, factory = getRemoteContractTransportFactory()) {
68
78
  const { targetDeviceId, dataContextId } = request;
69
79
  if (!targetDeviceId) {
70
80
  throw new Error("Remote contract endpoint requires a target device id");
71
81
  }
82
+ if (request.context?.callerPackageId && packageRefusedRemoteContractIds.has(request.contractId)) {
83
+ throw new Error(`Permission denied: packages cannot call contract ${request.contractId} on another device`);
84
+ }
72
85
  if (!dataContextId) {
73
86
  throw new Error("Remote contract endpoint requires an authorized data context id");
74
87
  }
@@ -85,6 +98,13 @@ function createRemoteContractEndpoint(request, factory = getRemoteContractTransp
85
98
  if (call.member !== "tool" || call.operation !== "run") {
86
99
  return Promise.reject(new Error(`Contract ${call.member} '${call.name}'.${call.operation} cannot be forwarded to a remote device`));
87
100
  }
101
+ // runTool refuses packages locally, but the target cannot recover their identity
102
+ // after forwarding. Without this gate it could invoke App Updates on their behalf.
103
+ if (request.context.callerPackageId &&
104
+ request.contractId === system_ids_1.deviceOperationsContractId &&
105
+ call.name === "runTool") {
106
+ return Promise.reject(new Error("Permission denied: packages cannot call Device Operations runTool on another device"));
107
+ }
88
108
  const { targetDeviceId: _stripped, ...outbound } = call;
89
109
  return transport.emit(contract_transport_1.CONTRACT_REQUEST_CHANNEL, { ...outbound, dataContextId });
90
110
  },
@@ -0,0 +1,109 @@
1
+ import type { Observable } from "../../observable";
2
+ import type { IContractConsumerBase } from "../contract-proxy";
3
+ import type { IContractDefinition } from "../types";
4
+ /** Version of the built-in System App Updates contract. */
5
+ export declare const APP_UPDATES_CONTRACT_VERSION = 1;
6
+ /**
7
+ * Lifecycle of the host application's update check, download, and install.
8
+ *
9
+ * - `idle` — no check has completed since launch.
10
+ * - `checking` — the update feed is being read.
11
+ * - `up-to-date` — the feed has nothing newer than the running version.
12
+ * - `available` — a newer version exists and its download has not finished.
13
+ * - `downloading` — the newer version is downloading (see {@link IAppUpdateStatus.downloadPercent}).
14
+ * - `downloaded` — the newer version is ready to install.
15
+ * - `installing` — the app is shutting down to run the installer and relaunch.
16
+ * - `restarting` — the app is restarting without installing anything.
17
+ * - `error` — the last check, download, or install failed (see {@link IAppUpdateStatus.error}).
18
+ */
19
+ export type AppUpdateState = "idle" | "checking" | "up-to-date" | "available" | "downloading" | "downloaded" | "installing" | "restarting" | "error";
20
+ /** Snapshot of the host application's update state, as reported by the device that runs it. */
21
+ export interface IAppUpdateStatus {
22
+ /** False when the host cannot update itself (for example an unpackaged development build). */
23
+ supported: boolean;
24
+ /** True when the host can restart itself on request (desktop apps, even unpackaged ones). */
25
+ canRestart: boolean;
26
+ /** Host operating system as reported by Node (`darwin`, `win32`, `linux`). */
27
+ platform: string;
28
+ /** Version of the running application. */
29
+ currentVersion: string;
30
+ /** Newest version the update feed reported, when a check has succeeded. */
31
+ latestVersion?: string;
32
+ /** Version whose installer has finished downloading in this session. */
33
+ downloadedVersion?: string;
34
+ /** Current lifecycle state. */
35
+ state: AppUpdateState;
36
+ /** Download progress from 0 to 100 while {@link state} is `downloading`. */
37
+ downloadPercent?: number;
38
+ /** Message from the last failed check, download, or install. */
39
+ error?: string;
40
+ /** Epoch milliseconds of the last completed feed check. */
41
+ lastCheckedAt?: number;
42
+ /** This device's "install updates when Peers restarts" setting. */
43
+ installOnRestart: boolean;
44
+ /**
45
+ * True when a downloaded update will install the next time the app quits. Usually mirrors
46
+ * {@link installOnRestart}; the quit itself re-reads the stored setting. On macOS it is also
47
+ * true once the system installer has accepted the update for an Update & restart that timed
48
+ * out, which cannot be undone.
49
+ */
50
+ installOnQuitArmed: boolean;
51
+ /** This device's "allow my other devices to update and restart this device" setting. */
52
+ remoteRestartAllowed: boolean;
53
+ /** True when installing prompts for an administrator password on this device (Linux `.deb`). */
54
+ installNeedsElevation?: boolean;
55
+ }
56
+ /**
57
+ * What `installUpdateAndRestart` started. The call returns as soon as the work is underway;
58
+ * read {@link IAppUpdateStatus} to follow it.
59
+ *
60
+ * - `installing` — a downloaded update is installing; the app will relaunch on the new version.
61
+ * - `downloading` — the update is still downloading; it installs and relaunches when done.
62
+ * - `checking` — no update was known; the device checks first, then installs or simply restarts.
63
+ * - `restarting` — the app is restarting without an update.
64
+ */
65
+ export type AppUpdateInstallAction = "installing" | "downloading" | "checking" | "restarting";
66
+ /** Result of `installUpdateAndRestart`. */
67
+ export interface IAppUpdateInstallResult {
68
+ /** What the device started. */
69
+ action: AppUpdateInstallAction;
70
+ /** Status at the moment the work started. */
71
+ status: IAppUpdateStatus;
72
+ }
73
+ /** Input naming the device a relayed App Updates call targets. */
74
+ export interface IAppUpdatesDeviceInput {
75
+ /** Another device of this user, or of a user who trusts this user as `Self`. */
76
+ deviceId: string;
77
+ }
78
+ /** Typed consumer proxy for System App Updates v1. */
79
+ export interface IAppUpdatesConsumer extends IContractConsumerBase {
80
+ tools: {
81
+ /** Return this device's current update status. */
82
+ getUpdateStatus(): Promise<IAppUpdateStatus>;
83
+ /** Read the update feed now and return the resulting status. */
84
+ checkForUpdates(): Promise<IAppUpdateStatus>;
85
+ /** Install an available update and relaunch, or restart when there is nothing to install. */
86
+ installUpdateAndRestart(): Promise<IAppUpdateInstallResult>;
87
+ /** Relay `getUpdateStatus` to another device. Local callers only. */
88
+ getDeviceUpdateStatus(input: IAppUpdatesDeviceInput): Promise<IAppUpdateStatus>;
89
+ /** Relay `installUpdateAndRestart` to another device. Local callers only. */
90
+ installUpdateAndRestartOnDevice(input: IAppUpdatesDeviceInput): Promise<IAppUpdateInstallResult>;
91
+ };
92
+ observables: {
93
+ /** Live update status; available over the local host transport only. */
94
+ status: Observable<IAppUpdateStatus>;
95
+ };
96
+ }
97
+ /**
98
+ * Return the System App Updates contract shape.
99
+ *
100
+ * Tools carry no `remoteAccessLevel`, so only callers this device trusts as `Self` (the
101
+ * user's own devices and users explicitly granted Self) can reach them remotely. The
102
+ * provider additionally honours the device's own remote-restart switch.
103
+ *
104
+ * The `*Device*` tools let a local UI or CLI reach another device: the host calls the target
105
+ * over a direct connection (or, for the user's own devices, the mesh) and the target applies
106
+ * its own trust check and switch. They refuse remote callers, so a device never relays.
107
+ * Observables cross the local host transport only.
108
+ */
109
+ export declare function getAppUpdatesContractDefinition(): IContractDefinition;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.APP_UPDATES_CONTRACT_VERSION = void 0;
4
+ exports.getAppUpdatesContractDefinition = getAppUpdatesContractDefinition;
5
+ const system_ids_1 = require("../../system-ids");
6
+ const field_type_1 = require("../../types/field-type");
7
+ const system_contracts_1 = require("../system-contracts");
8
+ /** Version of the built-in System App Updates contract. */
9
+ exports.APP_UPDATES_CONTRACT_VERSION = 1;
10
+ const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
11
+ const statusFields = [
12
+ field("supported", field_type_1.FieldType.boolean, "Whether this host can update itself."),
13
+ field("canRestart", field_type_1.FieldType.boolean, "Whether this host can restart itself on request."),
14
+ field("platform", field_type_1.FieldType.string, "Host operating system."),
15
+ field("currentVersion", field_type_1.FieldType.string, "Version of the running application."),
16
+ field("latestVersion", field_type_1.FieldType.string, "Newest version the update feed reported.", {
17
+ optional: true,
18
+ }),
19
+ field("downloadedVersion", field_type_1.FieldType.string, "Version ready to install.", { optional: true }),
20
+ field("state", field_type_1.FieldType.string, "Update lifecycle state."),
21
+ field("downloadPercent", field_type_1.FieldType.number, "Download progress from 0 to 100.", {
22
+ optional: true,
23
+ }),
24
+ field("error", field_type_1.FieldType.string, "Last update failure message.", { optional: true }),
25
+ field("lastCheckedAt", field_type_1.FieldType.number, "Epoch ms of the last completed check.", {
26
+ optional: true,
27
+ }),
28
+ field("installOnRestart", field_type_1.FieldType.boolean, "Install updates when the app restarts."),
29
+ field("installOnQuitArmed", field_type_1.FieldType.boolean, "A downloaded update will install on quit."),
30
+ field("remoteRestartAllowed", field_type_1.FieldType.boolean, "Whether trusted devices may update and restart this device."),
31
+ field("installNeedsElevation", field_type_1.FieldType.boolean, "Installing prompts for a password.", {
32
+ optional: true,
33
+ }),
34
+ ];
35
+ let definition;
36
+ const installResultFields = [
37
+ field("action", field_type_1.FieldType.string, "installing, downloading, checking, or restarting."),
38
+ field("status", field_type_1.FieldType.object, "Update status when the work started."),
39
+ ];
40
+ const deviceIdField = field("deviceId", field_type_1.FieldType.id, "Device to call: another of this user's devices, or one whose owner trusts this user as Self.");
41
+ /**
42
+ * Return the System App Updates contract shape.
43
+ *
44
+ * Tools carry no `remoteAccessLevel`, so only callers this device trusts as `Self` (the
45
+ * user's own devices and users explicitly granted Self) can reach them remotely. The
46
+ * provider additionally honours the device's own remote-restart switch.
47
+ *
48
+ * The `*Device*` tools let a local UI or CLI reach another device: the host calls the target
49
+ * over a direct connection (or, for the user's own devices, the mesh) and the target applies
50
+ * its own trust check and switch. They refuse remote callers, so a device never relays.
51
+ * Observables cross the local host transport only.
52
+ */
53
+ function getAppUpdatesContractDefinition() {
54
+ definition ??= {
55
+ contractId: system_ids_1.appUpdatesContractId,
56
+ version: exports.APP_UPDATES_CONTRACT_VERSION,
57
+ devTag: "dev",
58
+ name: "System App Updates",
59
+ description: "Desktop application update status, on-demand checks, and update-and-restart.",
60
+ tables: [],
61
+ tools: [
62
+ {
63
+ name: "getUpdateStatus",
64
+ usageDescription: "Return this device's application version and update status.",
65
+ inputFields: [],
66
+ outputFields: statusFields,
67
+ },
68
+ {
69
+ name: "checkForUpdates",
70
+ usageDescription: "Check the update feed now and return the resulting update status.",
71
+ inputFields: [],
72
+ outputFields: statusFields,
73
+ },
74
+ {
75
+ name: "installUpdateAndRestart",
76
+ usageDescription: "Install an available update and relaunch, or restart the app when there is nothing to install.",
77
+ inputFields: [],
78
+ outputFields: installResultFields,
79
+ },
80
+ {
81
+ name: "getDeviceUpdateStatus",
82
+ usageDescription: "Return another device's application version and update status.",
83
+ inputFields: [deviceIdField],
84
+ outputFields: statusFields,
85
+ },
86
+ {
87
+ name: "installUpdateAndRestartOnDevice",
88
+ usageDescription: "Ask another device to install its available update and relaunch, or restart when it has nothing to install.",
89
+ inputFields: [deviceIdField],
90
+ outputFields: installResultFields,
91
+ },
92
+ ],
93
+ observables: [
94
+ {
95
+ name: "status",
96
+ description: "Live application update status.",
97
+ valueType: field_type_1.FieldType.object,
98
+ writable: false,
99
+ },
100
+ ],
101
+ events: [],
102
+ };
103
+ return definition;
104
+ }
105
+ (0, system_contracts_1.registerSystemContract)({
106
+ providerPackageId: system_ids_1.peersSystemPackageId,
107
+ contractId: system_ids_1.appUpdatesContractId,
108
+ version: exports.APP_UPDATES_CONTRACT_VERSION,
109
+ getDefinition: getAppUpdatesContractDefinition,
110
+ });
@@ -2,6 +2,7 @@
2
2
  * Built-in system contracts. Importing this barrel runs each module's
3
3
  * {@link registerSystemContract} side effect so the catalog is populated.
4
4
  */
5
+ import "./app-updates-contract";
5
6
  import "./contract-catalog-contract";
6
7
  import "./device-operations-contract";
7
8
  import "./http-contract";
@@ -11,6 +12,7 @@ import "./logs-contract";
11
12
  import "./network-contracts";
12
13
  import "./scheduler-contract";
13
14
  import "./tool-access-control-contract";
15
+ export * from "./app-updates-contract";
14
16
  export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./device-operations-contract";
15
17
  export { getIdentityContractDefinition, IDENTITY_CONTRACT_VERSION, type IIdentityConsumer, type IIdentityKeyInfo, type IIdentityRegistryKey, type IIdentityRotateKeyInput, type IIdentityRotateKeyResult, type IIdentitySessionInfo, } from "./identity-contract";
16
18
  export { getInvitesContractDefinition, type IAcceptInviteResult, type ICreatedInvite, type IInvitePreview, type IInvitesConsumer, INVITES_CONTRACT_VERSION, } from "./invites-contract";
@@ -19,6 +19,7 @@ exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = exports.getToolAccessControlContr
19
19
  * Built-in system contracts. Importing this barrel runs each module's
20
20
  * {@link registerSystemContract} side effect so the catalog is populated.
21
21
  */
22
+ require("./app-updates-contract");
22
23
  require("./contract-catalog-contract");
23
24
  require("./device-operations-contract");
24
25
  require("./http-contract");
@@ -28,6 +29,7 @@ require("./logs-contract");
28
29
  require("./network-contracts");
29
30
  require("./scheduler-contract");
30
31
  require("./tool-access-control-contract");
32
+ __exportStar(require("./app-updates-contract"), exports);
31
33
  var device_operations_contract_1 = require("./device-operations-contract");
32
34
  Object.defineProperty(exports, "DEVICE_OPERATIONS_CONTRACT_VERSION", { enumerable: true, get: function () { return device_operations_contract_1.DEVICE_OPERATIONS_CONTRACT_VERSION; } });
33
35
  Object.defineProperty(exports, "DEVICE_OPERATIONS_MAX_QUERY_LENGTH", { enumerable: true, get: function () { return device_operations_contract_1.DEVICE_OPERATIONS_MAX_QUERY_LENGTH; } });
@@ -26,9 +26,14 @@ export declare function retainToolCatalogFields<T extends Partial<ITool>>(existi
26
26
  *
27
27
  * Catalog identities are permanent. Unresolved legacy rows are a holdover until their
28
28
  * tools move to contracts: they stay ordinary rows that local writers and older
29
- * runtimes may delete.
29
+ * runtimes may delete. A bare deterministic row with an empty executable and the
30
+ * member's own name may receive that member's canonical target; every other
31
+ * untargeted or quarantined identity stays rejected.
32
+ *
33
+ * @param catalogPatch - Only explicit local catalog patches may restore a bare
34
+ * deterministic row. Ordinary replacements and sync replay must leave it quarantined.
30
35
  */
31
- export declare function validateToolCatalogWrite(existing: Partial<ITool> | undefined, candidate: Partial<ITool> | undefined, recordId?: string | undefined): void;
36
+ export declare function validateToolCatalogWrite(existing: Partial<ITool> | undefined, candidate: Partial<ITool> | undefined, recordId?: string | undefined, catalogPatch?: boolean): void;
32
37
  /**
33
38
  * Per-context runtime owner of permanent Tools identities. Construction is inert;
34
39
  * ready() explicitly initializes storage after UserContext construction completes.
@@ -62,6 +62,35 @@ function hasToolCatalogEvidence(row) {
62
62
  row.contractVersion !== undefined ||
63
63
  row.toolName !== undefined);
64
64
  }
65
+ /**
66
+ * A catalog insert that lost its target columns, or was stored before those columns
67
+ * existed, still carries the member's deterministic ID, the member name and an empty
68
+ * executable. Publication may write the canonical target back, resolving the row in
69
+ * the same write; a repair cannot leave the target on a still-quarantined row. Any
70
+ * other bare `000tool0` row, including one that still holds executable source or a
71
+ * different name, stays unclaimable.
72
+ */
73
+ function canRestoreBareDeterministicRow(existing, candidate) {
74
+ if (candidate.catalogState !== "resolved" || candidate.catalogIdentity !== "deterministic")
75
+ return false;
76
+ if (existing.code !== "")
77
+ return false;
78
+ if (existing.catalogIdentity !== undefined && existing.catalogIdentity !== "deterministic")
79
+ return false;
80
+ let target;
81
+ try {
82
+ if (toolCatalogTarget(existing))
83
+ return false;
84
+ target = toolCatalogTarget(candidate);
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ if (!target || !candidate.toolId || existing.name !== target.toolName)
90
+ return false;
91
+ const ids = (0, tool_catalog_migrations_1.toolCatalogIds)(target);
92
+ return ids.length === 1 && ids[0] === candidate.toolId && candidate.toolId.startsWith("000tool0");
93
+ }
65
94
  /**
66
95
  * Shared pre-persistence identity guard for local writes and reconstructed sync rows.
67
96
  * Existing mappings cannot be retargeted, stripped or deleted. Rejected sync evidence
@@ -70,9 +99,14 @@ function hasToolCatalogEvidence(row) {
70
99
  *
71
100
  * Catalog identities are permanent. Unresolved legacy rows are a holdover until their
72
101
  * tools move to contracts: they stay ordinary rows that local writers and older
73
- * runtimes may delete.
102
+ * runtimes may delete. A bare deterministic row with an empty executable and the
103
+ * member's own name may receive that member's canonical target; every other
104
+ * untargeted or quarantined identity stays rejected.
105
+ *
106
+ * @param catalogPatch - Only explicit local catalog patches may restore a bare
107
+ * deterministic row. Ordinary replacements and sync replay must leave it quarantined.
74
108
  */
75
- function validateToolCatalogWrite(existing, candidate, recordId = candidate?.toolId ?? existing?.toolId) {
109
+ function validateToolCatalogWrite(existing, candidate, recordId = candidate?.toolId ?? existing?.toolId, catalogPatch = false) {
76
110
  if (!candidate) {
77
111
  if ((recordId !== undefined && isReservedToolCatalogId(recordId)) ||
78
112
  (existing && hasToolCatalogEvidence(existing)))
@@ -83,7 +117,9 @@ function validateToolCatalogWrite(existing, candidate, recordId = candidate?.too
83
117
  !(0, tool_catalog_identity_1.isCatalogToolId)(candidate.toolId) ||
84
118
  (existing && existing.toolId !== candidate.toolId))
85
119
  throw new Error("Tool catalog primary key mismatch");
86
- if (existing?.catalogState === "quarantined" && candidate.catalogState !== "quarantined")
120
+ if (existing?.catalogState === "quarantined" &&
121
+ candidate.catalogState !== "quarantined" &&
122
+ !(catalogPatch && canRestoreBareDeterministicRow(existing, candidate)))
87
123
  throw new Error(`Quarantined tool identity: ${candidate.toolId}`);
88
124
  // Quarantine preserves the original evidence byte-for-byte; it cannot retarget it.
89
125
  if (existing &&
@@ -99,7 +135,10 @@ function validateToolCatalogWrite(existing, candidate, recordId = candidate?.too
99
135
  if (previous && (!target || (0, tool_catalog_identity_1.toolCatalogTargetKey)(previous) !== (0, tool_catalog_identity_1.toolCatalogTargetKey)(target)))
100
136
  throw new Error(`Tool catalog identity cannot change: ${candidate.toolId}`);
101
137
  if (target) {
102
- if (existing && !previous && !legacy)
138
+ if (existing &&
139
+ !previous &&
140
+ !legacy &&
141
+ !(catalogPatch && canRestoreBareDeterministicRow(existing, candidate)))
103
142
  throw new Error(`Unresolved tool identity cannot be claimed: ${candidate.toolId}`);
104
143
  if (!(0, tool_catalog_migrations_1.toolCatalogIds)(target).includes(candidate.toolId))
105
144
  throw new Error(`Unknown tool catalog mapping: ${candidate.toolId}`);
@@ -364,9 +403,10 @@ class ToolCatalog {
364
403
  ...resolvedFields(target, toolId),
365
404
  };
366
405
  const stored = existing.get(toolId);
367
- if (stored?.catalogState === "quarantined")
406
+ if (stored?.catalogState === "quarantined" &&
407
+ !canRestoreBareDeterministicRow(stored, row))
368
408
  throw new Error(`Quarantined tool identity: ${toolId}`);
369
- validateToolCatalogWrite(stored, row);
409
+ validateToolCatalogWrite(stored, row, toolId, true);
370
410
  candidates.set(toolId, row);
371
411
  }
372
412
  }
@@ -374,6 +414,18 @@ class ToolCatalog {
374
414
  // Validate the entire batch before the first write. Catalog metadata never
375
415
  // overwrites provider-specific policy or another provider's descriptions.
376
416
  for (const row of candidates.values()) {
417
+ const previous = existing.get(row.toolId);
418
+ // Restore missing identity columns only after validating the entire batch.
419
+ // This marked patch leaves concurrent policy and historical fields intact.
420
+ if (previous && canRestoreBareDeterministicRow(previous, row)) {
421
+ const restored = await table.update((0, tools_1.toolCatalogPatch)({
422
+ toolId: row.toolId,
423
+ ...resolvedFields(toolCatalogTarget(row), row.toolId),
424
+ }));
425
+ validateToolCatalogWrite(row, restored);
426
+ if (restored.catalogState === "quarantined")
427
+ throw new Error(`Quarantined tool identity: ${row.toolId}`);
428
+ }
377
429
  // Older runtimes allowed provider removal to leave tombstones. Only a
378
430
  // validated public target may restore one; insert-only still preserves any
379
431
  // row that arrived concurrently, and the returned mapping is checked below.
@@ -30,6 +30,7 @@ export * from "./tool-access";
30
30
  export * from "./tool-access-approvals";
31
31
  export * from "./tool-tests";
32
32
  export * from "./tools";
33
+ export * from "./update-settings.pvar";
33
34
  export * from "./user-permissions";
34
35
  export * from "./user-trust-levels";
35
36
  export * from "./users";
@@ -46,6 +46,7 @@ __exportStar(require("./tool-access"), exports);
46
46
  __exportStar(require("./tool-access-approvals"), exports);
47
47
  __exportStar(require("./tool-tests"), exports);
48
48
  __exportStar(require("./tools"), exports);
49
+ __exportStar(require("./update-settings.pvar"), exports);
49
50
  __exportStar(require("./user-permissions"), exports);
50
51
  __exportStar(require("./user-trust-levels"), exports);
51
52
  __exportStar(require("./users"), exports);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Device-scoped settings for desktop application updates. Each device keeps its own values;
3
+ * they never sync to the user's other devices, so another device cannot change them.
4
+ *
5
+ * Like the other `*.pvar` modules, this creates pvars at load, so the data barrel must
6
+ * evaluate it after `persistent-vars` has initialized; its name sorts it there.
7
+ */
8
+ /**
9
+ * Install a downloaded application update automatically the next time Peers quits or
10
+ * restarts on this device.
11
+ *
12
+ * Defaults to true. When false, updates still download in the background but install only
13
+ * when someone chooses **Update & restart**.
14
+ *
15
+ * The desktop app reads this row by name each time it quits, rather than trusting the
16
+ * in-memory value, so keep the name stable.
17
+ */
18
+ export declare const installAppUpdatesOnRestart: import("./persistent-vars").PersistentVar<boolean>;
19
+ /**
20
+ * Allow callers this device trusts as `Self` (the user's other devices, and users explicitly
21
+ * granted Self trust) to run **Update & restart** on this device remotely.
22
+ *
23
+ * Defaults to true. This is a consent switch, not a security boundary: Self-trusted callers
24
+ * already have full remote access to this device's system contracts.
25
+ */
26
+ export declare const allowRemoteAppUpdateRestart: import("./persistent-vars").PersistentVar<boolean>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * Device-scoped settings for desktop application updates. Each device keeps its own values;
4
+ * they never sync to the user's other devices, so another device cannot change them.
5
+ *
6
+ * Like the other `*.pvar` modules, this creates pvars at load, so the data barrel must
7
+ * evaluate it after `persistent-vars` has initialized; its name sorts it there.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.allowRemoteAppUpdateRestart = exports.installAppUpdatesOnRestart = void 0;
11
+ const persistent_vars_1 = require("./persistent-vars");
12
+ /**
13
+ * Install a downloaded application update automatically the next time Peers quits or
14
+ * restarts on this device.
15
+ *
16
+ * Defaults to true. When false, updates still download in the background but install only
17
+ * when someone chooses **Update & restart**.
18
+ *
19
+ * The desktop app reads this row by name each time it quits, rather than trusting the
20
+ * in-memory value, so keep the name stable.
21
+ */
22
+ exports.installAppUpdatesOnRestart = (0, persistent_vars_1.deviceVar)("installAppUpdatesOnRestart", {
23
+ defaultValue: true,
24
+ });
25
+ /**
26
+ * Allow callers this device trusts as `Self` (the user's other devices, and users explicitly
27
+ * granted Self trust) to run **Update & restart** on this device remotely.
28
+ *
29
+ * Defaults to true. This is a consent switch, not a security boundary: Self-trusted callers
30
+ * already have full remote access to this device's system contracts.
31
+ */
32
+ exports.allowRemoteAppUpdateRestart = (0, persistent_vars_1.deviceVar)("allowRemoteAppUpdateRestart", {
33
+ defaultValue: true,
34
+ });
@@ -65,6 +65,12 @@ export declare const deviceOperationsContractId = "00mriijkhq94msfe9m2j6quwp";
65
65
  export declare const networkDiagnosticsContractId = "00ms1yb50w29oi116uwwrg7rl";
66
66
  /** Contract id for explicit local network and synchronization control operations. */
67
67
  export declare const networkControlContractId = "00ms1yb50yj9w5qbytl69qfht";
68
+ /**
69
+ * Contract id for **System App Updates**: desktop host update status, on-demand checks, and
70
+ * update-and-restart. Remotely callable by Self-trusted devices; hosts without an updater
71
+ * provide status and relay tools, while Electron also implements local updates.
72
+ */
73
+ export declare const appUpdatesContractId = "00mvczoafequsatpfjaj3r4g8";
68
74
  /**
69
75
  * Contract id for the local-only tool-access approval control surface.
70
76
  * Not available over device-to-device connections.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.desktopToolsContractId = exports.hostToolsContractId = exports.toolCatalogContractId = exports.schedulerToolIds = exports.schedulerContractId = exports.contractCatalogToolIds = exports.contractCatalogContractId = exports.httpToolIds = exports.httpContractId = exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCoreRevokedPublishPublicKeys = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
3
+ exports.desktopToolsContractId = exports.hostToolsContractId = exports.toolCatalogContractId = exports.schedulerToolIds = exports.schedulerContractId = exports.contractCatalogToolIds = exports.contractCatalogContractId = exports.httpToolIds = exports.httpContractId = exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.appUpdatesContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCoreRevokedPublishPublicKeys = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
4
4
  /** Stable user ID that owns built-in Peers system records. */
5
5
  exports.peersRootUserId = "000peers0user000000000001";
6
6
  /** Default assistant used when no user-selected assistant is available. */
@@ -72,6 +72,12 @@ exports.deviceOperationsContractId = "00mriijkhq94msfe9m2j6quwp";
72
72
  exports.networkDiagnosticsContractId = "00ms1yb50w29oi116uwwrg7rl";
73
73
  /** Contract id for explicit local network and synchronization control operations. */
74
74
  exports.networkControlContractId = "00ms1yb50yj9w5qbytl69qfht";
75
+ /**
76
+ * Contract id for **System App Updates**: desktop host update status, on-demand checks, and
77
+ * update-and-restart. Remotely callable by Self-trusted devices; hosts without an updater
78
+ * provide status and relay tools, while Electron also implements local updates.
79
+ */
80
+ exports.appUpdatesContractId = "00mvczoafequsatpfjaj3r4g8";
75
81
  /**
76
82
  * Contract id for the local-only tool-access approval control surface.
77
83
  * Not available over device-to-device connections.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"