@peers-app/peers-sdk 0.26.4 → 0.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/contracts/__tests__/contract-resolvers.test.js +24 -0
  2. package/dist/contracts/__tests__/legacy-removal.test.js +38 -0
  3. package/dist/contracts/__tests__/remote-contract-transport.test.js +23 -32
  4. package/dist/contracts/__tests__/system-contracts.test.js +80 -0
  5. package/dist/contracts/contract-resolvers.d.ts +1 -1
  6. package/dist/contracts/contract-resolvers.js +3 -2
  7. package/dist/contracts/create-local-consumer.d.ts +2 -1
  8. package/dist/contracts/create-local-consumer.js +1 -1
  9. package/dist/contracts/index.d.ts +2 -0
  10. package/dist/contracts/index.js +2 -0
  11. package/dist/contracts/remote-contract-transport.d.ts +3 -3
  12. package/dist/contracts/remote-contract-transport.js +19 -17
  13. package/dist/contracts/system/host-shell-contract.d.ts +191 -0
  14. package/dist/contracts/system/host-shell-contract.js +190 -0
  15. package/dist/contracts/system/index.d.ts +4 -0
  16. package/dist/contracts/system/index.js +4 -0
  17. package/dist/contracts/system/packages-contract.d.ts +257 -0
  18. package/dist/contracts/system/packages-contract.js +213 -0
  19. package/dist/data/create-group.d.ts +1 -1
  20. package/dist/data/create-group.js +1 -1
  21. package/dist/data/package-version-resolver.d.ts +8 -2
  22. package/dist/data/package-version-resolver.js +28 -2
  23. package/dist/data/package-version-resolver.test.js +42 -0
  24. package/dist/package-installer/package-author-signer.d.ts +19 -0
  25. package/dist/package-installer/package-author-signer.js +24 -0
  26. package/dist/package-installer/package-creator.d.ts +1 -7
  27. package/dist/package-installer/package-creator.js +0 -12
  28. package/dist/package-installer/package-creator.test.js +0 -16
  29. package/dist/package-installer/package-installer.d.ts +5 -0
  30. package/dist/package-installer/package-installer.js +60 -37
  31. package/dist/package-installer/package-installer.test.js +129 -0
  32. package/dist/package-installer/types.d.ts +5 -11
  33. package/dist/package-loader/index.d.ts +1 -0
  34. package/dist/package-loader/index.js +1 -0
  35. package/dist/package-loader/isolated-runtime.d.ts +18 -0
  36. package/dist/package-loader/package-loader.d.ts +67 -4
  37. package/dist/package-loader/package-loader.js +300 -52
  38. package/dist/package-loader/package-loader.test.js +413 -1
  39. package/dist/package-loader/package-runtime-state.d.ts +112 -0
  40. package/dist/package-loader/package-runtime-state.js +18 -0
  41. package/dist/package-loader/resolve-consumed-contracts.js +5 -4
  42. package/dist/package-loader/resolve-consumed-contracts.test.js +1 -1
  43. package/dist/rpc-types.d.ts +0 -121
  44. package/dist/rpc-types.js +0 -39
  45. package/dist/system-ids.d.ts +13 -0
  46. package/dist/system-ids.js +14 -1
  47. package/package.json +1 -1
@@ -345,6 +345,23 @@ describe("remote contract call authorization (group members)", () => {
345
345
  });
346
346
  await expect((0, index_1.authorizeRemoteContractCall)({ ...toolCall, targetDeviceId: (0, utils_1.newid)() }, userContext, remoteIdentity)).rejects.toThrow("cannot relay");
347
347
  });
348
+ it("lets a Self-trusted device administer System Packages on this device", async () => {
349
+ const { remoteIdentity, userContext } = setupGroupMember({ callerLevel: access_level_1.AccessLevel.None });
350
+ mockTrustGet.mockResolvedValue({ trustLevel: socket_type_1.TrustLevel.Self });
351
+ const route = await (0, index_1.authorizeRemoteContractCall)({ ...toolCall, contractId: system_ids_1.packagesContractId, name: "activatePackageVersion" }, userContext, remoteIdentity);
352
+ expect(route.dataContextId).toBe(groupId);
353
+ expect(route.context?.callerDeviceId).toBe(remoteIdentity.deviceId);
354
+ });
355
+ it.each([
356
+ ["Admin", access_level_1.AccessLevel.Admin],
357
+ ["Owner", access_level_1.AccessLevel.Owner],
358
+ ])("refuses System Packages to a group %s who is not trusted as Self", async (_role, level) => {
359
+ const { remoteIdentity, userContext, groupDataContext } = setupGroupMember({
360
+ callerLevel: level,
361
+ });
362
+ groupDataContext.packageLoader.contractRegistry.getDefinition.mockReturnValue((0, index_1.getPackagesContractDefinition)());
363
+ await expect((0, index_1.authorizeRemoteContractCall)({ ...toolCall, contractId: system_ids_1.packagesContractId, name: "listPackages" }, userContext, remoteIdentity)).rejects.toThrow("tool is not remotely callable");
364
+ });
348
365
  it("keeps full access for Self-trusted callers without consulting remoteAccessLevel", async () => {
349
366
  const { remoteIdentity, userContext } = setupGroupMember({ callerLevel: access_level_1.AccessLevel.None });
350
367
  mockTrustGet.mockResolvedValue({ trustLevel: socket_type_1.TrustLevel.Self });
@@ -352,6 +369,13 @@ describe("remote contract call authorization (group members)", () => {
352
369
  expect(route.dataContextId).toBe(groupId);
353
370
  expect(accessLevelMock.calls).toEqual([]);
354
371
  });
372
+ it("refuses a Self-trusted caller naming a group this device does not have", async () => {
373
+ const { remoteIdentity, userContext } = setupGroupMember({ callerLevel: access_level_1.AccessLevel.None });
374
+ mockTrustGet.mockResolvedValue({ trustLevel: socket_type_1.TrustLevel.Self });
375
+ const unknownGroupId = (0, utils_1.newid)();
376
+ await expect((0, index_1.authorizeRemoteContractCall)({ ...toolCall, dataContextId: unknownGroupId }, userContext, remoteIdentity)).rejects.toThrow("not available locally");
377
+ expect(userContext.getDataContext).not.toHaveBeenCalledWith(unknownGroupId);
378
+ });
355
379
  });
