@peers-app/peers-sdk 0.25.2 → 0.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/contracts/__tests__/contract-resolvers.test.js +35 -2
  2. package/dist/contracts/__tests__/host-contract-consumer.test.d.ts +1 -0
  3. package/dist/contracts/__tests__/host-contract-consumer.test.js +163 -0
  4. package/dist/contracts/__tests__/system-contracts.test.js +14 -0
  5. package/dist/contracts/contract-resolvers.d.ts +14 -2
  6. package/dist/contracts/contract-resolvers.js +69 -6
  7. package/dist/contracts/host-contract-consumer.d.ts +40 -0
  8. package/dist/contracts/host-contract-consumer.js +105 -0
  9. package/dist/contracts/index.d.ts +2 -0
  10. package/dist/contracts/index.js +7 -2
  11. package/dist/contracts/system/identity-contract.d.ts +43 -0
  12. package/dist/contracts/system/identity-contract.js +133 -0
  13. package/dist/contracts/system/index.d.ts +2 -0
  14. package/dist/contracts/system/index.js +5 -1
  15. package/dist/contracts/types.d.ts +6 -0
  16. package/dist/data/devices.d.ts +4 -4
  17. package/dist/data/group-share.js +0 -38
  18. package/dist/data/index.d.ts +1 -0
  19. package/dist/data/index.js +1 -0
  20. package/dist/data/invites.d.ts +40 -40
  21. package/dist/data/invites.js +4 -2
  22. package/dist/data/peers-services-registration.pvar.d.ts +37 -0
  23. package/dist/data/peers-services-registration.pvar.js +57 -0
  24. package/dist/data/tool-access-approvals.d.ts +3 -3
  25. package/dist/device/connection.d.ts +1 -1
  26. package/dist/device/device.d.ts +1 -1
  27. package/dist/device-pairing/constants.d.ts +0 -2
  28. package/dist/device-pairing/constants.js +1 -3
  29. package/dist/device-pairing/types.d.ts +80 -80
  30. package/dist/invites/invite-messages.d.ts +12 -12
  31. package/dist/rpc-types.d.ts +0 -14
  32. package/dist/rpc-types.js +0 -13
  33. package/dist/system-ids.d.ts +17 -0
  34. package/dist/system-ids.js +18 -1
  35. package/package.json +1 -1
@@ -164,7 +164,11 @@ describe("Self-trusted contract caller authorization", () => {
164
164
  })).rejects.toThrow("Permission denied");
165
165
  router.dispose();
166
166
  });
