@peers-app/peers-sdk 0.23.9 → 0.23.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/user-context-singleton.js +19 -4
- package/dist/contracts/__tests__/builder.test.js +25 -0
- package/dist/contracts/__tests__/contract-provider-router.test.js +45 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +67 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +133 -0
- package/dist/contracts/__tests__/remote-contract-transport.test.d.ts +1 -0
- package/dist/contracts/__tests__/remote-contract-transport.test.js +89 -0
- package/dist/contracts/builder.d.ts +47 -1
- package/dist/contracts/builder.js +80 -22
- package/dist/contracts/contract-provider-router.d.ts +6 -0
- package/dist/contracts/contract-provider-router.js +25 -3
- package/dist/contracts/contract-proxy.d.ts +41 -1
- package/dist/contracts/contract-proxy.js +103 -0
- package/dist/contracts/contract-resolvers.d.ts +47 -13
- package/dist/contracts/contract-resolvers.js +125 -35
- package/dist/contracts/extract.d.ts +4 -1
- package/dist/contracts/extract.js +3 -1
- package/dist/contracts/index.d.ts +4 -3
- package/dist/contracts/index.js +10 -2
- package/dist/contracts/remote-contract-transport.d.ts +69 -0
- package/dist/contracts/remote-contract-transport.js +95 -0
- package/dist/contracts/types.d.ts +57 -0
- package/dist/data/persistent-vars.d.ts +8 -0
- package/dist/data/persistent-vars.js +56 -11
- package/dist/data/tool-access-approvals.d.ts +2 -2
- package/dist/data/tools.d.ts +4 -0
- package/dist/data/tools.js +7 -0
- package/dist/package-loader/isolated-artifact.js +16 -1
- package/dist/package-loader/isolated-runtime.d.ts +5 -2
- package/dist/package-loader/package-loader.d.ts +15 -1
- package/dist/package-loader/package-loader.js +48 -0
- package/dist/package-loader/resolve-consumed-contracts.d.ts +6 -2
- package/dist/package-loader/resolve-consumed-contracts.js +30 -2
- package/dist/package-loader/resolve-consumed-contracts.test.js +43 -0
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PackageBuilder = exports.ContractBuilder = void 0;
|
|
4
|
+
exports.createDeferredContractHandle = createDeferredContractHandle;
|
|
4
5
|
exports.createConsumedContractHandle = createConsumedContractHandle;
|
|
6
|
+
exports.settleContractHandle = settleContractHandle;
|
|
7
|
+
exports.rejectContractHandle = rejectContractHandle;
|
|
5
8
|
exports.settleConsumedContractHandle = settleConsumedContractHandle;
|
|
6
9
|
exports.rejectConsumedContractHandle = rejectConsumedContractHandle;
|
|
7
10
|
exports.definePackage = definePackage;
|
|
@@ -106,14 +109,14 @@ class ContractBuilder {
|
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
exports.ContractBuilder = ContractBuilder;
|
|
109
|
-
const
|
|
112
|
+
const contractHandleDeferreds = new WeakMap();
|
|
110
113
|
/**
|
|
111
|
-
* Create a deferred
|
|
112
|
-
* {@link
|
|
114
|
+
* Create a deferred contract handle. Settlement is SDK-owned via
|
|
115
|
+
* {@link settleContractHandle} / {@link rejectContractHandle}.
|
|
113
116
|
*
|
|
114
117
|
* @internal
|
|
115
118
|
*/
|
|
116
|
-
function
|
|
119
|
+
function createDeferredContractHandle() {
|
|
117
120
|
let resolve;
|
|
118
121
|
let reject;
|
|
119
122
|
const consumer = new Promise((res, rej) => {
|
|
@@ -124,25 +127,53 @@ function createConsumedContractHandle() {
|
|
|
124
127
|
// error before any package code awaits the handle.
|
|
125
128
|
consumer.catch(() => { });
|
|
126
129
|
const handle = { consumer };
|
|
127
|
-
|
|
130
|
+
contractHandleDeferreds.set(handle, { resolve, reject, settled: false });
|
|
128
131
|
return handle;
|
|
129
132
|
}
|
|
130
133
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
134
|
+
* Create a deferred consumed-contract handle. Settlement is SDK-owned via
|
|
135
|
+
* {@link settleConsumedContractHandle} / {@link rejectConsumedContractHandle}.
|
|
133
136
|
*
|
|
134
|
-
* @internal
|
|
137
|
+
* @internal
|
|
135
138
|
*/
|
|
136
|
-
function
|
|
137
|
-
|
|
139
|
+
function createConsumedContractHandle() {
|
|
140
|
+
return createDeferredContractHandle();
|
|
141
|
+
}
|
|
142
|
+
function takeDeferred(handle) {
|
|
143
|
+
const deferred = contractHandleDeferreds.get(handle);
|
|
138
144
|
if (!deferred) {
|
|
139
|
-
throw new Error("Unknown
|
|
145
|
+
throw new Error("Unknown contract handle");
|
|
140
146
|
}
|
|
141
147
|
if (deferred.settled) {
|
|
142
|
-
throw new Error("
|
|
148
|
+
throw new Error("Contract handle has already been settled");
|
|
143
149
|
}
|
|
144
150
|
deferred.settled = true;
|
|
145
|
-
deferred
|
|
151
|
+
return deferred;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolve a deferred contract handle. One-shot — throws if already settled.
|
|
155
|
+
*
|
|
156
|
+
* @internal Used by the package-loader when settling `consumes` / `remote` handles.
|
|
157
|
+
*/
|
|
158
|
+
function settleContractHandle(handle, value) {
|
|
159
|
+
takeDeferred(handle).resolve(value);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Reject a deferred contract handle. One-shot — throws if already settled.
|
|
163
|
+
*
|
|
164
|
+
* @internal Used by the package-loader when settling `consumes` / `remote` handles.
|
|
165
|
+
*/
|
|
166
|
+
function rejectContractHandle(handle, reason) {
|
|
167
|
+
takeDeferred(handle).reject(reason);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Resolve a consumed-contract handle with a live proxy, or `undefined` for an
|
|
171
|
+
* unmatched optional dependency. One-shot — throws if already settled.
|
|
172
|
+
*
|
|
173
|
+
* @internal Used by the package-loader when settling `consumes` handles.
|
|
174
|
+
*/
|
|
175
|
+
function settleConsumedContractHandle(handle, consumer) {
|
|
176
|
+
settleContractHandle(handle, consumer);
|
|
146
177
|
}
|
|
147
178
|
/**
|
|
148
179
|
* Reject a consumed-contract handle (e.g. required dependency missing).
|
|
@@ -151,15 +182,7 @@ function settleConsumedContractHandle(handle, consumer) {
|
|
|
151
182
|
* @internal Used by the package-loader when settling `consumes` handles.
|
|
152
183
|
*/
|
|
153
184
|
function rejectConsumedContractHandle(handle, reason) {
|
|
154
|
-
|
|
155
|
-
if (!deferred) {
|
|
156
|
-
throw new Error("Unknown consumed-contract handle");
|
|
157
|
-
}
|
|
158
|
-
if (deferred.settled) {
|
|
159
|
-
throw new Error("Consumed-contract handle has already been settled");
|
|
160
|
-
}
|
|
161
|
-
deferred.settled = true;
|
|
162
|
-
deferred.reject(reason);
|
|
185
|
+
rejectContractHandle(handle, reason);
|
|
163
186
|
}
|
|
164
187
|
/**
|
|
165
188
|
* Top-level builder passed to the `definePackage` callback.
|
|
@@ -175,6 +198,7 @@ class PackageBuilder {
|
|
|
175
198
|
_version;
|
|
176
199
|
_contracts = [];
|
|
177
200
|
_consumes = [];
|
|
201
|
+
_remotes = [];
|
|
178
202
|
/** Package-level assistants (not part of any contract). */
|
|
179
203
|
assistants = [];
|
|
180
204
|
/** Package-level app navigation items (not part of any contract). */
|
|
@@ -248,6 +272,39 @@ class PackageBuilder {
|
|
|
248
272
|
});
|
|
249
273
|
return handle;
|
|
250
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Declare that this package calls a contract's tools on **other devices**.
|
|
277
|
+
*
|
|
278
|
+
* A sibling of {@link consumes} for the mesh: the resolved
|
|
279
|
+
* {@link IRemoteContractConsumer} hands out a tools-only consumer per target device
|
|
280
|
+
* (`remote.device(deviceId).tools.x(...)`). No local provider is needed — a package
|
|
281
|
+
* commonly targets its *own* contract on a peer's device — and there is no `optional`
|
|
282
|
+
* flag because whether the remote device can serve the call is only known per call.
|
|
283
|
+
* The loader settles the handle once the contract definition is registered locally
|
|
284
|
+
* (from this package's `provides` or any installed package) and rejects it otherwise.
|
|
285
|
+
*
|
|
286
|
+
* Await `handle.consumer` from runtime code after package definition, not inside
|
|
287
|
+
* this callback.
|
|
288
|
+
*
|
|
289
|
+
* @typeParam T - Consumer proxy type (defaults to {@link IContractConsumer}).
|
|
290
|
+
* @param contractId - Must be a valid peer ID (25-char alphanumeric from `newid()`).
|
|
291
|
+
* @param version - Positive integer version number.
|
|
292
|
+
*/
|
|
293
|
+
remote(contractId, version) {
|
|
294
|
+
if (!(0, utils_1.isid)(contractId)) {
|
|
295
|
+
throw new Error(`remote contractId must be a valid peer ID (25-char alphanumeric), got "${contractId}"`);
|
|
296
|
+
}
|
|
297
|
+
if (version < 1 || !Number.isInteger(version)) {
|
|
298
|
+
throw new Error(`remote version must be a positive integer, got ${version}`);
|
|
299
|
+
}
|
|
300
|
+
const handle = createDeferredContractHandle();
|
|
301
|
+
this._remotes.push({
|
|
302
|
+
contractId,
|
|
303
|
+
version,
|
|
304
|
+
handle: handle,
|
|
305
|
+
});
|
|
306
|
+
return handle;
|
|
307
|
+
}
|
|
251
308
|
/** @internal Finalize and return the package definition result. */
|
|
252
309
|
_build() {
|
|
253
310
|
if (!this._packageId) {
|
|
@@ -274,6 +331,7 @@ class PackageBuilder {
|
|
|
274
331
|
version: this._version,
|
|
275
332
|
contracts,
|
|
276
333
|
consumes: [...this._consumes],
|
|
334
|
+
remotes: [...this._remotes],
|
|
277
335
|
alsoImplements: alsoImplementsMap,
|
|
278
336
|
assistants: [...this.assistants],
|
|
279
337
|
appNavs: [...this.appNavs],
|
|
@@ -33,6 +33,12 @@ export interface IContractProviderEndpointRequest {
|
|
|
33
33
|
providerPackageId?: string;
|
|
34
34
|
/** Canonical data context returned by the authorization hook. */
|
|
35
35
|
dataContextId?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Device that should execute calls on this route when it differs from the local
|
|
38
|
+
* device. Copied from {@link IContractCall.targetDeviceId} by the router; the endpoint
|
|
39
|
+
* resolver decides whether forwarding is permitted for this router's principal.
|
|
40
|
+
*/
|
|
41
|
+
targetDeviceId?: string;
|
|
36
42
|
/** Authorized caller context for this route. */
|
|
37
43
|
context: IContractCallContext;
|
|
38
44
|
/** Sink for provider-to-consumer subscription notifications. */
|
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createContractProviderRouter = createContractProviderRouter;
|
|
4
4
|
const contract_transport_1 = require("./contract-transport");
|
|
5
|
-
const routeKey = (contractId, version, providerPackageId, dataContextId) => JSON.stringify([
|
|
5
|
+
const routeKey = (contractId, version, providerPackageId, dataContextId, targetDeviceId) => JSON.stringify([
|
|
6
|
+
contractId,
|
|
7
|
+
version,
|
|
8
|
+
providerPackageId ?? null,
|
|
9
|
+
dataContextId ?? null,
|
|
10
|
+
targetDeviceId ?? null,
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Reject remote-target operations the mesh path does not support yet. Only tool runs
|
|
14
|
+
* cross devices; subscriptions, tables, observables, and events stay local.
|
|
15
|
+
*/
|
|
16
|
+
function assertRemoteTargetCallSupported(call) {
|
|
17
|
+
if (call.member !== "tool" || call.operation !== "run") {
|
|
18
|
+
throw new Error(`Contract ${call.member} '${call.name}'.${call.operation} cannot target a remote device; only tool runs are remotely callable`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* Own the sole {@link CONTRACT_REQUEST_CHANNEL} handler for a shared connection.
|
|
8
23
|
*
|
|
@@ -91,7 +106,11 @@ function createContractProviderRouter(transport, opts) {
|
|
|
91
106
|
if (isSubscribe && subscriptionId) {
|
|
92
107
|
pendingSubscriptionIds.add(subscriptionId);
|
|
93
108
|
}
|
|
109
|
+
const targetDeviceId = call.targetDeviceId || undefined;
|
|
94
110
|
try {
|
|
111
|
+
if (targetDeviceId) {
|
|
112
|
+
assertRemoteTargetCallSupported(call);
|
|
113
|
+
}
|
|
95
114
|
const inboundContext = opts.context ?? {};
|
|
96
115
|
const authorization = await opts.authorizeCall(call, inboundContext);
|
|
97
116
|
if (disposed) {
|
|
@@ -106,14 +125,17 @@ function createContractProviderRouter(transport, opts) {
|
|
|
106
125
|
contractId: call.contractId,
|
|
107
126
|
version: call.version,
|
|
108
127
|
dataContextId: authorization.dataContextId,
|
|
128
|
+
targetDeviceId,
|
|
109
129
|
context,
|
|
110
130
|
notify,
|
|
111
131
|
};
|
|
112
|
-
|
|
132
|
+
// Provider selection happens on the executing device, so a remote route is
|
|
133
|
+
// cached by target device rather than by a locally selected provider package.
|
|
134
|
+
const providerPackageId = !targetDeviceId && opts.resolveProviderPackageId
|
|
113
135
|
? await opts.resolveProviderPackageId(endpointRequest)
|
|
114
136
|
: undefined;
|
|
115
137
|
endpointRequest.providerPackageId = providerPackageId;
|
|
116
|
-
const endpointKey = routeKey(call.contractId, call.version, providerPackageId, authorization.dataContextId);
|
|
138
|
+
const endpointKey = routeKey(call.contractId, call.version, providerPackageId, authorization.dataContextId, targetDeviceId);
|
|
117
139
|
const endpoint = await getEndpoint(endpointKey, endpointRequest);
|
|
118
140
|
const result = await endpoint.handleCall(routedCall, context);
|
|
119
141
|
if (isSubscribe && subscriptionId) {
|
|
@@ -4,7 +4,7 @@ import type { IToolInstance } from "../data/tools";
|
|
|
4
4
|
import type { Event, ISubscriptionResult } from "../events";
|
|
5
5
|
import { type Observable } from "../observable";
|
|
6
6
|
import { type IContractTransport } from "./contract-transport";
|
|
7
|
-
import type { IContractDefinition } from "./types";
|
|
7
|
+
import type { IContractDefinition, IRemoteContractConsumer } from "./types";
|
|
8
8
|
/**
|
|
9
9
|
* The RPC event name used to proxy a single {@link IContractCall} over a
|
|
10
10
|
* {@link Connection} (`connection.emit(CONTRACT_CALL_RPC, call)` on the consumer
|
|
@@ -53,6 +53,15 @@ export interface IContractCall {
|
|
|
53
53
|
* id to the local provider user's personal context.
|
|
54
54
|
*/
|
|
55
55
|
dataContextId?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Device that should execute this call when it is not the local device. Set by a
|
|
58
|
+
* {@link createRemoteContractConsumer} device consumer and honored only by the caller's own
|
|
59
|
+
* trusted host, which forwards the call over the device mesh and strips this field
|
|
60
|
+
* before it leaves the device. A provider router serving *remote* callers rejects any
|
|
61
|
+
* call that still carries it, so one device can never relay another device's call
|
|
62
|
+
* under its own identity.
|
|
63
|
+
*/
|
|
64
|
+
targetDeviceId?: string;
|
|
56
65
|
/**
|
|
57
66
|
* For any subscription call (a generic `event`, a table `dataChanged`, or an observable
|
|
58
67
|
* value stream): the consumer-chosen id that ties a `"subscribe"` (and its later
|
|
@@ -236,6 +245,8 @@ export interface IContractToolContext {
|
|
|
236
245
|
* consumer and provider boundaries. Disposal is idempotent and prevents new remote operations;
|
|
237
246
|
* cached observable reads remain available afterward.
|
|
238
247
|
*
|
|
248
|
+
* To run tools on another device use {@link createRemoteContractConsumer} instead.
|
|
249
|
+
*
|
|
239
250
|
* @param contract The contract definition (shape) being consumed.
|
|
240
251
|
* @param transport Bidirectional channel to the provider; requests go out on
|
|
241
252
|
* {@link CONTRACT_REQUEST_CHANNEL}, notifications arrive on {@link CONTRACT_NOTIFY_CHANNEL}.
|
|
@@ -244,6 +255,29 @@ export interface IContractToolContext {
|
|
|
244
255
|
export declare function createContractConsumer(contract: IContractDefinition, transport: IContractTransport, opts?: {
|
|
245
256
|
dataContextId?: string;
|
|
246
257
|
}): IContractConsumer;
|
|
258
|
+
/**
|
|
259
|
+
* Build the **remote** consumer side of a contract: a factory of tools-only consumers whose
|
|
260
|
+
* calls execute on other devices (the runtime behind {@link PackageBuilder.remote}).
|
|
261
|
+
*
|
|
262
|
+
* `device(deviceId)` returns a consumer of the contract that stamps
|
|
263
|
+
* {@link IContractCall.targetDeviceId} on every tool call. The trusted local host router
|
|
264
|
+
* behind `transport` recognizes the target, forwards the call over the device mesh, and the
|
|
265
|
+
* remote device authorizes it against the caller's group role and the tool's
|
|
266
|
+
* {@link IContractTool.remoteAccessLevel}. Consumers are cached per device; passing this
|
|
267
|
+
* device's own id resolves locally. No notify listener or observable mirror is created
|
|
268
|
+
* because subscriptions do not cross devices yet, so `tables`, `observables`, and `events`
|
|
269
|
+
* throw on access and `loadingPromise` resolves immediately. Disposing the factory disposes
|
|
270
|
+
* every device consumer; a device consumer may also be disposed on its own.
|
|
271
|
+
*
|
|
272
|
+
* @param contract The contract definition (shape) whose tools are called remotely.
|
|
273
|
+
* @param transport Channel to the caller's own trusted host router (never to the remote
|
|
274
|
+
* device directly).
|
|
275
|
+
* @param opts.dataContextId Data context every call is scoped to. Cross-account calls must
|
|
276
|
+
* name the shared group whose membership the remote device checks.
|
|
277
|
+
*/
|
|
278
|
+
export declare function createRemoteContractConsumer(contract: IContractDefinition, transport: IContractTransport, opts?: {
|
|
279
|
+
dataContextId?: string;
|
|
280
|
+
}): IRemoteContractConsumer<IContractConsumer>;
|
|
247
281
|
/**
|
|
248
282
|
* The live implementation a **provider** dispatches contract calls to. This is the
|
|
249
283
|
* runtime-callable counterpart to {@link IResolvedContract} (which additionally tracks
|
|
@@ -279,6 +313,12 @@ export interface IContractCallContext {
|
|
|
279
313
|
callerUserId?: string;
|
|
280
314
|
/** User id of the local device executing the call. */
|
|
281
315
|
localUserId?: string;
|
|
316
|
+
/**
|
|
317
|
+
* Device the call arrived from when it was made by another device. Absent for
|
|
318
|
+
* same-device callers. Informational for providers; authorization is decided by the
|
|
319
|
+
* host router before dispatch.
|
|
320
|
+
*/
|
|
321
|
+
callerDeviceId?: string;
|
|
282
322
|
/**
|
|
283
323
|
* Provider-side data context used to resolve the caller's group/personal role.
|
|
284
324
|
* In-process only — never serialized across a connection.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sameUserContractPermissionCheck = exports.CONTRACT_CALL_RPC = void 0;
|
|
4
4
|
exports.createContractConsumer = createContractConsumer;
|
|
5
|
+
exports.createRemoteContractConsumer = createRemoteContractConsumer;
|
|
5
6
|
exports.createContractProvider = createContractProvider;
|
|
6
7
|
exports.createContractProviderEndpoint = createContractProviderEndpoint;
|
|
7
8
|
exports.createContractProviderSession = createContractProviderSession;
|
|
@@ -66,6 +67,8 @@ exports.CONTRACT_CALL_RPC = contract_transport_1.CONTRACT_REQUEST_CHANNEL;
|
|
|
66
67
|
* consumer and provider boundaries. Disposal is idempotent and prevents new remote operations;
|
|
67
68
|
* cached observable reads remain available afterward.
|
|
68
69
|
*
|
|
70
|
+
* To run tools on another device use {@link createRemoteContractConsumer} instead.
|
|
71
|
+
*
|
|
69
72
|
* @param contract The contract definition (shape) being consumed.
|
|
70
73
|
* @param transport Bidirectional channel to the provider; requests go out on
|
|
71
74
|
* {@link CONTRACT_REQUEST_CHANNEL}, notifications arrive on {@link CONTRACT_NOTIFY_CHANNEL}.
|
|
@@ -334,6 +337,106 @@ function createContractConsumer(contract, transport, opts = {}) {
|
|
|
334
337
|
consumer.loadingPromise = Promise.all(observableLoads).then(() => undefined);
|
|
335
338
|
return consumer;
|
|
336
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Build a bag whose every string property access throws, used for the contract members a
|
|
342
|
+
* remote consumer cannot reach yet. Symbol access (inspection, `then` probing) stays inert.
|
|
343
|
+
*/
|
|
344
|
+
function unreachableRemoteMembers(member) {
|
|
345
|
+
return new Proxy({}, {
|
|
346
|
+
get(_target, prop) {
|
|
347
|
+
if (typeof prop !== "string" || prop === "then" || prop === "toJSON") {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
throw new Error(`Contract ${member} '${prop}' is not remotely accessible yet; only tools can be called on a remote device`);
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Build the **remote** consumer side of a contract: a factory of tools-only consumers whose
|
|
356
|
+
* calls execute on other devices (the runtime behind {@link PackageBuilder.remote}).
|
|
357
|
+
*
|
|
358
|
+
* `device(deviceId)` returns a consumer of the contract that stamps
|
|
359
|
+
* {@link IContractCall.targetDeviceId} on every tool call. The trusted local host router
|
|
360
|
+
* behind `transport` recognizes the target, forwards the call over the device mesh, and the
|
|
361
|
+
* remote device authorizes it against the caller's group role and the tool's
|
|
362
|
+
* {@link IContractTool.remoteAccessLevel}. Consumers are cached per device; passing this
|
|
363
|
+
* device's own id resolves locally. No notify listener or observable mirror is created
|
|
364
|
+
* because subscriptions do not cross devices yet, so `tables`, `observables`, and `events`
|
|
365
|
+
* throw on access and `loadingPromise` resolves immediately. Disposing the factory disposes
|
|
366
|
+
* every device consumer; a device consumer may also be disposed on its own.
|
|
367
|
+
*
|
|
368
|
+
* @param contract The contract definition (shape) whose tools are called remotely.
|
|
369
|
+
* @param transport Channel to the caller's own trusted host router (never to the remote
|
|
370
|
+
* device directly).
|
|
371
|
+
* @param opts.dataContextId Data context every call is scoped to. Cross-account calls must
|
|
372
|
+
* name the shared group whose membership the remote device checks.
|
|
373
|
+
*/
|
|
374
|
+
function createRemoteContractConsumer(contract, transport, opts = {}) {
|
|
375
|
+
const { contractId, version } = contract;
|
|
376
|
+
const { dataContextId } = opts;
|
|
377
|
+
const consumers = new Map();
|
|
378
|
+
let disposed = false;
|
|
379
|
+
const createDeviceConsumer = (targetDeviceId) => {
|
|
380
|
+
let deviceDisposed = false;
|
|
381
|
+
const tools = {};
|
|
382
|
+
for (const tool of contract.tools) {
|
|
383
|
+
const name = tool.name;
|
|
384
|
+
tools[name] = (...args) => deviceDisposed
|
|
385
|
+
? Promise.reject(new Error(`Remote contract consumer ${contractId} v${version} for device ${targetDeviceId} is disposed`))
|
|
386
|
+
: transport.emit(contract_transport_1.CONTRACT_REQUEST_CHANNEL, {
|
|
387
|
+
contractId,
|
|
388
|
+
version,
|
|
389
|
+
member: "tool",
|
|
390
|
+
name,
|
|
391
|
+
operation: "run",
|
|
392
|
+
args,
|
|
393
|
+
dataContextId,
|
|
394
|
+
targetDeviceId,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
contractId,
|
|
399
|
+
version,
|
|
400
|
+
tables: unreachableRemoteMembers("table"),
|
|
401
|
+
tools,
|
|
402
|
+
observables: unreachableRemoteMembers("observable"),
|
|
403
|
+
events: unreachableRemoteMembers("event"),
|
|
404
|
+
loadingPromise: Promise.resolve(),
|
|
405
|
+
dispose: () => {
|
|
406
|
+
deviceDisposed = true;
|
|
407
|
+
consumers.delete(targetDeviceId);
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
return {
|
|
412
|
+
contractId,
|
|
413
|
+
version,
|
|
414
|
+
device(deviceId) {
|
|
415
|
+
if (disposed) {
|
|
416
|
+
throw new Error(`Remote contract consumer ${contractId} v${version} is disposed`);
|
|
417
|
+
}
|
|
418
|
+
if (typeof deviceId !== "string" || deviceId.trim() === "") {
|
|
419
|
+
throw new Error("device(deviceId) requires a non-empty device id");
|
|
420
|
+
}
|
|
421
|
+
let consumer = consumers.get(deviceId);
|
|
422
|
+
if (!consumer) {
|
|
423
|
+
consumer = createDeviceConsumer(deviceId);
|
|
424
|
+
consumers.set(deviceId, consumer);
|
|
425
|
+
}
|
|
426
|
+
return consumer;
|
|
427
|
+
},
|
|
428
|
+
dispose() {
|
|
429
|
+
if (disposed) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
disposed = true;
|
|
433
|
+
for (const consumer of [...consumers.values()]) {
|
|
434
|
+
consumer.dispose();
|
|
435
|
+
}
|
|
436
|
+
consumers.clear();
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
337
440
|
/**
|
|
338
441
|
* Legacy gate that allows calls only when caller and provider user ids match.
|
|
339
442
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DataContext } from "../context/data-context";
|
|
2
2
|
import type { UserContext } from "../context/user-context";
|
|
3
3
|
import type { IDeviceInfo } from "../data/devices";
|
|
4
|
-
import { type IContractProviderRouter } from "./contract-provider-router";
|
|
4
|
+
import { type IAuthorizedContractRoute, type IContractProviderRouter } from "./contract-provider-router";
|
|
5
5
|
import { type IContractCall, type IContractResolution } from "./contract-proxy";
|
|
6
6
|
import type { IContractTransport } from "./contract-transport";
|
|
7
7
|
/**
|
|
@@ -48,6 +48,17 @@ export declare function registerContractResolver(contractId: string, version: nu
|
|
|
48
48
|
* yet adopted provider selection; multiple scoped providers remain ambiguous.
|
|
49
49
|
*/
|
|
50
50
|
export declare function getContractResolver(contractId: string, version: number, providerPackageId?: string): ContractResolver | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Verify that a connection- or message-authenticated remote identity matches a valid
|
|
53
|
+
* self-signed Users record in the provider's personal or shared-group data.
|
|
54
|
+
*
|
|
55
|
+
* Unsigned discovery stubs, malformed signatures, and key mismatches fail closed. This is
|
|
56
|
+
* the identity half of remote authorization; trust and role decisions build on it.
|
|
57
|
+
*
|
|
58
|
+
* @param userContext Provider's user context whose data is searched.
|
|
59
|
+
* @param remoteIdentity Identity derived from the verified transport.
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifyRemoteContractIdentity(userContext: UserContext, remoteIdentity: ContractRemoteIdentity): Promise<boolean>;
|
|
51
62
|
/**
|
|
52
63
|
* Check whether a connection-authenticated remote identity has full contract authority.
|
|
53
64
|
*
|
|
@@ -56,6 +67,29 @@ export declare function getContractResolver(contractId: string, version: number,
|
|
|
56
67
|
* {@link TrustLevel.Self}. Unsigned discovery stubs and key mismatches fail closed.
|
|
57
68
|
*/
|
|
58
69
|
export declare function isSelfTrustedContractCaller(userContext: UserContext, remoteIdentity: ContractRemoteIdentity): Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Authorize one inbound contract call from **another device**.
|
|
72
|
+
*
|
|
73
|
+
* Layered policy, evaluated in order:
|
|
74
|
+
* 1. Calls that still carry a `targetDeviceId` are denied: a remote caller may not use
|
|
75
|
+
* this device as a relay under its identity. The tool-access-control contract is
|
|
76
|
+
* local-only.
|
|
77
|
+
* 2. The caller's keys must match a valid signed Users record known to the provider.
|
|
78
|
+
* 3. A Self-trusted caller (the provider's own account, or an account granted exact
|
|
79
|
+
* `TrustLevel.Self`) keeps full access; an omitted data context selects the provider's
|
|
80
|
+
* personal context.
|
|
81
|
+
* 4. Any other verified caller is treated as a **group member**: the call must name a
|
|
82
|
+
* group the provider belongs to, must be a tool run, the contract definition registered
|
|
83
|
+
* for that group must mark the tool with {@link IContractTool.remoteAccessLevel}, and
|
|
84
|
+
* the caller's role in that group must meet it. Missing loader, definition, or level
|
|
85
|
+
* fails closed.
|
|
86
|
+
*
|
|
87
|
+
* @param call Inbound call.
|
|
88
|
+
* @param userContext Provider's user context.
|
|
89
|
+
* @param remoteIdentity Identity derived from the verified transport (never from the call).
|
|
90
|
+
* @returns The canonical route and caller context for the router.
|
|
91
|
+
*/
|
|
92
|
+
export declare function authorizeRemoteContractCall(call: IContractCall, userContext: UserContext, remoteIdentity: ContractRemoteIdentity): Promise<IAuthorizedContractRoute>;
|
|
59
93
|
/**
|
|
60
94
|
* Create a provider router for one authenticated UI-to-local-host session.
|
|
61
95
|
*
|
|
@@ -79,27 +113,27 @@ export declare function createLocalUserContextContractProviderRouter(transport:
|
|
|
79
113
|
resolveVariable?: LocalContractVariableResolver;
|
|
80
114
|
}): IContractProviderRouter;
|
|
81
115
|
/**
|
|
82
|
-
* Create the local-object provider router for one verified device
|
|
116
|
+
* Create the local-object provider router for one verified remote device, whether it is
|
|
117
|
+
* reached over a direct {@link Connection} or through device messages.
|
|
83
118
|
*
|
|
84
|
-
* The router derives caller identity from the
|
|
85
|
-
* authorizes every call before invoking a
|
|
86
|
-
* contexts to the local user's personal
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* retain their existing behavior after authorization.
|
|
119
|
+
* The router derives caller identity from the transport owner supplied by the host,
|
|
120
|
+
* authorizes every call with {@link authorizeRemoteContractCall} before invoking a
|
|
121
|
+
* registered resolver, normalizes omitted data contexts to the local user's personal
|
|
122
|
+
* context for Self callers, and caches stateful endpoints per contract/version/data-context
|
|
123
|
+
* route. Group members reach only tools marked with `remoteAccessLevel`.
|
|
90
124
|
*
|
|
91
|
-
* @param transport Contract transport backed by one verified
|
|
125
|
+
* @param transport Contract transport backed by one verified remote device.
|
|
92
126
|
* @param opts.userContext Local user context that owns provider data.
|
|
93
|
-
* @param opts.remoteIdentity Identity derived from the verified remote
|
|
127
|
+
* @param opts.remoteIdentity Identity derived from the verified remote device.
|
|
94
128
|
*/
|
|
95
129
|
export declare function createUserContextContractProviderRouter(transport: IContractTransport, opts: {
|
|
96
130
|
userContext: UserContext;
|
|
97
131
|
remoteIdentity: ContractRemoteIdentity;
|
|
98
132
|
}): IContractProviderRouter;
|
|
99
133
|
/**
|
|
100
|
-
* Provider-side request/response glue: apply
|
|
101
|
-
*
|
|
102
|
-
*
|
|
134
|
+
* Provider-side request/response glue: apply {@link authorizeRemoteContractCall}, resolve
|
|
135
|
+
* the contract for the authorized data context, build a stateless provider, and execute
|
|
136
|
+
* the call.
|
|
103
137
|
*
|
|
104
138
|
* Used by transports (e.g. the connection `CONTRACT_CALL_RPC` handler) to run an inbound
|
|
105
139
|
* {@link IContractCall} against local state.
|