@peers-app/peers-sdk 0.19.13 → 0.20.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.
- package/dist/contracts/__tests__/builder.test.js +24 -0
- package/dist/contracts/__tests__/contract-events.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-events.test.js +199 -0
- package/dist/contracts/__tests__/contract-observables.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-observables.test.js +290 -0
- package/dist/contracts/__tests__/contract-provider-router.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-provider-router.test.js +193 -0
- package/dist/contracts/__tests__/contract-proxy.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +424 -0
- package/dist/contracts/__tests__/contract-resolvers.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +148 -0
- package/dist/contracts/__tests__/contract-subscription-lifecycle.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-subscription-lifecycle.test.js +353 -0
- package/dist/contracts/__tests__/contract-table-events.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-table-events.test.js +209 -0
- package/dist/contracts/__tests__/contract-transport.test.d.ts +1 -0
- package/dist/contracts/__tests__/contract-transport.test.js +163 -0
- package/dist/contracts/__tests__/persistent-registry.test.d.ts +1 -0
- package/dist/contracts/__tests__/persistent-registry.test.js +109 -0
- package/dist/contracts/__tests__/playground.test.d.ts +1 -0
- package/dist/contracts/__tests__/playground.test.js +213 -0
- package/dist/contracts/__tests__/validate.test.js +122 -0
- package/dist/contracts/builder.d.ts +9 -1
- package/dist/contracts/builder.js +13 -0
- package/dist/contracts/contract-provider-router.d.ts +72 -0
- package/dist/contracts/contract-provider-router.js +164 -0
- package/dist/contracts/contract-proxy.d.ts +333 -0
- package/dist/contracts/contract-proxy.js +663 -0
- package/dist/contracts/contract-resolvers.d.ts +66 -0
- package/dist/contracts/contract-resolvers.js +138 -0
- package/dist/contracts/contract-transport.d.ts +94 -0
- package/dist/contracts/contract-transport.js +159 -0
- package/dist/contracts/contracts.table.d.ts +1 -1
- package/dist/contracts/contracts.table.js +2 -2
- package/dist/contracts/index.d.ts +6 -1
- package/dist/contracts/index.js +25 -1
- package/dist/contracts/persistent-registry.js +3 -1
- package/dist/contracts/system/logs-contract.d.ts +24 -0
- package/dist/contracts/system/logs-contract.js +60 -0
- package/dist/contracts/types.d.ts +27 -4
- package/dist/contracts/validate.d.ts +2 -2
- package/dist/contracts/validate.js +44 -2
- package/dist/data/orm/decorators.d.ts +6 -0
- package/dist/data/orm/decorators.js +21 -0
- package/dist/data/user-trust-levels.d.ts +2 -0
- package/dist/data/user-trust-levels.js +2 -1
- package/dist/data/user-trust-levels.test.d.ts +1 -0
- package/dist/data/user-trust-levels.test.js +37 -0
- package/dist/device/binary-peer-connection-v2.d.ts +2 -0
- package/dist/device/binary-peer-connection-v2.js +12 -2
- package/dist/device/binary-peer-connection-v2.test.js +23 -0
- package/dist/device/get-trust-level-fn.d.ts +6 -0
- package/dist/device/get-trust-level-fn.js +12 -7
- package/dist/device/get-trust-level-fn.test.js +16 -2
- package/dist/logging/console-logs.table.d.ts +3 -0
- package/dist/logging/console-logs.table.js +2 -1
- package/dist/system-ids.d.ts +11 -0
- package/dist/system-ids.js +12 -1
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { DataContext } from "../context/data-context";
|
|
2
|
+
import type { UserContext } from "../context/user-context";
|
|
3
|
+
import type { IDeviceInfo } from "../data/devices";
|
|
4
|
+
import { type IContractProviderRouter } from "./contract-provider-router";
|
|
5
|
+
import { type IContractCall, type IContractResolution } from "./contract-proxy";
|
|
6
|
+
import type { IContractTransport } from "./contract-transport";
|
|
7
|
+
/**
|
|
8
|
+
* Produces the live {@link IContractResolution} for a contract, scoped to a data context.
|
|
9
|
+
* System contracts register a resolver on import so the provider side can map a contract
|
|
10
|
+
* call to real tables / tools / observables at execution time.
|
|
11
|
+
*/
|
|
12
|
+
export type ContractResolver = (ctx: {
|
|
13
|
+
dataContext: DataContext;
|
|
14
|
+
}) => IContractResolution | Promise<IContractResolution>;
|
|
15
|
+
/** Connection-authenticated identity fields used to authorize remote contract calls. */
|
|
16
|
+
export type ContractRemoteIdentity = Pick<IDeviceInfo, "deviceId" | "userId" | "publicKey" | "publicBoxKey">;
|
|
17
|
+
/**
|
|
18
|
+
* Register the resolver that provides the live implementation for a contract version.
|
|
19
|
+
* Later registrations for the same contract+version replace earlier ones (matching the
|
|
20
|
+
* registry's "latest provider wins for dev contracts" behavior).
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerContractResolver(contractId: string, version: number, resolver: ContractResolver): void;
|
|
23
|
+
/** Look up the resolver for a contract version, if one is registered. */
|
|
24
|
+
export declare function getContractResolver(contractId: string, version: number): ContractResolver | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Check whether a connection-authenticated remote identity has full contract authority.
|
|
27
|
+
*
|
|
28
|
+
* The identity must match a valid self-signed Users record in the provider's personal or
|
|
29
|
+
* shared-group data, and the provider's personal context must assign the caller exactly
|
|
30
|
+
* {@link TrustLevel.Self}. Unsigned discovery stubs and key mismatches fail closed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isSelfTrustedContractCaller(userContext: UserContext, remoteIdentity: ContractRemoteIdentity): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Create the local-object provider router for one verified device connection.
|
|
35
|
+
*
|
|
36
|
+
* The router derives caller identity from the connection owner supplied by the host,
|
|
37
|
+
* authorizes every call before invoking a registered resolver, normalizes omitted data
|
|
38
|
+
* contexts to the local user's personal context, and caches stateful endpoints per
|
|
39
|
+
* contract/version/data-context route. The caller must match a valid signed Users record
|
|
40
|
+
* and have exact Self trust in the provider's personal context. Explicit data-context ids
|
|
41
|
+
* retain their existing behavior after authorization.
|
|
42
|
+
*
|
|
43
|
+
* @param transport Contract transport backed by one verified connection.
|
|
44
|
+
* @param opts.userContext Local user context that owns provider data.
|
|
45
|
+
* @param opts.remoteIdentity Identity derived from the verified remote connection.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createUserContextContractProviderRouter(transport: IContractTransport, opts: {
|
|
48
|
+
userContext: UserContext;
|
|
49
|
+
remoteIdentity: ContractRemoteIdentity;
|
|
50
|
+
}): IContractProviderRouter;
|
|
51
|
+
/**
|
|
52
|
+
* Provider-side request/response glue: apply the signed-identity Self-trust host gate,
|
|
53
|
+
* resolve the contract for the authorized data context, build a stateless provider, and
|
|
54
|
+
* execute the call.
|
|
55
|
+
*
|
|
56
|
+
* Used by transports (e.g. the connection `CONTRACT_CALL_RPC` handler) to run an inbound
|
|
57
|
+
* {@link IContractCall} against local state.
|
|
58
|
+
*
|
|
59
|
+
* @param call The inbound contract call.
|
|
60
|
+
* @param opts.userContext The local user context executing the call.
|
|
61
|
+
* @param opts.remoteIdentity Identity derived from the verified remote connection.
|
|
62
|
+
*/
|
|
63
|
+
export declare function handleContractCall(call: IContractCall, opts: {
|
|
64
|
+
userContext: UserContext;
|
|
65
|
+
remoteIdentity: ContractRemoteIdentity;
|
|
66
|
+
}): Promise<any>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerContractResolver = registerContractResolver;
|
|
4
|
+
exports.getContractResolver = getContractResolver;
|
|
5
|
+
exports.isSelfTrustedContractCaller = isSelfTrustedContractCaller;
|
|
6
|
+
exports.createUserContextContractProviderRouter = createUserContextContractProviderRouter;
|
|
7
|
+
exports.handleContractCall = handleContractCall;
|
|
8
|
+
const user_permissions_1 = require("../data/user-permissions");
|
|
9
|
+
const user_trust_levels_1 = require("../data/user-trust-levels");
|
|
10
|
+
const users_1 = require("../data/users");
|
|
11
|
+
const socket_type_1 = require("../device/socket.type");
|
|
12
|
+
const contract_provider_router_1 = require("./contract-provider-router");
|
|
13
|
+
const contract_proxy_1 = require("./contract-proxy");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
const resolvers = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Register the resolver that provides the live implementation for a contract version.
|
|
18
|
+
* Later registrations for the same contract+version replace earlier ones (matching the
|
|
19
|
+
* registry's "latest provider wins for dev contracts" behavior).
|
|
20
|
+
*/
|
|
21
|
+
function registerContractResolver(contractId, version, resolver) {
|
|
22
|
+
resolvers.set((0, types_1.contractKey)(contractId, version), resolver);
|
|
23
|
+
}
|
|
24
|
+
/** Look up the resolver for a contract version, if one is registered. */
|
|
25
|
+
function getContractResolver(contractId, version) {
|
|
26
|
+
return resolvers.get((0, types_1.contractKey)(contractId, version));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check whether a connection-authenticated remote identity has full contract authority.
|
|
30
|
+
*
|
|
31
|
+
* The identity must match a valid self-signed Users record in the provider's personal or
|
|
32
|
+
* shared-group data, and the provider's personal context must assign the caller exactly
|
|
33
|
+
* {@link TrustLevel.Self}. Unsigned discovery stubs and key mismatches fail closed.
|
|
34
|
+
*/
|
|
35
|
+
async function isSelfTrustedContractCaller(userContext, remoteIdentity) {
|
|
36
|
+
const identityContexts = [userContext.userDataContext, ...userContext.groupDataContexts.values()];
|
|
37
|
+
let hasVerifiedIdentity = false;
|
|
38
|
+
for (const dataContext of identityContexts) {
|
|
39
|
+
const user = await (0, users_1.Users)(dataContext).get(remoteIdentity.userId);
|
|
40
|
+
if (!user?.signature ||
|
|
41
|
+
user.userId !== remoteIdentity.userId ||
|
|
42
|
+
user.publicKey !== remoteIdentity.publicKey ||
|
|
43
|
+
user.publicBoxKey !== remoteIdentity.publicBoxKey) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
(0, user_permissions_1.verifyUserSignature)(user);
|
|
48
|
+
hasVerifiedIdentity = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// A different context may contain the valid signed record.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!hasVerifiedIdentity) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const trustLevel = remoteIdentity.userId === userContext.userId
|
|
59
|
+
? socket_type_1.TrustLevel.Self
|
|
60
|
+
: ((await (0, user_trust_levels_1.UserTrustLevels)(userContext.userDataContext).get(remoteIdentity.userId))
|
|
61
|
+
?.trustLevel ?? socket_type_1.TrustLevel.Unknown);
|
|
62
|
+
return trustLevel === socket_type_1.TrustLevel.Self;
|
|
63
|
+
}
|
|
64
|
+
async function authorizeUserContextCall(call, userContext, remoteIdentity) {
|
|
65
|
+
const context = {
|
|
66
|
+
callerUserId: remoteIdentity.userId,
|
|
67
|
+
localUserId: userContext.userId,
|
|
68
|
+
};
|
|
69
|
+
if (!(await isSelfTrustedContractCaller(userContext, remoteIdentity))) {
|
|
70
|
+
throw new Error(`Permission denied: ${call.member} '${call.name}'.${call.operation} on contract ${call.contractId}`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
dataContextId: call.dataContextId || userContext.userId,
|
|
74
|
+
context,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create the local-object provider router for one verified device connection.
|
|
79
|
+
*
|
|
80
|
+
* The router derives caller identity from the connection owner supplied by the host,
|
|
81
|
+
* authorizes every call before invoking a registered resolver, normalizes omitted data
|
|
82
|
+
* contexts to the local user's personal context, and caches stateful endpoints per
|
|
83
|
+
* contract/version/data-context route. The caller must match a valid signed Users record
|
|
84
|
+
* and have exact Self trust in the provider's personal context. Explicit data-context ids
|
|
85
|
+
* retain their existing behavior after authorization.
|
|
86
|
+
*
|
|
87
|
+
* @param transport Contract transport backed by one verified connection.
|
|
88
|
+
* @param opts.userContext Local user context that owns provider data.
|
|
89
|
+
* @param opts.remoteIdentity Identity derived from the verified remote connection.
|
|
90
|
+
*/
|
|
91
|
+
function createUserContextContractProviderRouter(transport, opts) {
|
|
92
|
+
const { userContext, remoteIdentity } = opts;
|
|
93
|
+
const context = {
|
|
94
|
+
callerUserId: remoteIdentity.userId,
|
|
95
|
+
localUserId: userContext.userId,
|
|
96
|
+
};
|
|
97
|
+
return (0, contract_provider_router_1.createContractProviderRouter)(transport, {
|
|
98
|
+
context,
|
|
99
|
+
authorizeCall: (call) => authorizeUserContextCall(call, userContext, remoteIdentity),
|
|
100
|
+
resolveEndpoint: async ({ contractId, version, dataContextId, notify }) => {
|
|
101
|
+
const resolver = getContractResolver(contractId, version);
|
|
102
|
+
if (!resolver) {
|
|
103
|
+
throw new Error(`No contract provider registered for ${contractId} v${version}`);
|
|
104
|
+
}
|
|
105
|
+
const dataContext = userContext.getDataContext(dataContextId || userContext.userId);
|
|
106
|
+
const resolution = await resolver({ dataContext });
|
|
107
|
+
return (0, contract_proxy_1.createContractProviderEndpoint)(resolution, { notify });
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Provider-side request/response glue: apply the signed-identity Self-trust host gate,
|
|
113
|
+
* resolve the contract for the authorized data context, build a stateless provider, and
|
|
114
|
+
* execute the call.
|
|
115
|
+
*
|
|
116
|
+
* Used by transports (e.g. the connection `CONTRACT_CALL_RPC` handler) to run an inbound
|
|
117
|
+
* {@link IContractCall} against local state.
|
|
118
|
+
*
|
|
119
|
+
* @param call The inbound contract call.
|
|
120
|
+
* @param opts.userContext The local user context executing the call.
|
|
121
|
+
* @param opts.remoteIdentity Identity derived from the verified remote connection.
|
|
122
|
+
*/
|
|
123
|
+
async function handleContractCall(call, opts) {
|
|
124
|
+
const { userContext, remoteIdentity } = opts;
|
|
125
|
+
const authorization = await authorizeUserContextCall(call, userContext, remoteIdentity);
|
|
126
|
+
const resolver = getContractResolver(call.contractId, call.version);
|
|
127
|
+
if (!resolver) {
|
|
128
|
+
throw new Error(`No contract provider registered for ${call.contractId} v${call.version}`);
|
|
129
|
+
}
|
|
130
|
+
const routedCall = {
|
|
131
|
+
...call,
|
|
132
|
+
dataContextId: authorization.dataContextId,
|
|
133
|
+
};
|
|
134
|
+
const dataContext = userContext.getDataContext(authorization.dataContextId || userContext.userId);
|
|
135
|
+
const resolution = await resolver({ dataContext });
|
|
136
|
+
const provider = (0, contract_proxy_1.createContractProvider)(resolution);
|
|
137
|
+
return provider(routedCall, authorization.context);
|
|
138
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic bidirectional channel that every contract proxy sits on.
|
|
3
|
+
*
|
|
4
|
+
* The transport is a dumb, symmetric duplex: `emit(channel, ...args)` sends on a
|
|
5
|
+
* named channel and resolves with the peer handler's return value (the ack), and
|
|
6
|
+
* `on(channel, handler)` registers a handler whose resolved value becomes that ack.
|
|
7
|
+
* All contract-specific routing (which contract, which member, subscriptions, …)
|
|
8
|
+
* lives *above* this layer in the payload; the transport itself is unaware of it.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately the common denominator of the primitives we already have:
|
|
11
|
+
* a device {@link IDuplexEmitter} ({@link Connection}), a socket.io `Socket`, and the
|
|
12
|
+
* `ISocket` abstraction all expose `emit(name, ...) -> Promise<ack>` + `on(name, handler)`
|
|
13
|
+
* + listener registration. Adapters below wrap each of those (plus an in-process pair for tests) into
|
|
14
|
+
* the same {@link IContractTransport} shape so proxies work unchanged whether the provider
|
|
15
|
+
* runs in the same process, another iframe, the UI↔server socket, or a remote device.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A minimal, symmetric, bidirectional channel for contract traffic.
|
|
19
|
+
*
|
|
20
|
+
* Both `emit` and `on` are available on each end, so provider→consumer pushes use the
|
|
21
|
+
* exact same code path as consumer→provider requests (the pusher just ignores the
|
|
22
|
+
* returned promise).
|
|
23
|
+
*/
|
|
24
|
+
export interface IContractTransport {
|
|
25
|
+
/**
|
|
26
|
+
* Send `args` on the named `channel`; resolves with the peer handler's returned
|
|
27
|
+
* value (the ack), or rejects if the peer handler throws / rejects.
|
|
28
|
+
*/
|
|
29
|
+
emit(channel: string, ...args: any[]): Promise<any>;
|
|
30
|
+
/**
|
|
31
|
+
* Register a `handler` for the named `channel`. The handler's resolved value is
|
|
32
|
+
* returned to the emitter as the ack. Returns an unsubscribe function that removes
|
|
33
|
+
* this handler.
|
|
34
|
+
*/
|
|
35
|
+
on(channel: string, handler: (...args: any[]) => any | Promise<any>): () => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The structural shape of a duplex RPC emitter (notably a device {@link Connection})
|
|
39
|
+
* that {@link connectionContractTransport} wraps.
|
|
40
|
+
*
|
|
41
|
+
* Declared structurally so `peers-sdk`'s contract layer does not import `Connection`
|
|
42
|
+
* (avoiding an import cycle) yet a `Connection` — and anything else exposing the same
|
|
43
|
+
* `emit`/`on` surface — satisfies it.
|
|
44
|
+
*/
|
|
45
|
+
export interface IDuplexEmitter {
|
|
46
|
+
/** Send an RPC and resolve with the remote handler's result. */
|
|
47
|
+
emit(eventName: string, ...args: any[]): Promise<any>;
|
|
48
|
+
/** Register a handler invoked with the emitter's positional args. */
|
|
49
|
+
on(eventName: string, handler: (...args: any[]) => any): void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fixed channel for consumer→provider request/response traffic (tool runs, table
|
|
53
|
+
* methods, observable get/set, and subscribe/unsubscribe). Value matches
|
|
54
|
+
* the legacy `CONTRACT_CALL_RPC` event name so existing provider registrations keep
|
|
55
|
+
* working unchanged.
|
|
56
|
+
*/
|
|
57
|
+
export declare const CONTRACT_REQUEST_CHANNEL = "contractCall";
|
|
58
|
+
/**
|
|
59
|
+
* Fixed channel for provider→consumer event, table `dataChanged`, and observable
|
|
60
|
+
* notifications. Subscription ids in each payload let many consumers share one
|
|
61
|
+
* underlying connection.
|
|
62
|
+
*/
|
|
63
|
+
export declare const CONTRACT_NOTIFY_CHANNEL = "contractNotify";
|
|
64
|
+
/**
|
|
65
|
+
* Wrap a duplex RPC emitter (e.g. a device {@link Connection}) as an
|
|
66
|
+
* {@link IContractTransport}. `emit` delegates directly. `on` uses one adapter-owned
|
|
67
|
+
* dispatcher per emitter/channel and returns an unsubscribe that removes only the caller's
|
|
68
|
+
* handler from that dispatcher; it never uses a coarse `removeAllListeners`.
|
|
69
|
+
*
|
|
70
|
+
* Multiple notify listeners may share one connection and dispose independently. The request
|
|
71
|
+
* channel intentionally accepts one handler only: a connection-wide provider router owns
|
|
72
|
+
* multiplexing contracts and data contexts. Stacking fixed-resolution provider sessions on
|
|
73
|
+
* one request channel is unsupported.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* const logs = createContractConsumer(logsContractDefinition, connectionContractTransport(connection));
|
|
77
|
+
* await logs.tables.ConsoleLogs.list({ level: "error" }, { pageSize: 20 });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @param emitter The underlying duplex emitter (Connection-like) to delegate to.
|
|
81
|
+
*/
|
|
82
|
+
export declare function connectionContractTransport(emitter: IDuplexEmitter): IContractTransport;
|
|
83
|
+
/**
|
|
84
|
+
* Create a cross-wired pair of {@link IContractTransport}s that talk to each other in the
|
|
85
|
+
* same process. Whatever one end `emit`s on a channel is delivered to the other end's
|
|
86
|
+
* handler for that channel; the handler's resolved value is returned to the emitter as
|
|
87
|
+
* the ack, and a thrown/rejected handler rejects the emit. Both ends are fully symmetric,
|
|
88
|
+
* so either side can act as consumer, provider, or both.
|
|
89
|
+
*
|
|
90
|
+
* Intended for tests and same-process contract wiring (no real network transport).
|
|
91
|
+
*
|
|
92
|
+
* @returns `[a, b]` — two transports wired to each other.
|
|
93
|
+
*/
|
|
94
|
+
export declare function inProcessTransportPair(): [IContractTransport, IContractTransport];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transport-agnostic bidirectional channel that every contract proxy sits on.
|
|
4
|
+
*
|
|
5
|
+
* The transport is a dumb, symmetric duplex: `emit(channel, ...args)` sends on a
|
|
6
|
+
* named channel and resolves with the peer handler's return value (the ack), and
|
|
7
|
+
* `on(channel, handler)` registers a handler whose resolved value becomes that ack.
|
|
8
|
+
* All contract-specific routing (which contract, which member, subscriptions, …)
|
|
9
|
+
* lives *above* this layer in the payload; the transport itself is unaware of it.
|
|
10
|
+
*
|
|
11
|
+
* This is deliberately the common denominator of the primitives we already have:
|
|
12
|
+
* a device {@link IDuplexEmitter} ({@link Connection}), a socket.io `Socket`, and the
|
|
13
|
+
* `ISocket` abstraction all expose `emit(name, ...) -> Promise<ack>` + `on(name, handler)`
|
|
14
|
+
* + listener registration. Adapters below wrap each of those (plus an in-process pair for tests) into
|
|
15
|
+
* the same {@link IContractTransport} shape so proxies work unchanged whether the provider
|
|
16
|
+
* runs in the same process, another iframe, the UI↔server socket, or a remote device.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.CONTRACT_NOTIFY_CHANNEL = exports.CONTRACT_REQUEST_CHANNEL = void 0;
|
|
20
|
+
exports.connectionContractTransport = connectionContractTransport;
|
|
21
|
+
exports.inProcessTransportPair = inProcessTransportPair;
|
|
22
|
+
/**
|
|
23
|
+
* Fixed channel for consumer→provider request/response traffic (tool runs, table
|
|
24
|
+
* methods, observable get/set, and subscribe/unsubscribe). Value matches
|
|
25
|
+
* the legacy `CONTRACT_CALL_RPC` event name so existing provider registrations keep
|
|
26
|
+
* working unchanged.
|
|
27
|
+
*/
|
|
28
|
+
exports.CONTRACT_REQUEST_CHANNEL = "contractCall";
|
|
29
|
+
/**
|
|
30
|
+
* Fixed channel for provider→consumer event, table `dataChanged`, and observable
|
|
31
|
+
* notifications. Subscription ids in each payload let many consumers share one
|
|
32
|
+
* underlying connection.
|
|
33
|
+
*/
|
|
34
|
+
exports.CONTRACT_NOTIFY_CHANNEL = "contractNotify";
|
|
35
|
+
const connectionDispatcherStates = new WeakMap();
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a duplex RPC emitter (e.g. a device {@link Connection}) as an
|
|
38
|
+
* {@link IContractTransport}. `emit` delegates directly. `on` uses one adapter-owned
|
|
39
|
+
* dispatcher per emitter/channel and returns an unsubscribe that removes only the caller's
|
|
40
|
+
* handler from that dispatcher; it never uses a coarse `removeAllListeners`.
|
|
41
|
+
*
|
|
42
|
+
* Multiple notify listeners may share one connection and dispose independently. The request
|
|
43
|
+
* channel intentionally accepts one handler only: a connection-wide provider router owns
|
|
44
|
+
* multiplexing contracts and data contexts. Stacking fixed-resolution provider sessions on
|
|
45
|
+
* one request channel is unsupported.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const logs = createContractConsumer(logsContractDefinition, connectionContractTransport(connection));
|
|
49
|
+
* await logs.tables.ConsoleLogs.list({ level: "error" }, { pageSize: 20 });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @param emitter The underlying duplex emitter (Connection-like) to delegate to.
|
|
53
|
+
*/
|
|
54
|
+
function connectionContractTransport(emitter) {
|
|
55
|
+
return {
|
|
56
|
+
emit: (channel, ...args) => emitter.emit(channel, ...args),
|
|
57
|
+
on: (channel, handler) => {
|
|
58
|
+
let channelStates = connectionDispatcherStates.get(emitter);
|
|
59
|
+
if (!channelStates) {
|
|
60
|
+
channelStates = new Map();
|
|
61
|
+
connectionDispatcherStates.set(emitter, channelStates);
|
|
62
|
+
}
|
|
63
|
+
let state = channelStates.get(channel);
|
|
64
|
+
if (!state) {
|
|
65
|
+
const handlers = new Set();
|
|
66
|
+
const dispatcher = async (...args) => {
|
|
67
|
+
const results = await Promise.all([...handlers].map((registered) => {
|
|
68
|
+
try {
|
|
69
|
+
return Promise.resolve(registered(...args));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return Promise.reject(error);
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
return channel === exports.CONTRACT_REQUEST_CHANNEL ? results[0] : undefined;
|
|
76
|
+
};
|
|
77
|
+
state = { handlers, dispatcher };
|
|
78
|
+
channelStates.set(channel, state);
|
|
79
|
+
emitter.on(channel, dispatcher);
|
|
80
|
+
}
|
|
81
|
+
if (channel === exports.CONTRACT_REQUEST_CHANNEL && state.handlers.size > 0) {
|
|
82
|
+
throw new Error("Contract request channel already has a handler; use one connection-wide provider router");
|
|
83
|
+
}
|
|
84
|
+
state.handlers.add(handler);
|
|
85
|
+
let active = true;
|
|
86
|
+
return () => {
|
|
87
|
+
if (!active) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
active = false;
|
|
91
|
+
state?.handlers.delete(handler);
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build one end of an in-process transport pair. `on` registers into `localHandlers`;
|
|
98
|
+
* `emit` invokes the peer's handlers concurrently and awaits them. A single handler's
|
|
99
|
+
* result is returned as its ack; multi-listener notification channels resolve `undefined`.
|
|
100
|
+
* The request channel enforces a single handler so tests cannot accidentally hide missing
|
|
101
|
+
* provider routing behind last-handler-wins behavior.
|
|
102
|
+
*/
|
|
103
|
+
function makeInProcessTransport(localHandlers, remoteHandlers) {
|
|
104
|
+
return {
|
|
105
|
+
emit: async (channel, ...args) => {
|
|
106
|
+
const handlers = remoteHandlers.get(channel);
|
|
107
|
+
if (!handlers || handlers.size === 0) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const results = await Promise.all([...handlers].map((handler) => {
|
|
111
|
+
try {
|
|
112
|
+
return Promise.resolve(handler(...args));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return Promise.reject(error);
|
|
116
|
+
}
|
|
117
|
+
}));
|
|
118
|
+
return results.length === 1 ? results[0] : undefined;
|
|
119
|
+
},
|
|
120
|
+
on: (channel, handler) => {
|
|
121
|
+
let set = localHandlers.get(channel);
|
|
122
|
+
if (!set) {
|
|
123
|
+
set = new Set();
|
|
124
|
+
localHandlers.set(channel, set);
|
|
125
|
+
}
|
|
126
|
+
if (channel === exports.CONTRACT_REQUEST_CHANNEL && set.size > 0) {
|
|
127
|
+
throw new Error("Contract request channel already has a handler; use one provider router");
|
|
128
|
+
}
|
|
129
|
+
set.add(handler);
|
|
130
|
+
let active = true;
|
|
131
|
+
return () => {
|
|
132
|
+
if (!active) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
active = false;
|
|
136
|
+
set?.delete(handler);
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Create a cross-wired pair of {@link IContractTransport}s that talk to each other in the
|
|
143
|
+
* same process. Whatever one end `emit`s on a channel is delivered to the other end's
|
|
144
|
+
* handler for that channel; the handler's resolved value is returned to the emitter as
|
|
145
|
+
* the ack, and a thrown/rejected handler rejects the emit. Both ends are fully symmetric,
|
|
146
|
+
* so either side can act as consumer, provider, or both.
|
|
147
|
+
*
|
|
148
|
+
* Intended for tests and same-process contract wiring (no real network transport).
|
|
149
|
+
*
|
|
150
|
+
* @returns `[a, b]` — two transports wired to each other.
|
|
151
|
+
*/
|
|
152
|
+
function inProcessTransportPair() {
|
|
153
|
+
const handlersA = new Map();
|
|
154
|
+
const handlersB = new Map();
|
|
155
|
+
return [
|
|
156
|
+
makeInProcessTransport(handlersA, handlersB),
|
|
157
|
+
makeInProcessTransport(handlersB, handlersA),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Table } from "../data/orm/table";
|
|
|
4
4
|
import { type ITableMetaData } from "../data/orm/types";
|
|
5
5
|
/**
|
|
6
6
|
* Zod schema for persisted contracts.
|
|
7
|
-
* The `shape` field stores the serialized `{ tables, tools, observables }` JSON
|
|
7
|
+
* The `shape` field stores the serialized `{ tables, tools, observables, events }` JSON
|
|
8
8
|
* so the full `IContractDefinition` can be rehydrated without re-extracting from
|
|
9
9
|
* live package code.
|
|
10
10
|
*/
|
|
@@ -10,7 +10,7 @@ const types_1 = require("../data/orm/types");
|
|
|
10
10
|
const zod_types_1 = require("../types/zod-types");
|
|
11
11
|
/**
|
|
12
12
|
* Zod schema for persisted contracts.
|
|
13
|
-
* The `shape` field stores the serialized `{ tables, tools, observables }` JSON
|
|
13
|
+
* The `shape` field stores the serialized `{ tables, tools, observables, events }` JSON
|
|
14
14
|
* so the full `IContractDefinition` can be rehydrated without re-extracting from
|
|
15
15
|
* live package code.
|
|
16
16
|
*/
|
|
@@ -20,7 +20,7 @@ const schema = zod_1.z.object({
|
|
|
20
20
|
devTag: zod_1.z.string().optional().describe("'dev' if the contract shape is still mutable"),
|
|
21
21
|
name: zod_1.z.string().describe("Human-readable contract name"),
|
|
22
22
|
description: zod_1.z.string().describe("Contract description"),
|
|
23
|
-
shape: zod_1.z.string().describe("JSON-serialized { tables, tools, observables }"),
|
|
23
|
+
shape: zod_1.z.string().describe("JSON-serialized { tables, tools, observables, events }"),
|
|
24
24
|
registeredAt: zod_1.z.string().describe("ISO 8601 timestamp of first registration"),
|
|
25
25
|
});
|
|
26
26
|
exports.contractsSchema = schema;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export { ContractBuilder, definePackage, PackageBuilder } from "./builder";
|
|
2
|
+
export { type ContractCallAuthorizer, type ContractProviderEndpointResolver, createContractProviderRouter, type IAuthorizedContractRoute, type IContractProviderEndpointRequest, type IContractProviderRouter, type IContractProviderRouterOptions, } from "./contract-provider-router";
|
|
2
3
|
export { ContractProviders, ContractProvidersTable, contractProvidersMetaData, contractProvidersSchema, type IContractProviderRecord, } from "./contract-providers.table";
|
|
4
|
+
export { CONTRACT_CALL_RPC, type ContractMemberType, type ContractNotificationSink, type ContractPermissionCheck, createContractConsumer, createContractProvider, createContractProviderEndpoint, createContractProviderSession, type IContractCall, type IContractCallContext, type IContractConsumer, type IContractNotification, type IContractProviderEndpoint, type IContractProviderEndpointOptions, type IContractProviderSession, type IContractProviderSessionOptions, type IContractResolution, sameUserContractPermissionCheck, } from "./contract-proxy";
|
|
5
|
+
export { type ContractRemoteIdentity, type ContractResolver, createUserContextContractProviderRouter, getContractResolver, handleContractCall, isSelfTrustedContractCaller, registerContractResolver, } from "./contract-resolvers";
|
|
6
|
+
export { CONTRACT_NOTIFY_CHANNEL, CONTRACT_REQUEST_CHANNEL, connectionContractTransport, type IContractTransport, type IDuplexEmitter, inProcessTransportPair, } from "./contract-transport";
|
|
3
7
|
export { Contracts, ContractsTable, contractsMetaData, contractsSchema, type IContractRecord, } from "./contracts.table";
|
|
4
8
|
export { extractTableShape, extractToolShape, type IExtractableTableDef, type IExtractableToolInstance, setSchemaToFieldsFn, } from "./extract";
|
|
5
9
|
export { PersistentContractRegistry } from "./persistent-registry";
|
|
6
10
|
export { ContractRegistry } from "./registry";
|
|
7
|
-
export
|
|
11
|
+
export { getLogsContractDefinition, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./system/logs-contract";
|
|
12
|
+
export type { IAlsoImplementsDeclaration, IConsumedContract, IContractDefinition, IContractEvent, IContractObservable, IContractTable, IContractTool, IPackageDefinitionResult, IResolvedContract, IValidationError, IValidationResult, } from "./types";
|
|
8
13
|
export { contractKey } from "./types";
|
|
9
14
|
export { validateAlsoImplements, validateImmutability, validateProviderSatisfiesContract, } from "./validate";
|
package/dist/contracts/index.js
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.ContractRegistry = exports.PersistentContractRegistry = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.contractProvidersSchema = exports.contractProvidersMetaData = exports.ContractProvidersTable = exports.ContractProviders = exports.PackageBuilder = exports.definePackage = exports.ContractBuilder = void 0;
|
|
3
|
+
exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.ContractRegistry = exports.PersistentContractRegistry = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.inProcessTransportPair = exports.connectionContractTransport = exports.CONTRACT_REQUEST_CHANNEL = exports.CONTRACT_NOTIFY_CHANNEL = exports.registerContractResolver = exports.isSelfTrustedContractCaller = exports.handleContractCall = exports.getContractResolver = exports.createUserContextContractProviderRouter = exports.sameUserContractPermissionCheck = exports.createContractProviderSession = exports.createContractProviderEndpoint = exports.createContractProvider = exports.createContractConsumer = exports.CONTRACT_CALL_RPC = exports.contractProvidersSchema = exports.contractProvidersMetaData = exports.ContractProvidersTable = exports.ContractProviders = exports.createContractProviderRouter = exports.PackageBuilder = exports.definePackage = exports.ContractBuilder = void 0;
|
|
4
4
|
var builder_1 = require("./builder");
|
|
5
5
|
Object.defineProperty(exports, "ContractBuilder", { enumerable: true, get: function () { return builder_1.ContractBuilder; } });
|
|
6
6
|
Object.defineProperty(exports, "definePackage", { enumerable: true, get: function () { return builder_1.definePackage; } });
|
|
7
7
|
Object.defineProperty(exports, "PackageBuilder", { enumerable: true, get: function () { return builder_1.PackageBuilder; } });
|
|
8
|
+
var contract_provider_router_1 = require("./contract-provider-router");
|
|
9
|
+
Object.defineProperty(exports, "createContractProviderRouter", { enumerable: true, get: function () { return contract_provider_router_1.createContractProviderRouter; } });
|
|
8
10
|
var contract_providers_table_1 = require("./contract-providers.table");
|
|
9
11
|
Object.defineProperty(exports, "ContractProviders", { enumerable: true, get: function () { return contract_providers_table_1.ContractProviders; } });
|
|
10
12
|
Object.defineProperty(exports, "ContractProvidersTable", { enumerable: true, get: function () { return contract_providers_table_1.ContractProvidersTable; } });
|
|
11
13
|
Object.defineProperty(exports, "contractProvidersMetaData", { enumerable: true, get: function () { return contract_providers_table_1.contractProvidersMetaData; } });
|
|
12
14
|
Object.defineProperty(exports, "contractProvidersSchema", { enumerable: true, get: function () { return contract_providers_table_1.contractProvidersSchema; } });
|
|
15
|
+
var contract_proxy_1 = require("./contract-proxy");
|
|
16
|
+
Object.defineProperty(exports, "CONTRACT_CALL_RPC", { enumerable: true, get: function () { return contract_proxy_1.CONTRACT_CALL_RPC; } });
|
|
17
|
+
Object.defineProperty(exports, "createContractConsumer", { enumerable: true, get: function () { return contract_proxy_1.createContractConsumer; } });
|
|
18
|
+
Object.defineProperty(exports, "createContractProvider", { enumerable: true, get: function () { return contract_proxy_1.createContractProvider; } });
|
|
19
|
+
Object.defineProperty(exports, "createContractProviderEndpoint", { enumerable: true, get: function () { return contract_proxy_1.createContractProviderEndpoint; } });
|
|
20
|
+
Object.defineProperty(exports, "createContractProviderSession", { enumerable: true, get: function () { return contract_proxy_1.createContractProviderSession; } });
|
|
21
|
+
Object.defineProperty(exports, "sameUserContractPermissionCheck", { enumerable: true, get: function () { return contract_proxy_1.sameUserContractPermissionCheck; } });
|
|
22
|
+
var contract_resolvers_1 = require("./contract-resolvers");
|
|
23
|
+
Object.defineProperty(exports, "createUserContextContractProviderRouter", { enumerable: true, get: function () { return contract_resolvers_1.createUserContextContractProviderRouter; } });
|
|
24
|
+
Object.defineProperty(exports, "getContractResolver", { enumerable: true, get: function () { return contract_resolvers_1.getContractResolver; } });
|
|
25
|
+
Object.defineProperty(exports, "handleContractCall", { enumerable: true, get: function () { return contract_resolvers_1.handleContractCall; } });
|
|
26
|
+
Object.defineProperty(exports, "isSelfTrustedContractCaller", { enumerable: true, get: function () { return contract_resolvers_1.isSelfTrustedContractCaller; } });
|
|
27
|
+
Object.defineProperty(exports, "registerContractResolver", { enumerable: true, get: function () { return contract_resolvers_1.registerContractResolver; } });
|
|
28
|
+
var contract_transport_1 = require("./contract-transport");
|
|
29
|
+
Object.defineProperty(exports, "CONTRACT_NOTIFY_CHANNEL", { enumerable: true, get: function () { return contract_transport_1.CONTRACT_NOTIFY_CHANNEL; } });
|
|
30
|
+
Object.defineProperty(exports, "CONTRACT_REQUEST_CHANNEL", { enumerable: true, get: function () { return contract_transport_1.CONTRACT_REQUEST_CHANNEL; } });
|
|
31
|
+
Object.defineProperty(exports, "connectionContractTransport", { enumerable: true, get: function () { return contract_transport_1.connectionContractTransport; } });
|
|
32
|
+
Object.defineProperty(exports, "inProcessTransportPair", { enumerable: true, get: function () { return contract_transport_1.inProcessTransportPair; } });
|
|
13
33
|
var contracts_table_1 = require("./contracts.table");
|
|
14
34
|
Object.defineProperty(exports, "Contracts", { enumerable: true, get: function () { return contracts_table_1.Contracts; } });
|
|
15
35
|
Object.defineProperty(exports, "ContractsTable", { enumerable: true, get: function () { return contracts_table_1.ContractsTable; } });
|
|
@@ -23,6 +43,10 @@ var persistent_registry_1 = require("./persistent-registry");
|
|
|
23
43
|
Object.defineProperty(exports, "PersistentContractRegistry", { enumerable: true, get: function () { return persistent_registry_1.PersistentContractRegistry; } });
|
|
24
44
|
var registry_1 = require("./registry");
|
|
25
45
|
Object.defineProperty(exports, "ContractRegistry", { enumerable: true, get: function () { return registry_1.ContractRegistry; } });
|
|
46
|
+
var logs_contract_1 = require("./system/logs-contract");
|
|
47
|
+
Object.defineProperty(exports, "getLogsContractDefinition", { enumerable: true, get: function () { return logs_contract_1.getLogsContractDefinition; } });
|
|
48
|
+
Object.defineProperty(exports, "LOGS_CONTRACT_VERSION", { enumerable: true, get: function () { return logs_contract_1.LOGS_CONTRACT_VERSION; } });
|
|
49
|
+
Object.defineProperty(exports, "resolveLogsContract", { enumerable: true, get: function () { return logs_contract_1.resolveLogsContract; } });
|
|
26
50
|
var types_1 = require("./types");
|
|
27
51
|
Object.defineProperty(exports, "contractKey", { enumerable: true, get: function () { return types_1.contractKey; } });
|
|
28
52
|
var validate_1 = require("./validate");
|
|
@@ -30,6 +30,7 @@ class PersistentContractRegistry {
|
|
|
30
30
|
tables: definition.tables,
|
|
31
31
|
tools: definition.tools,
|
|
32
32
|
observables: definition.observables,
|
|
33
|
+
events: definition.events ?? [],
|
|
33
34
|
});
|
|
34
35
|
if (existingContract) {
|
|
35
36
|
await contractsTable.save({
|
|
@@ -122,7 +123,7 @@ class PersistentContractRegistry {
|
|
|
122
123
|
}
|
|
123
124
|
/** Convert a persisted contract record back to an `IContractDefinition`. */
|
|
124
125
|
recordToDefinition(record) {
|
|
125
|
-
const { tables, tools, observables } = JSON.parse(record.shape);
|
|
126
|
+
const { tables, tools, observables, events } = JSON.parse(record.shape);
|
|
126
127
|
return {
|
|
127
128
|
contractId: record.contractId,
|
|
128
129
|
version: record.version,
|
|
@@ -132,6 +133,7 @@ class PersistentContractRegistry {
|
|
|
132
133
|
tables,
|
|
133
134
|
tools,
|
|
134
135
|
observables,
|
|
136
|
+
events: events ?? [],
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
139
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DataContext } from "../../context/data-context";
|
|
2
|
+
import type { IContractResolution } from "../contract-proxy";
|
|
3
|
+
import type { IContractDefinition } from "../types";
|
|
4
|
+
/** Version of the built-in System Logs contract. */
|
|
5
|
+
export declare const LOGS_CONTRACT_VERSION = 1;
|
|
6
|
+
/**
|
|
7
|
+
* The first built-in **system contract**: read and write the local device's console logs.
|
|
8
|
+
*
|
|
9
|
+
* It declares only the `ConsoleLogs` table — the table's own methods (`list`, `count`,
|
|
10
|
+
* `deleteOldLogs`, …) are the entire surface we need, so no bespoke tools are required.
|
|
11
|
+
* The table shape is derived from {@link consoleLogSchema} so the contract stays accurate.
|
|
12
|
+
*
|
|
13
|
+
* Built lazily and memoized: reading `consoleLogsMetaData` / `consoleLogSchema` at module
|
|
14
|
+
* load would race the import cycle between the logging table and the SDK barrel, so we defer
|
|
15
|
+
* construction until first use (well after all modules have finished initializing).
|
|
16
|
+
*/
|
|
17
|
+
export declare function getLogsContractDefinition(): IContractDefinition;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the logs contract to its live implementation for a data context: the
|
|
20
|
+
* `ConsoleLogs` table backing that context.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveLogsContract(ctx: {
|
|
23
|
+
dataContext: DataContext;
|
|
24
|
+
}): IContractResolution;
|