356
380
  describe("local user-context contract routing", () => {
357
381
  function setupLocalRouter() {
@@ -18,6 +18,44 @@ test("the SDK exports no executable registry", () => {
18
18
  for (const name of removedRegistryExports)
19
19
  expect(sdk).not.toHaveProperty(name);
20
20
  });
21
+ test("package administration has no RPC stubs; System Packages replaces them", () => {
22
+ for (const name of [
23
+ "addOrUpdatePackage",
24
+ "seedBundledPeersCore",
25
+ "signPackageAuthorVersion",
26
+ "injectUIBundle",
27
+ ])
28
+ expect(sdk.rpcServerCalls).not.toHaveProperty(name);
29
+ expect(sdk).not.toHaveProperty("unwrapAddOrUpdatePackageResult");
30
+ expect(sdk.getPackagesContractDefinition().name).toBe("System Packages");
31
+ });
32
+ test("the host shell has no RPC stubs; System Host Shell replaces them", () => {
33
+ for (const name of [
34
+ "openLinkInBrowser",
35
+ "openPath",
36
+ "openPackage",
37
+ "copyPvarValueToClipboard",
38
+ "appQuit",
39
+ "appRestart",
40
+ "uiReload",
41
+ "uiInspect",
42
+ "uiClick",
43
+ "uiSet",
44
+ "uiScroll",
45
+ ])
46
+ expect(sdk.rpcServerCalls).not.toHaveProperty(name);
47
+ for (const name of [
48
+ "setClientPath",
49
+ "openThread",
50
+ "uiGetState",
51
+ "uiDoClick",
52
+ "uiDoSet",
53
+ "uiDoScroll",
54
+ ])
55
+ expect(sdk.rpcClientCalls).not.toHaveProperty(name);
56
+ expect(sdk).not.toHaveProperty("UIState");
57
+ expect(sdk.getHostShellContractDefinition().name).toBe("System Host Shell");
58
+ });
21
59
  test("old SDK assignments warn once and never produce executables", () => {
22
60
  const builder = new builder_1.ContractBuilder((0, utils_1.newid)(), 1, "Old package");
23
61
  const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
@@ -30,6 +30,17 @@ afterEach(() => {
30
30
  (0, index_1.setRemoteContractTransportFactory)(undefined);
31
31
  });
32
32
  describe("remote contract endpoint", () => {
33
+ it("selects the current transport for every call on a cached endpoint", async () => {
34
+ const first = makeTransport();
35
+ const second = makeTransport();
36
+ let current = first.transport;
37
+ const endpoint = (0, index_1.createRemoteContractEndpoint)(makeRequest(), () => current);
38
+ await endpoint.handleCall(toolCall, {});
39
+ current = second.transport;
40
+ await endpoint.handleCall(toolCall, {});
41
+ expect(first.emit).toHaveBeenCalledTimes(1);
42
+ expect(second.emit).toHaveBeenCalledTimes(1);
43
+ });
33
44
  it("strips targetDeviceId and pins the authorized data context before emitting", async () => {
34
45
  const { transport, emit } = makeTransport();
35
46
  const factory = jest.fn(() => transport);
@@ -50,13 +61,21 @@ describe("remote contract endpoint", () => {
50
61
  await expect(endpoint.handleCall(toolCall, {})).rejects.toThrow("disposed");
51
62
  expect(emit).not.toHaveBeenCalled();
52
63
  });
53
- it("refuses package callers for contracts whose providers refuse packages", async () => {
64
+ it.each([
65
+ ["System App Updates", system_ids_1.appUpdatesContractId],
66
+ ["System Packages", system_ids_1.packagesContractId],
67
+ ["Device Operations", system_ids_1.deviceOperationsContractId],
68
+ ])("refuses to forward a package's call to %s", async (_name, contractId) => {
54
69
  const { transport, emit } = makeTransport();
55
70
  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");
71
+ expect(() => (0, index_1.createRemoteContractEndpoint)(makeRequest({ contractId, context: packageContext }), () => transport)).toThrow("packages cannot call system contract");
72
+ // A package endpoint for its own contract cannot be steered at a system contract.
59
73
  const packageTool = (0, index_1.createRemoteContractEndpoint)(makeRequest({ context: packageContext }), () => transport);
74
+ await expect(packageTool.handleCall({ ...toolCall, contractId }, packageContext)).rejects.toThrow("packages cannot call system contract");
75
+ expect(emit).not.toHaveBeenCalled();
76
+ // The user (no package) still reaches it, and the package still reaches its own contract.
77
+ const operator = (0, index_1.createRemoteContractEndpoint)(makeRequest({ contractId }), () => transport);
78
+ await expect(operator.handleCall({ ...toolCall, contractId }, {})).resolves.toBe("pong");
60
79
  await expect(packageTool.handleCall(toolCall, packageContext)).resolves.toBe("pong");
61
80
  expect(emit).toHaveBeenCalledTimes(2);
62
81
  });
@@ -65,34 +84,6 @@ describe("remote contract endpoint", () => {
65
84
  expect(() => (0, index_1.createRemoteContractEndpoint)(makeRequest({ dataContextId: undefined }), () => makeTransport().transport)).toThrow("data context");
66
85
  expect(() => (0, index_1.createRemoteContractEndpoint)(makeRequest({ targetDeviceId: undefined }), () => makeTransport().transport)).toThrow("target device");
67
86
  });
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
- });
96
87
  });
97
88
  describe("resolveRemoteTargetEndpoint", () => {
98
89
  it("returns undefined for local routes and for the local device itself", () => {
@@ -4,9 +4,12 @@ const system_ids_1 = require("../../system-ids");
4
4
  const utils_1 = require("../../utils");
5
5
  const contract_resolvers_1 = require("../contract-resolvers");
6
6
  const registry_1 = require("../registry");
7
+ const host_shell_contract_1 = require("../system/host-shell-contract");
7
8
  const identity_contract_1 = require("../system/identity-contract");
9
+ const packages_contract_1 = require("../system/packages-contract");
8
10
  const system_contracts_1 = require("../system-contracts");
9
11
  const tool_catalog_1 = require("../tool-catalog");
12
+ const tool_catalog_service_1 = require("../tool-catalog-service");
10
13
  // Populate the catalog with built-in system contracts.
11
14
  require("../system");
12
15
  describe("system contract catalog", () => {
@@ -25,6 +28,7 @@ describe("system contract catalog", () => {
25
28
  system_ids_1.networkControlContractId,
26
29
  system_ids_1.toolAccessControlContractId,
27
30
  system_ids_1.toolCatalogContractId,
31
+ system_ids_1.packagesContractId,
28
32
  ]));
29
33
  });
30
34
  it("registers the Logs live resolver", async () => {
@@ -165,3 +169,79 @@ test("host and desktop workflow members are present with workflow-only policy",
165
169
  expect(definition?.tools.find((tool) => tool.name === name)?.requiresWorkflowRun).toBe(true);
166
170
  }
167
171
  });
172
+ test("System Packages is remote only for Self and never workflow-invocable", () => {
173
+ const registry = new registry_1.ContractRegistry();
174
+ (0, system_contracts_1.installSystemContracts)(registry);
175
+ const definition = registry.resolve(system_ids_1.packagesContractId, 1);
176
+ expect(definition?.name).toBe("System Packages");
177
+ expect(registry.getProviderPackageId(system_ids_1.packagesContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
178
+ expect((0, contract_resolvers_1.isLocalOnlyContractId)(system_ids_1.packagesContractId)).toBe(false);
179
+ for (const tool of definition?.tools ?? []) {
180
+ expect(tool.remoteAccessLevel).toBeUndefined();
181
+ expect((0, tool_catalog_service_1.isWorkflowInvocableTarget)({
182
+ contractId: system_ids_1.packagesContractId,
183
+ version: 1,
184
+ toolName: tool.name,
185
+ })).toBe(false);
186
+ }
187
+ });
188
+ test("System Host Shell is remote only for Self and never workflow-invocable", () => {
189
+ const registry = new registry_1.ContractRegistry();
190
+ (0, system_contracts_1.installSystemContracts)(registry);
191
+ const definition = registry.resolve(system_ids_1.hostShellContractId, 1);
192
+ expect(definition?.name).toBe("System Host Shell");
193
+ expect(registry.getProviderPackageId(system_ids_1.hostShellContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
194
+ expect((0, contract_resolvers_1.isLocalOnlyContractId)(system_ids_1.hostShellContractId)).toBe(false);
195
+ expect(definition?.tools.map((tool) => tool.name)).toContain("screenshot");
196
+ for (const tool of definition?.tools ?? []) {
197
+ expect(tool.remoteAccessLevel).toBeUndefined();
198
+ expect((0, tool_catalog_service_1.isWorkflowInvocableTarget)({
199
+ contractId: system_ids_1.hostShellContractId,
200
+ version: 1,
201
+ toolName: tool.name,
202
+ })).toBe(false);
203
+ }
204
+ });
205
+ it.each([
206
+ ["https://peers.app/docs", { kind: "browser", url: "https://peers.app/docs" }],
207
+ [" http://localhost:3000 ", { kind: "browser", url: "http://localhost:3000" }],
208
+ ["mailto:someone@example.com", { kind: "browser", url: "mailto:someone@example.com" }],
209
+ ["~/peers/packages", { kind: "path", path: "~/peers/packages" }],
210
+ ["/tmp/a b", { kind: "path", path: "/tmp/a b" }],
211
+ ["C:\\Users\\me", { kind: "path", path: "C:\\Users\\me" }],
212
+ ["relative/dir", { kind: "path", path: "relative/dir" }],
213
+ ])("open routes %p", (target, expected) => {
214
+ expect((0, host_shell_contract_1.classifyHostOpenTarget)(target)).toEqual(expected);
215
+ });
216
+ it.each([
217
+ "file:///etc/passwd",
218
+ "javascript:alert(1)",
219
+ "vscode://file/x",
220
+ "smb://host/share",
221
+ "",
222
+ ])("open refuses %p", (target) => {
223
+ expect(() => (0, host_shell_contract_1.classifyHostOpenTarget)(target)).toThrow(/open (refuses|requires)/);
224
+ });
225
+ it.each([
226
+ [undefined, ["beta", "stable"]],
227
+ ["", ["beta", "stable"]],
228
+ ["dev", ["beta", "stable"]],
229
+ ["beta", ["stable"]],
230
+ ["stable", []],
231
+ ["nightly", []],
232
+ ])("promotion targets for %p are %p", (tag, targets) => {
233
+ expect((0, packages_contract_1.packageVersionPromotionTargets)(tag)).toEqual(targets);
234
+ });
235
+ it.each([
236
+ ["https://github.com/peers-app/x.git", true],
237
+ ["http://host/x", true],
238
+ ["git@github.com:peers-app/x", true],
239
+ [" git@github.com:peers-app/x.git", true],
240
+ ["git@notes", false],
241
+ ["../x.git", false],
242
+ ["/abs/x.git", false],
243
+ ["My Package", false],
244
+ ["00mvdhk66tdqrj3hy0bacmj32", false],
245
+ ])("routes %p to clonePackage: %p", (target, expected) => {
246
+ expect((0, packages_contract_1.isPackageRepositoryUrl)(target)).toBe(expected);
247
+ });
@@ -100,7 +100,7 @@ export declare function isLocalOnlyContractId(contractId: string): boolean;
100
100
  * 2. The caller's keys must match a valid signed Users record known to the provider.
101
101
  * 3. A Self-trusted caller (the provider's own account, or an account granted exact
102
102
  * `TrustLevel.Self`) keeps full access; an omitted data context selects the provider's
103
- * personal context.
103
+ * personal context, and a named one must be a group this device already has.
104
104
  * 4. Any other verified caller is treated as a **group member**: the call must name a
105
105
  * group the provider belongs to, must be a tool run, the contract definition registered
106
106
  * for that group must mark the tool with {@link IContractTool.remoteAccessLevel}, and
@@ -239,7 +239,7 @@ function isLocalOnlyContractId(contractId) {
239
239
  * 2. The caller's keys must match a valid signed Users record known to the provider.
240
240
  * 3. A Self-trusted caller (the provider's own account, or an account granted exact
241
241
  * `TrustLevel.Self`) keeps full access; an omitted data context selects the provider's
242
- * personal context.
242
+ * personal context, and a named one must be a group this device already has.
243
243
  * 4. Any other verified caller is treated as a **group member**: the call must name a
244
244
  * group the provider belongs to, must be a tool run, the contract definition registered
245
245
  * for that group must mark the tool with {@link IContractTool.remoteAccessLevel}, and
@@ -269,7 +269,8 @@ async function authorizeRemoteContractCall(call, userContext, remoteIdentity) {
269
269
  callerUserId: remoteIdentity.userId,
270
270
  localUserId: userContext.userId,
271
271
  callerDeviceId: remoteIdentity.deviceId,
272
- dataContext: userContext.getDataContext(dataContextId),
272
+ // Never create a context the caller names: only this device's own groups.
273
+ dataContext: getAuthorizedLocalDataContext(userContext, dataContextId),
273
274
  },
274
275
  };
275
276
  }
@@ -1,4 +1,4 @@
1
- import { type IContractConsumer, type IContractResolution } from "./contract-proxy";
1
+ import { type IContractCallContext, type IContractConsumer, type IContractResolution } from "./contract-proxy";
2
2
  import type { IContractDefinition } from "./types";
3
3
  /**
4
4
  * Build a same-process contract consumer wired to a live provider resolution.
@@ -9,4 +9,5 @@ import type { IContractDefinition } from "./types";
9
9
  */
10
10
  export declare function createLocalContractConsumer(definition: IContractDefinition, resolution: IContractResolution, opts?: {
11
11
  dataContextId?: string;
12
+ context?: IContractCallContext;
12
13
  }): IContractConsumer;
@@ -12,7 +12,7 @@ const contract_transport_1 = require("./contract-transport");
12
12
  */
13
13
  function createLocalContractConsumer(definition, resolution, opts) {
14
14
  const [consumerTransport, providerTransport] = (0, contract_transport_1.inProcessTransportPair)();
15
- (0, contract_proxy_1.createContractProviderSession)(resolution, providerTransport);
15
+ (0, contract_proxy_1.createContractProviderSession)(resolution, providerTransport, { context: opts?.context });
16
16
  return (0, contract_proxy_1.createContractConsumer)(definition, consumerTransport, {
17
17
  dataContextId: opts?.dataContextId,
18
18
  });
@@ -17,6 +17,7 @@ export * from "./system/app-updates-contract";
17
17
  export { CONTRACT_CATALOG_VERSION, contractCatalogProviderPackageId, getContractCatalogDefinition, type IContractCatalogConsumer, type IContractProviderInfo, type IListContractProvidersInput, } from "./system/contract-catalog-contract";
18
18
  export * from "./system/desktop-tools-contract";
19
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";
20
+ export * from "./system/host-shell-contract";
20
21
  export * from "./system/host-tools-contract";
21
22
  export { getHttpContractDefinition, HTTP_CONTRACT_VERSION, HTTP_DEFAULT_TIMEOUT_MS, HTTP_MAX_BODY_BYTES, HTTP_MAX_TIMEOUT_MS, httpProviderPackageId, type IHttpCancelInput, type IHttpConsumer, type IHttpCredentialInfo, type IHttpMultipartPart, type IHttpPolicyInfo, type IHttpRequestInput, type IHttpRequestResult, } from "./system/http-contract";
22
23
  export { getIdentityContractDefinition, IDENTITY_CONTRACT_VERSION, type IIdentityConsumer, type IIdentityKeyInfo, type IIdentityRegistryKey, type IIdentityRotateKeyInput, type IIdentityRotateKeyResult, type IIdentitySessionInfo, } from "./system/identity-contract";
@@ -24,6 +25,7 @@ export { getInvitesContractDefinition, type IAcceptInviteResult, type ICreatedIn
24
25
  export { getLocalFoldersContractDefinition, type ILocalFolderAssembleInput, type ILocalFolderAssembleResult, type ILocalFolderChange, type ILocalFolderChangedEvent, type ILocalFolderEntry, type ILocalFolderFileBlocks, type ILocalFolderFileBlocksInput, type ILocalFolderGrantInfo, type ILocalFolderIndexInput, type ILocalFolderIndexResult, type ILocalFolderJobFinishedEvent, type ILocalFolderJobHandle, type ILocalFolderJobInfo, type ILocalFolderJobProgressEvent, type ILocalFolderListInput, type ILocalFolderManifestBlock, type ILocalFolderMoveInput, type ILocalFolderPathInput, type ILocalFolderPurgeTrashInput, type ILocalFolderReadRangeInput, type ILocalFolderReadRangeResult, type ILocalFolderRequestGrantInput, type ILocalFolderStatInput, LOCAL_FOLDERS_ASSEMBLE_STALL_MS, LOCAL_FOLDERS_CHUNK_AVG, LOCAL_FOLDERS_CHUNK_MAX, LOCAL_FOLDERS_CHUNK_MIN, LOCAL_FOLDERS_CONTRACT_VERSION, LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS, LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES, LOCAL_FOLDERS_HAVE_BATCH, LOCAL_FOLDERS_JOB_TTL_MS, LOCAL_FOLDERS_READ_RANGE_MAX, LOCAL_FOLDERS_RESERVED_DIR, type LocalFolderChangeKind, type LocalFolderEntryKind, type LocalFolderJobKind, type LocalFolderMode, localFoldersProviderPackageId, } from "./system/local-folders-contract";
25
26
  export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./system/logs-contract";
26
27
  export * from "./system/network-contracts";
28
+ export * from "./system/packages-contract";
27
29
  export { getSchedulerContractDefinition, type IScheduleInfo, type IScheduleInput, type ISchedulerConsumer, SCHEDULER_CONTRACT_VERSION, SCHEDULER_MAX_DELAY_MS, schedulerProviderPackageId, } from "./system/scheduler-contract";
28
30
  export { getToolAccessControlContractDefinition, type IToolAccessControlConsumer, TOOL_ACCESS_CONTROL_CONTRACT_VERSION, } from "./system/tool-access-control-contract";
29
31
  export * from "./system/tool-catalog-contract";
@@ -98,6 +98,7 @@ Object.defineProperty(exports, "DEVICE_OPERATIONS_MAX_TOOLS", { enumerable: true
98
98
  Object.defineProperty(exports, "deviceOperationsProviderPackageId", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsProviderPackageId; } });
99
99
  Object.defineProperty(exports, "deviceOperationsToolIds", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsToolIds; } });
100
100
  Object.defineProperty(exports, "getDeviceOperationsContractDefinition", { enumerable: true, get: function () { return device_operations_contract_1.getDeviceOperationsContractDefinition; } });
101
+ __exportStar(require("./system/host-shell-contract"), exports);
101
102
  __exportStar(require("./system/host-tools-contract"), exports);
102
103
  var http_contract_1 = require("./system/http-contract");
103
104
  Object.defineProperty(exports, "getHttpContractDefinition", { enumerable: true, get: function () { return http_contract_1.getHttpContractDefinition; } });
@@ -131,6 +132,7 @@ Object.defineProperty(exports, "getLogsContractDefinition", { enumerable: true,
131
132
  Object.defineProperty(exports, "LOGS_CONTRACT_VERSION", { enumerable: true, get: function () { return logs_contract_1.LOGS_CONTRACT_VERSION; } });
132
133
  Object.defineProperty(exports, "resolveLogsContract", { enumerable: true, get: function () { return logs_contract_1.resolveLogsContract; } });
133
134
  __exportStar(require("./system/network-contracts"), exports);
135
+ __exportStar(require("./system/packages-contract"), exports);
134
136
  var scheduler_contract_1 = require("./system/scheduler-contract");
135
137
  Object.defineProperty(exports, "getSchedulerContractDefinition", { enumerable: true, get: function () { return scheduler_contract_1.getSchedulerContractDefinition; } });
136
138
  Object.defineProperty(exports, "SCHEDULER_CONTRACT_VERSION", { enumerable: true, get: function () { return scheduler_contract_1.SCHEDULER_CONTRACT_VERSION; } });
@@ -61,12 +61,12 @@ export declare function resolveRemoteTargetEndpoint(request: IContractProviderEn
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
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
+ * reach system contracts on another device (System Packages, App Updates, Device
65
+ * Operations, …): the target would see only this device, acting for the user.
66
66
  *
67
67
  * @param request Authorized route whose `targetDeviceId` and `dataContextId` are set.
68
68
  * @param factory Transport factory; defaults to the process-wide registration.
69
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.
70
+ * a package targets a system contract.
71
71
  */
72
72
  export declare function createRemoteContractEndpoint(request: IContractProviderEndpointRequest, factory?: RemoteContractTransportFactory | undefined): IContractProviderEndpoint;
@@ -4,14 +4,18 @@ 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");
8
7
  const contract_transport_1 = require("./contract-transport");
8
+ const system_contracts_1 = require("./system-contracts");
9
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.
10
+ * Whether this host refuses to forward a package's call to another device. A forwarded
11
+ * call reaches the target signed by this device alone, as the user, so the target cannot
12
+ * tell that a package made it and would grant it the user's full Self authority. Packages
13
+ * may still call their own (and other packages') contracts there; system contracts, whose
14
+ * tools administer the device, are refused here instead.
13
15
  */
14
- const packageRefusedRemoteContractIds = new Set([system_ids_1.appUpdatesContractId]);
16
+ function refusesForwardedPackageCall(context, contractId) {
17
+ return !!context?.callerPackageId && (0, system_contracts_1.isSystemContractId)(contractId);
18
+ }
15
19
  const factoriesByUserContext = new WeakMap();
16
20
  let defaultFactory;
17
21
  /**
@@ -66,21 +70,21 @@ function resolveRemoteTargetEndpoint(request, host) {
66
70
  * emitting so the receiving device sees an ordinary call addressed to itself and the
67
71
  * message is signed with this device's own identity. Only tool runs are forwarded; the
68
72
  * 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.
73
+ * reach system contracts on another device (System Packages, App Updates, Device
74
+ * Operations, …): the target would see only this device, acting for the user.
71
75
  *
72
76
  * @param request Authorized route whose `targetDeviceId` and `dataContextId` are set.
73
77
  * @param factory Transport factory; defaults to the process-wide registration.
74
78
  * @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.
79
+ * a package targets a system contract.
76
80
  */
77
81
  function createRemoteContractEndpoint(request, factory = getRemoteContractTransportFactory()) {
78
82
  const { targetDeviceId, dataContextId } = request;
79
83
  if (!targetDeviceId) {
80
84
  throw new Error("Remote contract endpoint requires a target device id");
81
85
  }
82
- if (request.context?.callerPackageId && packageRefusedRemoteContractIds.has(request.contractId)) {
83
- throw new Error(`Permission denied: packages cannot call contract ${request.contractId} on another device`);
86
+ if (refusesForwardedPackageCall(request.context, request.contractId)) {
87
+ throw new Error(`Permission denied: packages cannot call system contract ${request.contractId} on another device`);
84
88
  }
85
89
  if (!dataContextId) {
86
90
  throw new Error("Remote contract endpoint requires an authorized data context id");
@@ -88,7 +92,6 @@ function createRemoteContractEndpoint(request, factory = getRemoteContractTransp
88
92
  if (!factory) {
89
93
  throw new Error(`Cannot reach device ${targetDeviceId}: no remote contract transport is registered on this host`);
90
94
  }
91
- const transport = factory({ deviceId: targetDeviceId, dataContextId });
92
95
  let disposed = false;
93
96
  return {
94
97
  handleCall(call) {
@@ -98,14 +101,13 @@ function createRemoteContractEndpoint(request, factory = getRemoteContractTransp
98
101
  if (call.member !== "tool" || call.operation !== "run") {
99
102
  return Promise.reject(new Error(`Contract ${call.member} '${call.name}'.${call.operation} cannot be forwarded to a remote device`));
100
103
  }
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"));
104
+ if (refusesForwardedPackageCall(request.context, call.contractId)) {
105
+ return Promise.reject(new Error(`Permission denied: packages cannot call system contract ${call.contractId} on another device`));
107
106
  }
108
107
  const { targetDeviceId: _stripped, ...outbound } = call;
108
+ // Endpoints are cached longer than connections. Select the current route for each
109
+ // call, without replaying a call whose previous transport may have executed it.
110
+ const transport = factory({ deviceId: targetDeviceId, dataContextId });
109
111
  return transport.emit(contract_transport_1.CONTRACT_REQUEST_CHANNEL, { ...outbound, dataContextId });
110
112
  },
111
113
  dispose() {
@@ -0,0 +1,191 @@
1
+ import type { IContractConsumerBase } from "../contract-proxy";
2
+ import type { IContractDefinition } from "../types";
3
+ /** Version of the built-in System Host Shell contract. */
4
+ export declare const HOST_SHELL_CONTRACT_VERSION = 1;
5
+ /** What the host behind a System Host Shell provider can do. Unsupported tools throw. */
6
+ export interface IHostShellCapabilities {
7
+ /** The host has a UI window it can inspect, click, type into, scroll, and reload. */
8
+ ui: boolean;
9
+ /** The host can capture its UI window as an image. */
10
+ screenshot: boolean;
11
+ /** The host can quit and restart itself. */
12
+ appLifecycle: boolean;
13
+ /** The host can open web links and local paths (`open`). */
14
+ open: boolean;
15
+ /** The host can open a folder in a code editor (`openInEditor`). */
16
+ editor: boolean;
17
+ /** The host can copy a variable's value to the system clipboard. */
18
+ clipboard: boolean;
19
+ }
20
+ /** Serializable description of one interactive UI element. */
21
+ export interface IUIElement {
22
+ /** Lower-case tag name. */
23
+ tag: string;
24
+ /** Visible text or accessible label. */
25
+ text?: string;
26
+ /** CSS selector that finds this element again (for `clickUI`, `setUIText`, `scrollUI`). */
27
+ selector: string;
28
+ /** Input type, for inputs. */
29
+ type?: string;
30
+ /** Current value, for inputs (never for passwords). */
31
+ value?: string;
32
+ /** Placeholder, for inputs. */
33
+ placeholder?: string;
34
+ /** Target, for links. */
35
+ href?: string;
36
+ /** Whether the element is disabled. */
37
+ disabled?: boolean;
38
+ /** Position and size in CSS pixels, when requested. */
39
+ rect?: {
40
+ x: number;
41
+ y: number;
42
+ width: number;
43
+ height: number;
44
+ };
45
+ }
46
+ /** Snapshot of the UI window: location, visible text, and interactive elements. */
47
+ export interface IUIState {
48
+ /** Page URL. */
49
+ url: string;
50
+ /** Document title. */
51
+ title: string;
52
+ /** Current in-app route, when known. */
53
+ route?: string;
54
+ /** Visible text content. */
55
+ text: string;
56
+ /** Interactive elements. */
57
+ elements: IUIElement[];
58
+ }
59
+ /** Outcome of a UI action (`clickUI`, `setUIText`, `scrollUI`). */
60
+ export interface IUIActionResult {
61
+ /** Whether the action was performed. */
62
+ success: boolean;
63
+ /** Why it was not, e.g. no element matched the selector. */
64
+ error?: string;
65
+ }
66
+ /** Direction for `scrollUI`. */
67
+ export type UIScrollDirection = "up" | "down" | "left" | "right";
68
+ /** Input for `scrollUI`: scroll an element (or the page) by a direction or offsets. */
69
+ export interface IUIScrollInput {
70
+ /** Element to scroll; the page when omitted. */
71
+ selector?: string;
72
+ /** Horizontal offset in pixels. */
73
+ x?: number;
74
+ /** Vertical offset in pixels. */
75
+ y?: number;
76
+ /** Direction to scroll by {@link amount}, instead of offsets. */
77
+ direction?: UIScrollDirection;
78
+ /** Pixels to scroll in {@link direction} (default 300). */
79
+ amount?: number;
80
+ }
81
+ /** Image format for `screenshot`. */
82
+ export type ScreenshotFormat = "png" | "jpeg";
83
+ /** A captured image of the UI window. */
84
+ export interface IHostScreenshot {
85
+ /** `image/png` or `image/jpeg`. */
86
+ mimeType: string;
87
+ /** Width in pixels. */
88
+ width: number;
89
+ /** Height in pixels. */
90
+ height: number;
91
+ /** Base64-encoded image bytes. */
92
+ data: string;
93
+ }
94
+ /** How `open` handled its target. */
95
+ export type HostOpenKind = "browser" | "path";
96
+ /** Typed consumer proxy for System Host Shell v1. */
97
+ export interface IHostShellConsumer extends IContractConsumerBase {
98
+ tools: {
99
+ /** Report what this host supports. */
100
+ getCapabilities(): Promise<{
101
+ capabilities: IHostShellCapabilities;
102
+ }>;
103
+ /** Return the UI window's location, visible text, and interactive elements. */
104
+ inspectUI(input?: {
105
+ includeRects?: boolean;
106
+ }): Promise<IUIState>;
107
+ /** Click the element matching a CSS selector. */
108
+ clickUI(input: {
109
+ selector: string;
110
+ }): Promise<IUIActionResult>;
111
+ /** Type text into the input matching a CSS selector. */
112
+ setUIText(input: {
113
+ selector: string;
114
+ text: string;
115
+ clear?: boolean;
116
+ pressEnter?: boolean;
117
+ }): Promise<IUIActionResult>;
118
+ /** Scroll an element or the page. */
119
+ scrollUI(input: IUIScrollInput): Promise<IUIActionResult>;
120
+ /** Reload the UI window. */
121
+ reloadUI(input?: {
122
+ ignoreCache?: boolean;
123
+ }): Promise<{
124
+ reloaded: boolean;
125
+ }>;
126
+ /** Capture the UI window as an image. */
127
+ screenshot(input?: {
128
+ format?: ScreenshotFormat;
129
+ maxWidth?: number;
130
+ }): Promise<IHostScreenshot>;
131
+ /** Quit the app. */
132
+ quitApp(): Promise<{
133
+ quitting: boolean;
134
+ }>;
135
+ /** Restart the app. */
136
+ restartApp(): Promise<{
137
+ restarting: boolean;
138
+ }>;
139
+ /** Open a web link in the browser, or a local file or folder with the OS. */
140
+ open(input: {
141
+ target: string;
142
+ }): Promise<{
143
+ opened: HostOpenKind;
144
+ }>;
145
+ /** Open a folder in a code editor, falling back to the OS. */
146
+ openInEditor(input: {
147
+ path: string;
148
+ }): Promise<{
149
+ editor: string;
150
+ }>;
151
+ /** Copy a persistent variable's value to the system clipboard. */
152
+ copyVariableToClipboard(input: {
153
+ persistentVarId: string;
154
+ groupId?: string;
155
+ }): Promise<{
156
+ copied: boolean;
157
+ clearsAfterMs?: number;
158
+ }>;
159
+ };
160
+ }
161
+ /** URL schemes `open` hands to the browser or mail client; any other scheme is refused. */
162
+ export declare const HOST_OPEN_URL_SCHEMES: readonly string[];
163
+ /** Where `open` sends a target: a URL for the browser, or a local path for the OS. */
164
+ export type HostOpenTarget = {
165
+ kind: "browser";
166
+ url: string;
167
+ } | {
168
+ kind: "path";
169
+ path: string;
170
+ };
171
+ /**
172
+ * Decide how `open` handles a target. `http`, `https`, and `mailto` links go to the browser;
173
+ * any other URL scheme (`file:`, `javascript:`, custom app schemes, …) is refused; everything
174
+ * else is a local path (`~` is expanded by the host). Windows drive paths are paths.
175
+ *
176
+ * @param target - What the caller asked to open.
177
+ * @throws When the target is empty or uses a scheme that is not allowed.
178
+ */
179
+ export declare function classifyHostOpenTarget(target: string): HostOpenTarget;
180
+ /**
181
+ * Return the System Host Shell contract shape.
182
+ *
183
+ * Tools carry no `remoteAccessLevel`, so only callers this device trusts as `Self` (the
184
+ * user's own devices and users explicitly granted Self) can reach them remotely; group
185
+ * members, Admins included, are refused. Package code and workflows are refused everywhere.
186
+ * `quitApp` and `restartApp` from another device also need the device's "Allow my other
187
+ * devices to update and restart this device" setting. Tools the host cannot perform throw
188
+ * `System Host Shell <tool> is not supported on this host (...)`; `getCapabilities` reports
189
+ * support up front.
190
+ */
191
+ export declare function getHostShellContractDefinition(): IContractDefinition;