@peers-app/peers-sdk 0.21.7 → 0.23.0

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 (53) hide show
  1. package/dist/context/user-context-singleton.d.ts +42 -0
  2. package/dist/context/user-context-singleton.js +45 -0
  3. package/dist/contracts/__tests__/contract-proxy.test.js +51 -1
  4. package/dist/contracts/__tests__/contract-resolvers.test.js +65 -0
  5. package/dist/contracts/__tests__/playground.test.js +5 -2
  6. package/dist/contracts/__tests__/system-contracts.test.js +3 -0
  7. package/dist/contracts/contract-proxy.d.ts +27 -0
  8. package/dist/contracts/contract-proxy.js +37 -1
  9. package/dist/contracts/contract-resolvers.d.ts +10 -0
  10. package/dist/contracts/contract-resolvers.js +21 -3
  11. package/dist/contracts/index.d.ts +3 -2
  12. package/dist/contracts/index.js +5 -2
  13. package/dist/contracts/system/index.d.ts +2 -0
  14. package/dist/contracts/system/index.js +5 -1
  15. package/dist/contracts/system/tool-access-control-contract.d.ts +35 -0
  16. package/dist/contracts/system/tool-access-control-contract.js +70 -0
  17. package/dist/data/access-level.d.ts +72 -0
  18. package/dist/data/access-level.js +109 -0
  19. package/dist/data/access-level.test.d.ts +1 -0
  20. package/dist/data/access-level.test.js +53 -0
  21. package/dist/data/assistants.d.ts +24 -0
  22. package/dist/data/assistants.js +21 -0
  23. package/dist/data/group-member-roles.d.ts +9 -0
  24. package/dist/data/group-member-roles.js +9 -0
  25. package/dist/data/group-permissions.d.ts +55 -1
  26. package/dist/data/group-permissions.js +76 -6
  27. package/dist/data/index.d.ts +3 -1
  28. package/dist/data/index.js +3 -1
  29. package/dist/data/orm/sql.data-source.d.ts +10 -0
  30. package/dist/data/orm/sql.data-source.js +48 -30
  31. package/dist/data/orm/sql.data-source.test.js +13 -0
  32. package/dist/data/package-version-resolver.test.js +2 -0
  33. package/dist/data/tool-access-approvals.d.ts +126 -0
  34. package/dist/data/tool-access-approvals.js +86 -0
  35. package/dist/data/tool-access.d.ts +162 -0
  36. package/dist/data/tool-access.js +235 -0
  37. package/dist/data/tool-access.test.d.ts +1 -0
  38. package/dist/data/tool-access.test.js +159 -0
  39. package/dist/data/tools.d.ts +15 -0
  40. package/dist/data/tools.js +11 -0
  41. package/dist/data/workflow-logs.d.ts +5 -5
  42. package/dist/data/workflow-runs.d.ts +2 -2
  43. package/dist/device-pairing/types.d.ts +6 -6
  44. package/dist/package-installer/package-creator.d.ts +31 -0
  45. package/dist/package-installer/package-creator.js +140 -40
  46. package/dist/package-installer/package-creator.test.js +113 -11
  47. package/dist/rpc-types.d.ts +0 -17
  48. package/dist/rpc-types.js +0 -15
  49. package/dist/system-ids.d.ts +13 -0
  50. package/dist/system-ids.js +14 -1
  51. package/package.json +1 -1
  52. package/dist/data/voice-messages.d.ts +0 -33
  53. package/dist/data/voice-messages.js +0 -33
@@ -1,12 +1,54 @@
1
1
  import type { Table } from "../data/orm";
2
2
  import type { DataContext } from "./data-context";
3
3
  import { UserContext } from "./user-context";
4
+ /**
5
+ * Return the process user context, waiting until {@link setUserContext} if needed.
6
+ *
7
+ * Prefer an explicit `UserContext` when one is already in hand (multi-device
8
+ * tests and connection providers). Use {@link peekUserContext} to fail closed
9
+ * instead of waiting.
10
+ */
4
11
  export declare function getUserContext(): Promise<UserContext>;
12
+ /**
13
+ * Return the process user context if {@link setUserContext} has already run.
14
+ *
15
+ * Does not wait. Callers that must not hang (role checks, contract tools)
16
+ * should treat `undefined` as fail-closed.
17
+ */
18
+ export declare function peekUserContext(): UserContext | undefined;
19
+ /**
20
+ * Install the process-wide user context. Throws if one is already set, except
21
+ * in a browser/PWA where HMR and navigations may replace it.
22
+ *
23
+ * @param userContext - Context that owns local personal and group data.
24
+ */
5
25
  export declare function setUserContext(userContext: UserContext): void;
26
+ /**
27
+ * Return the current user's default data context after the singleton is ready.
28
+ */
6
29
  export declare function getDefaultDataContext(): Promise<DataContext>;
30
+ /**
31
+ * Return the table container for `dataContext`, or the singleton default.
32
+ *
33
+ * @param dataContext - Context to read tables from. Falls back to the singleton.
34
+ */
7
35
  export declare function getTableContainer(dataContext?: DataContext | undefined): import("../data/orm").TableContainer;
