@peers-app/peers-sdk 0.26.6 → 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 (182) 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/orm/sql.data-source.d.ts +9 -0
  55. package/dist/data/orm/sql.data-source.js +47 -2
  56. package/dist/data/orm/sql.data-source.test.js +142 -0
  57. package/dist/data/orm/table-container.js +9 -9
  58. package/dist/data/orm/table-container.test.js +44 -0
  59. package/dist/data/orm/types.d.ts +41 -1
  60. package/dist/data/orm/types.js +159 -28
  61. package/dist/data/orm/types.test.js +129 -0
  62. package/dist/data/peer-types.d.ts +8 -62
  63. package/dist/data/persistent-vars.d.ts +4 -1
  64. package/dist/data/persistent-vars.js +7 -2
  65. package/dist/data/tools.d.ts +29 -221
  66. package/dist/define/also-implements.test.d.ts +1 -0
  67. package/dist/define/also-implements.test.js +123 -0
  68. package/dist/define/context.d.ts +77 -0
  69. package/dist/define/context.js +2 -0
  70. package/dist/define/contract-ref.d.ts +177 -0
  71. package/dist/define/contract-ref.js +67 -0
  72. package/dist/define/declare-contract.d.ts +167 -0
  73. package/dist/define/declare-contract.js +220 -0
  74. package/dist/define/declare-contract.test.d.ts +1 -0
  75. package/dist/define/declare-contract.test.js +573 -0
  76. package/dist/define/define-package.d.ts +203 -0
  77. package/dist/define/define-package.js +289 -0
  78. package/dist/define/define.test.d.ts +1 -0
  79. package/dist/define/define.test.js +1010 -0
  80. package/dist/define/empty-fields.d.ts +15 -0
  81. package/dist/define/empty-fields.js +134 -0
  82. package/dist/define/empty-fields.test.d.ts +1 -0
  83. package/dist/define/empty-fields.test.js +55 -0
  84. package/dist/define/events-observables.test.d.ts +1 -0
  85. package/dist/define/events-observables.test.js +1013 -0
  86. package/dist/define/events.d.ts +81 -0
  87. package/dist/define/events.js +81 -0
  88. package/dist/define/generated-field.d.ts +20 -0
  89. package/dist/define/generated-field.js +25 -0
  90. package/dist/define/index.d.ts +28 -0
  91. package/dist/define/index.js +62 -0
  92. package/dist/define/lazy-members.d.ts +8 -0
  93. package/dist/define/lazy-members.js +38 -0
  94. package/dist/define/manifest.d.ts +63 -0
  95. package/dist/define/manifest.js +653 -0
  96. package/dist/define/observables.d.ts +146 -0
  97. package/dist/define/observables.js +187 -0
  98. package/dist/define/serve-members.d.ts +21 -0
  99. package/dist/define/serve-members.js +152 -0
  100. package/dist/define/serve-members.test.d.ts +1 -0
  101. package/dist/define/serve-members.test.js +630 -0
  102. package/dist/define/serve.d.ts +22 -0
  103. package/dist/define/serve.js +58 -0
  104. package/dist/define/system-contracts.d.ts +41 -0
  105. package/dist/define/system-contracts.js +21 -0
  106. package/dist/define/table-handle.d.ts +13 -0
  107. package/dist/define/table-handle.js +36 -0
  108. package/dist/define/table.d.ts +128 -0
  109. package/dist/define/table.js +122 -0
  110. package/dist/define/tool.d.ts +129 -0
  111. package/dist/define/tool.js +91 -0
  112. package/dist/define/vars.d.ts +82 -0
  113. package/dist/define/vars.js +256 -0
  114. package/dist/define/vars.test.d.ts +1 -0
  115. package/dist/define/vars.test.js +39 -0
  116. package/dist/define/wire-value.d.ts +12 -0
  117. package/dist/define/wire-value.js +76 -0
  118. package/dist/define/worker-state.d.ts +15 -0
  119. package/dist/define/worker-state.js +45 -0
  120. package/dist/device/tx-encoding.test.js +10 -0
  121. package/dist/index.d.ts +3 -0
  122. package/dist/index.js +5 -1
  123. package/dist/observable.d.ts +42 -2
  124. package/dist/observable.js +25 -7
  125. package/dist/package-build/build-package.d.ts +60 -0
  126. package/dist/package-build/build-package.js +312 -0
  127. package/dist/package-build/build-package.test.d.ts +1 -0
  128. package/dist/package-build/build-package.test.js +204 -0
  129. package/dist/package-build/cli.d.ts +7 -0
  130. package/dist/package-build/cli.js +218 -0
  131. package/dist/package-build/contract-add.d.ts +35 -0
  132. package/dist/package-build/contract-add.js +97 -0
  133. package/dist/package-build/contract-add.test.d.ts +1 -0
  134. package/dist/package-build/contract-add.test.js +467 -0
  135. package/dist/package-build/contract-source.d.ts +59 -0
  136. package/dist/package-build/contract-source.js +471 -0
  137. package/dist/package-build/freeze-contracts.d.ts +56 -0
  138. package/dist/package-build/freeze-contracts.js +198 -0
  139. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  140. package/dist/package-build/freeze-contracts.test.js +355 -0
  141. package/dist/package-installer/package-creator.d.ts +4 -0
  142. package/dist/package-installer/package-creator.js +57 -16
  143. package/dist/package-installer/package-creator.test.js +86 -0
  144. package/dist/package-loader/contract-members.d.ts +53 -0
  145. package/dist/package-loader/contract-members.js +31 -0
  146. package/dist/package-loader/contract-package-loader.js +5 -1
  147. package/dist/package-loader/contract-package-loader.test.js +11 -0
  148. package/dist/package-loader/host-authority-loading.test.js +9 -5
  149. package/dist/package-loader/index.d.ts +1 -0
  150. package/dist/package-loader/index.js +1 -0
  151. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  152. package/dist/package-loader/isolated-artifact.js +257 -51
  153. package/dist/package-loader/isolated-artifact.test.js +294 -21
  154. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  155. package/dist/package-loader/isolated-table-gateway.js +30 -11
  156. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  157. package/dist/package-loader/package-loader.d.ts +8 -1
  158. package/dist/package-loader/package-loader.js +39 -20
  159. package/dist/package-loader/package-loader.test.js +47 -0
  160. package/dist/peer-id.d.ts +25 -0
  161. package/dist/peer-id.js +99 -0
  162. package/dist/serial-json.compat.test.d.ts +1 -0
  163. package/dist/serial-json.compat.test.js +369 -0
  164. package/dist/serial-json.d.ts +41 -5
  165. package/dist/serial-json.js +216 -157
  166. package/dist/serial-json.oracle.d.ts +21 -0
  167. package/dist/serial-json.oracle.js +222 -0
  168. package/dist/serial-json.test.js +315 -0
  169. package/dist/testing/index.d.ts +4 -0
  170. package/dist/testing/index.js +8 -0
  171. package/dist/testing/mock-host-contract.d.ts +123 -0
  172. package/dist/testing/mock-host-contract.js +434 -0
  173. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  174. package/dist/testing/mock-host-contract.test.js +782 -0
  175. package/dist/types/field-type.d.ts +35 -18
  176. package/dist/types/field-type.js +4 -1
  177. package/dist/types/zod-types.d.ts +9 -0
  178. package/dist/types/zod-types.js +8 -2
  179. package/dist/utils.d.ts +1 -25
  180. package/dist/utils.js +10 -90
  181. package/package.json +9 -2
  182. package/testing/package.json +4 -0
