@peers-app/peers-sdk 0.26.5 → 0.27.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 (194) hide show
  1. package/README.md +3 -1
  2. package/bin/peers-package.js +13 -0
  3. package/define/package.json +4 -0
  4. package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
  5. package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/also-implements.test.js +110 -0
  7. package/dist/contracts/__tests__/builder.test.js +45 -0
  8. package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
  9. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
  10. package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
  11. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
  12. package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
  14. package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
  15. package/dist/contracts/__tests__/registry.test.js +68 -0
  16. package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
  17. package/dist/contracts/__tests__/target-dispatch.js +24 -0
  18. package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
  19. package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
  20. package/dist/contracts/__tests__/validate.test.js +177 -7
  21. package/dist/contracts/__tests__/workflow-context.test.js +0 -11
  22. package/dist/contracts/builder.d.ts +16 -2
  23. package/dist/contracts/builder.js +44 -3
  24. package/dist/contracts/contract-definition-schema.d.ts +251 -0
  25. package/dist/contracts/contract-definition-schema.js +48 -0
  26. package/dist/contracts/contract-provider-router.d.ts +2 -0
  27. package/dist/contracts/contract-provider-router.js +5 -2
  28. package/dist/contracts/contract-proxy.d.ts +52 -18
  29. package/dist/contracts/contract-proxy.js +144 -203
  30. package/dist/contracts/contract-resolvers.d.ts +6 -4
  31. package/dist/contracts/contract-resolvers.js +32 -9
  32. package/dist/contracts/frozen-contracts.d.ts +59 -0
  33. package/dist/contracts/frozen-contracts.js +86 -0
  34. package/dist/contracts/host-contract-client.d.ts +134 -0
  35. package/dist/contracts/host-contract-client.js +396 -0
  36. package/dist/contracts/host-contract-transport.d.ts +2 -0
  37. package/dist/contracts/host-contract-transport.js +8 -0
  38. package/dist/contracts/index.d.ts +6 -4
  39. package/dist/contracts/index.js +11 -5
  40. package/dist/contracts/registry.d.ts +31 -1
  41. package/dist/contracts/registry.js +100 -6
  42. package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
  43. package/dist/contracts/system/contract-catalog-contract.js +17 -5
  44. package/dist/contracts/tool-catalog-service.js +26 -6
  45. package/dist/contracts/tool-catalog.d.ts +1 -1
  46. package/dist/contracts/tool-catalog.js +4 -1
  47. package/dist/contracts/types.d.ts +32 -2
  48. package/dist/contracts/validate.d.ts +16 -1
  49. package/dist/contracts/validate.js +139 -25
  50. package/dist/data/change-tracking.d.ts +17 -5
  51. package/dist/data/change-tracking.js +22 -6
  52. package/dist/data/change-tracking.test.d.ts +1 -0
  53. package/dist/data/change-tracking.test.js +62 -0
  54. package/dist/data/group-members.d.ts +6 -1
  55. package/dist/data/group-members.js +12 -4
  56. package/dist/data/groups.d.ts +6 -1
  57. package/dist/data/groups.js +12 -4
  58. package/dist/data/orm/sql.data-source.d.ts +9 -0
  59. package/dist/data/orm/sql.data-source.js +47 -2
  60. package/dist/data/orm/sql.data-source.test.js +142 -0
  61. package/dist/data/orm/table-container.js +9 -9
  62. package/dist/data/orm/table-container.test.js +44 -0
  63. package/dist/data/orm/types.d.ts +41 -1
  64. package/dist/data/orm/types.js +159 -28
  65. package/dist/data/orm/types.test.js +129 -0
  66. package/dist/data/package-versions.d.ts +5 -1
  67. package/dist/data/package-versions.js +11 -4
  68. package/dist/data/packages.d.ts +7 -1
  69. package/dist/data/packages.js +13 -4
  70. package/dist/data/peer-types.d.ts +8 -62
  71. package/dist/data/persistent-vars.d.ts +4 -1
  72. package/dist/data/persistent-vars.js +7 -2
  73. package/dist/data/table-signing.test.d.ts +1 -0
  74. package/dist/data/table-signing.test.js +118 -0
  75. package/dist/data/tools.d.ts +29 -221
  76. package/dist/data/users.d.ts +1 -2
  77. package/dist/data/users.js +8 -5
  78. package/dist/define/also-implements.test.d.ts +1 -0
  79. package/dist/define/also-implements.test.js +123 -0
  80. package/dist/define/context.d.ts +77 -0
  81. package/dist/define/context.js +2 -0
  82. package/dist/define/contract-ref.d.ts +177 -0
  83. package/dist/define/contract-ref.js +67 -0
  84. package/dist/define/declare-contract.d.ts +167 -0
  85. package/dist/define/declare-contract.js +220 -0
  86. package/dist/define/declare-contract.test.d.ts +1 -0
  87. package/dist/define/declare-contract.test.js +573 -0
  88. package/dist/define/define-package.d.ts +203 -0
  89. package/dist/define/define-package.js +289 -0
  90. package/dist/define/define.test.d.ts +1 -0
  91. package/dist/define/define.test.js +1010 -0
  92. package/dist/define/empty-fields.d.ts +15 -0
  93. package/dist/define/empty-fields.js +134 -0
  94. package/dist/define/empty-fields.test.d.ts +1 -0
  95. package/dist/define/empty-fields.test.js +55 -0
  96. package/dist/define/events-observables.test.d.ts +1 -0
  97. package/dist/define/events-observables.test.js +1013 -0
  98. package/dist/define/events.d.ts +81 -0
  99. package/dist/define/events.js +81 -0
  100. package/dist/define/generated-field.d.ts +20 -0
  101. package/dist/define/generated-field.js +25 -0
  102. package/dist/define/index.d.ts +28 -0
  103. package/dist/define/index.js +62 -0
  104. package/dist/define/lazy-members.d.ts +8 -0
  105. package/dist/define/lazy-members.js +38 -0
  106. package/dist/define/manifest.d.ts +63 -0
  107. package/dist/define/manifest.js +653 -0
  108. package/dist/define/observables.d.ts +146 -0
  109. package/dist/define/observables.js +187 -0
  110. package/dist/define/serve-members.d.ts +21 -0
  111. package/dist/define/serve-members.js +152 -0
  112. package/dist/define/serve-members.test.d.ts +1 -0
  113. package/dist/define/serve-members.test.js +630 -0
  114. package/dist/define/serve.d.ts +22 -0
  115. package/dist/define/serve.js +58 -0
  116. package/dist/define/system-contracts.d.ts +41 -0
  117. package/dist/define/system-contracts.js +21 -0
  118. package/dist/define/table-handle.d.ts +13 -0
  119. package/dist/define/table-handle.js +36 -0
  120. package/dist/define/table.d.ts +128 -0
  121. package/dist/define/table.js +122 -0
  122. package/dist/define/tool.d.ts +129 -0
  123. package/dist/define/tool.js +91 -0
  124. package/dist/define/vars.d.ts +82 -0
  125. package/dist/define/vars.js +256 -0
  126. package/dist/define/vars.test.d.ts +1 -0
  127. package/dist/define/vars.test.js +39 -0
  128. package/dist/define/wire-value.d.ts +12 -0
  129. package/dist/define/wire-value.js +76 -0
  130. package/dist/define/worker-state.d.ts +15 -0
  131. package/dist/define/worker-state.js +45 -0
  132. package/dist/device/tx-encoding.test.js +10 -0
  133. package/dist/index.d.ts +3 -0
  134. package/dist/index.js +5 -1
  135. package/dist/observable.d.ts +42 -2
  136. package/dist/observable.js +25 -7
  137. package/dist/package-build/build-package.d.ts +60 -0
  138. package/dist/package-build/build-package.js +312 -0
  139. package/dist/package-build/build-package.test.d.ts +1 -0
  140. package/dist/package-build/build-package.test.js +204 -0
  141. package/dist/package-build/cli.d.ts +7 -0
  142. package/dist/package-build/cli.js +218 -0
  143. package/dist/package-build/contract-add.d.ts +35 -0
  144. package/dist/package-build/contract-add.js +97 -0
  145. package/dist/package-build/contract-add.test.d.ts +1 -0
  146. package/dist/package-build/contract-add.test.js +467 -0
  147. package/dist/package-build/contract-source.d.ts +59 -0
  148. package/dist/package-build/contract-source.js +471 -0
  149. package/dist/package-build/freeze-contracts.d.ts +56 -0
  150. package/dist/package-build/freeze-contracts.js +198 -0
  151. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  152. package/dist/package-build/freeze-contracts.test.js +355 -0
  153. package/dist/package-installer/package-creator.d.ts +4 -0
  154. package/dist/package-installer/package-creator.js +57 -16
  155. package/dist/package-installer/package-creator.test.js +86 -0
  156. package/dist/package-loader/contract-members.d.ts +53 -0
  157. package/dist/package-loader/contract-members.js +31 -0
  158. package/dist/package-loader/contract-package-loader.js +5 -1
  159. package/dist/package-loader/contract-package-loader.test.js +11 -0
  160. package/dist/package-loader/host-authority-loading.test.js +9 -5
  161. package/dist/package-loader/index.d.ts +1 -0
  162. package/dist/package-loader/index.js +1 -0
  163. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  164. package/dist/package-loader/isolated-artifact.js +257 -51
  165. package/dist/package-loader/isolated-artifact.test.js +294 -21
  166. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  167. package/dist/package-loader/isolated-table-gateway.js +30 -11
  168. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  169. package/dist/package-loader/package-loader.d.ts +8 -1
  170. package/dist/package-loader/package-loader.js +39 -20
  171. package/dist/package-loader/package-loader.test.js +47 -0
  172. package/dist/peer-id.d.ts +25 -0
  173. package/dist/peer-id.js +99 -0
  174. package/dist/serial-json.compat.test.d.ts +1 -0
  175. package/dist/serial-json.compat.test.js +369 -0
  176. package/dist/serial-json.d.ts +41 -5
  177. package/dist/serial-json.js +216 -157
  178. package/dist/serial-json.oracle.d.ts +21 -0
  179. package/dist/serial-json.oracle.js +222 -0
  180. package/dist/serial-json.test.js +315 -0
  181. package/dist/testing/index.d.ts +4 -0
  182. package/dist/testing/index.js +8 -0
  183. package/dist/testing/mock-host-contract.d.ts +123 -0
  184. package/dist/testing/mock-host-contract.js +434 -0
  185. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  186. package/dist/testing/mock-host-contract.test.js +782 -0
  187. package/dist/types/field-type.d.ts +35 -18
  188. package/dist/types/field-type.js +4 -1
  189. package/dist/types/zod-types.d.ts +9 -0
  190. package/dist/types/zod-types.js +8 -2
  191. package/dist/utils.d.ts +1 -25
  192. package/dist/utils.js +10 -90
  193. package/package.json +9 -2
  194. 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)` and package-owned canonical aliases through `context.dispatchAction(alias, args)`. Isolated packages use `callConsumedTool`; all use the same runtime authorizer. Private aliases stay pinned to their owning implementation and target levels are read live on every call. Catalog `authorizedActions` is advisory; execution rechecks. Tool arguments never supply authority.
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
+ );
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "../dist/define/index.js",
3
+ "types": "../dist/define/index.d.ts"
4
+ }
@@ -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
- const VOICE_ACTION_CONTRACT_ID = "00muxgugn6xvs8f2r80ru205t";
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
- describe("strict voice/touch action transport in a group", () => {
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: VOICE_ACTION_CONTRACT_ID,
113
+ contractId: VOICE_HUB_CONTRACT_ID,
97
114
  version: 1,
115
+ forwardCallContext: true,
98
116
  tools: {
99
- invoke: tool("invoke", 20, (input, context) => context.dispatchAction?.(input.tool, input.args ?? {})),
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, { context });
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: VOICE_ACTION_CONTRACT_ID,
133
+ contractId: VOICE_HUB_CONTRACT_ID,
131
134
  version: 1,
132
- name: "Voice Skill",
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
- resolution,
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, including touch envelopes", async () => {
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: "add_item", args: { name: "milk" } })).resolves.toEqual({ result: "milk" });
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: "list_items" })).resolves.toEqual({
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: "add_item",
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, and ignores grants", async () => {
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: "reader_hop" })).rejects.toThrow("Permission denied");
181
- await expect(p.consumer.tools.invoke({ tool: "writer_hop" })).rejects.toThrow("Permission denied");
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: "writer_hop" })).rejects.toThrow("Permission denied");
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: "writer_self" })).rejects.toThrow("Permission denied");
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 after catalog projection", async () => {
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: "add_item" })).rejects.toThrow("Permission denied");
199
+ await expect(p.consumer.tools.invoke({ tool: "add" })).rejects.toThrow("Permission denied");
199
200
  p.setRole(group_member_roles_1.GroupMemberRole.Writer);
200
- if (!p.resolution.actionTools)
201
- throw new Error("Missing actions");
202
- p.resolution.actionTools.add.tool.accessLevel = 80;
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: "add_item" })).rejects.toThrow("Permission denied");
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: "list_items", args: { callerUserId: p.userId } })).rejects.toThrow("missing caller identity");
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 = tool("route", entryLevel, (_, context) => context.dispatchAction?.("act", {}));
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.each([
297
- [VOICE_ACTION_CONTRACT_ID, 1, "invoke"],
298
- ["00mskqamhdq1327mqehy58pps", 3, "voice-hub-run-action"],
299
- ])("honors declared preserve on %s v%s %s without raising an inherited ceiling", async (contractId, version, name) => {
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
- tools: {
306
- [name]: tool(name, 20, (_, context) => context.dispatchAction?.("write", {})),
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
- getDefinition: jest.fn((id, version) => id === contractId && version === 1 ? definition : undefined),
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.getDefinition).not.toHaveBeenCalled();
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.getDefinition).not.toHaveBeenCalled();
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.getDefinition.mockReturnValue((0, index_1.getPackagesContractDefinition)());
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 () => {