36
+ /**
37
+ * Install a client-proxy user context for renderer/RPC processes.
38
+ *
39
+ * No-ops when a real server-side context is already set in-process.
40
+ *
41
+ * @param userId - Local user id used as the personal data-context id.
42
+ */
8
43
  export declare function setDefaultClientUserContext(userId: string): void;
44
+ /**
45
+ * Return every table currently loaded in the default table container, keyed by
46
+ * full table name.
47
+ */
9
48
  export declare function getAllTables(): {
10
49
  [tableName: string]: Table<any>;
11
50
  };
51
+ /**
52
+ * Return the singleton user's id. Throws if {@link setUserContext} has not run.
53
+ */
12
54
  export declare const myUserId: () => string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.myUserId = void 0;
4
4
  exports.getUserContext = getUserContext;
5
+ exports.peekUserContext = peekUserContext;
5
6
  exports.setUserContext = setUserContext;
6
7
  exports.getDefaultDataContext = getDefaultDataContext;
7
8
  exports.getTableContainer = getTableContainer;
@@ -16,6 +17,13 @@ const user_context_1 = require("./user-context");
16
17
  var userContextInstance;
17
18
  var userContextResolved;
18
19
  var userContextPromise;
20
+ /**
21
+ * Return the process user context, waiting until {@link setUserContext} if needed.
22
+ *
23
+ * Prefer an explicit `UserContext` when one is already in hand (multi-device
24
+ * tests and connection providers). Use {@link peekUserContext} to fail closed
25
+ * instead of waiting.
26
+ */
19
27
  async function getUserContext() {
20
28
  if (userContextInstance) {
21
29
  return userContextInstance.loadingPromise;
@@ -30,6 +38,21 @@ async function getUserContext() {
30
38
  }
31
39
  return userContextPromise;
32
40
  }
41
+ /**
42
+ * Return the process user context if {@link setUserContext} has already run.
43
+ *
44
+ * Does not wait. Callers that must not hang (role checks, contract tools)
45
+ * should treat `undefined` as fail-closed.
46
+ */
47
+ function peekUserContext() {
48
+ return userContextInstance;
49
+ }
50
+ /**
51
+ * Install the process-wide user context. Throws if one is already set, except
52
+ * in a browser/PWA where HMR and navigations may replace it.
53
+ *
54
+ * @param userContext - Context that owns local personal and group data.
55
+ */
33
56
  function setUserContext(userContext) {
34
57
  if (userContextInstance) {
35
58
  if (typeof window !== "undefined") {
@@ -51,10 +74,18 @@ function setUserContext(userContext) {
51
74
  window.userContext = userContext;
52
75
  }
53
76
  }
77
+ /**
78
+ * Return the current user's default data context after the singleton is ready.
79
+ */
54
80
  async function getDefaultDataContext() {
55
81
  const userContext = await getUserContext();
56
82
  return userContext.defaultDataContext();
57
83
  }
84
+ /**
85
+ * Return the table container for `dataContext`, or the singleton default.
86
+ *
87
+ * @param dataContext - Context to read tables from. Falls back to the singleton.
88
+ */
58
89
  function getTableContainer(dataContext) {
59
90
  dataContext ??= userContextInstance?.defaultDataContext();
60
91
  if (!dataContext) {
@@ -62,6 +93,13 @@ function getTableContainer(dataContext) {
62
93
  }
63
94
  return dataContext.tableContainer;
64
95
  }
96
+ /**
97
+ * Install a client-proxy user context for renderer/RPC processes.
98
+ *
99
+ * No-ops when a real server-side context is already set in-process.
100
+ *
101
+ * @param userId - Local user id used as the personal data-context id.
102
+ */
65
103
  function setDefaultClientUserContext(userId) {
66
104
  // If a real (server-side) user context is already set in the same process
67
105
  // (e.g. PWA where peers-device runs in the browser), don't overwrite it
@@ -83,6 +121,10 @@ function setDefaultClientUserContext(userId) {
83
121
  })();
84
122
  setUserContext(userContext);
85
123
  }
124
+ /**
125
+ * Return every table currently loaded in the default table container, keyed by
126
+ * full table name.
127
+ */
86
128
  function getAllTables() {
87
129
  const tables = getTableContainer().getAllTables();
88
130
  const tableMap = {};
@@ -117,6 +159,9 @@ if (!rpc_types_1.isClient) {
117
159
  return table[methodName](...args);
118
160
  };
119
161
  }
162
+ /**
163
+ * Return the singleton user's id. Throws if {@link setUserContext} has not run.
164
+ */
120
165
  const myUserId = () => {
121
166
  if (!userContextInstance) {
122
167
  throw new Error(`myUserId called before userContext singleton was initialized`);
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const groupPermissions = require("../../data/group-permissions");
3
4
  const decorators_1 = require("../../data/orm/decorators");
4
5
  const observable_1 = require("../../observable");
5
6
  const field_type_1 = require("../../types/field-type");
@@ -76,7 +77,7 @@ function makeResolution() {
76
77
  tables: { Widgets: table },
77
78
  tools: {
78
79
  "make-widget": {
79
- tool: { toolId: (0, utils_1.newid)(), name: "make-widget" },
80
+ tool: { toolId: (0, utils_1.newid)(), name: "make-widget", accessLevel: 0 },
80
81
  toolFn: makeWidget,
81
82
  },
82
83
  },
@@ -147,6 +148,55 @@ describe("contract proxy — consumer/provider round-trip", () => {
147
148
  expect(makeWidget).toHaveBeenCalledWith({ color: "blue" });
148
149
  expect(result.color).toBe("blue");
149
150
  });
151
+ it("allows a classified tool when the provider data context grants access", async () => {
152
+ const getUserAccessLevel = jest
153
+ .spyOn(groupPermissions, "getUserAccessLevel")
154
+ .mockResolvedValue(100);
155
+ const contract = makeContract();
156
+ const { resolution, makeWidget } = makeResolution();
157
+ const makeWidgetTool = resolution.tools?.["make-widget"];
158
+ if (!makeWidgetTool) {
159
+ throw new Error("expected make-widget tool");
160
+ }
161
+ makeWidgetTool.tool.accessLevel = 80;
162
+ const provider = (0, contract_proxy_1.createContractProvider)(resolution);
163
+ const dataContext = { userContext: { userId: "u1" } };
164
+ const consumer = consumerVia(contract, provider, {
165
+ callerUserId: "u1",
166
+ localUserId: "u1",
167
+ dataContext,
168
+ });
169
+ await expect(consumer.tools["make-widget"]({ color: "blue" })).resolves.toEqual(expect.objectContaining({ color: "blue" }));
170
+ expect(makeWidget).toHaveBeenCalledWith({ color: "blue" });
171
+ expect(getUserAccessLevel).toHaveBeenCalledWith(dataContext, "u1");
172
+ });
173
+ it("fails closed quickly when a classified tool has no user context", async () => {
174
+ const contract = makeContract();
175
+ const { resolution } = makeResolution();
176
+ const makeWidgetTool = resolution.tools?.["make-widget"];
177
+ if (!makeWidgetTool) {
178
+ throw new Error("expected make-widget tool");
179
+ }
180
+ makeWidgetTool.tool.accessLevel = 20;
181
+ const provider = (0, contract_proxy_1.createContractProvider)(resolution);
182
+ const consumer = consumerVia(contract, provider, {
183
+ callerUserId: (0, utils_1.newid)(),
184
+ localUserId: (0, utils_1.newid)(),
185
+ });
186
+ await expect(consumer.tools["make-widget"]({ color: "blue" })).rejects.toThrow("User context is not available");
187
+ });
188
+ it("denies unclassified tools when the caller identity is missing", async () => {
189
+ const contract = makeContract();
190
+ const { resolution } = makeResolution();
191
+ const makeWidgetTool = resolution.tools?.["make-widget"];
192
+ if (!makeWidgetTool) {
193
+ throw new Error("expected make-widget tool");
194
+ }
195
+ makeWidgetTool.tool.accessLevel = undefined;
196
+ const provider = (0, contract_proxy_1.createContractProvider)(resolution);
197
+ const consumer = consumerVia(contract, provider, {});
198
+ await expect(consumer.tools["make-widget"]({ color: "blue" })).rejects.toThrow("missing caller identity");
199
+ });
150
200
  it("forwards dataContextId on methods, tools, observables, and subscriptions", async () => {
151
201
  const dataContextId = (0, utils_1.newid)();
152
202
  const calls = [];
@@ -5,8 +5,16 @@ const user_trust_levels_1 = require("../../data/user-trust-levels");
5
5
  const users_1 = require("../../data/users");
6
6
  const socket_type_1 = require("../../device/socket.type");
7
7
  const keys_1 = require("../../keys");
8
+ const system_ids_1 = require("../../system-ids");
8
9
  const utils_1 = require("../../utils");
9
10
  const index_1 = require("../index");
11
+ jest.mock("../../data/group-permissions", () => {
12
+ const actual = jest.requireActual("../../data/group-permissions");
13
+ return {
14
+ ...actual,
15
+ getUserRole: jest.fn(async () => actual.GroupMemberRole.Founder),
16
+ };
17
+ });
10
18
  jest.mock("../../data/user-trust-levels", () => ({
11
19
  UserTrustLevels: jest.fn(),
12
20
  }));
@@ -145,6 +153,26 @@ describe("Self-trusted contract caller authorization", () => {
145
153
  })).rejects.toThrow("Permission denied");
146
154
  router.dispose();
147
155
  });
156
+ it("rejects remote Tool Access Control as local-only", async () => {
157
+ const { remoteIdentity, user } = makeIdentity();
158
+ const records = new Map();
159
+ const { personalContext, userContext } = setupUserContext(remoteIdentity.userId, records);
160
+ records.set(personalContext, user);
161
+ mockTrustGet.mockResolvedValue({ trustLevel: socket_type_1.TrustLevel.Self });
162
+ const [consumerTransport, providerTransport] = (0, index_1.inProcessTransportPair)();
163
+ const router = (0, index_1.createUserContextContractProviderRouter)(providerTransport, {
164
+ userContext,
165
+ remoteIdentity,
166
+ });
167
+ await expect(consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, {
168
+ ...call,
169
+ contractId: system_ids_1.toolAccessControlContractId,
170
+ member: "tool",
171
+ name: "approve",
172
+ operation: "run",
173
+ })).rejects.toThrow("local-only");
174
+ router.dispose();
175
+ });
148
176
  });
149
177
  describe("local user-context contract routing", () => {
150
178
  function setupLocalRouter() {
@@ -180,6 +208,7 @@ describe("local user-context contract routing", () => {
180
208
  outputSchema: { type: "simple", fields: [] },
181
209
  code: "",
182
210
  packageId: (0, utils_1.newid)(),
211
+ accessLevel: 0,
183
212
  },
184
213
  toolFn: () => dataContext.dataContextId,
185
214
  },
@@ -239,4 +268,40 @@ describe("local user-context contract routing", () => {
239
268
  await expect(setup.consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, makeToolCall(setup.contractId))).rejects.toThrow("contract");
240
269
  router.dispose();
241
270
  });
271
+ it("appends host-only variable access to local package tool calls", async () => {
272
+ const setup = setupLocalRouter();
273
+ const resolveVariable = jest.fn(async ({ dataContext, variableName, requestingToolId }) => [dataContext.dataContextId, variableName, requestingToolId].join(":"));
274
+ (0, index_1.registerContractResolver)(setup.contractId, 1, () => ({
275
+ contractId: setup.contractId,
276
+ version: 1,
277
+ tools: {
278
+ inspect: {
279
+ tool: {
280
+ toolId: "00localcontractsecrettool",
281
+ name: "inspect",
282
+ usageDescription: "Resolve a local variable.",
283
+ inputSchema: { type: "simple", fields: [] },
284
+ outputSchema: { type: "simple", fields: [] },
285
+ code: "",
286
+ accessLevel: 0,
287
+ },
288
+ toolFn: async (variableName, context) => context.getVariable(variableName, "00localcontractsecrettool"),
289
+ },
290
+ },
291
+ }));
292
+ const router = (0, index_1.createLocalUserContextContractProviderRouter)(setup.providerTransport, {
293
+ getUserContext: async () => setup.userContext,
294
+ resolveVariable,
295
+ });
296
+ await expect(setup.consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, {
297
+ ...makeToolCall(setup.contractId),
298
+ args: ["OPENAI_API_KEY"],
299
+ })).resolves.toBe(`${setup.userId}:OPENAI_API_KEY:00localcontractsecrettool`);
300
+ expect(resolveVariable).toHaveBeenCalledWith(expect.objectContaining({
301
+ dataContext: expect.objectContaining({ dataContextId: setup.userId }),
302
+ variableName: "OPENAI_API_KEY",
303
+ requestingToolId: "00localcontractsecrettool",
304
+ }));
305
+ router.dispose();
306
+ });
242
307
  });
@@ -154,7 +154,10 @@ describe("playground", () => {
154
154
  contractId: CONTRACT_ID,
155
155
  version: 1,
156
156
  tools: {
157
- logText: { tool: { toolId: "", name: "logText" }, toolFn: logTextFn },
157
+ logText: {
158
+ tool: { toolId: "", name: "logText", accessLevel: 0 },
159
+ toolFn: logTextFn,
160
+ },
158
161
  },
159
162
  observables: { logCount: logCountObs },
160
163
  observableWritability: { logCount: false },
@@ -208,6 +211,6 @@ describe("playground", () => {
208
211
  expect(logs).toEqual(["log1", "log2"]);
209
212
  expect(observedLogCount).toBe(2);
210
213
  // TODO test tables so we can see what this looks like and how it works for both providers and consumers
211
- // TODO consider renaming ContractResolution to ContractProviderInstance or something like that. ContractResolution is awkward.
214
+ // TODO consider renaming ContractResolution to ContractProviderInstance or something like that. ContractResolution is awkward.
212
215
  });
213
216
  });
@@ -17,6 +17,7 @@ describe("system contract catalog", () => {
17
17
  system_ids_1.deviceOperationsContractId,
18
18
  system_ids_1.networkDiagnosticsContractId,
19
19
  system_ids_1.networkControlContractId,
20
+ system_ids_1.toolAccessControlContractId,
20
21
  ]));