@@ -1,10 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sameUserContractPermissionCheck = exports.CONTRACT_CALL_RPC = void 0;
4
+ exports.createContractConsumerMembers = createContractConsumerMembers;
4
5
  exports.createContractConsumer = createContractConsumer;
5
6
  exports.createRemoteContractConsumer = createRemoteContractConsumer;
6
- exports.validateContractActions = validateContractActions;
7
- exports.authorizedContractActionNames = authorizedContractActionNames;
8
7
  exports.createContractProvider = createContractProvider;
9
8
  exports.createContractProviderEndpoint = createContractProviderEndpoint;
10
9
  exports.createContractProviderSession = createContractProviderSession;
@@ -48,40 +47,16 @@ const FORBIDDEN_REMOTE_TABLE_PROPERTIES = new Set([
48
47
  */
49
48
  exports.CONTRACT_CALL_RPC = contract_transport_1.CONTRACT_REQUEST_CHANNEL;
50
49
  /**
51
- * Build the **consumer** side of a contract proxy from a contract definition and a
52
- * transport. Member names come from the contract shape; table members are backed by a
53
- * `Proxy` that forwards accessed method names. The provider accepts only standard data
54
- * operations and custom methods marked by `ProxyClientTableMethodCalls`.
55
- *
56
- * Every access is turned into an {@link IContractCall} sent on the transport's fixed
57
- * {@link CONTRACT_REQUEST_CHANNEL}; the resolved ack is the provider's return value.
58
- *
59
- * Provider-only tables are omitted from {@link IContractConsumer.tables}; consumers must
60
- * call a declared provider method instead of raw CRUD.
61
- *
62
- * The consumer also subscribes to provider pushes: `events[name].subscribe(handler)` and
63
- * `tables[name].dataChanged.subscribe(handler)` both send a `"subscribe"` request and
64
- * demultiplex provider pushes arriving on {@link CONTRACT_NOTIFY_CHANNEL} by their
65
- * `subscriptionId` (shared `subscribeVia` path). Observables use the same push channel but
66
- * are *not* gated: each `observables[name]` is a live {@link Observable} mirror that subscribes
67
- * implicitly at creation, loads an initial snapshot (awaitable via
68
- * {@link IContractConsumer.loadingPromise}), and streams every subsequent change; local writes
69
- * send a fire-and-forget `set`. Call {@link IContractConsumer.dispose} to remove the notify
70
- * listener and unsubscribe every stream. Read-only observable writes are rejected on both
71
- * consumer and provider boundaries. Disposal is idempotent and prevents new remote operations;
72
- * cached observable reads remain available afterward.
50
+ * Build the member builders for one contract over `transport`. See
51
+ * {@link IContractConsumerMembers}.
73
52
  *
74
- * To run tools on another device use {@link createRemoteContractConsumer} instead.
75
- *
76
- * @param contract The contract definition (shape) being consumed.
77
- * @param transport Bidirectional channel to the provider; requests go out on
78
- * {@link CONTRACT_REQUEST_CHANNEL}, notifications arrive on {@link CONTRACT_NOTIFY_CHANNEL}.
79
- * @param opts.dataContextId Optional data context to scope every call to.
80
- * @param opts.providerPackageId Optional provider to address. The local host
81
- * accepts it only when that package is registered for this contract version.
53
+ * @param contractId The contract's id.
54
+ * @param version The contract's version.
55
+ * @param transport Bidirectional channel to the provider.
56
+ * @param opts.dataContextId Optional data context to scope every call to.
57
+ * @param opts.providerPackageId Optional provider to address.
82
58
  */
83
- function createContractConsumer(contract, transport, opts = {}) {
84
- const { contractId, version } = contract;
59
+ function createContractConsumerMembers(contractId, version, transport, opts = {}) {
85
60
  const { dataContextId, providerPackageId } = opts;
86
61
  let disposed = false;
87
62
  const emitRequest = (call) => transport.emit(contract_transport_1.CONTRACT_REQUEST_CHANNEL, {
@@ -140,6 +115,7 @@ function createContractConsumer(contract, transport, opts = {}) {
140
115
  }
141
116
  };
142
117
  const subscription = {
118
+ ready: subscribeAck,
143
119
  unsubscribe: () => {
144
120
  if (!active) {
145
121
  return false;
@@ -158,13 +134,11 @@ function createContractConsumer(contract, transport, opts = {}) {
158
134
  subscriptions.add(subscription);
159
135
  return subscription;
160
136
  };
161
- const tables = {};
162
- for (const table of contract.tables) {
163
- const name = table.name;
137
+ const table = (name) => {
164
138
  // `dataChanged` is a subscribable that rides the notify channel (created lazily and
165
139
  // cached); every other accessed name forwards as a remote method call.
166
140
  let dataChanged;
167
- tables[name] = new Proxy({}, {
141
+ return new Proxy({}, {
168
142
  get(_target, operation) {
169
143
  if (typeof operation !== "string") {
170
144
  return undefined;
@@ -196,39 +170,28 @@ function createContractConsumer(contract, transport, opts = {}) {
196
170
  });
197
171
  },
198
172
  });
199
- }
200
- const tools = {};
201
- for (const tool of contract.tools) {
202
- const name = tool.name;
203
- tools[name] = (...args) => request({
173
+ };
174
+ const tool = (name) => (...args) => request({
175
+ contractId,
176
+ version,
177
+ member: "tool",
178
+ name,
179
+ operation: "run",
180
+ args,
181
+ dataContextId,
182
+ });
183
+ const event = (name) => ({
184
+ subscribe: subscribeVia((subscriptionId, operation) => ({
204
185
  contractId,
205
186
  version,
206
- member: "tool",
187
+ member: "event",
207
188
  name,
208
- operation: "run",
209
- args,
189
+ operation,
190
+ subscriptionId,
191
+ args: [],
210
192
  dataContextId,
211
- });
212
- }
213
- const events = {};
214
- for (const evt of contract.events ?? []) {
215
- const name = evt.name;
216
- events[name] = {
217
- subscribe: subscribeVia((subscriptionId, operation) => ({
218
- contractId,
219
- version,
220
- member: "event",
221
- name,
222
- operation,
223
- subscriptionId,
224
- args: [],
225
- dataContextId,
226
- })),
227
- };
228
- }
229
- // Observables are backed by real, callable Observable mirrors (see below).
230
- const observables = {};
231
- const observableLoads = [];
193
+ })),
194
+ });
232
195
  const dispose = () => {
233
196
  if (disposed) {
234
197
  return;
@@ -243,32 +206,30 @@ function createContractConsumer(contract, transport, opts = {}) {
243
206
  offNotify();
244
207
  notifyHandlers.clear();
245
208
  };
246
- const consumer = {
209
+ const setObservable = (name, value) => request({
247
210
  contractId,
248
211
  version,
249
- tables,
250
- tools,
251
- observables,
252
- events,
253
- loadingPromise: Promise.resolve(),
254
- dispose,
255
- };
256
- // Build each observable as a locally-mirrored Observable kept current by provider pushes
212
+ member: "observable",
213
+ name,
214
+ operation: "set",
215
+ args: [value],
216
+ dataContextId,
217
+ });
218
+ // Each observable is a locally-mirrored Observable kept current by provider pushes
257
219
  // (decision 4: observables are always streamed so reads are synchronous). Writes are
258
- // fire-and-forget and chained onto the proxy-level loadingPromise for awaitable durability
259
- // — the same shape as a persistent var (see data/persistent-vars.ts).
260
- for (const obs of contract.observables) {
261
- const name = obs.name;
220
+ // fire-and-forget, ordered by the caller's `chainWrite` — the same shape as a persistent var
221
+ // (see data/persistent-vars.ts).
222
+ const observableMirror = (name, writable, chainWrite) => {
262
223
  const mirror = (0, observable_1.observable)();
263
224
  // Guards against echo loops: while applying a provider push we must not re-`set` it, and
264
225
  // once a push has updated the mirror a later (possibly stale) initial `get` is ignored.
265
226
  let applyingRemote = false;
266
- let pushed = false;
227
+ let revision = 0;
267
228
  const applyRemote = (value) => {
268
229
  if (disposed) {
269
230
  return;
270
231
  }
271
- pushed = true;
232
+ revision++;
272
233
  applyingRemote = true;
273
234
  try {
274
235
  mirror(value);
@@ -277,9 +238,18 @@ function createContractConsumer(contract, transport, opts = {}) {
277
238
  applyingRemote = false;
278
239
  }
279
240
  };
241
+ const get = () => request({
242
+ contractId,
243
+ version,
244
+ member: "observable",
245
+ name,
246
+ operation: "get",
247
+ args: [],
248
+ dataContextId,
249
+ });
280
250
  // Implicit, always-on subscription (not gated). `subscribeVia` registers the handler
281
251
  // before asking the provider to start streaming, so no push can arrive unhandled.
282
- subscribeVia((subscriptionId, operation) => ({
252
+ const subscription = subscribeVia((subscriptionId, operation) => ({
283
253
  contractId,
284
254
  version,
285
255
  member: "observable",
@@ -299,7 +269,7 @@ function createContractConsumer(contract, transport, opts = {}) {
299
269
  if (disposed) {
300
270
  throw new Error(`Contract consumer ${contractId} v${version} is disposed`);
301
271
  }
302
- if (!obs.writable) {
272
+ if (!writable) {
303
273
  throw new Error(`Observable '${name}' is read-only`);
304
274
  }
305
275
  return mirror(args[0]);
@@ -309,39 +279,104 @@ function createContractConsumer(contract, transport, opts = {}) {
309
279
  exposedMirror.subscriberCount = mirror.subscriberCount;
310
280
  // Initial snapshot so the mirror has a value to read synchronously. A push that arrived
311
281
  // first wins — the snapshot may already be stale by the time it resolves.
312
- observableLoads.push(request({
313
- contractId,
314
- version,
315
- member: "observable",
316
- name,
317
- operation: "get",
318
- args: [],
319
- dataContextId,
320
- })
321
- .then((value) => {
322
- if (!disposed && !pushed) {
282
+ const loaded = subscription.ready
283
+ .then(async () => {
284
+ if (disposed)
285
+ return;
286
+ const value = await get();
287
+ if (!disposed && revision === 0) {
323
288
  applyRemote(value);
324
289
  }
325
290
  })
326
- .catch((err) => console.error("Contract observable initial get failed", err)));
327
- // Local writes → fire-and-forget `set`, chained onto loadingPromise for durability.
291
+ .catch((err) => console.error("Contract observable initial get failed", err));
292
+ // Local writes → `set` through the caller's write chain.
328
293
  observableWriteSubscriptions.push(mirror.subscribe((value) => {
329
294
  if (applyingRemote || disposed) {
330
295
  return;
331
296
  }
332
- consumer.loadingPromise = consumer.loadingPromise.then(() => request({
333
- contractId,
334
- version,
335
- member: "observable",
336
- name,
337
- operation: "set",
338
- args: [value],
339
- dataContextId,
340
- })
341
- .then(() => undefined)
342
- .catch((err) => console.error("Contract observable set failed", err)));
297
+ revision++;
298
+ chainWrite(() => setObservable(name, value).then(() => undefined));
343
299
  }));
344
- observables[name] = exposedMirror;
300
+ const refresh = () => {
301
+ const before = revision;
302
+ return get()
303
+ .then((value) => {
304
+ if (revision === before)
305
+ applyRemote(value);
306
+ })
307
+ .catch((err) => console.error("Contract observable get failed", err));
308
+ };
309
+ return { observable: exposedMirror, loaded, refresh };
310
+ };
311
+ return { table, tool, event, observable: observableMirror, setObservable, dispose };
312
+ }
313
+ /**
314
+ * Build the **consumer** side of a contract proxy from a contract definition and a
315
+ * transport. Member names come from the contract shape; table members are backed by a
316
+ * `Proxy` that forwards accessed method names. The provider accepts only standard data
317
+ * operations and custom methods marked by `ProxyClientTableMethodCalls`.
318
+ *
319
+ * Every access is turned into an {@link IContractCall} sent on the transport's fixed
320
+ * {@link CONTRACT_REQUEST_CHANNEL}; the resolved ack is the provider's return value.
321
+ *
322
+ * Provider-only tables are omitted from {@link IContractConsumer.tables}; consumers must
323
+ * call a declared provider method instead of raw CRUD.
324
+ *
325
+ * The consumer also subscribes to provider pushes: `events[name].subscribe(handler)` and
326
+ * `tables[name].dataChanged.subscribe(handler)` both send a `"subscribe"` request and
327
+ * demultiplex provider pushes arriving on {@link CONTRACT_NOTIFY_CHANNEL} by their
328
+ * `subscriptionId` (shared `subscribeVia` path). Observables use the same push channel but
329
+ * are *not* gated: each `observables[name]` is a live {@link Observable} mirror that subscribes
330
+ * implicitly at creation, loads an initial snapshot (awaitable via
331
+ * {@link IContractConsumer.loadingPromise}), and streams every subsequent change; local writes
332
+ * send a fire-and-forget `set`. Call {@link IContractConsumer.dispose} to remove the notify
333
+ * listener and unsubscribe every stream. Read-only observable writes are rejected on both
334
+ * consumer and provider boundaries. Disposal is idempotent and prevents new remote operations;
335
+ * cached observable reads remain available afterward.
336
+ *
337
+ * To run tools on another device use {@link createRemoteContractConsumer} instead.
338
+ *
339
+ * @param contract The contract definition (shape) being consumed.
340
+ * @param transport Bidirectional channel to the provider; requests go out on
341
+ * {@link CONTRACT_REQUEST_CHANNEL}, notifications arrive on {@link CONTRACT_NOTIFY_CHANNEL}.
342
+ * @param opts.dataContextId Optional data context to scope every call to.
343
+ * @param opts.providerPackageId Optional provider to address. The local host
344
+ * accepts it only when that package is registered for this contract version.
345
+ */
346
+ function createContractConsumer(contract, transport, opts = {}) {
347
+ const { contractId, version } = contract;
348
+ const members = createContractConsumerMembers(contractId, version, transport, opts);
349
+ const tables = {};
350
+ for (const table of contract.tables) {
351
+ tables[table.name] = members.table(table.name);
352
+ }
353
+ const tools = {};
354
+ for (const tool of contract.tools) {
355
+ tools[tool.name] = members.tool(tool.name);
356
+ }
357
+ const events = {};
358
+ for (const evt of contract.events ?? []) {
359
+ events[evt.name] = members.event(evt.name);
360
+ }
361
+ const observables = {};
362
+ const consumer = {
363
+ contractId,
364
+ version,
365
+ tables,
366
+ tools,
367
+ observables,
368
+ events,
369
+ loadingPromise: Promise.resolve(),
370
+ dispose: members.dispose,
371
+ };
372
+ // Writes are chained onto the proxy-level loadingPromise for awaitable durability.
373
+ const observableLoads = [];
374
+ for (const obs of contract.observables) {
375
+ const { observable: mirror, loaded } = members.observable(obs.name, Boolean(obs.writable), (send) => {
376
+ consumer.loadingPromise = consumer.loadingPromise.then(() => send().catch((err) => console.error("Contract observable set failed", err)));
377
+ });
378
+ observables[obs.name] = mirror;
379
+ observableLoads.push(loaded);
345
380
  }
346
381
  consumer.loadingPromise = Promise.all(observableLoads).then(() => undefined);
347
382
  return consumer;
@@ -502,70 +537,6 @@ async function assertContractToolAccess(tool, call, context) {
502
537
  : access_level_1.AccessLevel.None;
503
538
  return (0, action_dispatch_1.authorizeToolDispatch)({ ...context.authority, userId, userAccessLevel: userLevel }, required, mode, strict);
504
539
  }
505
- /** Validate private alias registrations before exposing any action wrapper. */
506
- function validateContractActions(resolution) {
507
- for (const [name, target] of Object.entries(resolution.actionAliases ?? {})) {
508
- const tool = resolution.actionTools?.[target];
509
- if (!name || !tool || tool.tool.name !== target || target === resolution.actionDispatcher)
510
- throw new Error(`Unregistered action target: ${name} -> ${target}`);
511
- }
512
- }
513
- /**
514
- * Project executable aliases using live tool metadata and trusted caller identity.
515
- *
516
- * Catalog filtering is advisory; execution rechecks every target.
517
- */
518
- async function authorizedContractActionNames(resolution, context) {
519
- validateContractActions(resolution);
520
- if (!(context.callerUserId ?? context.localUserId) || !context.dataContext)
521
- return [];
522
- const wrapperName = resolution.actionDispatcher ??
523
- Object.keys(resolution.tools ?? {}).find((name) => resolution.tools?.[name].tool.authorityPropagation === "preserve");
524
- const wrapper = wrapperName ? resolution.tools?.[wrapperName] : undefined;
525
- if (!wrapper)
526
- return [];
527
- let wrapperAuthority;
528
- try {
529
- wrapperAuthority = await assertContractToolAccess(wrapper, {
530
- contractId: resolution.contractId,
531
- version: resolution.version,
532
- member: "tool",
533
- name: wrapperName,
534
- operation: "run",
535
- args: [],
536
- }, context);
537
- }
538
- catch {
539
- return [];
540
- }
541
- const allowed = [];
542
- for (const [name, target] of Object.entries(resolution.actionAliases ?? {})) {
543
- try {
544
- const action = resolution.actionTools?.[target];
545
- if (!action)
546
- continue;
547
- await assertContractToolAccess(action, {
548
- contractId: resolution.contractId,
549
- version: resolution.version,
550
- member: "tool",
551
- name: target,
552
- operation: "run",
553
- args: [],
554
- }, {
555
- ...context,
556
- authority: {
557
- ...wrapperAuthority,
558
- strictActionScope: true,
559
- },
560
- });
561
- allowed.push(name);
562
- }
563
- catch {
564
- /* Denied aliases are not executable. */
565
- }
566
- }
567
- return allowed;
568
- }
569
540
  /**
570
541
  * Build the **provider** side of a contract proxy: an async handler that permission-checks
571
542
  * an {@link IContractCall} and dispatches it to the live table method / tool fn / observable
@@ -580,7 +551,6 @@ async function authorizedContractActionNames(resolution, context) {
580
551
  */
581
552
  function createContractProvider(resolution, opts = {}) {
582
553
  const { permissionCheck } = opts;
583
- validateContractActions(resolution);
584
554
  return async (call, context = {}) => {
585
555
  if (call.contractId !== resolution.contractId || call.version !== resolution.version) {
586
556
  throw new Error(`Contract mismatch: resolution provides ${resolution.contractId} v${resolution.version} ` +
@@ -622,13 +592,10 @@ function createContractProvider(resolution, opts = {}) {
622
592
  if (!tool) {
623
593
  throw new Error(`Contract ${call.contractId} does not provide tool '${call.name}'`);
624
594
  }
625
- if (resolution.actionTools && !Object.keys(resolution.actionAliases ?? {}).length)
626
- throw new Error("Action dispatcher requires registered checked actions");
627
595
  const { workflow, ...caller } = context;
628
596
  const authority = await assertContractToolAccess(tool, call, caller);
629
597
  if (resolution.toolContext ||
630
598
  resolution.forwardCallContext ||
631
- resolution.actionAliases ||
632
599
  authority.strictActionScope) {
633
600
  if (call.args.length > 1) {
634
601
  throw new Error("Permission denied: tool runs accept a single argument when host context is forwarded");
@@ -654,29 +621,6 @@ function createContractProvider(resolution, opts = {}) {
654
621
  authority: (0, action_dispatch_1.narrowActionPrincipal)(frame, true),
655
622
  });
656
623
  };
657
- const checkedActionDispatcher = (frame, capabilities) => async (name, args) => {
658
- const target = resolution.actionAliases?.[name];
659
- const action = target ? resolution.actionTools?.[target] : undefined;
660
- if (!target || !action)
661
- throw new Error(`Unknown action selector: ${name}`);
662
- const actionCall = { ...call, name: target, args: [args] };
663
- const actionContext = {
664
- ...caller,
665
- authority: { ...frame, strictActionScope: true },
666
- };
667
- const checked = await assertContractToolAccess(action, actionCall, actionContext);
668
- return action.toolFn(args, {
669
- ...capabilities,
670
- workflow: undefined,
671
- callerPackageId: caller.callerPackageId,
672
- actorUserId: checked.userId,
673
- authority: checked,
674
- callContext: { ...caller, authority: checked },
675
- incomingCallContext: actionContext,
676
- dispatchAction: checkedActionDispatcher(checked, capabilities),
677
- dispatchTool: dispatchTool(checked),
678
- });
679
- };
680
624
  return tool.toolFn(call.args[0], {
681
625
  ...base,
682
626
  workflow: caller.callerDeviceId || caller.callerPackageId ? undefined : workflow,
@@ -686,9 +630,6 @@ function createContractProvider(resolution, opts = {}) {
686
630
  incomingCallContext: caller,
687
631
  dispatchTool: dispatchTool(authority),
688
632
  authority,
689
- dispatchAction: resolution.actionAliases
690
- ? checkedActionDispatcher(authority, base)
691
- : undefined,
692
633
  });
693
634
  }
694
635
  return tool.toolFn(...call.args);
@@ -102,10 +102,12 @@ export declare function isLocalOnlyContractId(contractId: string): boolean;
102
102
  * `TrustLevel.Self`) keeps full access; an omitted data context selects the provider's
103
103
  * personal context, and a named one must be a group this device already has.
104
104
  * 4. Any other verified caller is treated as a **group member**: the call must name a
105
- * group the provider belongs to, must be a tool run, the contract definition registered
106
- * for that group must mark the tool with {@link IContractTool.remoteAccessLevel}, and
107
- * the caller's role in that group must meet it. Missing loader, definition, or level
108
- * fails closed. Catalog readiness precedes reading the group's definitions.
105
+ * group the provider belongs to, must be a tool run, the selected provider's definition
106
+ * must mark the tool with {@link IContractTool.remoteAccessLevel}, and
107
+ * the caller's role in that group must meet it. For a version the provider also implements,
108
+ * the requested version must declare the tool and the implementing contract's tool gives the
109
+ * level. Missing loader, definition, or level fails closed. Catalog readiness precedes
110
+ * reading the group's definitions.
109
111
  *
110
112
  * @param call Inbound call.
111
113
  * @param userContext Provider's user context.
@@ -241,10 +241,12 @@ function isLocalOnlyContractId(contractId) {
241
241
  * `TrustLevel.Self`) keeps full access; an omitted data context selects the provider's
242
242
  * personal context, and a named one must be a group this device already has.
243
243
  * 4. Any other verified caller is treated as a **group member**: the call must name a
244
- * group the provider belongs to, must be a tool run, the contract definition registered
245
- * for that group must mark the tool with {@link IContractTool.remoteAccessLevel}, and
246
- * the caller's role in that group must meet it. Missing loader, definition, or level
247
- * fails closed. Catalog readiness precedes reading the group's definitions.
244
+ * group the provider belongs to, must be a tool run, the selected provider's definition
245
+ * must mark the tool with {@link IContractTool.remoteAccessLevel}, and
246
+ * the caller's role in that group must meet it. For a version the provider also implements,
247
+ * the requested version must declare the tool and the implementing contract's tool gives the
248
+ * level. Missing loader, definition, or level fails closed. Catalog readiness precedes
249
+ * reading the group's definitions.
248
250
  *
249
251
  * @param call Inbound call.
250
252
  * @param userContext Provider's user context.
@@ -284,8 +286,28 @@ async function authorizeRemoteContractCall(call, userContext, remoteIdentity) {
284
286
  }
285
287
  const dataContext = userContext.getDataContext(dataContextId);
286
288
  await dataContext.packageLoader?.catalogReady;
287
- const definition = dataContext.packageLoader?.contractRegistry?.getDefinition(call.contractId, call.version);
288
- const tool = definition?.tools.find((entry) => entry.name === call.name);
289
+ const callerLevel = await (0, group_permissions_1.getUserAccessLevel)(dataContext, remoteIdentity.userId);
290
+ // Policy belongs to the executing provider, not the latest registration of this shape.
291
+ // Select after the async role lookup and retain the selection through endpoint resolution.
292
+ const registry = dataContext.packageLoader?.contractRegistry;
293
+ const selected = registry?.selectProvider({
294
+ contractId: call.contractId,
295
+ version: call.version,
296
+ });
297
+ // A version the provider also implements runs its implementing contract's tool, so that tool's
298
+ // policy decides; the requested version must still declare the tool.
299
+ const implementation = selected &&
300
+ registry?.getProviderImplementation(call.contractId, call.version, selected.providerPackageId);
301
+ const running = implementation &&
302
+ registry?.selectProvider({
303
+ contractId: implementation.contractId,
304
+ version: implementation.version,
305
+ providerPackageIds: [selected.providerPackageId],
306
+ });
307
+ const declared = selected?.definition.tools.some((entry) => entry.name === call.name);
308
+ const tool = declared
309
+ ? running?.definition.tools.find((entry) => entry.name === call.name)
310
+ : undefined;
289
311
  if (!tool) {
290
312
  throw permissionDenied(call, "tool is not provided in this data context");
291
313
  }
@@ -293,12 +315,12 @@ async function authorizeRemoteContractCall(call, userContext, remoteIdentity) {
293
315
  throw permissionDenied(call, "tool is not remotely callable");
294
316
  }
295
317
  const requiredLevel = (0, access_level_1.resolveAccessLevel)(tool.remoteAccessLevel);
296
- const callerLevel = await (0, group_permissions_1.getUserAccessLevel)(dataContext, remoteIdentity.userId);
297
318
  if (callerLevel <= access_level_1.AccessLevel.None || callerLevel < requiredLevel) {
298
319
  throw permissionDenied(call, `requires ${(0, access_level_1.getAccessLevelLabel)(requiredLevel)} (${requiredLevel}), caller has ${(0, access_level_1.getAccessLevelLabel)(callerLevel)} (${callerLevel})`);
299
320
  }
300
321
  return {
301
322
  dataContextId,
323
+ providerPackageId: selected?.providerPackageId,
302
324
  context: {
303
325
  callerUserId: remoteIdentity.userId,
304
326
  localUserId: userContext.userId,
@@ -509,7 +531,8 @@ async function handleContractCall(call, opts) {
509
531
  const endpoint = await dataContext.packageLoader.resolveContractEndpoint({
510
532
  contractId: call.contractId,
511
533
  version: call.version,
512
- providerPackageId: await getActiveProviderPackageId(dataContext, call.contractId, call.version),
534
+ providerPackageId: authorization.providerPackageId ??
535
+ (await getActiveProviderPackageId(dataContext, call.contractId, call.version)),
513
536
  dataContextId: authorization.dataContextId,
514
537
  context: authorization.context ?? {},
515
538
  notify: async () => undefined,
@@ -521,7 +544,7 @@ async function handleContractCall(call, opts) {
521
544
  endpoint.dispose();
522
545
  }
523
546
  }
524
- const resolver = getContractResolver(call.contractId, call.version, undefined, dataContext);
547
+ const resolver = getContractResolver(call.contractId, call.version, authorization.providerPackageId, dataContext);
525
548
  if (!resolver) {
526
549
  throw new Error(`No contract provider registered for ${call.contractId} v${call.version}`);
527
550
  }
@@ -0,0 +1,59 @@
1
+ import type { IContractDefinition } from "./types";
2
+ /** The file, at a package's root, that records the contract versions its stable releases froze. */
3
+ export declare const FROZEN_CONTRACTS_FILE = "frozen-contracts.json";
4
+ /** One contract version a stable release froze: its shape as released, and that release. */
5
+ export interface IFrozenContract extends Omit<IContractDefinition, "devTag"> {
6
+ /** The package version whose stable release froze it. */
7
+ frozenBy: string;
8
+ }
9
+ /** What `frozen-contracts.json` holds. */
10
+ export interface IFrozenContracts {
11
+ /** What the file is, for someone who opens it. */
12
+ about?: string;
13
+ /** Every frozen contract version, by contract id and version. */
14
+ contracts: IFrozenContract[];
15
+ }
16
+ /** A frozen contract version the package still provides, but with another shape. */
17
+ export interface IFrozenContractChange {
18
+ /** The contract version as frozen. */
19
+ contract: IFrozenContract;
20
+ /** What changed, one member per line (`tool add-item changed`). */
21
+ changes: string[];
22
+ }
23
+ /** What {@link checkFrozenContracts} found. */
24
+ export interface IFrozenContractsCheck {
25
+ /** Frozen contract versions the package provides with a different shape. */
26
+ changed: IFrozenContractChange[];
27
+ /** Frozen contract versions the package no longer provides (allowed; a consumer may miss it). */
28
+ notProvided: IFrozenContract[];
29
+ }
30
+ /**
31
+ * Check the contracts a package provides against the versions its stable releases froze.
32
+ *
33
+ * @param frozen - The package's `frozen-contracts.json`.
34
+ * @param provided - The contracts the package provides now. A `devTag` is ignored.
35
+ * @returns The frozen versions it changed, and those it no longer provides.
36
+ * @throws When the package provides the same contract identity more than once.
37
+ */
38
+ export declare function checkFrozenContracts(frozen: IFrozenContracts, provided: readonly IContractDefinition[]): IFrozenContractsCheck;
39
+ /**
40
+ * Record every contract version a release provides as frozen by that release, keeping the
41
+ * versions already frozen. Refuses (records nothing) when the release changed a frozen version.
42
+ *
43
+ * @param frozen - The package's `frozen-contracts.json`, or undefined when it has none yet.
44
+ * @param provided - The contracts the release provides.
45
+ * @param packageVersion - The release's package version.
46
+ * @returns The file's new content, the versions it added, and the check that decided.
47
+ * @throws When the package version is blank or provided contract identities repeat.
48
+ */
49
+ export declare function freezeContracts(frozen: IFrozenContracts | undefined, provided: readonly IContractDefinition[], packageVersion: string): {
50
+ frozen: IFrozenContracts;
51
+ added: IFrozenContract[];
52
+ check: IFrozenContractsCheck;
53
+ };
54
+ /**
55
+ * Describe a check's failures, one line per changed contract version, for a build or test error.
56
+ *
57
+ * @param check - What {@link checkFrozenContracts} found.
58
+ */
59
+ export declare function describeFrozenContractChanges(check: IFrozenContractsCheck): string[];