@peers-app/peers-sdk 0.26.6 → 0.27.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/README.md +3 -1
- package/bin/peers-package.js +13 -0
- package/define/package.json +4 -0
- package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
- package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
- package/dist/contracts/__tests__/also-implements.test.js +110 -0
- package/dist/contracts/__tests__/builder.test.js +45 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
- package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
- package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
- package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
- package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
- package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
- package/dist/contracts/__tests__/registry.test.js +68 -0
- package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
- package/dist/contracts/__tests__/target-dispatch.js +24 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
- package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
- package/dist/contracts/__tests__/validate.test.js +177 -7
- package/dist/contracts/__tests__/workflow-context.test.js +0 -11
- package/dist/contracts/builder.d.ts +16 -2
- package/dist/contracts/builder.js +44 -3
- package/dist/contracts/contract-definition-schema.d.ts +251 -0
- package/dist/contracts/contract-definition-schema.js +48 -0
- package/dist/contracts/contract-provider-router.d.ts +2 -0
- package/dist/contracts/contract-provider-router.js +5 -2
- package/dist/contracts/contract-proxy.d.ts +52 -18
- package/dist/contracts/contract-proxy.js +144 -203
- package/dist/contracts/contract-resolvers.d.ts +6 -4
- package/dist/contracts/contract-resolvers.js +32 -9
- package/dist/contracts/frozen-contracts.d.ts +59 -0
- package/dist/contracts/frozen-contracts.js +86 -0
- package/dist/contracts/host-contract-client.d.ts +134 -0
- package/dist/contracts/host-contract-client.js +396 -0
- package/dist/contracts/host-contract-transport.d.ts +2 -0
- package/dist/contracts/host-contract-transport.js +8 -0
- package/dist/contracts/index.d.ts +6 -4
- package/dist/contracts/index.js +11 -5
- package/dist/contracts/registry.d.ts +31 -1
- package/dist/contracts/registry.js +100 -6
- package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
- package/dist/contracts/system/contract-catalog-contract.js +17 -5
- package/dist/contracts/tool-catalog-service.js +26 -6
- package/dist/contracts/tool-catalog.d.ts +1 -1
- package/dist/contracts/tool-catalog.js +4 -1
- package/dist/contracts/types.d.ts +32 -2
- package/dist/contracts/validate.d.ts +16 -1
- package/dist/contracts/validate.js +139 -25
- package/dist/data/change-tracking.d.ts +17 -5
- package/dist/data/change-tracking.js +22 -6
- package/dist/data/change-tracking.test.d.ts +1 -0
- package/dist/data/change-tracking.test.js +62 -0
- package/dist/data/create-group.js +1 -1
- package/dist/data/group-members.d.ts +14 -0
- package/dist/data/group-members.js +36 -0
- package/dist/data/group-members.test.js +49 -0
- package/dist/data/orm/sql.data-source.d.ts +9 -0
- package/dist/data/orm/sql.data-source.js +47 -2
- package/dist/data/orm/sql.data-source.test.js +142 -0
- package/dist/data/orm/table-container.js +9 -9
- package/dist/data/orm/table-container.test.js +44 -0
- package/dist/data/orm/types.d.ts +41 -1
- package/dist/data/orm/types.js +159 -28
- package/dist/data/orm/types.test.js +129 -0
- package/dist/data/peer-types.d.ts +8 -62
- package/dist/data/persistent-vars.d.ts +4 -1
- package/dist/data/persistent-vars.js +7 -2
- package/dist/data/tools.d.ts +29 -221
- package/dist/define/also-implements.test.d.ts +1 -0
- package/dist/define/also-implements.test.js +123 -0
- package/dist/define/context.d.ts +77 -0
- package/dist/define/context.js +2 -0
- package/dist/define/contract-ref.d.ts +177 -0
- package/dist/define/contract-ref.js +67 -0
- package/dist/define/declare-contract.d.ts +167 -0
- package/dist/define/declare-contract.js +220 -0
- package/dist/define/declare-contract.test.d.ts +1 -0
- package/dist/define/declare-contract.test.js +573 -0
- package/dist/define/define-package.d.ts +203 -0
- package/dist/define/define-package.js +289 -0
- package/dist/define/define.test.d.ts +1 -0
- package/dist/define/define.test.js +1010 -0
- package/dist/define/empty-fields.d.ts +15 -0
- package/dist/define/empty-fields.js +134 -0
- package/dist/define/empty-fields.test.d.ts +1 -0
- package/dist/define/empty-fields.test.js +55 -0
- package/dist/define/events-observables.test.d.ts +1 -0
- package/dist/define/events-observables.test.js +1013 -0
- package/dist/define/events.d.ts +81 -0
- package/dist/define/events.js +81 -0
- package/dist/define/generated-field.d.ts +20 -0
- package/dist/define/generated-field.js +25 -0
- package/dist/define/index.d.ts +28 -0
- package/dist/define/index.js +62 -0
- package/dist/define/lazy-members.d.ts +8 -0
- package/dist/define/lazy-members.js +38 -0
- package/dist/define/manifest.d.ts +63 -0
- package/dist/define/manifest.js +653 -0
- package/dist/define/observables.d.ts +146 -0
- package/dist/define/observables.js +187 -0
- package/dist/define/serve-members.d.ts +21 -0
- package/dist/define/serve-members.js +152 -0
- package/dist/define/serve-members.test.d.ts +1 -0
- package/dist/define/serve-members.test.js +630 -0
- package/dist/define/serve.d.ts +22 -0
- package/dist/define/serve.js +58 -0
- package/dist/define/system-contracts.d.ts +41 -0
- package/dist/define/system-contracts.js +21 -0
- package/dist/define/table-handle.d.ts +13 -0
- package/dist/define/table-handle.js +36 -0
- package/dist/define/table.d.ts +128 -0
- package/dist/define/table.js +122 -0
- package/dist/define/tool.d.ts +129 -0
- package/dist/define/tool.js +91 -0
- package/dist/define/vars.d.ts +82 -0
- package/dist/define/vars.js +256 -0
- package/dist/define/vars.test.d.ts +1 -0
- package/dist/define/vars.test.js +39 -0
- package/dist/define/wire-value.d.ts +12 -0
- package/dist/define/wire-value.js +76 -0
- package/dist/define/worker-state.d.ts +15 -0
- package/dist/define/worker-state.js +45 -0
- package/dist/device/tx-encoding.test.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/invites/group-approval.js +4 -2
- package/dist/invites/group-approval.test.js +2 -0
- package/dist/observable.d.ts +42 -2
- package/dist/observable.js +25 -7
- package/dist/package-build/build-package.d.ts +60 -0
- package/dist/package-build/build-package.js +312 -0
- package/dist/package-build/build-package.test.d.ts +1 -0
- package/dist/package-build/build-package.test.js +204 -0
- package/dist/package-build/cli.d.ts +7 -0
- package/dist/package-build/cli.js +218 -0
- package/dist/package-build/contract-add.d.ts +35 -0
- package/dist/package-build/contract-add.js +97 -0
- package/dist/package-build/contract-add.test.d.ts +1 -0
- package/dist/package-build/contract-add.test.js +467 -0
- package/dist/package-build/contract-source.d.ts +59 -0
- package/dist/package-build/contract-source.js +471 -0
- package/dist/package-build/freeze-contracts.d.ts +56 -0
- package/dist/package-build/freeze-contracts.js +198 -0
- package/dist/package-build/freeze-contracts.test.d.ts +1 -0
- package/dist/package-build/freeze-contracts.test.js +355 -0
- package/dist/package-installer/package-creator.d.ts +4 -0
- package/dist/package-installer/package-creator.js +57 -16
- package/dist/package-installer/package-creator.test.js +86 -0
- package/dist/package-loader/contract-members.d.ts +53 -0
- package/dist/package-loader/contract-members.js +31 -0
- package/dist/package-loader/contract-package-loader.js +5 -1
- package/dist/package-loader/contract-package-loader.test.js +11 -0
- package/dist/package-loader/host-authority-loading.test.js +9 -5
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-artifact.d.ts +80 -17
- package/dist/package-loader/isolated-artifact.js +257 -51
- package/dist/package-loader/isolated-artifact.test.js +294 -21
- package/dist/package-loader/isolated-runtime.d.ts +22 -7
- package/dist/package-loader/isolated-table-gateway.js +30 -11
- package/dist/package-loader/isolated-table-gateway.test.js +243 -0
- package/dist/package-loader/package-loader.d.ts +8 -1
- package/dist/package-loader/package-loader.js +39 -20
- package/dist/package-loader/package-loader.test.js +47 -0
- package/dist/peer-id.d.ts +25 -0
- package/dist/peer-id.js +99 -0
- package/dist/serial-json.compat.test.d.ts +1 -0
- package/dist/serial-json.compat.test.js +369 -0
- package/dist/serial-json.d.ts +41 -5
- package/dist/serial-json.js +216 -157
- package/dist/serial-json.oracle.d.ts +21 -0
- package/dist/serial-json.oracle.js +222 -0
- package/dist/serial-json.test.js +315 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/mock-host-contract.d.ts +123 -0
- package/dist/testing/mock-host-contract.js +434 -0
- package/dist/testing/mock-host-contract.test.d.ts +1 -0
- package/dist/testing/mock-host-contract.test.js +782 -0
- package/dist/types/field-type.d.ts +35 -18
- package/dist/types/field-type.js +4 -1
- package/dist/types/zod-types.d.ts +9 -0
- package/dist/types/zod-types.js +8 -2
- package/dist/utils.d.ts +22 -25
- package/dist/utils.js +45 -92
- package/dist/utils.test.js +36 -0
- package/package.json +9 -2
- package/testing/package.json +4 -0
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Peers is a local-first personal computing platform. Your data lives on your devi
|
|
|
12
12
|
|
|
13
13
|
`definePackage()` and the contract builder API let you declare tables, tools, assistants, screens, and navigation entries. The runtime installs, loads, and hot-reloads your package across all devices.
|
|
14
14
|
|
|
15
|
+
Isolated packages declare themselves with `@peers-app/peers-sdk/define`: `definePackage`, `defineTable`, `defineTool`, and `contractRef`, in a `src/package.ts`, with the package's own state in `deviceVar`, `userVar`, and the other persistent variables. The `peers-package` command (`peers-package manifest`, `peers-package build`) turns that declaration into the package's manifest, plain JSON in Peers IR, and bundles the same code for the sandbox. `peers-package contract freeze` records the contract versions a stable release froze (`frozen-contracts.json`), and the build refuses a change to one. It uses the package's own `esbuild`. See [Declaring a package](https://peers-app.github.io/Packages/declaring-a-package).
|
|
16
|
+
|
|
15
17
|
### ORM
|
|
16
18
|
|
|
17
19
|
Table definitions with Zod schemas, data queries with a Mongo-style filter syntax, and async cursors. Tables sync across devices and encrypt automatically — you just define the schema.
|
|
@@ -75,7 +77,7 @@ Dynamic tool dispatch carries trusted signed identity and a strict delegated cei
|
|
|
75
77
|
|
|
76
78
|
`accessLevel` is independent admission policy. A `preserve` tool with no entry level admits at None (0); an ordinary unclassified tool retains Self (100). Inside strict dispatch, an ordinary tool contributes its own effective level to the outgoing ceiling, while a `preserve` tool omits it but still retains the live user role, any inherited ceiling, and the contract consume ceiling. An Admin entering a Reader `preserve` router can therefore select an Admin target, but an Admin arriving through an ordinary Reader tool stays capped at Reader. Even None `preserve` tools require trusted identity and context. Strict dispatch denies step-up and unsupported remote or cross-context hops.
|
|
77
79
|
|
|
78
|
-
Host packages select public targets through `context.dispatchTool({contractId, version, toolName, providerPackageId}, args)
|
|
80
|
+
Host packages select public targets through `context.dispatchTool({contractId, version, toolName, providerPackageId}, args)`. Isolated packages use `callConsumedTool`; both use the same runtime authorizer. A `private` tool is callable only by its own package, and target levels are read live on every call. The Contract Catalog's per-caller listings are advisory; execution rechecks. Tool arguments never supply authority.
|
|
79
81
|
|
|
80
82
|
## License
|
|
81
83
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build an isolated package from its declarations. See `peers-package --help`.
|
|
3
|
+
require("../dist/package-build/cli")
|
|
4
|
+
.main(process.argv.slice(2))
|
|
5
|
+
.then(
|
|
6
|
+
(code) => {
|
|
7
|
+
process.exitCode = code;
|
|
8
|
+
},
|
|
9
|
+
(error) => {
|
|
10
|
+
console.error(error instanceof Error ? error.message : error);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
},
|
|
13
|
+
);
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const group_member_roles_1 = require("../../data/group-member-roles");
|
|
4
4
|
const utils_1 = require("../../utils");
|
|
5
|
-
|
|
5
|
+
/** Voice Hub's contract, whose `voice-hub-run-action` declares preserve. */
|
|
6
|
+
const VOICE_HUB_CONTRACT_ID = "00mskqamhdq1327mqehy58pps";
|
|
7
|
+
/** The contract whose tools a test's entry tool dispatches to. */
|
|
8
|
+
const TARGETS = (0, utils_1.newid)();
|
|
6
9
|
const contract_provider_router_1 = require("../contract-provider-router");
|
|
7
10
|
const contract_proxy_1 = require("../contract-proxy");
|
|
8
11
|
const contract_transport_1 = require("../contract-transport");
|
|
12
|
+
const target_dispatch_1 = require("./target-dispatch");
|
|
9
13
|
function groupContext() {
|
|
10
14
|
const userId = (0, utils_1.newid)();
|
|
11
15
|
const groupId = (0, utils_1.newid)();
|
|
@@ -55,7 +59,11 @@ function tool(name, level, toolFn) {
|
|
|
55
59
|
toolFn,
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
|
-
|
|
62
|
+
/** A tool that runs the target `name` through `context.dispatchTool`. */
|
|
63
|
+
function hop(name, level, target) {
|
|
64
|
+
return tool(name, level, (_, context) => context.dispatchTool?.({ contractId: TARGETS, version: 1, toolName: target }, {}));
|
|
65
|
+
}
|
|
66
|
+
describe("strict delegated tool dispatch in a group", () => {
|
|
59
67
|
it("leaves ordinary host-tool admission outside the action scope on its existing user-role path", async () => {
|
|
60
68
|
const group = groupContext();
|
|
61
69
|
const contractId = (0, utils_1.newid)();
|
|
@@ -92,29 +100,21 @@ describe("strict voice/touch action transport in a group", () => {
|
|
|
92
100
|
function setup(authority) {
|
|
93
101
|
const group = groupContext();
|
|
94
102
|
const writes = jest.fn(async () => ({ result: "milk" }));
|
|
103
|
+
const targets = {
|
|
104
|
+
list: tool("list", 20, async () => ({ result: [] })),
|
|
105
|
+
add: tool("add", 40, writes),
|
|
106
|
+
reader: hop("reader", 20, "add"),
|
|
107
|
+
writer: hop("writer", 40, "history"),
|
|
108
|
+
"writer-self": hop("writer-self", 40, "self"),
|
|
109
|
+
history: tool("history", 80, writes),
|
|
110
|
+
self: tool("self", undefined, writes),
|
|
111
|
+
};
|
|
95
112
|
const resolution = {
|
|
96
|
-
contractId:
|
|
113
|
+
contractId: VOICE_HUB_CONTRACT_ID,
|
|
97
114
|
version: 1,
|
|
115
|
+
forwardCallContext: true,
|
|
98
116
|
tools: {
|
|
99
|
-
invoke: tool("invoke", 20, (input, context) => context.
|
|
100
|
-
},
|
|
101
|
-
actionAliases: {
|
|
102
|
-
list_items: "list",
|
|
103
|
-
add_item: "add",
|
|
104
|
-
reader_hop: "reader",
|
|
105
|
-
writer_hop: "writer",
|
|
106
|
-
writer_self: "writer-self",
|
|
107
|
-
owner: "history",
|
|
108
|
-
self: "self",
|
|
109
|
-
},
|
|
110
|
-
actionTools: {
|
|
111
|
-
list: tool("list", 20, async () => ({ result: [] })),
|
|
112
|
-
add: tool("add", 40, writes),
|
|
113
|
-
reader: tool("reader", 20, (_, context) => context.dispatchAction?.("add_item", {})),
|
|
114
|
-
writer: tool("writer", 40, (_, context) => context.dispatchAction?.("owner", {})),
|
|
115
|
-
"writer-self": tool("writer-self", 40, (_, context) => context.dispatchAction?.("self", {})),
|
|
116
|
-
history: tool("history", 80, writes),
|
|
117
|
-
self: tool("self", undefined, writes),
|
|
117
|
+
invoke: tool("invoke", 20, (input, context) => context.dispatchTool?.({ contractId: TARGETS, version: 1, toolName: input.tool }, input.args ?? {})),
|
|
118
118
|
},
|
|
119
119
|
};
|
|
120
120
|
Object.assign(resolution.tools.invoke.tool, { authorityPropagation: "preserve" });
|
|
@@ -125,11 +125,14 @@ describe("strict voice/touch action transport in a group", () => {
|
|
|
125
125
|
authority,
|
|
126
126
|
};
|
|
127
127
|
const [client, server] = (0, contract_transport_1.inProcessTransportPair)();
|
|
128
|
-
const session = (0, contract_proxy_1.createContractProviderSession)(resolution, server, {
|
|
128
|
+
const session = (0, contract_proxy_1.createContractProviderSession)(resolution, server, {
|
|
129
|
+
context,
|
|
130
|
+
dispatchTool: (0, target_dispatch_1.targetDispatch)(TARGETS, targets),
|
|
131
|
+
});
|
|
129
132
|
const consumer = (0, contract_proxy_1.createContractConsumer)({
|
|
130
|
-
contractId:
|
|
133
|
+
contractId: VOICE_HUB_CONTRACT_ID,
|
|
131
134
|
version: 1,
|
|
132
|
-
name: "
|
|
135
|
+
name: "Hub",
|
|
133
136
|
description: "",
|
|
134
137
|
tables: [],
|
|
135
138
|
observables: [],
|
|
@@ -139,7 +142,7 @@ describe("strict voice/touch action transport in a group", () => {
|
|
|
139
142
|
return {
|
|
140
143
|
...group,
|
|
141
144
|
writes,
|
|
142
|
-
|
|
145
|
+
targets,
|
|
143
146
|
context,
|
|
144
147
|
consumer,
|
|
145
148
|
dispose: () => {
|
|
@@ -148,16 +151,16 @@ describe("strict voice/touch action transport in a group", () => {
|
|
|
148
151
|
},
|
|
149
152
|
};
|
|
150
153
|
}
|
|
151
|
-
it("allows direct Writer dispatch while a group Reader can only list,
|
|
154
|
+
it("allows direct Writer dispatch while a group Reader can only list, ignoring payload grants", async () => {
|
|
152
155
|
const p = setup();
|
|
153
156
|
try {
|
|
154
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
157
|
+
await expect(p.consumer.tools.invoke({ tool: "add", args: { name: "milk" } })).resolves.toEqual({ result: "milk" });
|
|
155
158
|
p.setRole(group_member_roles_1.GroupMemberRole.Reader);
|
|
156
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
159
|
+
await expect(p.consumer.tools.invoke({ tool: "list" })).resolves.toEqual({
|
|
157
160
|
result: [],
|
|
158
161
|
});
|
|
159
162
|
await expect(p.consumer.tools.invoke({
|
|
160
|
-
tool: "
|
|
163
|
+
tool: "add",
|
|
161
164
|
args: {
|
|
162
165
|
name: "milk",
|
|
163
166
|
userId: p.userId,
|
|
@@ -168,51 +171,36 @@ describe("strict voice/touch action transport in a group", () => {
|
|
|
168
171
|
},
|
|
169
172
|
})).rejects.toThrow("Permission denied");
|
|
170
173
|
expect(p.writes).toHaveBeenCalledTimes(1);
|
|
171
|
-
expect(await (0, contract_proxy_1.authorizedContractActionNames)(p.resolution, p.context)).toEqual(expect.not.arrayContaining(["add_item", "owner", "self"]));
|
|
172
174
|
}
|
|
173
175
|
finally {
|
|
174
176
|
p.dispose();
|
|
175
177
|
}
|
|
176
178
|
});
|
|
177
|
-
it("caps Reader tools through invoke, denies Owner and Self from Writer tools
|
|
179
|
+
it("caps Reader tools through invoke, and denies Owner and Self from Writer tools", async () => {
|
|
178
180
|
const p = setup();
|
|
179
181
|
try {
|
|
180
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
181
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
182
|
+
await expect(p.consumer.tools.invoke({ tool: "reader" })).rejects.toThrow("Permission denied");
|
|
183
|
+
await expect(p.consumer.tools.invoke({ tool: "writer" })).rejects.toThrow("Permission denied");
|
|
182
184
|
await expect(p.consumer.tools.invoke({ tool: "self" })).rejects.toThrow("Permission denied");
|
|
183
185
|
p.setRole(group_member_roles_1.GroupMemberRole.Owner);
|
|
184
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
186
|
+
await expect(p.consumer.tools.invoke({ tool: "writer" })).rejects.toThrow("Permission denied");
|
|
185
187
|
p.setRole(group_member_roles_1.GroupMemberRole.Founder);
|
|
186
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
188
|
+
await expect(p.consumer.tools.invoke({ tool: "writer-self" })).rejects.toThrow("Permission denied");
|
|
187
189
|
expect(p.writes).not.toHaveBeenCalled();
|
|
188
190
|
}
|
|
189
191
|
finally {
|
|
190
192
|
p.dispose();
|
|
191
193
|
}
|
|
192
194
|
});
|
|
193
|
-
it("rechecks role and live metadata
|
|
195
|
+
it("rechecks role and live metadata on every call", async () => {
|
|
194
196
|
const p = setup();
|
|
195
197
|
try {
|
|
196
|
-
expect(await (0, contract_proxy_1.authorizedContractActionNames)(p.resolution, p.context)).toContain("add_item");
|
|
197
198
|
p.setRole(group_member_roles_1.GroupMemberRole.Reader);
|
|
198
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
199
|
+
await expect(p.consumer.tools.invoke({ tool: "add" })).rejects.toThrow("Permission denied");
|
|
199
200
|
p.setRole(group_member_roles_1.GroupMemberRole.Writer);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
p.
|
|
203
|
-
await expect(p.consumer.tools.invoke({ tool: "add_item" })).rejects.toThrow("Permission denied");
|
|
204
|
-
await expect(p.consumer.tools.invoke({ tool: "invented" })).rejects.toThrow("Unknown action");
|
|
205
|
-
expect(p.writes).not.toHaveBeenCalled();
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
p.dispose();
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
it("refuses a Reader wrapper with no checked actions before an old raw handler can run", async () => {
|
|
212
|
-
const p = setup();
|
|
213
|
-
try {
|
|
214
|
-
delete p.resolution.actionAliases;
|
|
215
|
-
await expect(p.consumer.tools.invoke({ tool: "add_item" })).rejects.toThrow("Action dispatcher requires registered checked actions");
|
|
201
|
+
p.targets.add.tool.accessLevel = 80;
|
|
202
|
+
await expect(p.consumer.tools.invoke({ tool: "add" })).rejects.toThrow("Permission denied");
|
|
203
|
+
await expect(p.consumer.tools.invoke({ tool: "invented" })).rejects.toThrow("does not provide tool 'invented'");
|
|
216
204
|
expect(p.writes).not.toHaveBeenCalled();
|
|
217
205
|
}
|
|
218
206
|
finally {
|
|
@@ -222,10 +210,10 @@ describe("strict voice/touch action transport in a group", () => {
|
|
|
222
210
|
it("retains a delegated Reader frame from a trusted caller and fails missing identity closed", async () => {
|
|
223
211
|
const p = setup({ userAccessLevel: 40, assistantAccessLevel: 20, strictActionScope: true });
|
|
224
212
|
try {
|
|
225
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
213
|
+
await expect(p.consumer.tools.invoke({ tool: "add" })).rejects.toThrow("Permission denied");
|
|
226
214
|
delete p.context.callerUserId;
|
|
227
215
|
delete p.context.localUserId;
|
|
228
|
-
await expect(p.consumer.tools.invoke({ tool: "
|
|
216
|
+
await expect(p.consumer.tools.invoke({ tool: "list", args: { callerUserId: p.userId } })).rejects.toThrow("missing caller identity");
|
|
229
217
|
expect(p.writes).not.toHaveBeenCalled();
|
|
230
218
|
}
|
|
231
219
|
finally {
|
|
@@ -271,17 +259,11 @@ describe("declared tool authority propagation", () => {
|
|
|
271
259
|
const group = groupContext();
|
|
272
260
|
group.setRole(userLevel);
|
|
273
261
|
const effect = jest.fn(() => "executed");
|
|
274
|
-
const router =
|
|
262
|
+
const router = hop("route", entryLevel, "target");
|
|
275
263
|
if (preserve)
|
|
276
264
|
Object.assign(router.tool, { authorityPropagation: "preserve" });
|
|
277
265
|
const contractId = (0, utils_1.newid)();
|
|
278
|
-
const dispatch = (0, contract_proxy_1.createContractProvider)({
|
|
279
|
-
contractId,
|
|
280
|
-
version: 1,
|
|
281
|
-
tools: { route: router },
|
|
282
|
-
actionAliases: { act: "target" },
|
|
283
|
-
actionTools: { target: tool("target", targetLevel, effect) },
|
|
284
|
-
});
|
|
266
|
+
const dispatch = (0, contract_proxy_1.createContractProvider)({ contractId, version: 1, tools: { route: router }, forwardCallContext: true }, { dispatchTool: (0, target_dispatch_1.targetDispatch)(TARGETS, { target: tool("target", targetLevel, effect) }) });
|
|
285
267
|
const result = dispatch({ contractId, version: 1, member: "tool", name: "route", operation: "run", args: [{}] }, {
|
|
286
268
|
callerUserId: group.userId,
|
|
287
269
|
dataContext: group.dataContext,
|
|
@@ -293,21 +275,19 @@ describe("declared tool authority propagation", () => {
|
|
|
293
275
|
await expect(result).rejects.toThrow(/Permission denied/);
|
|
294
276
|
expect(effect).toHaveBeenCalledTimes(allowed ? 1 : 0);
|
|
295
277
|
});
|
|
296
|
-
it
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
278
|
+
it("honors declared preserve on Voice Hub's voice-hub-run-action without raising an inherited ceiling", async () => {
|
|
279
|
+
const contractId = VOICE_HUB_CONTRACT_ID;
|
|
280
|
+
const version = 4;
|
|
281
|
+
const name = "voice-hub-run-action";
|
|
300
282
|
const group = groupContext();
|
|
301
283
|
const effect = jest.fn(() => ({ writerTargetExecuted: true }));
|
|
302
284
|
const resolution = {
|
|
303
285
|
contractId,
|
|
304
286
|
version,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
},
|
|
308
|
-
actionAliases: { write: "write" },
|
|
309
|
-
actionTools: { write: tool("write", 40, effect) },
|
|
287
|
+
forwardCallContext: true,
|
|
288
|
+
tools: { [name]: hop(name, 20, "write") },
|
|
310
289
|
};
|
|
290
|
+
const dispatchTool = (0, target_dispatch_1.targetDispatch)(TARGETS, { write: tool("write", 40, effect) });
|
|
311
291
|
const call = {
|
|
312
292
|
contractId,
|
|
313
293
|
version,
|
|
@@ -318,12 +298,12 @@ describe("declared tool authority propagation", () => {
|
|
|
318
298
|
};
|
|
319
299
|
const context = { callerUserId: group.userId, dataContext: group.dataContext };
|
|
320
300
|
// An ordinary Reader wrapper caps a direct Writer at Reader inside the action scope.
|
|
321
|
-
const capped = (0, contract_proxy_1.createContractProvider)(resolution);
|
|
301
|
+
const capped = (0, contract_proxy_1.createContractProvider)(resolution, { dispatchTool });
|
|
322
302
|
await expect(capped(call, context)).rejects.toThrow("Permission denied: strict delegation");
|
|
323
303
|
expect(effect).not.toHaveBeenCalled();
|
|
324
304
|
// The same wrapper declared preserve passes the Writer through to a Writer target.
|
|
325
305
|
Object.assign(resolution.tools[name].tool, { authorityPropagation: "preserve" });
|
|
326
|
-
const preserving = (0, contract_proxy_1.createContractProvider)(resolution);
|
|
306
|
+
const preserving = (0, contract_proxy_1.createContractProvider)(resolution, { dispatchTool });
|
|
327
307
|
await expect(preserving(call, context)).resolves.toEqual({ writerTargetExecuted: true });
|
|
328
308
|
// An inherited Reader frame from an upstream ordinary tool is never raised.
|
|
329
309
|
await expect(preserving(call, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const field_type_1 = require("../../types/field-type");
|
|
4
|
+
const utils_1 = require("../../utils");
|
|
5
|
+
const registry_1 = require("../registry");
|
|
6
|
+
const validate_1 = require("../validate");
|
|
7
|
+
const contractId = (0, utils_1.newid)();
|
|
8
|
+
/** Notes vN with one `add` tool taking `inputs`, returning `outputs`, and one `Notes` table. */
|
|
9
|
+
function notes(version, { inputs = [], outputs = [], rows = [], } = {}, devTag) {
|
|
10
|
+
return {
|
|
11
|
+
contractId,
|
|
12
|
+
version,
|
|
13
|
+
...(devTag ? { devTag } : {}),
|
|
14
|
+
name: "Notes",
|
|
15
|
+
description: "",
|
|
16
|
+
tables: [
|
|
17
|
+
{
|
|
18
|
+
name: "Notes",
|
|
19
|
+
description: "",
|
|
20
|
+
primaryKeyName: "noteId",
|
|
21
|
+
fields: [{ name: "noteId", type: field_type_1.FieldType.id }, ...rows],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
tools: [{ name: "add", usageDescription: "Add.", inputFields: inputs, outputFields: outputs }],
|
|
25
|
+
observables: [],
|
|
26
|
+
events: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const field = (name, optional = false) => ({
|
|
30
|
+
name,
|
|
31
|
+
type: field_type_1.FieldType.string,
|
|
32
|
+
...(optional ? { optional } : {}),
|
|
33
|
+
});
|
|
34
|
+
describe("alsoImplements compatibility, by what callers send and receive", () => {
|
|
35
|
+
const satisfies = (implementation, target) => (0, validate_1.validateProviderSatisfiesContract)(implementation, target).valid;
|
|
36
|
+
it("lets an input become optional, but not an optional one required", () => {
|
|
37
|
+
expect(satisfies(notes(2, { inputs: [field("title", true)] }), notes(1, { inputs: [field("title")] }))).toBe(true);
|
|
38
|
+
expect(satisfies(notes(2, { inputs: [field("title")] }), notes(1, { inputs: [field("title", true)] }))).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it("keeps a required output required, and a table field's optionality as it was", () => {
|
|
41
|
+
expect(satisfies(notes(2, { outputs: [field("id", true)] }), notes(1, { outputs: [field("id")] }))).toBe(false);
|
|
42
|
+
expect(satisfies(notes(2, { outputs: [field("id")] }), notes(1, { outputs: [field("id", true)] }))).toBe(true);
|
|
43
|
+
expect(satisfies(notes(2, { rows: [field("title", true)] }), notes(1, { rows: [field("title")] }))).toBe(false);
|
|
44
|
+
expect(satisfies(notes(2, { rows: [field("title")] }), notes(1, { rows: [field("title", true)] }))).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("keeps a required event payload field required, while allowing optional ones to become required", () => {
|
|
47
|
+
const event = (optional) => ({
|
|
48
|
+
...notes(1),
|
|
49
|
+
events: [{ name: "added", description: "", payloadFields: [field("title", optional)] }],
|
|
50
|
+
});
|
|
51
|
+
expect(satisfies(event(true), event(false))).toBe(false);
|
|
52
|
+
expect(satisfies(event(false), event(true))).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("a carried alsoImplements definition", () => {
|
|
56
|
+
const providerId = (0, utils_1.newid)();
|
|
57
|
+
const v1 = notes(1, { inputs: [field("title")] });
|
|
58
|
+
const v2 = notes(2, { inputs: [field("title"), field("tag", true)] });
|
|
59
|
+
const alias = (definition = v1) => ({
|
|
60
|
+
definition: v2,
|
|
61
|
+
alsoImplements: [{ contractId, version: 1, definition }],
|
|
62
|
+
});
|
|
63
|
+
it("registers the target when the registry has none, and keeps one it already has", () => {
|
|
64
|
+
const cold = new registry_1.ContractRegistry();
|
|
65
|
+
expect(cold.registerBatch(providerId, [alias()]).valid).toBe(true);
|
|
66
|
+
expect(cold.getDefinition(contractId, 1)).toEqual(v1);
|
|
67
|
+
expect(cold.getProviderImplementation(contractId, 1, providerId)).toEqual({
|
|
68
|
+
contractId,
|
|
69
|
+
version: 2,
|
|
70
|
+
});
|
|
71
|
+
const warm = new registry_1.ContractRegistry();
|
|
72
|
+
const devV1 = { ...v1, devTag: "dev" };
|
|
73
|
+
warm.registerBatch((0, utils_1.newid)(), [{ definition: devV1 }]);
|
|
74
|
+
warm.registerBatch(providerId, [alias()]);
|
|
75
|
+
expect(warm.getDefinition(contractId, 1)).toEqual(devV1);
|
|
76
|
+
});
|
|
77
|
+
it("must match a frozen registered target, and be the version it declares", () => {
|
|
78
|
+
const registry = new registry_1.ContractRegistry();
|
|
79
|
+
registry.registerBatch((0, utils_1.newid)(), [{ definition: v1 }]);
|
|
80
|
+
const changed = notes(1, { inputs: [field("title"), field("body", true)] });
|
|
81
|
+
const refused = registry.registerBatch(providerId, [alias(changed)]);
|
|
82
|
+
expect(refused.valid).toBe(false);
|
|
83
|
+
expect(refused.errors[0].message).toContain("is frozen and cannot be modified");
|
|
84
|
+
expect(registry.getProviderPackageId(contractId, 2)).toBeUndefined();
|
|
85
|
+
const mislabeled = new registry_1.ContractRegistry().registerBatch(providerId, [
|
|
86
|
+
alias(notes(3, { inputs: [field("title")] })),
|
|
87
|
+
]);
|
|
88
|
+
expect(mislabeled.errors[0].message).toContain("alsoImplements carries a definition that isn't");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
test("legacy ranges retain explicitly provided handlers and alias the remaining versions", () => {
|
|
92
|
+
const registry = new registry_1.ContractRegistry();
|
|
93
|
+
const providerId = (0, utils_1.newid)();
|
|
94
|
+
expect(registry.register((0, utils_1.newid)(), notes(2)).valid).toBe(true);
|
|
95
|
+
expect(registry.registerBatch(providerId, [
|
|
96
|
+
{ definition: notes(1) },
|
|
97
|
+
{
|
|
98
|
+
definition: notes(3),
|
|
99
|
+
alsoImplements: [{ contractId, version: { from: 1, to: 2 } }],
|
|
100
|
+
},
|
|
101
|
+
]).valid).toBe(true);
|
|
102
|
+
expect(registry.getProviderImplementation(contractId, 1, providerId)).toEqual({
|
|
103
|
+
contractId,
|
|
104
|
+
version: 1,
|
|
105
|
+
});
|
|
106
|
+
expect(registry.getProviderImplementation(contractId, 2, providerId)).toEqual({
|
|
107
|
+
contractId,
|
|
108
|
+
version: 3,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -18,6 +18,17 @@ const PROJECTS_ID = (0, utils_1.newid)();
|
|
|
18
18
|
const FANCY_TASKS_ID = (0, utils_1.newid)();
|
|
19
19
|
const GENERIC_ID = (0, utils_1.newid)();
|
|
20
20
|
describe("definePackage", () => {
|
|
21
|
+
it("carries a contract's description, or an empty one", () => {
|
|
22
|
+
const result = (0, builder_1.definePackage)((pkg) => {
|
|
23
|
+
pkg.packageId = TEST_PKG_ID;
|
|
24
|
+
pkg.contract(TASKS_ID, 1, "Tasks", "Your tasks, by status.");
|
|
25
|
+
pkg.contract(NOTES_ID, 1, "Notes");
|
|
26
|
+
});
|
|
27
|
+
expect(result.contracts.map(({ name, description }) => [name, description])).toEqual([
|
|
28
|
+
["Tasks", "Your tasks, by status."],
|
|
29
|
+
["Notes", ""],
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
21
32
|
it("produces a contract definition from a single contract with tables and tools", () => {
|
|
22
33
|
const result = (0, builder_1.definePackage)((pkg) => {
|
|
23
34
|
pkg.packageId = TEST_PKG_ID;
|
|
@@ -320,6 +331,40 @@ describe("definePackage", () => {
|
|
|
320
331
|
expect(decls[0]).toEqual({ contractId: TASKS_ID, version: 1 });
|
|
321
332
|
expect(decls[1]).toEqual({ contractId: TASKS_ID, version: { from: 1, to: 3 } });
|
|
322
333
|
});
|
|
334
|
+
it("records the contracts a hub implementation exposes", () => {
|
|
335
|
+
const hubId = (0, utils_1.newid)();
|
|
336
|
+
const result = (0, builder_1.definePackage)((pkg) => {
|
|
337
|
+
pkg.packageId = TEST_PKG_ID;
|
|
338
|
+
pkg.contract(GENERIC_ID, 1, "Generic");
|
|
339
|
+
const hub = pkg.contract(hubId, 2, "Hub");
|
|
340
|
+
hub.exposes(GENERIC_ID, 1);
|
|
341
|
+
});
|
|
342
|
+
expect(result.exposes?.get((0, types_2.contractKey)(hubId, 2))).toEqual([
|
|
343
|
+
{ contractId: GENERIC_ID, version: 1 },
|
|
344
|
+
]);
|
|
345
|
+
expect(result.exposes?.has((0, types_2.contractKey)(GENERIC_ID, 1))).toBe(false);
|
|
346
|
+
expect(result.contracts.find((contract) => contract.contractId === hubId)).not.toHaveProperty("exposes");
|
|
347
|
+
const hub = (0, builder_1.definePackage)((pkg) => {
|
|
348
|
+
pkg.packageId = TEST_PKG_ID;
|
|
349
|
+
const contract = pkg.contract(hubId, 2, "Hub");
|
|
350
|
+
expect(() => contract.exposes("short", 1)).toThrow("valid peer ID");
|
|
351
|
+
expect(() => contract.exposes(GENERIC_ID, 0)).toThrow("positive integer");
|
|
352
|
+
});
|
|
353
|
+
expect(hub.exposes?.size).toBe(0);
|
|
354
|
+
});
|
|
355
|
+
it("rejects invalid legacy exposures before installation", () => {
|
|
356
|
+
const hubId = (0, utils_1.newid)();
|
|
357
|
+
const build = (targets) => (0, builder_1.definePackage)((pkg) => {
|
|
358
|
+
pkg.packageId = TEST_PKG_ID;
|
|
359
|
+
pkg.contract(GENERIC_ID, 1, "Generic");
|
|
360
|
+
const hub = pkg.contract(hubId, 1, "Hub");
|
|
361
|
+
for (const target of targets)
|
|
362
|
+
hub.exposes(target, 1);
|
|
363
|
+
});
|
|
364
|
+
expect(() => build([(0, utils_1.newid)()])).toThrow("doesn't provide");
|
|
365
|
+
expect(() => build([hubId])).toThrow("can't expose itself");
|
|
366
|
+
expect(() => build([GENERIC_ID, GENERIC_ID])).toThrow("twice");
|
|
367
|
+
});
|
|
323
368
|
it("includes package-level assistants and appNavs", () => {
|
|
324
369
|
const assistant = { id: "asst1", name: "Task Bot" };
|
|
325
370
|
const nav = { name: "Tasks", iconClassName: "bi bi-list-task", navigationPath: "app" };
|
|
@@ -240,7 +240,13 @@ describe("remote contract call authorization (group members)", () => {
|
|
|
240
240
|
groupId,
|
|
241
241
|
packageLoader: {
|
|
242
242
|
contractRegistry: {
|
|
243
|
-
|
|
243
|
+
selectProvider: jest.fn(({ contractId: id, version }) => id === contractId && version === 1
|
|
244
|
+
? { providerPackageId: "provider-a", definition }
|
|
245
|
+
: undefined),
|
|
246
|
+
getProviderImplementation: jest.fn((id, version) => ({
|
|
247
|
+
contractId: id,
|
|
248
|
+
version,
|
|
249
|
+
})),
|
|
244
250
|
},
|
|
245
251
|
},
|
|
246
252
|
};
|
|
@@ -263,6 +269,127 @@ describe("remote contract call authorization (group members)", () => {
|
|
|
263
269
|
expect(route.context?.dataContext).toBe(groupDataContext);
|
|
264
270
|
expect(accessLevelMock.calls).toEqual([[groupDataContext, remoteIdentity.userId]]);
|
|
265
271
|
});
|
|
272
|
+
it("uses the active frozen provider's remote policy, not the last registration's", async () => {
|
|
273
|
+
const { remoteIdentity, userContext, groupDataContext } = setupGroupMember({
|
|
274
|
+
remoteAccessLevel: access_level_1.AccessLevel.Admin,
|
|
275
|
+
callerLevel: access_level_1.AccessLevel.Reader,
|
|
276
|
+
});
|
|
277
|
+
const selected = groupDataContext.packageLoader.contractRegistry.selectProvider({
|
|
278
|
+
contractId,
|
|
279
|
+
version: 1,
|
|
280
|
+
});
|
|
281
|
+
if (!selected)
|
|
282
|
+
throw new Error("Missing test provider");
|
|
283
|
+
const { definition } = selected;
|
|
284
|
+
const registry = new index_1.ContractRegistry();
|
|
285
|
+
expect(registry.register("strict", definition).valid).toBe(true);
|
|
286
|
+
const permissive = {
|
|
287
|
+
...definition,
|
|
288
|
+
tools: definition.tools.map((tool) => ({ ...tool, remoteAccessLevel: access_level_1.AccessLevel.Reader })),
|
|
289
|
+
};
|
|
290
|
+
expect(registry.register("permissive", permissive).valid).toBe(true);
|
|
291
|
+
Object.assign(groupDataContext.packageLoader, { contractRegistry: registry });
|
|
292
|
+
await expect((0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity)).rejects.toThrow("requires Admin");
|
|
293
|
+
registry.setActiveProvider(contractId, 1, "permissive");
|
|
294
|
+
await expect((0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity)).resolves.toMatchObject({ providerPackageId: "permissive" });
|
|
295
|
+
expect(registry.register("dev", { ...permissive, devTag: "dev" }).valid).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
it("uses the implementing tool's remote policy for a version the provider also implements", async () => {
|
|
298
|
+
const { remoteIdentity, userContext, groupDataContext } = setupGroupMember({
|
|
299
|
+
remoteAccessLevel: access_level_1.AccessLevel.Reader,
|
|
300
|
+
callerLevel: access_level_1.AccessLevel.Reader,
|
|
301
|
+
});
|
|
302
|
+
const selected = groupDataContext.packageLoader.contractRegistry.selectProvider({
|
|
303
|
+
contractId,
|
|
304
|
+
version: 1,
|
|
305
|
+
});
|
|
306
|
+
if (!selected)
|
|
307
|
+
throw new Error("Missing test provider");
|
|
308
|
+
const v1 = selected.definition;
|
|
309
|
+
// v2 keeps v1's tools but asks a remote caller for Admin.
|
|
310
|
+
const v2 = {
|
|
311
|
+
...v1,
|
|
312
|
+
version: 2,
|
|
313
|
+
tools: v1.tools.map((tool) => ({ ...tool, remoteAccessLevel: access_level_1.AccessLevel.Admin })),
|
|
314
|
+
};
|
|
315
|
+
const registry = new index_1.ContractRegistry();
|
|
316
|
+
const registered = registry.registerBatch("upgraded", [
|
|
317
|
+
{ definition: v2, alsoImplements: [{ contractId, version: 1, definition: v1 }] },
|
|
318
|
+
]);
|
|
319
|
+
expect(registered.valid).toBe(true);
|
|
320
|
+
Object.assign(groupDataContext.packageLoader, { contractRegistry: registry });
|
|
321
|
+
await expect((0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity)).rejects.toThrow("requires Admin");
|
|
322
|
+
// Another provider's permissive policy must not admit this provider's alias.
|
|
323
|
+
expect(registry.register("permissive", { ...v2, tools: v1.tools }).valid).toBe(true);
|
|
324
|
+
expect(registry.setActiveProvider(contractId, 2, "permissive")).toBe(true);
|
|
325
|
+
await expect((0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity)).rejects.toThrow("requires Admin");
|
|
326
|
+
});
|
|
327
|
+
it("denies an alias when its implementing tool has no remote policy, and denies newer-only tools", async () => {
|
|
328
|
+
const { remoteIdentity, userContext, groupDataContext } = setupGroupMember({
|
|
329
|
+
remoteAccessLevel: access_level_1.AccessLevel.Reader,
|
|
330
|
+
callerLevel: access_level_1.AccessLevel.Reader,
|
|
331
|
+
});
|
|
332
|
+
const selected = groupDataContext.packageLoader.contractRegistry.selectProvider({
|
|
333
|
+
contractId,
|
|
334
|
+
version: 1,
|
|
335
|
+
});
|
|
336
|
+
if (!selected)
|
|
337
|
+
throw new Error("Missing test provider");
|
|
338
|
+
const v1 = selected.definition;
|
|
339
|
+
const v2 = {
|
|
340
|
+
...v1,
|
|
341
|
+
version: 2,
|
|
342
|
+
tools: [
|
|
343
|
+
...v1.tools.map(({ remoteAccessLevel: _level, ...tool }) => tool),
|
|
344
|
+
{ ...v1.tools[0], name: "newTool" },
|
|
345
|
+
],
|
|
346
|
+
};
|
|
347
|
+
const registry = new index_1.ContractRegistry();
|
|
348
|
+
expect(registry.register("upgraded", v2, [{ contractId, version: 1, definition: v1 }]).valid).toBe(true);
|
|
349
|
+
Object.assign(groupDataContext.packageLoader, { contractRegistry: registry });
|
|
350
|
+
await expect((0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity)).rejects.toThrow("not remotely callable");
|
|
351
|
+
await expect((0, index_1.authorizeRemoteContractCall)({ ...toolCall, name: "newTool" }, userContext, remoteIdentity)).rejects.toThrow("not provided in this data context");
|
|
352
|
+
await expect((0, index_1.authorizeRemoteContractCall)({ ...toolCall, version: 2, name: "newTool" }, userContext, remoteIdentity)).resolves.toMatchObject({ providerPackageId: "upgraded" });
|
|
353
|
+
});
|
|
354
|
+
it.each([
|
|
355
|
+
"request",
|
|
356
|
+
"router",
|
|
357
|
+
])("keeps the authorized provider through %s dispatch", async (route) => {
|
|
358
|
+
const { remoteIdentity, userContext, groupDataContext } = setupGroupMember({
|
|
359
|
+
remoteAccessLevel: access_level_1.AccessLevel.Reader,
|
|
360
|
+
callerLevel: access_level_1.AccessLevel.Reader,
|
|
361
|
+
});
|
|
362
|
+
const handleCall = jest.fn(async () => "ok");
|
|
363
|
+
const resolveContractEndpoint = jest.fn(async (_request) => ({
|
|
364
|
+
handleCall,
|
|
365
|
+
dispose: jest.fn(),
|
|
366
|
+
}));
|
|
367
|
+
const getActiveContractProviderPackageId = jest.fn(() => "different-provider");
|
|
368
|
+
Object.assign(groupDataContext.packageLoader, {
|
|
369
|
+
resolveContractEndpoint,
|
|
370
|
+
getActiveContractProviderPackageId,
|
|
371
|
+
});
|
|
372
|
+
if (route === "request") {
|
|
373
|
+
await expect((0, index_1.handleContractCall)(toolCall, { userContext, remoteIdentity })).resolves.toBe("ok");
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const [consumer, provider] = (0, index_1.inProcessTransportPair)();
|
|
377
|
+
const router = (0, index_1.createUserContextContractProviderRouter)(provider, {
|
|
378
|
+
userContext,
|
|
379
|
+
remoteIdentity,
|
|
380
|
+
});
|
|
381
|
+
try {
|
|
382
|
+
await expect(consumer.emit(index_1.CONTRACT_REQUEST_CHANNEL, toolCall)).resolves.toBe("ok");
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
router.dispose();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
expect(resolveContractEndpoint.mock.calls[0][0]).toMatchObject({
|
|
389
|
+
providerPackageId: "provider-a",
|
|
390
|
+
});
|
|
391
|
+
expect(getActiveContractProviderPackageId).not.toHaveBeenCalled();
|
|
392
|
+
});
|
|
266
393
|
it("waits for catalog persistence before authorizing a cold group's first call", async () => {
|
|
267
394
|
const { remoteIdentity, userContext, groupDataContext } = setupGroupMember({
|
|
268
395
|
remoteAccessLevel: access_level_1.AccessLevel.Writer,
|
|
@@ -276,7 +403,7 @@ describe("remote contract call authorization (group members)", () => {
|
|
|
276
403
|
const pending = (0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity);
|
|
277
404
|
try {
|
|
278
405
|
await new Promise((resolve) => setImmediate(resolve));
|
|
279
|
-
expect(groupDataContext.packageLoader.contractRegistry.
|
|
406
|
+
expect(groupDataContext.packageLoader.contractRegistry.selectProvider).not.toHaveBeenCalled();
|
|
280
407
|
expect(accessLevelMock.calls).toEqual([]);
|
|
281
408
|
}
|
|
282
409
|
finally {
|
|
@@ -294,7 +421,7 @@ describe("remote contract call authorization (group members)", () => {
|
|
|
294
421
|
get: () => (fail ? Promise.reject(new Error("catalog unavailable")) : Promise.resolve()),
|
|
295
422
|
});
|
|
296
423
|
await expect((0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity)).rejects.toThrow("catalog unavailable");
|
|
297
|
-
expect(groupDataContext.packageLoader.contractRegistry.
|
|
424
|
+
expect(groupDataContext.packageLoader.contractRegistry.selectProvider).not.toHaveBeenCalled();
|
|
298
425
|
expect(accessLevelMock.calls).toEqual([]);
|
|
299
426
|
fail = false;
|
|
300
427
|
await expect((0, index_1.authorizeRemoteContractCall)(toolCall, userContext, remoteIdentity)).resolves.toMatchObject({ dataContextId: groupId });
|
|
@@ -359,7 +486,10 @@ describe("remote contract call authorization (group members)", () => {
|
|
|
359
486
|
const { remoteIdentity, userContext, groupDataContext } = setupGroupMember({
|
|
360
487
|
callerLevel: level,
|
|
361
488
|
});
|
|
362
|
-
groupDataContext.packageLoader.contractRegistry.
|
|
489
|
+
groupDataContext.packageLoader.contractRegistry.selectProvider.mockReturnValue({
|
|
490
|
+
providerPackageId: "system",
|
|
491
|
+
definition: (0, index_1.getPackagesContractDefinition)(),
|
|
492
|
+
});
|
|
363
493
|
await expect((0, index_1.authorizeRemoteContractCall)({ ...toolCall, contractId: system_ids_1.packagesContractId, name: "listPackages" }, userContext, remoteIdentity)).rejects.toThrow("tool is not remotely callable");
|
|
364
494
|
});
|
|
365
495
|
it("keeps full access for Self-trusted callers without consulting remoteAccessLevel", async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|