167
- it("rejects remote Tool Access Control as local-only", async () => {
167
+ it.each([
168
+ ["Tool Access Control", system_ids_1.toolAccessControlContractId],
169
+ ["Identity", system_ids_1.identityContractId],
170
+ ["Invites", system_ids_1.invitesContractId],
171
+ ])("rejects remote %s as local-only", async (_name, contractId) => {
168
172
  const { remoteIdentity, user } = makeIdentity();
169
173
  const records = new Map();
170
174
  const { personalContext, userContext } = setupUserContext(remoteIdentity.userId, records);
@@ -177,7 +181,7 @@ describe("Self-trusted contract caller authorization", () => {
177
181
  });
178
182
  await expect(consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, {
179
183
  ...call,
180
- contractId: system_ids_1.toolAccessControlContractId,
184
+ contractId,
181
185
  member: "tool",
182
186
  name: "approve",
183
187
  operation: "run",
@@ -381,6 +385,35 @@ describe("local user-context contract routing", () => {
381
385
  await expect(result).resolves.toBe(setup.userId);
382
386
  router.dispose();
383
387
  });
388
+ it("resolves a signed-out contract without waiting for a user context", async () => {
389
+ const contractId = (0, utils_1.newid)();
390
+ (0, index_1.registerContractResolver)(contractId, 1, () => ({
391
+ contractId,
392
+ version: 1,
393
+ tools: {
394
+ inspect: {
395
+ tool: {
396
+ toolId: (0, utils_1.newid)(),
397
+ name: "inspect",
398
+ usageDescription: "Report that no user was required.",
399
+ inputSchema: { type: "simple", fields: [] },
400
+ outputSchema: { type: "simple", fields: [] },
401
+ code: "",
402
+ packageId: (0, utils_1.newid)(),
403
+ accessLevel: 0,
404
+ },
405
+ toolFn: () => "signed-out",
406
+ },
407
+ },
408
+ }), { requiresUserContext: false });
409
+ const [consumerTransport, providerTransport] = (0, index_1.inProcessTransportPair)();
410
+ const router = (0, index_1.createLocalUserContextContractProviderRouter)(providerTransport, {
411
+ getUserContext: () => new Promise(() => undefined),
412
+ peekUserContext: () => undefined,
413
+ });
414
+ await expect(consumerTransport.emit(index_1.CONTRACT_REQUEST_CHANNEL, makeToolCall(contractId))).resolves.toBe("signed-out");
415
+ router.dispose();
416
+ });
384
417
  it("routes known groups and rejects unknown contexts without creating them", async () => {
385
418
  const setup = setupLocalRouter();
386
419
  const router = (0, index_1.createLocalUserContextContractProviderRouter)(setup.providerTransport, {
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const field_type_1 = require("../../types/field-type");
4
+ const contract_transport_1 = require("../contract-transport");
5
+ const index_1 = require("../index");
6
+ const definition = {
7
+ contractId: "hostconsumercontract00001",
8
+ version: 1,
9
+ devTag: "dev",
10
+ name: "Host consumer test",
11
+ description: "Exercises hostContractConsumer caching.",
12
+ tables: [],
13
+ tools: [
14
+ {
15
+ name: "ping",
16
+ usageDescription: "Echo the provider label.",
17
+ inputFields: [],
18
+ outputFields: [{ name: "label", type: field_type_1.FieldType.string, description: "Provider label." }],
19
+ },
20
+ ],
21
+ observables: [
22
+ {
23
+ name: "state",
24
+ description: "Snapshot withheld until the test releases it.",
25
+ valueType: field_type_1.FieldType.object,
26
+ writable: false,
27
+ },
28
+ ],
29
+ events: [],
30
+ };
31
+ function setupHost(label, opts = {}) {
32
+ const [consumerTransport, providerTransport] = (0, index_1.inProcessTransportPair)();
33
+ let releaseSnapshot;
34
+ const snapshot = opts.holdSnapshot
35
+ ? new Promise((resolve) => {
36
+ releaseSnapshot = resolve;
37
+ })
38
+ : Promise.resolve({ phase: "idle" });
39
+ const calls = [];
40
+ providerTransport.on(contract_transport_1.CONTRACT_REQUEST_CHANNEL, (call) => {
41
+ calls.push(call);
42
+ if (call.member === "observable" && call.operation === "get") {
43
+ return snapshot;
44
+ }
45
+ if (call.member === "tool" && call.name === "ping") {
46
+ return { label };
47
+ }
48
+ return undefined;
49
+ });
50
+ return {
51
+ transport: consumerTransport,
52
+ calls,
53
+ release: () => releaseSnapshot?.({ phase: "idle" }),
54
+ dispose: (0, index_1.registerHostContractTransport)(consumerTransport),
55
+ };
56
+ }
57
+ function waitForCall(host, match) {
58
+ return new Promise((resolve, reject) => {
59
+ const started = Date.now();
60
+ const timer = setInterval(() => {
61
+ if (host.calls.some(match)) {
62
+ clearInterval(timer);
63
+ resolve();
64
+ return;
65
+ }
66
+ if (Date.now() - started > 1000) {
67
+ clearInterval(timer);
68
+ reject(new Error(`timed out waiting for a host call; saw ${JSON.stringify(host.calls)}`));
69
+ }
70
+ }, 5);
71
+ });
72
+ }
73
+ describe("hostContractConsumer", () => {
74
+ const handles = [];
75
+ const hosts = [];
76
+ afterEach(() => {
77
+ while (handles.length > 0) {
78
+ handles.pop()?.dispose();
79
+ }
80
+ while (hosts.length > 0) {
81
+ hosts.pop()?.dispose();
82
+ }
83
+ });
84
+ function open() {
85
+ const handle = (0, index_1.hostContractConsumer)(definition, {
86
+ dataContextId: async () => "user-context",
87
+ });
88
+ handles.push(handle);
89
+ return handle;
90
+ }
91
+ it("waits for a transport, then returns the same consumer", async () => {
92
+ const handle = open();
93
+ const pending = handle.get();
94
+ const host = setupHost("first");
95
+ hosts.push(host);
96
+ const consumer = await pending;
97
+ expect(await handle.get()).toBe(consumer);
98
+ await expect(consumer.tools.ping()).resolves.toEqual({ label: "first" });
99
+ expect(host.calls).toContainEqual(expect.objectContaining({
100
+ member: "tool",
101
+ name: "ping",
102
+ dataContextId: "user-context",
103
+ }));
104
+ });
105
+ it("shares one in-flight load across concurrent callers", async () => {
106
+ const host = setupHost("shared");
107
+ hosts.push(host);
108
+ const handle = open();
109
+ const [first, second] = await Promise.all([handle.get(), handle.get()]);
110
+ expect(second).toBe(first);
111
+ const snapshots = host.calls.filter((call) => call.member === "observable" && call.operation === "get");
112
+ expect(snapshots).toHaveLength(1);
113
+ });
114
+ it("disposes the cached consumer when the transport is replaced", async () => {
115
+ const first = setupHost("first");
116
+ hosts.push(first);
117
+ const handle = open();
118
+ const firstConsumer = await handle.get();
119
+ const second = setupHost("second");
120
+ hosts.push(second);
121
+ const secondConsumer = await handle.get();
122
+ expect(secondConsumer).not.toBe(firstConsumer);
123
+ await expect(firstConsumer.tools.ping()).rejects.toThrow("disposed");
124
+ await expect(secondConsumer.tools.ping()).resolves.toEqual({ label: "second" });
125
+ });
126
+ it("drops an in-flight consumer when the transport changes during load", async () => {
127
+ const first = setupHost("first", { holdSnapshot: true });
128
+ hosts.push(first);
129
+ const handle = open();
130
+ const pending = handle.get();
131
+ await waitForCall(first, (call) => call.member === "observable" && call.operation === "get");
132
+ const second = setupHost("second");
133
+ hosts.push(second);
134
+ first.release();
135
+ const consumer = await pending;
136
+ await expect(consumer.tools.ping()).resolves.toEqual({ label: "second" });
137
+ await waitForCall(first, (call) => call.operation === "unsubscribe");
138
+ });
139
+ it("rejects get after dispose and disposes the cached consumer", async () => {
140
+ const host = setupHost("first");
141
+ hosts.push(host);
142
+ const handle = open();
143
+ const consumer = await handle.get();
144
+ handle.dispose();
145
+ handle.dispose();
146
+ await expect(consumer.tools.ping()).rejects.toThrow("disposed");
147
+ await expect(handle.get()).rejects.toThrow("is disposed");
148
+ });
149
+ it("rejects when no transport arrives before timeoutMs", async () => {
150
+ jest.useFakeTimers();
151
+ try {
152
+ const handle = (0, index_1.hostContractConsumer)(definition, { timeoutMs: 25 });
153
+ handles.push(handle);
154
+ const pending = handle.get();
155
+ const expectation = expect(pending).rejects.toThrow("Timed out waiting for host contract transport after 25ms");
156
+ await jest.advanceTimersByTimeAsync(25);
157
+ await expectation;
158
+ }
159
+ finally {
160
+ jest.useRealTimers();
161
+ }
162
+ });
163
+ });
@@ -4,6 +4,7 @@ const system_ids_1 = require("../../system-ids");
4
4
  const utils_1 = require("../../utils");
5
5
  const contract_resolvers_1 = require("../contract-resolvers");
6
6
  const registry_1 = require("../registry");
7
+ const identity_contract_1 = require("../system/identity-contract");
7
8
  const system_contracts_1 = require("../system-contracts");
8
9
  // Populate the catalog with built-in system contracts.
9
10
  require("../system");
@@ -15,6 +16,7 @@ describe("system contract catalog", () => {
15
16
  expect(ids).toEqual(expect.arrayContaining([
16
17
  system_ids_1.logsContractId,
17
18
  system_ids_1.deviceOperationsContractId,
19
+ system_ids_1.identityContractId,
18
20
  system_ids_1.networkDiagnosticsContractId,
19
21
  system_ids_1.networkControlContractId,
20
22
  system_ids_1.toolAccessControlContractId,
@@ -31,6 +33,8 @@ describe("system contract catalog", () => {
31
33
  expect(registry.getProviderPackageId(system_ids_1.logsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
32
34
  expect(registry.resolve(system_ids_1.deviceOperationsContractId, 1)?.name).toBe("System Device Operations");
33
35
  expect(registry.getProviderPackageId(system_ids_1.deviceOperationsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
36
+ expect(registry.resolve(system_ids_1.identityContractId, 1)?.name).toBe("System Identity");
37
+ expect(registry.getProviderPackageId(system_ids_1.identityContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
34
38
  expect(registry.resolve(system_ids_1.networkDiagnosticsContractId, 1)?.name).toBe("System Network Diagnostics");
35
39
  expect(registry.getProviderPackageId(system_ids_1.networkDiagnosticsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
36
40
  expect(registry.resolve(system_ids_1.networkControlContractId, 1)?.name).toBe("System Network Control");
@@ -38,6 +42,16 @@ describe("system contract catalog", () => {
38
42
  expect(registry.resolve(system_ids_1.toolAccessControlContractId, 1)?.name).toBe("System Tool Access Control");
39
43
  expect(registry.getProviderPackageId(system_ids_1.toolAccessControlContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
40
44
  });
45
+ it("declares DevicePairingState as the identity ceremony result", async () => {
46
+ // Loaded lazily. A top-level import cycles through device tables before they finish initializing.
47
+ const { devicePairingStateSchema } = await Promise.resolve().then(() => require("../../device-pairing/types"));
48
+ const stateFields = Object.keys(devicePairingStateSchema.shape).sort();
49
+ const definition = (0, identity_contract_1.getIdentityContractDefinition)();
50
+ for (const name of ["startSource", "startDestination", "approve", "reject", "cancel"]) {
51
+ const tool = definition.tools.find((entry) => entry.name === name);
52
+ expect(tool?.outputFields.map((entry) => entry.name).sort()).toEqual(stateFields);
53
+ }
54
+ });
41
55
  it("registerSystemContract makes new contracts installable without loader changes", () => {
42
56
  const fakeContractId = (0, utils_1.newid)();
43
57
  const providerPackageId = (0, utils_1.newid)();
@@ -30,6 +30,12 @@ export interface IContractResolverRegistrationOptions {
30
30
  * resolver used as a compatibility fallback.
31
31
  */
32
32
  providerPackageId?: string;
33
+ /**
34
+ * When `false`, the local host router resolves this contract without waiting
35
+ * for a signed-in user. The resolver must not read `dataContext` in that case.
36
+ * Defaults to requiring a user.
37
+ */
38
+ requiresUserContext?: boolean;
33
39
  }
34
40
  /** Connection-authenticated identity fields used to authorize remote contract calls. */
35
41
  export type ContractRemoteIdentity = Pick<IDeviceInfo, "deviceId" | "userId" | "publicKey" | "publicBoxKey">;
@@ -72,8 +78,7 @@ export declare function isSelfTrustedContractCaller(userContext: UserContext, re
72
78
  *
73
79
  * Layered policy, evaluated in order:
74
80
  * 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.
81
+ * this device as a relay under its identity. Host-only system contracts are local-only.
77
82
  * 2. The caller's keys must match a valid signed Users record known to the provider.
78
83
  * 3. A Self-trusted caller (the provider's own account, or an account granted exact
79
84
  * `TrustLevel.Self`) keeps full access; an omitted data context selects the provider's
@@ -99,16 +104,23 @@ export declare function authorizeRemoteContractCall(call: IContractCall, userCon
99
104
  * currently known group context; arbitrary valid IDs are rejected before
100
105
  * `UserContext.getDataContext` can create a new context.
101
106
  *
107
+ * Contracts registered with `requiresUserContext: false` resolve before sign-in
108
+ * when `peekUserContext` reports that no user is loaded. Every other call still
109
+ * waits for `getUserContext`.
110
+ *
102
111
  * This helper trusts the host transport's session authentication. It does not
103
112
  * infer package-level authority from renderer-supplied data.
104
113
  *
105
114
  * @param transport Contract transport backed by an authenticated local session.
106
115
  * @param opts.getUserContext Lazy accessor for the host's loaded user context.
116
+ * @param opts.peekUserContext Non-blocking view of the same user context. Required
117
+ * for contracts that resolve on the signed-out setup screen.
107
118
  * @param opts.allowedContractIds Optional contract allowlist for this session.
108
119
  * @param opts.resolveVariable Optional host-only pvar resolver exposed to local package tools.
109
120
  */
110
121
  export declare function createLocalUserContextContractProviderRouter(transport: IContractTransport, opts: {
111
122
  getUserContext: () => Promise<UserContext>;
123
+ peekUserContext?: () => UserContext | undefined;
112
124
  allowedContractIds?: ReadonlySet<string>;
113
125
  resolveVariable?: LocalContractVariableResolver;
114
126
  }): IContractProviderRouter;
@@ -20,6 +20,8 @@ const contract_proxy_1 = require("./contract-proxy");
20
20
  const remote_contract_transport_1 = require("./remote-contract-transport");
21
21
  const types_1 = require("./types");
22
22
  const resolvers = new Map();
23
+ /** Resolver keys that may run before a user context exists. */
24
+ const resolversWithoutUser = new Set();
23
25
  const resolverKey = (contractId, version, providerPackageId) => providerPackageId
24
26
  ? `${(0, types_1.contractKey)(contractId, version)}#provider:${providerPackageId}`
25
27
  : (0, types_1.contractKey)(contractId, version);
@@ -29,7 +31,32 @@ const resolverKey = (contractId, version, providerPackageId) => providerPackageI
29
31
  * registry's "latest provider wins for dev contracts" behavior).
30
32
  */
31
33
  function registerContractResolver(contractId, version, resolver, opts = {}) {
32
- resolvers.set(resolverKey(contractId, version, opts.providerPackageId), resolver);
34
+ const key = resolverKey(contractId, version, opts.providerPackageId);
35
+ resolvers.set(key, resolver);
36
+ if (opts.requiresUserContext === false) {
37
+ resolversWithoutUser.add(key);
38
+ }
39
+ else {
40
+ resolversWithoutUser.delete(key);
41
+ }
42
+ }
43
+ /**
44
+ * Whether the local host must wait for a signed-in user before resolving this contract.
45
+ *
46
+ * A contract is user-optional when any of its resolvers registered with
47
+ * `requiresUserContext: false`.
48
+ */
49
+ function contractRequiresUserContext(contractId, version) {
50
+ if (resolversWithoutUser.has(resolverKey(contractId, version))) {
51
+ return false;
52
+ }
53
+ const providerPrefix = `${(0, types_1.contractKey)(contractId, version)}#provider:`;
54
+ for (const key of resolversWithoutUser) {
55
+ if (key.startsWith(providerPrefix)) {
56
+ return false;
57
+ }
58
+ }
59
+ return true;
33
60
  }
34
61
  /**
35
62
  * Look up a resolver for a contract version.
@@ -119,13 +146,17 @@ async function isSelfTrustedContractCaller(userContext, remoteIdentity) {
119
146
  return hasSelfTrust(userContext, remoteIdentity);
120
147
  }
121
148
  const permissionDenied = (call, reason) => new Error(`Permission denied: ${call.member} '${call.name}'.${call.operation} on contract ${call.contractId}${reason ? ` (${reason})` : ""}`);
149
+ const localOnlyContractIds = new Set([
150
+ system_ids_1.identityContractId,
151
+ system_ids_1.invitesContractId,
152
+ system_ids_1.toolAccessControlContractId,
153
+ ]);
122
154
  /**
123
155
  * Authorize one inbound contract call from **another device**.
124
156
  *
125
157
  * Layered policy, evaluated in order:
126
158
  * 1. Calls that still carry a `targetDeviceId` are denied: a remote caller may not use
127
- * this device as a relay under its identity. The tool-access-control contract is
128
- * local-only.
159
+ * this device as a relay under its identity. Host-only system contracts are local-only.
129
160
  * 2. The caller's keys must match a valid signed Users record known to the provider.
130
161
  * 3. A Self-trusted caller (the provider's own account, or an account granted exact
131
162
  * `TrustLevel.Self`) keeps full access; an omitted data context selects the provider's
@@ -145,8 +176,8 @@ async function authorizeRemoteContractCall(call, userContext, remoteIdentity) {
145
176
  if (call.targetDeviceId) {
146
177
  throw permissionDenied(call, "remote callers cannot relay through this device");
147
178
  }
148
- if (call.contractId === system_ids_1.toolAccessControlContractId) {
149
- throw new Error("Permission denied: tool access control is local-only");
179
+ if (localOnlyContractIds.has(call.contractId)) {
180
+ throw permissionDenied(call, "host system contract is local-only");
150
181
  }
151
182
  if (!(await verifyRemoteContractIdentity(userContext, remoteIdentity))) {
152
183
  throw permissionDenied(call, "unverified caller identity");
@@ -247,6 +278,23 @@ async function resolveDataContextEndpoint(dataContext, request, resolveVariable)
247
278
  notify: request.notify,
248
279
  });
249
280
  }
281
+ async function resolveSignedOutEndpoint(request) {
282
+ const resolver = getContractResolver(request.contractId, request.version, request.providerPackageId);
283
+ if (!resolver) {
284
+ throw new Error(`No contract provider registered for ${request.contractId} v${request.version}`);
285
+ }
286
+ // Signed-out contracts declare that they do not read dataContext.
287
+ const resolution = await resolver({ dataContext: undefined });
288
+ return (0, contract_proxy_1.createContractProviderEndpoint)(resolution, {
289
+ context: request.context,
290
+ notify: request.notify,
291
+ });
292
+ }
293
+ function resolvesWithoutUser(contractId, version, peekUserContext) {
294
+ return (!contractRequiresUserContext(contractId, version) &&
295
+ peekUserContext !== undefined &&
296
+ !peekUserContext());
297
+ }
250
298
  /**
251
299
  * Create a provider router for one authenticated UI-to-local-host session.
252
300
  *
@@ -256,21 +304,30 @@ async function resolveDataContextEndpoint(dataContext, request, resolveVariable)
256
304
  * currently known group context; arbitrary valid IDs are rejected before
257
305
  * `UserContext.getDataContext` can create a new context.
258
306
  *
307
+ * Contracts registered with `requiresUserContext: false` resolve before sign-in
308
+ * when `peekUserContext` reports that no user is loaded. Every other call still
309
+ * waits for `getUserContext`.
310
+ *
259
311
  * This helper trusts the host transport's session authentication. It does not
260
312
  * infer package-level authority from renderer-supplied data.
261
313
  *
262
314
  * @param transport Contract transport backed by an authenticated local session.
263
315
  * @param opts.getUserContext Lazy accessor for the host's loaded user context.
316
+ * @param opts.peekUserContext Non-blocking view of the same user context. Required
317
+ * for contracts that resolve on the signed-out setup screen.
264
318
  * @param opts.allowedContractIds Optional contract allowlist for this session.
265
319
  * @param opts.resolveVariable Optional host-only pvar resolver exposed to local package tools.
266
320
  */
267
321
  function createLocalUserContextContractProviderRouter(transport, opts) {
268
- const { getUserContext, allowedContractIds, resolveVariable } = opts;
322
+ const { getUserContext, peekUserContext, allowedContractIds, resolveVariable } = opts;
269
323
  return (0, contract_provider_router_1.createContractProviderRouter)(transport, {
270
324
  authorizeCall: async (call) => {
271
325
  if (allowedContractIds && !allowedContractIds.has(call.contractId)) {
272
326
  throw new Error(`Permission denied: contract ${call.contractId} is not available locally`);
273
327
  }
328
+ if (resolvesWithoutUser(call.contractId, call.version, peekUserContext)) {
329
+ return { context: {} };
330
+ }
274
331
  const userContext = await getUserContext();
275
332
  const dataContext = getAuthorizedLocalDataContext(userContext, call.dataContextId);
276
333
  const context = {
@@ -284,11 +341,17 @@ function createLocalUserContextContractProviderRouter(transport, opts) {
284
341
  };
285
342
  },
286
343
  resolveProviderPackageId: async ({ contractId, version, dataContextId }) => {
344
+ if (resolvesWithoutUser(contractId, version, peekUserContext)) {
345
+ return undefined;
346
+ }
287
347
  const userContext = await getUserContext();
288
348
  const dataContext = getAuthorizedLocalDataContext(userContext, dataContextId);
289
349
  return getActiveProviderPackageId(dataContext, contractId, version);
290
350
  },
291
351
  resolveEndpoint: async (request) => {
352
+ if (resolvesWithoutUser(request.contractId, request.version, peekUserContext)) {
353
+ return resolveSignedOutEndpoint(request);
354
+ }
292
355
  const userContext = await getUserContext();
293
356
  const dataContext = getAuthorizedLocalDataContext(userContext, request.dataContextId);
294
357
  return resolveDataContextEndpoint(dataContext, request, resolveVariable);
@@ -0,0 +1,40 @@
1
+ import { type IContractConsumer, type IContractConsumerBase } from "./contract-proxy";
2
+ import type { IContractDefinition } from "./types";
3
+ /** Options for {@link hostContractConsumer}. */
4
+ export interface IHostContractConsumerOptions {
5
+ /**
6
+ * Data context bound into every call. Resolved when a consumer is built, not when
7
+ * the handle is created. Omit it for contracts that resolve before sign-in.
8
+ */
9
+ dataContextId?: () => Promise<string | undefined>;
10
+ /**
11
+ * How long `get` waits for a host transport on the first call. Later calls reuse
12
+ * the cached consumer and do not wait again.
13
+ */
14
+ timeoutMs?: number;
15
+ }
16
+ /**
17
+ * Cached handle for one contract consumed over the UI-to-host transport.
18
+ *
19
+ * `get` returns the same consumer until the transport is replaced (socket reconnect).
20
+ * The stale consumer is disposed. A load that is in flight when the transport changes
21
+ * disposes its half-built consumer and resolves with one bound to the new transport.
22
+ */
23
+ export interface IHostContractConsumer<T extends IContractConsumerBase> {
24
+ /** Return the live consumer, building or rebuilding it when the transport changed. */
25
+ get(): Promise<T>;
26
+ /** Dispose the cached consumer and ignore later transport changes. Idempotent. */
27
+ dispose(): void;
28
+ }
29
+ /**
30
+ * Cache one local-host contract consumer per active UI-to-host transport.
31
+ *
32
+ * UI modules that talk to a system contract should share one handle instead of
33
+ * reimplementing transport wait, swap, and dispose. The CLI does not use this;
34
+ * it opens a socket per invocation and has no transport swaps.
35
+ *
36
+ * @param definition Contract shape to consume.
37
+ * @param options Data-context resolver and transport wait.
38
+ * @returns A handle whose `get` is safe to call concurrently.
39
+ */
40
+ export declare function hostContractConsumer<T extends IContractConsumerBase = IContractConsumer>(definition: IContractDefinition, options?: IHostContractConsumerOptions): IHostContractConsumer<T>;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hostContractConsumer = hostContractConsumer;
4
+ const contract_proxy_1 = require("./contract-proxy");
5
+ const host_contract_transport_1 = require("./host-contract-transport");
6
+ /** Default wait for a UI-to-host transport before `get` rejects. */
7
+ const DEFAULT_TIMEOUT_MS = 10_000;
8
+ /**
9
+ * Cache one local-host contract consumer per active UI-to-host transport.
10
+ *
11
+ * UI modules that talk to a system contract should share one handle instead of
12
+ * reimplementing transport wait, swap, and dispose. The CLI does not use this;
13
+ * it opens a socket per invocation and has no transport swaps.
14
+ *
15
+ * @param definition Contract shape to consume.
16
+ * @param options Data-context resolver and transport wait.
17
+ * @returns A handle whose `get` is safe to call concurrently.
18
+ */
19
+ function hostContractConsumer(definition, options = {}) {
20
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
21
+ let cached;
22
+ let pending;
23
+ let disposed = false;
24
+ const disposedMessage = () => `Host contract consumer ${definition.contractId} v${definition.version} is disposed`;
25
+ const dropCached = () => {
26
+ cached?.consumer.dispose();
27
+ cached = undefined;
28
+ };
29
+ const unsubscribe = (0, host_contract_transport_1.subscribeHostContractTransport)((transport) => {
30
+ if (cached && cached.transport !== transport) {
31
+ dropCached();
32
+ }
33
+ if (pending && pending.transport !== transport) {
34
+ pending = undefined;
35
+ }
36
+ }, { emitCurrent: false });
37
+ async function load(transport) {
38
+ const dataContextId = options.dataContextId ? await options.dataContextId() : undefined;
39
+ if (disposed) {
40
+ throw new Error(disposedMessage());
41
+ }
42
+ if ((0, host_contract_transport_1.getHostContractTransport)() !== transport) {
43
+ return get();
44
+ }
45
+ const consumer = (0, contract_proxy_1.createContractConsumer)(definition, transport, dataContextId ? { dataContextId } : {});
46
+ try {
47
+ await consumer.loadingPromise;
48
+ if (disposed || (0, host_contract_transport_1.getHostContractTransport)() !== transport) {
49
+ consumer.dispose();
50
+ if (disposed) {
51
+ throw new Error(disposedMessage());
52
+ }
53
+ return get();
54
+ }
55
+ cached = { transport, consumer };
56
+ return consumer;
57
+ }
58
+ catch (error) {
59
+ consumer.dispose();
60
+ throw error;
61
+ }
62
+ }
63
+ async function get() {
64
+ if (disposed) {
65
+ throw new Error(disposedMessage());
66
+ }
67
+ const transport = (0, host_contract_transport_1.getHostContractTransport)() ?? (await (0, host_contract_transport_1.waitForHostContractTransport)({ timeoutMs }));
68
+ if (disposed) {
69
+ throw new Error(disposedMessage());
70
+ }
71
+ if ((0, host_contract_transport_1.getHostContractTransport)() !== transport) {
72
+ return get();
73
+ }
74
+ if (cached?.transport === transport) {
75
+ return cached.consumer;
76
+ }
77
+ if (pending?.transport === transport) {
78
+ return pending.promise;
79
+ }
80
+ const flight = { transport, promise: undefined };
81
+ const promise = load(transport);
82
+ flight.promise = promise;
83
+ pending = flight;
84
+ try {
85
+ return await promise;
86
+ }
87
+ finally {
88
+ if (pending === flight) {
89
+ pending = undefined;
90
+ }
91
+ }
92
+ }
93
+ return {
94
+ get,
95
+ dispose() {
96
+ if (disposed) {
97
+ return;
98
+ }
99
+ disposed = true;
100
+ unsubscribe();
101
+ pending = undefined;
102
+ dropCached();
103
+ },
104
+ };
105
+ }
@@ -7,11 +7,13 @@ export { CONTRACT_NOTIFY_CHANNEL, CONTRACT_REQUEST_CHANNEL, connectionContractTr
7
7
  export { Contracts, ContractsTable, contractsMetaData, contractsSchema, type IContractRecord, } from "./contracts.table";
8
8
  export { createLocalContractConsumer } from "./create-local-consumer";
9
9
  export { extractTableShape, extractToolShape, type IExtractableTableDef, type IExtractableToolInstance, setSchemaToFieldsFn, } from "./extract";
10
+ export { hostContractConsumer, type IHostContractConsumer, type IHostContractConsumerOptions, } from "./host-contract-consumer";
10
11
  export { getHostContractTransport, type HostContractTransportListener, registerHostContractTransport, subscribeHostContractTransport, waitForHostContractTransport, } from "./host-contract-transport";
11
12
  export { PersistentContractRegistry } from "./persistent-registry";
12
13
  export { ContractRegistry, type IContractProviderRegistration } from "./registry";
13
14
  export { createRemoteContractEndpoint, getRemoteContractTransportFactory, type IRemoteContractHost, type IRemoteContractTarget, type RemoteContractTransportFactory, resolveRemoteTargetEndpoint, setRemoteContractTransportFactory, } from "./remote-contract-transport";
14
15
  export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./system/device-operations-contract";
16
+ export { getIdentityContractDefinition, IDENTITY_CONTRACT_VERSION, type IIdentityConsumer, type IIdentitySessionInfo, } from "./system/identity-contract";
15
17
  export { getInvitesContractDefinition, type IAcceptInviteResult, type ICreatedInvite, type IInvitePreview, type IInvitesConsumer, INVITES_CONTRACT_VERSION, } from "./system/invites-contract";
16
18
  export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./system/logs-contract";
17
19
  export * from "./system/network-contracts";
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.DEVICE_OPERATIONS_MAX_TOOLS = exports.DEVICE_OPERATIONS_MAX_ROWS = exports.DEVICE_OPERATIONS_MAX_RESULT_BYTES = exports.DEVICE_OPERATIONS_MAX_QUERY_LENGTH = exports.DEVICE_OPERATIONS_CONTRACT_VERSION = exports.setRemoteContractTransportFactory = exports.resolveRemoteTargetEndpoint = exports.getRemoteContractTransportFactory = exports.createRemoteContractEndpoint = exports.ContractRegistry = exports.PersistentContractRegistry = exports.waitForHostContractTransport = exports.subscribeHostContractTransport = exports.registerHostContractTransport = exports.getHostContractTransport = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.createLocalContractConsumer = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.inProcessTransportPair = exports.connectionContractTransport = exports.CONTRACT_REQUEST_CHANNEL = exports.CONTRACT_NOTIFY_CHANNEL = exports.verifyRemoteContractIdentity = exports.registerContractResolver = exports.isSelfTrustedContractCaller = exports.handleContractCall = exports.getContractResolver = exports.createUserContextContractProviderRouter = exports.createLocalUserContextContractProviderRouter = exports.authorizeRemoteContractCall = exports.sameUserContractPermissionCheck = exports.createRemoteContractConsumer = 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;
18
- exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.registerSystemContract = exports.listSystemContracts = exports.installSystemContracts = exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = exports.getToolAccessControlContractDefinition = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.INVITES_CONTRACT_VERSION = exports.getInvitesContractDefinition = exports.getDeviceOperationsContractDefinition = exports.deviceOperationsToolIds = exports.deviceOperationsProviderPackageId = void 0;
17
+ exports.DEVICE_OPERATIONS_MAX_ROWS = exports.DEVICE_OPERATIONS_MAX_RESULT_BYTES = exports.DEVICE_OPERATIONS_MAX_QUERY_LENGTH = exports.DEVICE_OPERATIONS_CONTRACT_VERSION = exports.setRemoteContractTransportFactory = exports.resolveRemoteTargetEndpoint = exports.getRemoteContractTransportFactory = exports.createRemoteContractEndpoint = exports.ContractRegistry = exports.PersistentContractRegistry = exports.waitForHostContractTransport = exports.subscribeHostContractTransport = exports.registerHostContractTransport = exports.getHostContractTransport = exports.hostContractConsumer = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.createLocalContractConsumer = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.inProcessTransportPair = exports.connectionContractTransport = exports.CONTRACT_REQUEST_CHANNEL = exports.CONTRACT_NOTIFY_CHANNEL = exports.verifyRemoteContractIdentity = exports.registerContractResolver = exports.isSelfTrustedContractCaller = exports.handleContractCall = exports.getContractResolver = exports.createUserContextContractProviderRouter = exports.createLocalUserContextContractProviderRouter = exports.authorizeRemoteContractCall = exports.sameUserContractPermissionCheck = exports.createRemoteContractConsumer = 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;
18
+ exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.registerSystemContract = exports.listSystemContracts = exports.installSystemContracts = exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = exports.getToolAccessControlContractDefinition = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.INVITES_CONTRACT_VERSION = exports.getInvitesContractDefinition = exports.IDENTITY_CONTRACT_VERSION = exports.getIdentityContractDefinition = exports.getDeviceOperationsContractDefinition = exports.deviceOperationsToolIds = exports.deviceOperationsProviderPackageId = exports.DEVICE_OPERATIONS_MAX_TOOLS = void 0;
19
19
  var builder_1 = require("./builder");
20
20
  Object.defineProperty(exports, "ContractBuilder", { enumerable: true, get: function () { return builder_1.ContractBuilder; } });
21
21
  Object.defineProperty(exports, "definePackage", { enumerable: true, get: function () { return builder_1.definePackage; } });
@@ -60,6 +60,8 @@ var extract_1 = require("./extract");
60
60
  Object.defineProperty(exports, "extractTableShape", { enumerable: true, get: function () { return extract_1.extractTableShape; } });
61
61
  Object.defineProperty(exports, "extractToolShape", { enumerable: true, get: function () { return extract_1.extractToolShape; } });
62
62
  Object.defineProperty(exports, "setSchemaToFieldsFn", { enumerable: true, get: function () { return extract_1.setSchemaToFieldsFn; } });
63
+ var host_contract_consumer_1 = require("./host-contract-consumer");
64
+ Object.defineProperty(exports, "hostContractConsumer", { enumerable: true, get: function () { return host_contract_consumer_1.hostContractConsumer; } });
63
65
  var host_contract_transport_1 = require("./host-contract-transport");
64
66
  Object.defineProperty(exports, "getHostContractTransport", { enumerable: true, get: function () { return host_contract_transport_1.getHostContractTransport; } });
65
67
  Object.defineProperty(exports, "registerHostContractTransport", { enumerable: true, get: function () { return host_contract_transport_1.registerHostContractTransport; } });
@@ -83,6 +85,9 @@ Object.defineProperty(exports, "DEVICE_OPERATIONS_MAX_TOOLS", { enumerable: true
83
85
  Object.defineProperty(exports, "deviceOperationsProviderPackageId", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsProviderPackageId; } });
84
86
  Object.defineProperty(exports, "deviceOperationsToolIds", { enumerable: true, get: function () { return device_operations_contract_1.deviceOperationsToolIds; } });
85
87
  Object.defineProperty(exports, "getDeviceOperationsContractDefinition", { enumerable: true, get: function () { return device_operations_contract_1.getDeviceOperationsContractDefinition; } });
88
+ var identity_contract_1 = require("./system/identity-contract");
89
+ Object.defineProperty(exports, "getIdentityContractDefinition", { enumerable: true, get: function () { return identity_contract_1.getIdentityContractDefinition; } });
90
+ Object.defineProperty(exports, "IDENTITY_CONTRACT_VERSION", { enumerable: true, get: function () { return identity_contract_1.IDENTITY_CONTRACT_VERSION; } });
86
91
  var invites_contract_1 = require("./system/invites-contract");
87
92
  Object.defineProperty(exports, "getInvitesContractDefinition", { enumerable: true, get: function () { return invites_contract_1.getInvitesContractDefinition; } });
88
93
  Object.defineProperty(exports, "INVITES_CONTRACT_VERSION", { enumerable: true, get: function () { return invites_contract_1.INVITES_CONTRACT_VERSION; } });