@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
|
@@ -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
|
|
52
|
-
*
|
|
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
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* @param
|
|
77
|
-
* @param
|
|
78
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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: "
|
|
187
|
+
member: "event",
|
|
207
188
|
name,
|
|
208
|
-
operation
|
|
209
|
-
|
|
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
|
|
209
|
+
const setObservable = (name, value) => request({
|
|
247
210
|
contractId,
|
|
248
211
|
version,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
259
|
-
//
|
|
260
|
-
|
|
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
|
|
227
|
+
let revision = 0;
|
|
267
228
|
const applyRemote = (value) => {
|
|
268
229
|
if (disposed) {
|
|
269
230
|
return;
|
|
270
231
|
}
|
|
271
|
-
|
|
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 (!
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 →
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
*
|
|
107
|
-
* the caller's role in that group must meet it.
|
|
108
|
-
*
|
|
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
|
|
245
|
-
*
|
|
246
|
-
* the caller's role in that group must meet it.
|
|
247
|
-
*
|
|
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
|
|
288
|
-
|
|
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:
|
|
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,
|
|
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[];
|