21
22
  });
22
23
  it("registers the Logs live resolver", async () => {
@@ -34,6 +35,8 @@ describe("system contract catalog", () => {
34
35
  expect(registry.getProviderPackageId(system_ids_1.networkDiagnosticsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
35
36
  expect(registry.resolve(system_ids_1.networkControlContractId, 1)?.name).toBe("System Network Control");
36
37
  expect(registry.getProviderPackageId(system_ids_1.networkControlContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
38
+ expect(registry.resolve(system_ids_1.toolAccessControlContractId, 1)?.name).toBe("System Tool Access Control");
39
+ expect(registry.getProviderPackageId(system_ids_1.toolAccessControlContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
37
40
  });
38
41
  it("registerSystemContract makes new contracts installable without loader changes", () => {
39
42
  const fakeContractId = (0, utils_1.newid)();
@@ -1,3 +1,4 @@
1
+ import type { DataContext } from "../context/data-context";
1
2
  import type { Table } from "../data/orm/table";
2
3
  import type { IToolInstance } from "../data/tools";
3
4
  import type { Event, ISubscriptionResult } from "../events";
@@ -195,6 +196,22 @@ export interface IContractConsumer extends IContractConsumerBase {
195
196
  subscribe: (handler: (value: any) => void) => ISubscriptionResult;
196
197
  }>;
197
198
  }
199
+ /**
200
+ * Trusted host capabilities appended to local package-contract tool calls.
201
+ *
202
+ * The context is constructed by the provider host after it authorizes the data-context
203
+ * route. It is never serialized to the consumer and is intentionally absent for remote
204
+ * contract calls unless a host explicitly opts in.
205
+ */
206
+ export interface IContractToolContext {
207
+ /** Authorized data context in which the tool is executing. */
208
+ dataContextId: string;
209
+ /**
210
+ * Resolve a persistent variable in the authorized context. Secret values are decrypted
211
+ * by the host and returned only to the provider-side tool implementation.
212
+ */
213
+ getVariable(variableName: string, requestingToolId: string): Promise<string>;
214
+ }
198
215
  /**
199
216
  * Build the **consumer** side of a contract proxy from a contract definition and a
200
217
  * transport. Member names come from the contract shape; table members are backed by a
@@ -234,6 +251,11 @@ export interface IContractResolution {
234
251
  version: number;
235
252
  tables?: Record<string, Table<any>>;
236
253
  tools?: Record<string, IToolInstance>;
254
+ /**
255
+ * Host-only capabilities appended as the final argument to provider tool functions.
256
+ * Omit this for remote or otherwise untrusted provider routes.
257
+ */
258
+ toolContext?: IContractToolContext;
237
259
  observables?: Record<string, Observable<any>>;
238
260
  /**
239
261
  * Provider-owned write policy for live observables. A `set` is allowed only when the
@@ -254,6 +276,11 @@ export interface IContractCallContext {
254
276
  callerUserId?: string;
255
277
  /** User id of the local device executing the call. */
256
278
  localUserId?: string;
279
+ /**
280
+ * Provider-side data context used to resolve the caller's group/personal role.
281
+ * In-process only — never serialized across a connection.
282
+ */
283
+ dataContext?: DataContext;
257
284
  }
258
285
  /**
259
286
  * A gate that decides whether a given call is allowed. Runs before dispatch on the
@@ -5,6 +5,8 @@ exports.createContractConsumer = createContractConsumer;
5
5
  exports.createContractProvider = createContractProvider;
6
6
  exports.createContractProviderEndpoint = createContractProviderEndpoint;
7
7
  exports.createContractProviderSession = createContractProviderSession;
8
+ const access_level_1 = require("../data/access-level");
9
+ const group_permissions_1 = require("../data/group-permissions");
8
10
  const decorators_1 = require("../data/orm/decorators");
9
11
  const observable_1 = require("../observable");
10
12
  const utils_1 = require("../utils");
@@ -340,6 +342,37 @@ const sameUserContractPermissionCheck = (_call, context) => {
340
342
  return Boolean(context.localUserId) && context.callerUserId === context.localUserId;
341
343
  };
342
344
  exports.sameUserContractPermissionCheck = sameUserContractPermissionCheck;
345
+ /**
346
+ * Deny a contract tool when the caller’s group/personal access is below the tool.
347
+ *
348
+ * Tools at {@link AccessLevel.None} are unrestricted and do not need a caller
349
+ * identity. Higher levels fail closed if the transport omitted `callerUserId`
350
+ * and `localUserId`. Role lookup uses {@link IContractCallContext.dataContext}
351
+ * when the host attached one, so multi-device processes do not wait on the
352
+ * user-context singleton.
353
+ *
354
+ * Remote Self-trust is a separate, stricter gate applied by the connection router.
355
+ *
356
+ * @param tool - Live tool instance being dispatched.
357
+ * @param call - Inbound contract call (provides dataContextId).
358
+ * @param context - Caller identity from the transport.
359
+ */
360
+ async function assertContractToolAccess(tool, call, context) {
361
+ const required = (0, access_level_1.resolveAccessLevel)(tool.tool.accessLevel);
362
+ if (required <= access_level_1.AccessLevel.None) {
363
+ return;
364
+ }
365
+ const userId = context.callerUserId ?? context.localUserId;
366
+ if (!userId) {
367
+ throw new Error(`Permission denied: missing caller identity for tool '${tool.tool.name}'`);
368
+ }
369
+ const userLevel = context.dataContext
370
+ ? await (0, group_permissions_1.getUserAccessLevel)(context.dataContext, userId)
371
+ : (0, access_level_1.accessLevelFromGroupRole)(await (0, group_permissions_1.getUserRole)(call.dataContextId || context.localUserId || userId, userId));
372
+ if (userLevel < required) {
373
+ throw new Error(`Permission denied: ${tool.tool.name} requires ${(0, access_level_1.getAccessLevelLabel)(required)} (${required})`);
374
+ }
375
+ }
343
376
  /**
344
377
  * Build the **provider** side of a contract proxy: an async handler that permission-checks
345
378
  * an {@link IContractCall} and dispatches it to the live table method / tool fn / observable
@@ -394,7 +427,10 @@ function createContractProvider(resolution, opts = {}) {
394
427
  if (!tool) {
395
428
  throw new Error(`Contract ${call.contractId} does not provide tool '${call.name}'`);
396
429
  }
397
- return tool.toolFn(...call.args);
430
+ await assertContractToolAccess(tool, call, context);
431
+ return resolution.toolContext
432
+ ? tool.toolFn(...call.args, resolution.toolContext)
433
+ : tool.toolFn(...call.args);
398
434
  }
399
435
  case "observable": {
400
436
  const obs = resolution.observables?.[call.name];
@@ -12,6 +12,14 @@ import type { IContractTransport } from "./contract-transport";
12
12
  export type ContractResolver = (ctx: {
13
13
  dataContext: DataContext;
14
14
  }) => IContractResolution | Promise<IContractResolution>;
15
+ /** Input supplied to a trusted local host when a package tool resolves a pvar. */
16
+ export interface ILocalContractVariableRequest {
17
+ dataContext: DataContext;
18
+ variableName: string;
19
+ requestingToolId: string;
20
+ }
21
+ /** Host callback that resolves plaintext only inside a local provider process. */
22
+ export type LocalContractVariableResolver = (request: ILocalContractVariableRequest) => Promise<string>;
15
23
  /** Connection-authenticated identity fields used to authorize remote contract calls. */
16
24
  export type ContractRemoteIdentity = Pick<IDeviceInfo, "deviceId" | "userId" | "publicKey" | "publicBoxKey">;
17
25
  /**
@@ -45,10 +53,12 @@ export declare function isSelfTrustedContractCaller(userContext: UserContext, re
45
53
  * @param transport Contract transport backed by an authenticated local session.
46
54
  * @param opts.getUserContext Lazy accessor for the host's loaded user context.
47
55
  * @param opts.allowedContractIds Optional contract allowlist for this session.
56
+ * @param opts.resolveVariable Optional host-only pvar resolver exposed to local package tools.
48
57
  */
49
58
  export declare function createLocalUserContextContractProviderRouter(transport: IContractTransport, opts: {
50
59
  getUserContext: () => Promise<UserContext>;
51
60
  allowedContractIds?: ReadonlySet<string>;
61
+ resolveVariable?: LocalContractVariableResolver;
52
62
  }): IContractProviderRouter;
53
63
  /**
54
64
  * Create the local-object provider router for one verified device connection.
@@ -10,6 +10,7 @@ const user_permissions_1 = require("../data/user-permissions");
10
10
  const user_trust_levels_1 = require("../data/user-trust-levels");
11
11
  const users_1 = require("../data/users");
12
12
  const socket_type_1 = require("../device/socket.type");
13
+ const system_ids_1 = require("../system-ids");
13
14
  const contract_provider_router_1 = require("./contract-provider-router");
14
15
  const contract_proxy_1 = require("./contract-proxy");
15
16
  const types_1 = require("./types");
@@ -63,15 +64,20 @@ async function isSelfTrustedContractCaller(userContext, remoteIdentity) {
63
64
  return trustLevel === socket_type_1.TrustLevel.Self;
64
65
  }
65
66
  async function authorizeUserContextCall(call, userContext, remoteIdentity) {
67
+ if (call.contractId === system_ids_1.toolAccessControlContractId) {
68
+ throw new Error("Permission denied: tool access control is local-only");
69
+ }
70
+ const dataContextId = call.dataContextId || userContext.userId;
66
71
  const context = {
67
72
  callerUserId: remoteIdentity.userId,
68
73
  localUserId: userContext.userId,
74
+ dataContext: userContext.getDataContext(dataContextId),
69
75
  };
70
76
  if (!(await isSelfTrustedContractCaller(userContext, remoteIdentity))) {
71
77
  throw new Error(`Permission denied: ${call.member} '${call.name}'.${call.operation} on contract ${call.contractId}`);
72
78
  }
73
79
  return {
74
- dataContextId: call.dataContextId || userContext.userId,
80
+ dataContextId,
75
81
  context,
76
82
  };
77
83
  }
@@ -100,9 +106,10 @@ function getAuthorizedLocalDataContext(userContext, requestedId) {
100
106
  * @param transport Contract transport backed by an authenticated local session.
101
107
  * @param opts.getUserContext Lazy accessor for the host's loaded user context.
102
108
  * @param opts.allowedContractIds Optional contract allowlist for this session.
109
+ * @param opts.resolveVariable Optional host-only pvar resolver exposed to local package tools.
103
110
  */
104
111
  function createLocalUserContextContractProviderRouter(transport, opts) {
105
- const { getUserContext, allowedContractIds } = opts;
112
+ const { getUserContext, allowedContractIds, resolveVariable } = opts;
106
113
  return (0, contract_provider_router_1.createContractProviderRouter)(transport, {
107
114
  authorizeCall: async (call) => {
108
115
  if (allowedContractIds && !allowedContractIds.has(call.contractId)) {
@@ -113,6 +120,7 @@ function createLocalUserContextContractProviderRouter(transport, opts) {
113
120
  const context = {
114
121
  callerUserId: userContext.userId,
115
122
  localUserId: userContext.userId,
123
+ dataContext,
116
124
  };
117
125
  return {
118
126
  dataContextId: dataContext.dataContextId,
@@ -127,7 +135,16 @@ function createLocalUserContextContractProviderRouter(transport, opts) {
127
135
  }
128
136
  const dataContext = getAuthorizedLocalDataContext(userContext, dataContextId);
129
137
  const resolution = await resolver({ dataContext });
130
- return (0, contract_proxy_1.createContractProviderEndpoint)(resolution, { context, notify });
138
+ const localResolution = resolveVariable
139
+ ? {
140
+ ...resolution,
141
+ toolContext: {
142
+ dataContextId: dataContext.dataContextId,
143
+ getVariable: (variableName, requestingToolId) => resolveVariable({ dataContext, variableName, requestingToolId }),
144
+ },
145
+ }
146
+ : resolution;
147
+ return (0, contract_proxy_1.createContractProviderEndpoint)(localResolution, { context, notify });
131
148
  },
132
149
  });
133
150
  }
@@ -150,6 +167,7 @@ function createUserContextContractProviderRouter(transport, opts) {
150
167
  const context = {
151
168
  callerUserId: remoteIdentity.userId,
152
169
  localUserId: userContext.userId,
170
+ dataContext: userContext.userDataContext,
153
171
  };
154
172
  return (0, contract_provider_router_1.createContractProviderRouter)(transport, {
155
173
  context,
@@ -1,8 +1,8 @@
1
1
  export { ContractBuilder, definePackage, PackageBuilder } from "./builder";
2
2
  export { type ContractCallAuthorizer, type ContractProviderEndpointResolver, createContractProviderRouter, type IAuthorizedContractRoute, type IContractProviderEndpointRequest, type IContractProviderRouter, type IContractProviderRouterOptions, } from "./contract-provider-router";
3
3
  export { ContractProviders, ContractProvidersTable, contractProvidersMetaData, contractProvidersSchema, type IContractProviderRecord, } from "./contract-providers.table";
4
- export { CONTRACT_CALL_RPC, type ContractMemberType, type ContractNotificationSink, type ContractPermissionCheck, createContractConsumer, createContractProvider, createContractProviderEndpoint, createContractProviderSession, type IContractCall, type IContractCallContext, type IContractConsumer, type IContractConsumerBase, type IContractNotification, type IContractProviderEndpoint, type IContractProviderEndpointOptions, type IContractProviderSession, type IContractProviderSessionOptions, type IContractResolution, type IContractTableProxy, type ITableProxy, sameUserContractPermissionCheck, } from "./contract-proxy";
5
- export { type ContractRemoteIdentity, type ContractResolver, createLocalUserContextContractProviderRouter, createUserContextContractProviderRouter, getContractResolver, handleContractCall, isSelfTrustedContractCaller, registerContractResolver, } from "./contract-resolvers";
4
+ export { CONTRACT_CALL_RPC, type ContractMemberType, type ContractNotificationSink, type ContractPermissionCheck, createContractConsumer, createContractProvider, createContractProviderEndpoint, createContractProviderSession, type IContractCall, type IContractCallContext, type IContractConsumer, type IContractConsumerBase, type IContractNotification, type IContractProviderEndpoint, type IContractProviderEndpointOptions, type IContractProviderSession, type IContractProviderSessionOptions, type IContractResolution, type IContractTableProxy, type IContractToolContext, type ITableProxy, sameUserContractPermissionCheck, } from "./contract-proxy";
5
+ export { type ContractRemoteIdentity, type ContractResolver, createLocalUserContextContractProviderRouter, createUserContextContractProviderRouter, getContractResolver, handleContractCall, type ILocalContractVariableRequest, isSelfTrustedContractCaller, type LocalContractVariableResolver, registerContractResolver, } from "./contract-resolvers";
6
6
  export { CONTRACT_NOTIFY_CHANNEL, CONTRACT_REQUEST_CHANNEL, connectionContractTransport, type IContractTransport, type IDuplexEmitter, inProcessTransportPair, } from "./contract-transport";
7
7
  export { Contracts, ContractsTable, contractsMetaData, contractsSchema, type IContractRecord, } from "./contracts.table";
8
8
  export { createLocalContractConsumer } from "./create-local-consumer";
@@ -13,6 +13,7 @@ export { ContractRegistry } from "./registry";
13
13
  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";
14
14
  export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./system/logs-contract";
15
15
  export * from "./system/network-contracts";
16
+ export { getToolAccessControlContractDefinition, type IToolAccessControlConsumer, TOOL_ACCESS_CONTROL_CONTRACT_VERSION, } from "./system/tool-access-control-contract";
16
17
  export { type ISystemContractRegistration, installSystemContracts, listSystemContracts, registerSystemContract, } from "./system-contracts";
17
18
  export type { IAlsoImplementsDeclaration, IConsumedContract, IConsumedContractHandle, IConsumedContractOptions, IContractDefinition, IContractEvent, IContractObservable, IContractTable, IContractTool, IPackageDefinitionResult, IResolvedContract, IValidationError, IValidationResult, } from "./types";
18
19
  export { contractKey } from "./types";
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.installSystemContracts = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.getDeviceOperationsContractDefinition = exports.deviceOperationsToolIds = exports.deviceOperationsProviderPackageId = exports.DEVICE_OPERATIONS_MAX_TOOLS = exports.DEVICE_OPERATIONS_MAX_ROWS = exports.DEVICE_OPERATIONS_MAX_RESULT_BYTES = exports.DEVICE_OPERATIONS_MAX_QUERY_LENGTH = exports.DEVICE_OPERATIONS_CONTRACT_VERSION = exports.ContractRegistry = exports.PersistentContractRegistry = exports.waitForHostContractTransport = exports.subscribeHostContractTransport = exports.registerHostContractTransport = exports.getHostContractTransport = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.createLocalContractConsumer = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.inProcessTransportPair = exports.connectionContractTransport = exports.CONTRACT_REQUEST_CHANNEL = exports.CONTRACT_NOTIFY_CHANNEL = exports.registerContractResolver = exports.isSelfTrustedContractCaller = exports.handleContractCall = exports.getContractResolver = exports.createUserContextContractProviderRouter = exports.createLocalUserContextContractProviderRouter = exports.sameUserContractPermissionCheck = exports.createContractProviderSession = exports.createContractProviderEndpoint = exports.createContractProvider = exports.createContractConsumer = exports.CONTRACT_CALL_RPC = exports.contractProvidersSchema = exports.contractProvidersMetaData = exports.ContractProvidersTable = exports.ContractProviders = exports.createContractProviderRouter = exports.PackageBuilder = exports.definePackage = exports.ContractBuilder = void 0;
18
- exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.registerSystemContract = exports.listSystemContracts = void 0;
17
+ exports.getToolAccessControlContractDefinition = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.getDeviceOperationsContractDefinition = exports.deviceOperationsToolIds = exports.deviceOperationsProviderPackageId = exports.DEVICE_OPERATIONS_MAX_TOOLS = exports.DEVICE_OPERATIONS_MAX_ROWS = exports.DEVICE_OPERATIONS_MAX_RESULT_BYTES = exports.DEVICE_OPERATIONS_MAX_QUERY_LENGTH = exports.DEVICE_OPERATIONS_CONTRACT_VERSION = exports.ContractRegistry = exports.PersistentContractRegistry = exports.waitForHostContractTransport = exports.subscribeHostContractTransport = exports.registerHostContractTransport = exports.getHostContractTransport = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.createLocalContractConsumer = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.inProcessTransportPair = exports.connectionContractTransport = exports.CONTRACT_REQUEST_CHANNEL = exports.CONTRACT_NOTIFY_CHANNEL = exports.registerContractResolver = exports.isSelfTrustedContractCaller = exports.handleContractCall = exports.getContractResolver = exports.createUserContextContractProviderRouter = exports.createLocalUserContextContractProviderRouter = exports.sameUserContractPermissionCheck = exports.createContractProviderSession = exports.createContractProviderEndpoint = exports.createContractProvider = exports.createContractConsumer = exports.CONTRACT_CALL_RPC = exports.contractProvidersSchema = exports.contractProvidersMetaData = exports.ContractProvidersTable = exports.ContractProviders = exports.createContractProviderRouter = exports.PackageBuilder = exports.definePackage = exports.ContractBuilder = void 0;
18
+ exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.registerSystemContract = exports.listSystemContracts = exports.installSystemContracts = exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = void 0;
19
19
  var builder_1 = require("./builder");
20
20
  Object.defineProperty(exports, "ContractBuilder", { enumerable: true, get: function () { return builder_1.ContractBuilder; } });
21
21
  Object.defineProperty(exports, "definePackage", { enumerable: true, get: function () { return builder_1.definePackage; } });
@@ -80,6 +80,9 @@ Object.defineProperty(exports, "getLogsContractDefinition", { enumerable: true,
80
80
  Object.defineProperty(exports, "LOGS_CONTRACT_VERSION", { enumerable: true, get: function () { return logs_contract_1.LOGS_CONTRACT_VERSION; } });
81
81
  Object.defineProperty(exports, "resolveLogsContract", { enumerable: true, get: function () { return logs_contract_1.resolveLogsContract; } });
82
82
  __exportStar(require("./system/network-contracts"), exports);
83
+ var tool_access_control_contract_1 = require("./system/tool-access-control-contract");
84
+ Object.defineProperty(exports, "getToolAccessControlContractDefinition", { enumerable: true, get: function () { return tool_access_control_contract_1.getToolAccessControlContractDefinition; } });
85
+ Object.defineProperty(exports, "TOOL_ACCESS_CONTROL_CONTRACT_VERSION", { enumerable: true, get: function () { return tool_access_control_contract_1.TOOL_ACCESS_CONTROL_CONTRACT_VERSION; } });
83
86
  var system_contracts_1 = require("./system-contracts");
84
87
  Object.defineProperty(exports, "installSystemContracts", { enumerable: true, get: function () { return system_contracts_1.installSystemContracts; } });
85
88
  Object.defineProperty(exports, "listSystemContracts", { enumerable: true, get: function () { return system_contracts_1.listSystemContracts; } });
@@ -5,6 +5,8 @@
5
5
  import "./device-operations-contract";
6
6
  import "./logs-contract";
7
7
  import "./network-contracts";
8
+ import "./tool-access-control-contract";
8
9
  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";
9
10
  export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./logs-contract";
10
11
  export * from "./network-contracts";
12
+ export { getToolAccessControlContractDefinition, type IToolAccessControlConsumer, TOOL_ACCESS_CONTROL_CONTRACT_VERSION, } from "./tool-access-control-contract";