@robota-sdk/agent-transport-link 0.0.0-stage → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/LICENSE +661 -0
- package/README.md +73 -2
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.cts +195 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +195 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +77 -3
- package/src/__tests__/harness.ts +322 -0
- package/src/__tests__/link-stream.test.ts +52 -0
- package/src/__tests__/owner-vocabulary.test.ts +140 -0
- package/src/__tests__/runtime-link.test.ts +616 -0
- package/src/__tests__/squatter.test.ts +147 -0
- package/src/index.ts +35 -0
- package/src/link-bounds.ts +21 -0
- package/src/link-ports.ts +118 -0
- package/src/link-request.ts +20 -0
- package/src/link-stream.ts +136 -0
- package/src/open-runtime-link.ts +441 -0
- package/src/owner-vocabulary.ts +389 -0
- package/src/serve-runtime-link.ts +303 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client's end of an owner link: the spawner of a runtime, or its owner.
|
|
3
|
+
*
|
|
4
|
+
* A client writes the request line and its hello, checks the runtime's challenge (the keyring's
|
|
5
|
+
* initiator decides), and only then sends its proof; a runtime that is not the one it must be gets
|
|
6
|
+
* nothing more, and opening rejects with the keyring's `LinkRefusedError`. The link is open once
|
|
7
|
+
* the runtime's first sealed frame, its custody notice, arrives: a runtime that refused the proof
|
|
8
|
+
* closes instead. After that the client asks the owner vocabulary, sends read grants (owner links
|
|
9
|
+
* only) and carries the session protocol, and everything it receives is strictly decoded; anything
|
|
10
|
+
* that does not decode, or answers a request it did not ask, ends the link.
|
|
11
|
+
*/
|
|
12
|
+
import type { Duplex } from 'node:stream';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
KeyringError,
|
|
16
|
+
LINK_CHALLENGE_MAX_BYTES,
|
|
17
|
+
LINK_FRAME_OVERHEAD_BYTES,
|
|
18
|
+
createLinkInitiator,
|
|
19
|
+
linkFrameKind,
|
|
20
|
+
type IInitiatorLink,
|
|
21
|
+
type ILinkBinding,
|
|
22
|
+
type IOwnerHandover,
|
|
23
|
+
type ITrustRoot,
|
|
24
|
+
type SpawnerKey,
|
|
25
|
+
type TLinkInitiatorAuth,
|
|
26
|
+
type TLinkMode,
|
|
27
|
+
} from '@robota-sdk/agent-keyring';
|
|
28
|
+
import {
|
|
29
|
+
decodeFrame,
|
|
30
|
+
decodeServerMessage,
|
|
31
|
+
type TClientMessage,
|
|
32
|
+
type TServerMessage,
|
|
33
|
+
} from '@robota-sdk/agent-transport';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
OwnerLinkClosedError,
|
|
37
|
+
OwnerRequestRefusedError,
|
|
38
|
+
type IOwnerControlRequest,
|
|
39
|
+
type IRuntimeCustodyState,
|
|
40
|
+
type TGrantOutcome,
|
|
41
|
+
type TOwnerControlResult,
|
|
42
|
+
type TOwnerSessionQuestion,
|
|
43
|
+
type TRequestedLockReason,
|
|
44
|
+
} from './link-ports.js';
|
|
45
|
+
import {
|
|
46
|
+
MAX_REQUESTS_IN_FLIGHT,
|
|
47
|
+
OWNER_FRAME_MAX_BYTES,
|
|
48
|
+
SEALED_FRAME_MAX_BYTES,
|
|
49
|
+
SESSION_FRAME_MAX_BYTES,
|
|
50
|
+
} from './link-bounds.js';
|
|
51
|
+
import { encodeLinkFrame, encodeLinkOpening, readLinkFrames } from './link-stream.js';
|
|
52
|
+
import {
|
|
53
|
+
decodeRuntimeMessage,
|
|
54
|
+
encodeOwnerRequest,
|
|
55
|
+
type TOwnerRequest,
|
|
56
|
+
type TRuntimeMessage,
|
|
57
|
+
} from './owner-vocabulary.js';
|
|
58
|
+
|
|
59
|
+
/** A client gives up on a runtime that has not admitted it by then. */
|
|
60
|
+
export const DEFAULT_OPEN_TIMEOUT_MS = 10_000;
|
|
61
|
+
|
|
62
|
+
export interface IOpenRuntimeLinkOptions {
|
|
63
|
+
readonly timeoutMs?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The session protocol over the link: what an attach client or a desktop relay sends and hears. */
|
|
67
|
+
export interface ILinkSessionChannel {
|
|
68
|
+
send(message: TClientMessage): void;
|
|
69
|
+
onMessage(listener: (message: TServerMessage) => void): () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One open link, as its client holds it. Only so many requests (read grants included) may wait for
|
|
74
|
+
* the runtime's answer at once; one more is refused at once with an `OwnerRequestRefusedError` coded
|
|
75
|
+
* `busy`, is not sent, and leaves the link open.
|
|
76
|
+
*/
|
|
77
|
+
export interface IRuntimeLinkClient {
|
|
78
|
+
readonly standing: TLinkMode;
|
|
79
|
+
/** The runtime start the handshake proved. */
|
|
80
|
+
readonly binding: ILinkBinding;
|
|
81
|
+
/** The runtime's instance key: the exchange's (spawner) or the certified one (owner). */
|
|
82
|
+
readonly instancePublicKey: Uint8Array;
|
|
83
|
+
/** The custody state the runtime announced last. */
|
|
84
|
+
readonly custody: IRuntimeCustodyState;
|
|
85
|
+
onCustody(listener: (state: IRuntimeCustodyState) => void): () => void;
|
|
86
|
+
installTrustRoot(root: ITrustRoot): Promise<void>;
|
|
87
|
+
lock(reason: TRequestedLockReason): Promise<void>;
|
|
88
|
+
question(sessionId: string): Promise<TOwnerSessionQuestion>;
|
|
89
|
+
openAccepted(sessionId: string, acceptedDigest: string): Promise<void>;
|
|
90
|
+
control(request: IOwnerControlRequest): Promise<TOwnerControlResult>;
|
|
91
|
+
readonly session: ILinkSessionChannel;
|
|
92
|
+
/** Settles once the link has closed, for whatever reason. */
|
|
93
|
+
readonly closed: Promise<void>;
|
|
94
|
+
close(): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** An owner link, which alone can unlock the runtime. */
|
|
98
|
+
export interface IOwnerRuntimeLinkClient extends IRuntimeLinkClient {
|
|
99
|
+
/** Seals the owner's read keys for this runtime and sends them. */
|
|
100
|
+
unlock(): Promise<TGrantOutcome>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type TExpectedReply = TRuntimeMessage['op'];
|
|
104
|
+
|
|
105
|
+
interface IPendingRequest {
|
|
106
|
+
readonly expects: TExpectedReply;
|
|
107
|
+
readonly resolve: (message: TRuntimeMessage) => void;
|
|
108
|
+
readonly reject: (error: Error) => void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: true });
|
|
112
|
+
const textEncoder = new TextEncoder();
|
|
113
|
+
|
|
114
|
+
/** Calls every listener; one that throws is the caller's bug and must not end the link for the rest. */
|
|
115
|
+
function notify<TValue>(listeners: ReadonlySet<(value: TValue) => void>, value: TValue): void {
|
|
116
|
+
for (const listener of [...listeners]) {
|
|
117
|
+
try {
|
|
118
|
+
listener(value);
|
|
119
|
+
} catch {
|
|
120
|
+
// allow-fallback: the frame was valid; a failing listener cannot make the runtime's message wrong.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class RuntimeLinkClient implements IOwnerRuntimeLinkClient {
|
|
126
|
+
readonly closed: Promise<void>;
|
|
127
|
+
readonly session: ILinkSessionChannel;
|
|
128
|
+
readonly #stream: Duplex;
|
|
129
|
+
readonly #link: IInitiatorLink;
|
|
130
|
+
readonly #handover: IOwnerHandover | undefined;
|
|
131
|
+
readonly #pending = new Map<string, IPendingRequest>();
|
|
132
|
+
readonly #custodyListeners = new Set<(state: IRuntimeCustodyState) => void>();
|
|
133
|
+
readonly #sessionListeners = new Set<(message: TServerMessage) => void>();
|
|
134
|
+
#custody: IRuntimeCustodyState;
|
|
135
|
+
#requests = 0;
|
|
136
|
+
#grants = 0;
|
|
137
|
+
#ended = false;
|
|
138
|
+
#resolveClosed!: () => void;
|
|
139
|
+
|
|
140
|
+
constructor(
|
|
141
|
+
stream: Duplex,
|
|
142
|
+
link: IInitiatorLink,
|
|
143
|
+
custody: IRuntimeCustodyState,
|
|
144
|
+
handover: IOwnerHandover | undefined,
|
|
145
|
+
) {
|
|
146
|
+
this.#stream = stream;
|
|
147
|
+
this.#link = link;
|
|
148
|
+
this.#custody = custody;
|
|
149
|
+
this.#handover = handover;
|
|
150
|
+
this.closed = new Promise((resolve) => {
|
|
151
|
+
this.#resolveClosed = resolve;
|
|
152
|
+
});
|
|
153
|
+
this.session = {
|
|
154
|
+
send: (message) => this.#sendSession(message),
|
|
155
|
+
onMessage: (listener) => {
|
|
156
|
+
this.#sessionListeners.add(listener);
|
|
157
|
+
return () => {
|
|
158
|
+
this.#sessionListeners.delete(listener);
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get standing(): TLinkMode {
|
|
165
|
+
return this.#link.standing;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get binding(): ILinkBinding {
|
|
169
|
+
return this.#link.binding;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get instancePublicKey(): Uint8Array {
|
|
173
|
+
return this.#link.instancePublicKey;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get custody(): IRuntimeCustodyState {
|
|
177
|
+
return this.#custody;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onCustody(listener: (state: IRuntimeCustodyState) => void): () => void {
|
|
181
|
+
this.#custodyListeners.add(listener);
|
|
182
|
+
return () => {
|
|
183
|
+
this.#custodyListeners.delete(listener);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async installTrustRoot(root: ITrustRoot): Promise<void> {
|
|
188
|
+
await this.#request((requestId) => ({ op: 'install-trust-root', requestId, root }), 'done');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async lock(reason: TRequestedLockReason): Promise<void> {
|
|
192
|
+
await this.#request((requestId) => ({ op: 'lock', requestId, reason }), 'done');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async question(sessionId: string): Promise<TOwnerSessionQuestion> {
|
|
196
|
+
const reply = await this.#request(
|
|
197
|
+
(requestId) => ({ op: 'question', requestId, sessionId }),
|
|
198
|
+
'question',
|
|
199
|
+
);
|
|
200
|
+
return (reply as Extract<TRuntimeMessage, { op: 'question' }>).question;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async openAccepted(sessionId: string, acceptedDigest: string): Promise<void> {
|
|
204
|
+
await this.#request(
|
|
205
|
+
(requestId) => ({ op: 'open-accepted', requestId, sessionId, acceptedDigest }),
|
|
206
|
+
'done',
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async control(request: IOwnerControlRequest): Promise<TOwnerControlResult> {
|
|
211
|
+
const reply = await this.#request(
|
|
212
|
+
(requestId) => ({ op: 'control', requestId, request }),
|
|
213
|
+
'control',
|
|
214
|
+
);
|
|
215
|
+
return (reply as Extract<TRuntimeMessage, { op: 'control' }>).result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async unlock(): Promise<TGrantOutcome> {
|
|
219
|
+
if (this.#handover === undefined) {
|
|
220
|
+
throw new OwnerRequestRefusedError('not-owner', 'Only an owner link can unlock the runtime.');
|
|
221
|
+
}
|
|
222
|
+
const handover = this.#handover;
|
|
223
|
+
const requestId = `g${this.#grants + 1}`;
|
|
224
|
+
const reply = await this.#await(requestId, 'unlocked', () => {
|
|
225
|
+
this.#grants += 1;
|
|
226
|
+
this.#write(this.#link.sealReadGrant(handover));
|
|
227
|
+
});
|
|
228
|
+
return (reply as Extract<TRuntimeMessage, { op: 'unlocked' }>).sealingKey;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
close(): void {
|
|
232
|
+
this.#stream.destroy();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** One frame from the runtime; anything that is not exactly what it may send ends the link. */
|
|
236
|
+
receive(frame: Uint8Array): void {
|
|
237
|
+
const kind = linkFrameKind(frame);
|
|
238
|
+
const bound = kind === 'owner' ? OWNER_FRAME_MAX_BYTES : SESSION_FRAME_MAX_BYTES;
|
|
239
|
+
if (kind === 'grant' || frame.length > bound + LINK_FRAME_OVERHEAD_BYTES) {
|
|
240
|
+
throw new RangeError('The runtime sent a frame it may not.');
|
|
241
|
+
}
|
|
242
|
+
const { plaintext } = this.#link.open(frame);
|
|
243
|
+
if (kind === 'session') {
|
|
244
|
+
const decoded = decodeFrame(textDecoder.decode(plaintext), decodeServerMessage);
|
|
245
|
+
if (!decoded.ok)
|
|
246
|
+
throw new RangeError('The runtime sent a session message that does not decode.');
|
|
247
|
+
notify(this.#sessionListeners, decoded.message);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const message = decodeRuntimeMessage(plaintext);
|
|
251
|
+
if (message === undefined)
|
|
252
|
+
throw new RangeError('The runtime sent a message outside the vocabulary.');
|
|
253
|
+
if (message.op === 'custody') {
|
|
254
|
+
this.#custody = message.state;
|
|
255
|
+
notify(this.#custodyListeners, message.state);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const pending = this.#pending.get(message.requestId);
|
|
259
|
+
if (pending === undefined || (message.op !== pending.expects && message.op !== 'refused')) {
|
|
260
|
+
throw new RangeError('The runtime answered a request that was not asked.');
|
|
261
|
+
}
|
|
262
|
+
this.#pending.delete(message.requestId);
|
|
263
|
+
if (message.op === 'refused')
|
|
264
|
+
pending.reject(new OwnerRequestRefusedError(message.code, message.message));
|
|
265
|
+
else pending.resolve(message);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The link ended: everything still waiting is rejected. */
|
|
269
|
+
end(): void {
|
|
270
|
+
if (this.#ended) return;
|
|
271
|
+
this.#ended = true;
|
|
272
|
+
this.#link.close();
|
|
273
|
+
for (const pending of this.#pending.values()) pending.reject(new OwnerLinkClosedError());
|
|
274
|
+
this.#pending.clear();
|
|
275
|
+
this.#resolveClosed();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#sendSession(message: TClientMessage): void {
|
|
279
|
+
const bytes = textEncoder.encode(JSON.stringify(message));
|
|
280
|
+
if (bytes.length > SESSION_FRAME_MAX_BYTES) {
|
|
281
|
+
throw new RangeError('A session message over the frame bound.');
|
|
282
|
+
}
|
|
283
|
+
if (this.#ended) throw new OwnerLinkClosedError();
|
|
284
|
+
this.#write(this.#link.seal('session', bytes));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#request(
|
|
288
|
+
build: (requestId: string) => TOwnerRequest,
|
|
289
|
+
expects: TExpectedReply,
|
|
290
|
+
): Promise<TRuntimeMessage> {
|
|
291
|
+
const requestId = `r${this.#requests + 1}`;
|
|
292
|
+
// Encoding first, so a request outside the vocabulary is the caller's error and never reaches the link.
|
|
293
|
+
const bytes = encodeOwnerRequest(build(requestId));
|
|
294
|
+
return this.#await(requestId, expects, () => {
|
|
295
|
+
this.#requests += 1;
|
|
296
|
+
this.#write(this.#link.seal('owner', bytes));
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#await(requestId: string, expects: TExpectedReply, send: () => void): Promise<TRuntimeMessage> {
|
|
301
|
+
if (this.#ended) return Promise.reject(new OwnerLinkClosedError());
|
|
302
|
+
// Refused here, not queued: a runtime ends a link whose peer has more waiting than it allows.
|
|
303
|
+
if (this.#pending.size >= MAX_REQUESTS_IN_FLIGHT) {
|
|
304
|
+
return Promise.reject(
|
|
305
|
+
new OwnerRequestRefusedError('busy', 'The link has as many requests waiting as it allows.'),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
this.#pending.set(requestId, { expects, resolve, reject });
|
|
310
|
+
try {
|
|
311
|
+
send();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
this.#pending.delete(requestId);
|
|
314
|
+
reject(error);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#write(sealed: Uint8Array): void {
|
|
320
|
+
this.#stream.write(encodeLinkFrame(sealed));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function openRuntimeLink(
|
|
325
|
+
stream: Duplex,
|
|
326
|
+
auth: TLinkInitiatorAuth,
|
|
327
|
+
options: IOpenRuntimeLinkOptions,
|
|
328
|
+
): Promise<RuntimeLinkClient> {
|
|
329
|
+
return new Promise((resolve, reject) => {
|
|
330
|
+
let initiator: ReturnType<typeof createLinkInitiator>;
|
|
331
|
+
try {
|
|
332
|
+
initiator = createLinkInitiator(auth);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
stream.destroy();
|
|
335
|
+
reject(error);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
let link: IInitiatorLink | undefined;
|
|
339
|
+
let client: RuntimeLinkClient | undefined;
|
|
340
|
+
/** Why the initiator refused the runtime's answer, if it did; any other end says nothing. */
|
|
341
|
+
let refusal: unknown;
|
|
342
|
+
const fail = (): void => {
|
|
343
|
+
stream.destroy();
|
|
344
|
+
};
|
|
345
|
+
// Rejects itself: a stream that does not report its own close must not leave the opening waiting.
|
|
346
|
+
const timer = setTimeout(() => {
|
|
347
|
+
fail();
|
|
348
|
+
link?.close();
|
|
349
|
+
reject(new OwnerLinkClosedError());
|
|
350
|
+
}, options.timeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS);
|
|
351
|
+
timer.unref?.();
|
|
352
|
+
|
|
353
|
+
readLinkFrames(stream, {
|
|
354
|
+
maxBytes: () => (link === undefined ? LINK_CHALLENGE_MAX_BYTES : SEALED_FRAME_MAX_BYTES),
|
|
355
|
+
onFrame: (frame): void | 'yield' => {
|
|
356
|
+
if (client !== undefined) {
|
|
357
|
+
try {
|
|
358
|
+
client.receive(frame);
|
|
359
|
+
} catch {
|
|
360
|
+
stream.destroy();
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (link === undefined) {
|
|
365
|
+
try {
|
|
366
|
+
const answered = initiator.answer(frame);
|
|
367
|
+
link = answered.link;
|
|
368
|
+
stream.write(encodeLinkFrame(answered.proof));
|
|
369
|
+
} catch (error) {
|
|
370
|
+
refusal = error;
|
|
371
|
+
fail();
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// The runtime's first frame once it admitted the proof: its custody notice.
|
|
376
|
+
try {
|
|
377
|
+
const opened = link.open(frame);
|
|
378
|
+
const notice =
|
|
379
|
+
opened.kind === 'owner' ? decodeRuntimeMessage(opened.plaintext) : undefined;
|
|
380
|
+
if (notice?.op !== 'custody')
|
|
381
|
+
throw new RangeError('The runtime did not announce its custody.');
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
client = new RuntimeLinkClient(
|
|
384
|
+
stream,
|
|
385
|
+
link,
|
|
386
|
+
notice.state,
|
|
387
|
+
auth.mode === 'owner' ? auth.handover : undefined,
|
|
388
|
+
);
|
|
389
|
+
resolve(client);
|
|
390
|
+
// The caller attaches its listeners as the open resolves; frames behind the notice wait for that.
|
|
391
|
+
return 'yield';
|
|
392
|
+
} catch {
|
|
393
|
+
fail();
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
onEnd: () => {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
if (client !== undefined) {
|
|
399
|
+
client.end();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
link?.close();
|
|
403
|
+
// A refusal of the runtime's answer says which check failed; a close says nothing.
|
|
404
|
+
reject(refusal instanceof KeyringError ? refusal : new OwnerLinkClosedError());
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
stream.write(encodeLinkOpening(initiator.hello()));
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Opens a spawner link on `stream`, already connected to the runtime's control endpoint, with the
|
|
413
|
+
* key and instance key the spawner exchange gave. Rejects with `LinkRefusedError` when the runtime
|
|
414
|
+
* is not the one the exchange started, before sending anything secret-derived, and with
|
|
415
|
+
* `OwnerLinkClosedError` when the runtime closes or does not admit the link in time. Listeners
|
|
416
|
+
* attached in the same tick the open resolves receive everything the runtime sends after its
|
|
417
|
+
* custody notice.
|
|
418
|
+
*/
|
|
419
|
+
export async function openSpawnerLink(
|
|
420
|
+
stream: Duplex,
|
|
421
|
+
spawnerKey: SpawnerKey,
|
|
422
|
+
instancePublicKey: Uint8Array,
|
|
423
|
+
options: IOpenRuntimeLinkOptions = {},
|
|
424
|
+
): Promise<IRuntimeLinkClient> {
|
|
425
|
+
return openRuntimeLink(stream, { mode: 'spawner', spawnerKey, instancePublicKey }, options);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Opens an owner link on `stream` with the owner's handover. Rejects with `LinkRefusedError` when the
|
|
430
|
+
* runtime shows no certificate the handover's attach key made for it (an impostor, a runtime nobody
|
|
431
|
+
* certified, or one another keyring certified), before sending anything secret-derived. Listeners
|
|
432
|
+
* attached in the same tick the open resolves receive everything the runtime sends after its
|
|
433
|
+
* custody notice.
|
|
434
|
+
*/
|
|
435
|
+
export async function openOwnerLink(
|
|
436
|
+
stream: Duplex,
|
|
437
|
+
handover: IOwnerHandover,
|
|
438
|
+
options: IOpenRuntimeLinkOptions = {},
|
|
439
|
+
): Promise<IOwnerRuntimeLinkClient> {
|
|
440
|
+
return openRuntimeLink(stream, { mode: 'owner', handover }, options);
|
|
441
|
+
}
|