@peers-app/peers-sdk 0.22.0 → 0.23.1
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.
- package/dist/context/user-context-singleton.d.ts +42 -0
- package/dist/context/user-context-singleton.js +45 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +51 -1
- package/dist/contracts/__tests__/contract-resolvers.test.js +30 -0
- package/dist/contracts/__tests__/playground.test.js +5 -2
- package/dist/contracts/__tests__/system-contracts.test.js +3 -0
- package/dist/contracts/contract-proxy.d.ts +6 -0
- package/dist/contracts/contract-proxy.js +34 -0
- package/dist/contracts/contract-resolvers.js +9 -1
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +5 -2
- package/dist/contracts/system/index.d.ts +2 -0
- package/dist/contracts/system/index.js +5 -1
- package/dist/contracts/system/tool-access-control-contract.d.ts +35 -0
- package/dist/contracts/system/tool-access-control-contract.js +70 -0
- package/dist/data/access-level.d.ts +72 -0
- package/dist/data/access-level.js +109 -0
- package/dist/data/access-level.test.d.ts +1 -0
- package/dist/data/access-level.test.js +53 -0
- package/dist/data/assistants.d.ts +24 -0
- package/dist/data/assistants.js +21 -0
- package/dist/data/group-member-roles.d.ts +9 -0
- package/dist/data/group-member-roles.js +9 -0
- package/dist/data/group-permissions.d.ts +55 -1
- package/dist/data/group-permissions.js +76 -6
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/orm/sql.data-source.d.ts +10 -0
- package/dist/data/orm/sql.data-source.js +48 -30
- package/dist/data/orm/sql.data-source.test.js +13 -0
- package/dist/data/package-version-resolver.test.js +2 -0
- package/dist/data/tool-access-approvals.d.ts +126 -0
- package/dist/data/tool-access-approvals.js +86 -0
- package/dist/data/tool-access.d.ts +162 -0
- package/dist/data/tool-access.js +235 -0
- package/dist/data/tool-access.test.d.ts +1 -0
- package/dist/data/tool-access.test.js +159 -0
- package/dist/data/tools.d.ts +15 -0
- package/dist/data/tools.js +11 -0
- package/dist/data/workflow-logs.d.ts +5 -5
- package/dist/data/workflow-runs.d.ts +2 -2
- package/dist/device-pairing/types.d.ts +6 -6
- package/dist/system-ids.d.ts +18 -0
- package/dist/system-ids.js +19 -1
- package/package.json +1 -1
|
@@ -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
|
},
|
|
@@ -254,6 +283,7 @@ describe("local user-context contract routing", () => {
|
|
|
254
283
|
inputSchema: { type: "simple", fields: [] },
|
|
255
284
|
outputSchema: { type: "simple", fields: [] },
|
|
256
285
|
code: "",
|
|
286
|
+
accessLevel: 0,
|
|
257
287
|
},
|
|
258
288
|
toolFn: async (variableName, context) => context.getVariable(variableName, "00localcontractsecrettool"),
|
|
259
289
|
},
|
|
@@ -154,7 +154,10 @@ describe("playground", () => {
|
|
|
154
154
|
contractId: CONTRACT_ID,
|
|
155
155
|
version: 1,
|
|
156
156
|
tools: {
|
|
157
|
-
logText: {
|
|
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";
|
|
@@ -275,6 +276,11 @@ export interface IContractCallContext {
|
|
|
275
276
|
callerUserId?: string;
|
|
276
277
|
/** User id of the local device executing the call. */
|
|
277
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;
|
|
278
284
|
}
|
|
279
285
|
/**
|
|
280
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,6 +427,7 @@ function createContractProvider(resolution, opts = {}) {
|
|
|
394
427
|
if (!tool) {
|
|
395
428
|
throw new Error(`Contract ${call.contractId} does not provide tool '${call.name}'`);
|
|
396
429
|
}
|
|
430
|
+
await assertContractToolAccess(tool, call, context);
|
|
397
431
|
return resolution.toolContext
|
|
398
432
|
? tool.toolFn(...call.args, resolution.toolContext)
|
|
399
433
|
: tool.toolFn(...call.args);
|
|
@@ -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
|
|
80
|
+
dataContextId,
|
|
75
81
|
context,
|
|
76
82
|
};
|
|
77
83
|
}
|
|
@@ -114,6 +120,7 @@ function createLocalUserContextContractProviderRouter(transport, opts) {
|
|
|
114
120
|
const context = {
|
|
115
121
|
callerUserId: userContext.userId,
|
|
116
122
|
localUserId: userContext.userId,
|
|
123
|
+
dataContext,
|
|
117
124
|
};
|
|
118
125
|
return {
|
|
119
126
|
dataContextId: dataContext.dataContextId,
|
|
@@ -160,6 +167,7 @@ function createUserContextContractProviderRouter(transport, opts) {
|
|
|
160
167
|
const context = {
|
|
161
168
|
callerUserId: remoteIdentity.userId,
|
|
162
169
|
localUserId: userContext.userId,
|
|
170
|
+
dataContext: userContext.userDataContext,
|
|
163
171
|
};
|
|
164
172
|
return (0, contract_provider_router_1.createContractProviderRouter)(transport, {
|
|
165
173
|
context,
|
|
@@ -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";
|
package/dist/contracts/index.js
CHANGED
|
@@ -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.
|
|
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";
|
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
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 = void 0;
|
|
17
|
+
exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = 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 = void 0;
|
|
18
18
|
/**
|
|
19
19
|
* Built-in system contracts. Importing this barrel runs each module's
|
|
20
20
|
* {@link registerSystemContract} side effect so the catalog is populated.
|
|
@@ -22,6 +22,7 @@ exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsCon
|
|
|
22
22
|
require("./device-operations-contract");
|
|
23
23
|
require("./logs-contract");
|
|
24
24
|
require("./network-contracts");
|
|
25
|
+
require("./tool-access-control-contract");
|
|
25
26
|
var device_operations_contract_1 = require("./device-operations-contract");
|
|
26
27
|
Object.defineProperty(exports, "DEVICE_OPERATIONS_CONTRACT_VERSION", { enumerable: true, get: function () { return device_operations_contract_1.DEVICE_OPERATIONS_CONTRACT_VERSION; } });
|
|
27
28
|
Object.defineProperty(exports, "DEVICE_OPERATIONS_MAX_QUERY_LENGTH", { enumerable: true, get: function () { return device_operations_contract_1.DEVICE_OPERATIONS_MAX_QUERY_LENGTH; } });
|
|
@@ -36,3 +37,6 @@ Object.defineProperty(exports, "getLogsContractDefinition", { enumerable: true,
|
|
|
36
37
|
Object.defineProperty(exports, "LOGS_CONTRACT_VERSION", { enumerable: true, get: function () { return logs_contract_1.LOGS_CONTRACT_VERSION; } });
|
|
37
38
|
Object.defineProperty(exports, "resolveLogsContract", { enumerable: true, get: function () { return logs_contract_1.resolveLogsContract; } });
|
|
38
39
|
__exportStar(require("./network-contracts"), exports);
|
|
40
|
+
var tool_access_control_contract_1 = require("./tool-access-control-contract");
|
|
41
|
+
Object.defineProperty(exports, "getToolAccessControlContractDefinition", { enumerable: true, get: function () { return tool_access_control_contract_1.getToolAccessControlContractDefinition; } });
|
|
42
|
+
Object.defineProperty(exports, "TOOL_ACCESS_CONTROL_CONTRACT_VERSION", { enumerable: true, get: function () { return tool_access_control_contract_1.TOOL_ACCESS_CONTROL_CONTRACT_VERSION; } });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IContractConsumerBase } from "../contract-proxy";
|
|
2
|
+
import type { IContractDefinition } from "../types";
|
|
3
|
+
/** Version of the built-in System Tool Access Control contract. */
|
|
4
|
+
export declare const TOOL_ACCESS_CONTROL_CONTRACT_VERSION = 1;
|
|
5
|
+
/**
|
|
6
|
+
* Local-only host contract for approving or rejecting step-up tool access.
|
|
7
|
+
*
|
|
8
|
+
* Assistants never receive these tools. Remote device connections must not
|
|
9
|
+
* resolve this contract.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getToolAccessControlContractDefinition(): IContractDefinition;
|
|
12
|
+
/** Consumer-side TypeScript shape for System Tool Access Control. */
|
|
13
|
+
export interface IToolAccessControlConsumer extends IContractConsumerBase {
|
|
14
|
+
tools: {
|
|
15
|
+
approve(input: {
|
|
16
|
+
approvalId: string;
|
|
17
|
+
nonce: string;
|
|
18
|
+
scope?: "once" | "thread" | "context";
|
|
19
|
+
}): Promise<{
|
|
20
|
+
result: unknown;
|
|
21
|
+
}>;
|
|
22
|
+
reject(input: {
|
|
23
|
+
approvalId: string;
|
|
24
|
+
nonce: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
result: unknown;
|
|
27
|
+
}>;
|
|
28
|
+
listPending(input?: {
|
|
29
|
+
threadMessageId?: string;
|
|
30
|
+
workflowRunId?: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
approvals: unknown[];
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = void 0;
|
|
4
|
+
exports.getToolAccessControlContractDefinition = getToolAccessControlContractDefinition;
|
|
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 Tool Access Control contract. */
|
|
9
|
+
exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = 1;
|
|
10
|
+
const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
|
|
11
|
+
let definition;
|
|
12
|
+
/**
|
|
13
|
+
* Local-only host contract for approving or rejecting step-up tool access.
|
|
14
|
+
*
|
|
15
|
+
* Assistants never receive these tools. Remote device connections must not
|
|
16
|
+
* resolve this contract.
|
|
17
|
+
*/
|
|
18
|
+
function getToolAccessControlContractDefinition() {
|
|
19
|
+
definition ??= {
|
|
20
|
+
contractId: system_ids_1.toolAccessControlContractId,
|
|
21
|
+
version: exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION,
|
|
22
|
+
devTag: "dev",
|
|
23
|
+
name: "System Tool Access Control",
|
|
24
|
+
description: "Approve or reject privileged tool calls requested by Assistants.",
|
|
25
|
+
tables: [],
|
|
26
|
+
tools: [
|
|
27
|
+
{
|
|
28
|
+
name: "approve",
|
|
29
|
+
usageDescription: "Approve a pending tool-access request and execute the frozen call.",
|
|
30
|
+
inputFields: [
|
|
31
|
+
field("approvalId", field_type_1.FieldType.id, "Pending approval id."),
|
|
32
|
+
field("nonce", field_type_1.FieldType.string, "Single-use nonce from the pending row."),
|
|
33
|
+
field("scope", field_type_1.FieldType.string, "once, thread, or context.", { optional: true }),
|
|
34
|
+
],
|
|
35
|
+
outputFields: [field("result", field_type_1.FieldType.object, "Approval and execution result.")],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "reject",
|
|
39
|
+
usageDescription: "Reject a pending tool-access request.",
|
|
40
|
+
inputFields: [
|
|
41
|
+
field("approvalId", field_type_1.FieldType.id, "Pending approval id."),
|
|
42
|
+
field("nonce", field_type_1.FieldType.string, "Single-use nonce from the pending row."),
|
|
43
|
+
],
|
|
44
|
+
outputFields: [field("result", field_type_1.FieldType.object, "Rejection result.")],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "listPending",
|
|
48
|
+
usageDescription: "List pending tool-access approvals for a thread or workflow.",
|
|
49
|
+
inputFields: [
|
|
50
|
+
field("threadMessageId", field_type_1.FieldType.id, "Thread root or parent user message.", {
|
|
51
|
+
optional: true,
|
|
52
|
+
}),
|
|
53
|
+
field("workflowRunId", field_type_1.FieldType.id, "Workflow run id.", { optional: true }),
|
|
54
|
+
],
|
|
55
|
+
outputFields: [
|
|
56
|
+
field("approvals", field_type_1.FieldType.object, "Pending approval rows.", { isArray: true }),
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
observables: [],
|
|
61
|
+
events: [],
|
|
62
|
+
};
|
|
63
|
+
return definition;
|
|
64
|
+
}
|
|
65
|
+
(0, system_contracts_1.registerSystemContract)({
|
|
66
|
+
providerPackageId: system_ids_1.peersSystemPackageId,
|
|
67
|
+
contractId: system_ids_1.toolAccessControlContractId,
|
|
68
|
+
version: exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION,
|
|
69
|
+
getDefinition: getToolAccessControlContractDefinition,
|
|
70
|
+
});
|