@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
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @robota-sdk/agent-transport-link
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The owner link carrier for the Robota SDK. It runs the owner link of `@robota-sdk/agent-keyring`
|
|
4
|
+
over a local byte stream, such as a supervised runtime's control socket, so a client reaches a
|
|
5
|
+
runtime only by proof: the process that started the runtime by the key it derived with it, and the
|
|
6
|
+
owner by the attach key behind the passphrase. Any other program that connects, takes the socket
|
|
7
|
+
over or answers in the runtime's place gets nothing past the client's first message.
|
|
8
|
+
|
|
9
|
+
Inside the link it carries the unchanged session protocol of `@robota-sdk/agent-transport` and the
|
|
10
|
+
owner vocabulary: installing the runtime's trust root, a read grant that unlocks the runtime, lock,
|
|
11
|
+
the question asked before opening a session not verified as the owner's, and the runtime's existing
|
|
12
|
+
control commands.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @robota-sdk/agent-transport-link @robota-sdk/agent-keyring
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node.js 22.12 or later; it also runs on Bun.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
The runtime's control endpoint routes the request line `{"command":"link","protocol":1}` to
|
|
25
|
+
`serveRuntimeLink`, with the bytes that followed it:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createRuntimeIdentity } from '@robota-sdk/agent-keyring';
|
|
29
|
+
import { serveRuntimeLink } from '@robota-sdk/agent-transport-link';
|
|
30
|
+
|
|
31
|
+
const identity = createRuntimeIdentity(); // answers the spawner exchange at start
|
|
32
|
+
serveRuntimeLink(socket, {
|
|
33
|
+
identity,
|
|
34
|
+
binding: { runtimeId, generation },
|
|
35
|
+
rest,
|
|
36
|
+
admit: (link) => runtimeCustody.portsFor(link), // decides what link.standing may do
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The spawner certifies the runtime it started; the owner then unlocks it:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { openOwnerLink, openSpawnerLink } from '@robota-sdk/agent-transport-link';
|
|
44
|
+
|
|
45
|
+
const spawner = await openSpawnerLink(socket, spawnerKey, instancePublicKey);
|
|
46
|
+
await spawner.installTrustRoot(handover.certify(spawner.instancePublicKey, spawner.binding));
|
|
47
|
+
spawner.close();
|
|
48
|
+
spawnerKey.wipe();
|
|
49
|
+
|
|
50
|
+
const owner = await openOwnerLink(otherSocket, handover);
|
|
51
|
+
await owner.unlock(); // 'verified' or 'mismatch'
|
|
52
|
+
owner.session.send({ type: 'submit', prompt: 'hello' });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Opening rejects with the keyring's `LinkRefusedError` when the runtime is not the one it must be
|
|
56
|
+
(`reason` says which check failed, for example `certificate-mismatch` for a runtime another keyring
|
|
57
|
+
certified), and with `OwnerLinkClosedError` when the runtime closes the link.
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
| Export | Description |
|
|
62
|
+
| -------------------------------------- | --------------------------------------------------------------------------------- |
|
|
63
|
+
| `serveRuntimeLink` | The runtime's end of one link, after the control endpoint routed the request line |
|
|
64
|
+
| `openSpawnerLink`, `openOwnerLink` | The client's end, by the spawner key or by the owner handover |
|
|
65
|
+
| `LINK_REQUEST_LINE`, `isLinkRequest` | The request line that opens a link, and its strict check |
|
|
66
|
+
| `IOwnerLinkDesk`, `IOwnerLinkSessions` | The runtime's ports for one admitted link |
|
|
67
|
+
| `OwnerRequestRefusedError` | A refusal the runtime decided, with its code; what a desk throws to refuse |
|
|
68
|
+
| `OwnerLinkClosedError` | The link closed, or never opened |
|
|
69
|
+
|
|
70
|
+
See [`docs/SPEC.md`](./docs/SPEC.md) for the contract.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
This package is dual-licensed under the [GNU AGPL-3.0](../../LICENSE) or a [commercial license](../../COMMERCIAL.md). See [LICENSING.md](../../LICENSING.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@robota-sdk/agent-keyring"),t=require("node:buffer"),n=require("@robota-sdk/agent-transport");const r=`${JSON.stringify({command:`link`,protocol:1})}\n`;function i(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=e;return Object.keys(t).length===2&&t.command===`link`&&t.protocol===1}const a=/^[a-z][a-z0-9-]{0,63}$/u;var o=class extends Error{code;constructor(e,t){if(!a.test(e)||t.length===0||t.length>1024)throw RangeError(`A refusal has a lower-case code and a message of 1-1024 characters.`);super(t),this.name=`OwnerRequestRefusedError`,this.code=e}},s=class extends Error{constructor(){super(`The owner link closed.`),this.name=`OwnerLinkClosedError`}};function c(e){let n=t.Buffer.alloc(4);return n.writeUInt32BE(e.length),t.Buffer.concat([n,e])}function l(e){return t.Buffer.concat([t.Buffer.from(r,`utf8`),c(e)])}var u=class{#e=[];#t=0;push(e){e.length!==0&&(this.#e.push(t.Buffer.from(e.buffer,e.byteOffset,e.byteLength)),this.#t+=e.length)}next(e){if(this.#t<4)return;this.#e[0].length<4&&this.#n();let n=this.#e[0].readUInt32BE(0);if(n>e)throw RangeError(`A frame was announced over its bound.`);if(this.#t<4+n)return;let r=this.#n(),i=4+n,a=t.Buffer.from(r.subarray(4,i)),o=r.subarray(i);return this.#e=o.length>0?[o]:[],this.#t=o.length,a}#n(){let e=this.#e.length===1?this.#e[0]:t.Buffer.concat(this.#e,this.#t);return this.#e=[e],e}};function d(e,t){let n=new u,r=!1,i=()=>{r||(r=!0,t.onEnd())},a=!1,o=()=>{if(!a)try{for(let r=n.next(t.maxBytes());r!==void 0;r=n.next(t.maxBytes())){if(e.destroyed)return;let n=t.onFrame(r);if(e.destroyed)return;if(n===`yield`){a=!0,e.pause(),setImmediate(()=>{a=!1,o(),a||e.resume()});return}}}catch{e.destroy()}};e.on(`data`,t=>{if(typeof t==`string`){e.destroy();return}n.push(t),o()}),e.on(`error`,()=>e.destroy()),e.once(`close`,i),e.once(`end`,()=>e.destroy()),t.rest!==void 0&&n.push(t.rest),o(),e.resume(),e.destroyed&&process.nextTick(i)}const f=n.MAX_INBOUND_FRAME_BYTES,p=1024*1024,m=Math.max(f,p)+e.LINK_FRAME_OVERHEAD_BYTES,h=/^r[1-9][0-9]{0,15}$/u,g=/^g[1-9][0-9]{0,15}$/u,_=/^[A-Za-z0-9._:-]{1,256}$/u,v=/^[A-Za-z0-9._:+/=-]{1,256}$/u,y=/^[a-z][a-z0-9-]{0,31}$/u,b=/^[A-Za-z][A-Za-z0-9]{0,31}$/u,x=/^[a-z][a-z0-9-]{0,63}$/u,S=/^[A-Za-z0-9_-]+$/u,C=/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/u,w=/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/u,T=4096,E=new Set([`explicit`,`screen-lock`,`suspend`]),D=new Set([`verified`,`mismatch`]),O=new TextDecoder(`utf-8`,{fatal:!0});function k(e){try{return JSON.parse(O.decode(e))}catch{return}}function A(e,t){if(!(typeof e!=`object`||!e||Array.isArray(e))&&!(Object.keys(e).length!==t.length||!t.every(t=>Object.hasOwn(e,t))))return e}const j=(e,t)=>typeof e==`string`&&t.test(e),M=(e,t)=>typeof e==`string`&&e.length>0&&e.length<=t&&!C.test(e);function N(e,n){if(!j(e,S))return;let r=t.Buffer.from(e,`base64url`);return r.length===n&&r.toString(`base64url`)===e?Uint8Array.from(r):void 0}function P(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=Object.entries(e);if(t.length>16)return;let n={};for(let[e,r]of t){if(!b.test(e)||typeof r!=`string`||r.length>4096||w.test(r))return;n[e]=r}return n}function F(e){let t=0,n=(e,r)=>{if(t+=1,t>4096)return;if(e===null||typeof e==`boolean`)return e;if(typeof e==`number`)return Number.isFinite(e)?e:void 0;if(typeof e==`string`)return e.length<=65536?e:void 0;if(r>=8||typeof e!=`object`)return;if(Array.isArray(e)){let t=[];for(let i of e){let e=n(i,r+1);if(e===void 0)return;t.push(e)}return t}let i={};for(let[t,a]of Object.entries(e)){if(t===`__proto__`||t.length>256)return;let e=n(a,r+1);if(e===void 0)return;i[t]=e}return i};if(typeof e!=`object`||!e||Array.isArray(e))return;let r=n(e,0);return r===void 0?void 0:r}function I(e){let t=typeof e==`object`&&e?e.kind:void 0;if(t===`open`)return A(e,[`kind`])?{kind:`open`}:void 0;if(t===`accept`){let t=A(e,[`kind`,`digest`,`notice`,`question`]);return t===void 0||!j(t.digest,v)||!M(t.notice,T)||!M(t.question,T)?void 0:{kind:`accept`,digest:t.digest,notice:t.notice,question:t.question}}if(t===`refused`){let t=A(e,[`kind`,`message`]);return t!==void 0&&M(t.message,T)?{kind:`refused`,message:t.message}:void 0}}function L(e){let t=A(e,[`unlocked`,`certified`]);return t!==void 0&&typeof t.unlocked==`boolean`&&typeof t.certified==`boolean`?{unlocked:t.unlocked,certified:t.certified}:void 0}function R(e){let t=k(e),n=typeof t==`object`&&t?t.op:void 0;switch(n){case`install-trust-root`:{let e=A(t,[`op`,`requestId`,`attachPublicKey`,`certificate`]),r=N(e?.attachPublicKey,32),i=N(e?.certificate,64);return!j(e?.requestId,h)||!r||!i?void 0:{op:n,requestId:e.requestId,root:{attachPublicKey:r,certificate:i}}}case`lock`:{let e=A(t,[`op`,`requestId`,`reason`]),r=e?.reason;return!j(e?.requestId,h)||typeof r!=`string`||!E.has(r)?void 0:{op:n,requestId:e.requestId,reason:r}}case`question`:{let e=A(t,[`op`,`requestId`,`sessionId`]);return!j(e?.requestId,h)||!j(e.sessionId,_)?void 0:{op:n,requestId:e.requestId,sessionId:e.sessionId}}case`open-accepted`:{let e=A(t,[`op`,`requestId`,`sessionId`,`acceptedDigest`]);return!j(e?.requestId,h)||!j(e.sessionId,_)||!j(e.acceptedDigest,v)?void 0:{op:n,requestId:e.requestId,sessionId:e.sessionId,acceptedDigest:e.acceptedDigest}}case`control`:{let e=A(t,[`op`,`requestId`,`command`,`fields`]),r=P(e?.fields);return!j(e?.requestId,h)||!j(e.command,y)||!r?void 0:{op:n,requestId:e.requestId,request:{command:e.command,fields:r}}}default:return}}function z(e){let t=k(e),n=typeof t==`object`&&t?t.op:void 0;switch(n){case`done`:{let e=A(t,[`op`,`requestId`]);return j(e?.requestId,h)?{op:n,requestId:e.requestId}:void 0}case`unlocked`:{let e=A(t,[`op`,`requestId`,`sealingKey`]),r=e?.sealingKey;return!j(e?.requestId,g)||typeof r!=`string`||!D.has(r)?void 0:{op:n,requestId:e.requestId,sealingKey:r}}case`question`:{let e=A(t,[`op`,`requestId`,`question`]),r=I(e?.question);return j(e?.requestId,h)&&r?{op:n,requestId:e.requestId,question:r}:void 0}case`refused`:{let e=A(t,[`op`,`requestId`,`code`,`message`]),r=e?.requestId;return!(j(r,h)||j(r,g))||!j(e?.code,x)||!M(e.message,1024)?void 0:{op:n,requestId:r,code:e.code,message:e.message}}case`control`:{let e=A(t,[`op`,`requestId`,`result`]),r=F(e?.result);return j(e?.requestId,h)&&r?{op:n,requestId:e.requestId,result:r}:void 0}case`custody`:{let e=L(A(t,[`op`,`state`])?.state);return e?{op:n,state:e}:void 0}default:return}}function B(e){switch(e.op){case`install-trust-root`:return{op:e.op,requestId:e.requestId,attachPublicKey:t.Buffer.from(e.root.attachPublicKey).toString(`base64url`),certificate:t.Buffer.from(e.root.certificate).toString(`base64url`)};case`control`:return{op:e.op,requestId:e.requestId,command:e.request.command,fields:{...e.request.fields}};default:return{...e}}}function V(e,t){if(t(e)===void 0)throw RangeError(`The message is outside the owner vocabulary.`);return e}function H(e){return V(t.Buffer.from(JSON.stringify(B(e)),`utf8`),R)}function U(e){return V(t.Buffer.from(JSON.stringify(e),`utf8`),z)}const W=new TextDecoder(`utf-8`,{fatal:!0}),G=new TextEncoder;function K(e){return e instanceof o?{code:e.code,message:e.message}:{code:`failed`,message:`The runtime could not do this.`}}function q(t,n){let r;try{r=(0,e.createLinkResponder)(n.identity,n.binding)}catch(e){throw t.destroy(),e}let i=`hello`,a,o,s=0,l=0,u=setTimeout(()=>t.destroy(),n.handshakeTimeoutMs??1e4);u.unref?.();let h,g=(e,n)=>{let r=()=>{a===void 0||a.closed||t.destroyed||t.write(c(a.seal(e,n)))};h===void 0?r():h.push(r)},_=e=>{a===void 0||a.closed||t.destroyed||g(`owner`,U(e))},v=()=>l<64?!0:(t.destroy(),!1),y=(e,n)=>{l+=1,n().then(e=>e,t=>({op:`refused`,requestId:e,...K(t)})).then(t=>{--l;try{_(t)}catch{_({op:`refused`,requestId:e,...K(void 0)})}}).catch(()=>t.destroy())},b=(e,t)=>{let{requestId:n}=e;switch(e.op){case`install-trust-root`:return y(n,async()=>(await t.desk.installTrustRoot(e.root),{op:`done`,requestId:n}));case`lock`:return y(n,async()=>(await t.desk.lock(e.reason),{op:`done`,requestId:n}));case`question`:return y(n,async()=>({op:`question`,requestId:n,question:await t.sessions.question(e.sessionId)}));case`open-accepted`:return y(n,async()=>(await t.sessions.openAccepted(e.sessionId,e.acceptedDigest),{op:`done`,requestId:n}));case`control`:return y(n,async()=>({op:`control`,requestId:n,result:await t.desk.control(e.request)}))}},x=(e,t)=>{s+=1;let n=`g${s}`;y(n,async()=>{try{return{op:`unlocked`,requestId:n,sealingKey:await t.desk.unlock(e)}}catch(t){throw e.lock(),t}})},S=(t,n,r)=>{let i=(0,e.linkFrameKind)(t);if(i===`grant`){if(t.length!==e.LINK_GRANT_FRAME_BYTES)throw RangeError(`A grant has one size.`);if(!v())return;x(r.openReadGrant(t),n);return}let a=i===`owner`?p:f;if(t.length>a+e.LINK_FRAME_OVERHEAD_BYTES)throw RangeError(`A frame over its bound.`);let{plaintext:o}=r.open(t);if(i===`session`){n.session.receive(W.decode(o));return}let s=R(o);if(s===void 0)throw RangeError(`A request outside the vocabulary.`);v()&&b(s,n)},C={get standing(){return a.standing},binding:n.binding,get closed(){return a===void 0||a.closed||t.destroyed},sendSession(e){let n=G.encode(e);if(n.length>f)throw RangeError(`A session message over the frame bound.`);a===void 0||a.closed||t.destroyed||g(`session`,n)},notifyCustody(e){_({op:`custody`,state:{unlocked:e.unlocked,certified:e.certified}})},close(){t.destroy()}};d(t,{...n.rest===void 0?{}:{rest:n.rest},maxBytes:()=>i===`hello`?e.LINK_HELLO_BYTES:i===`proof`?e.LINK_PROOF_MAX_BYTES:m,onFrame:e=>{try{if(i===`hello`){t.write(c(r.challenge(e))),i=`proof`;return}if(i===`proof`){a=r.accept(e).link,clearTimeout(u),i=`linked`;let t=[];h=t,o=n.admit(C);let s=o.desk.custody();h=void 0,C.notifyCustody(s);for(let e of t)e();return}S(e,o,a)}catch{t.destroy()}},onEnd:()=>{if(clearTimeout(u),a?.close(),o!==void 0)try{o.desk.closed()}catch{}}})}const J=new TextDecoder(`utf-8`,{fatal:!0}),Y=new TextEncoder;function X(e,t){for(let n of[...e])try{n(t)}catch{}}var Z=class{closed;session;#e;#t;#n;#r=new Map;#i=new Set;#a=new Set;#o;#s=0;#c=0;#l=!1;#u;constructor(e,t,n,r){this.#e=e,this.#t=t,this.#o=n,this.#n=r,this.closed=new Promise(e=>{this.#u=e}),this.session={send:e=>this.#d(e),onMessage:e=>(this.#a.add(e),()=>{this.#a.delete(e)})}}get standing(){return this.#t.standing}get binding(){return this.#t.binding}get instancePublicKey(){return this.#t.instancePublicKey}get custody(){return this.#o}onCustody(e){return this.#i.add(e),()=>{this.#i.delete(e)}}async installTrustRoot(e){await this.#f(t=>({op:`install-trust-root`,requestId:t,root:e}),`done`)}async lock(e){await this.#f(t=>({op:`lock`,requestId:t,reason:e}),`done`)}async question(e){return(await this.#f(t=>({op:`question`,requestId:t,sessionId:e}),`question`)).question}async openAccepted(e,t){await this.#f(n=>({op:`open-accepted`,requestId:n,sessionId:e,acceptedDigest:t}),`done`)}async control(e){return(await this.#f(t=>({op:`control`,requestId:t,request:e}),`control`)).result}async unlock(){if(this.#n===void 0)throw new o(`not-owner`,`Only an owner link can unlock the runtime.`);let e=this.#n,t=`g${this.#c+1}`;return(await this.#p(t,`unlocked`,()=>{this.#c+=1,this.#m(this.#t.sealReadGrant(e))})).sealingKey}close(){this.#e.destroy()}receive(t){let r=(0,e.linkFrameKind)(t),i=r===`owner`?p:f;if(r===`grant`||t.length>i+e.LINK_FRAME_OVERHEAD_BYTES)throw RangeError(`The runtime sent a frame it may not.`);let{plaintext:a}=this.#t.open(t);if(r===`session`){let e=(0,n.decodeFrame)(J.decode(a),n.decodeServerMessage);if(!e.ok)throw RangeError(`The runtime sent a session message that does not decode.`);X(this.#a,e.message);return}let s=z(a);if(s===void 0)throw RangeError(`The runtime sent a message outside the vocabulary.`);if(s.op===`custody`){this.#o=s.state,X(this.#i,s.state);return}let c=this.#r.get(s.requestId);if(c===void 0||s.op!==c.expects&&s.op!==`refused`)throw RangeError(`The runtime answered a request that was not asked.`);this.#r.delete(s.requestId),s.op===`refused`?c.reject(new o(s.code,s.message)):c.resolve(s)}end(){if(!this.#l){this.#l=!0,this.#t.close();for(let e of this.#r.values())e.reject(new s);this.#r.clear(),this.#u()}}#d(e){let t=Y.encode(JSON.stringify(e));if(t.length>f)throw RangeError(`A session message over the frame bound.`);if(this.#l)throw new s;this.#m(this.#t.seal(`session`,t))}#f(e,t){let n=`r${this.#s+1}`,r=H(e(n));return this.#p(n,t,()=>{this.#s+=1,this.#m(this.#t.seal(`owner`,r))})}#p(e,t,n){return this.#l?Promise.reject(new s):this.#r.size>=64?Promise.reject(new o(`busy`,`The link has as many requests waiting as it allows.`)):new Promise((r,i)=>{this.#r.set(e,{expects:t,resolve:r,reject:i});try{n()}catch(t){this.#r.delete(e),i(t)}})}#m(e){this.#e.write(c(e))}};function Q(t,n,r){return new Promise((i,a)=>{let o;try{o=(0,e.createLinkInitiator)(n)}catch(e){t.destroy(),a(e);return}let u,f,p,h=()=>{t.destroy()},g=setTimeout(()=>{h(),u?.close(),a(new s)},r.timeoutMs??1e4);g.unref?.(),d(t,{maxBytes:()=>u===void 0?e.LINK_CHALLENGE_MAX_BYTES:m,onFrame:e=>{if(f!==void 0){try{f.receive(e)}catch{t.destroy()}return}if(u===void 0){try{let n=o.answer(e);u=n.link,t.write(c(n.proof))}catch(e){p=e,h()}return}try{let r=u.open(e),a=r.kind===`owner`?z(r.plaintext):void 0;if(a?.op!==`custody`)throw RangeError(`The runtime did not announce its custody.`);return clearTimeout(g),f=new Z(t,u,a.state,n.mode===`owner`?n.handover:void 0),i(f),`yield`}catch{h()}},onEnd:()=>{if(clearTimeout(g),f!==void 0){f.end();return}u?.close(),a(p instanceof e.KeyringError?p:new s)}}),t.write(l(o.hello()))})}async function $(e,t,n,r={}){return Q(e,{mode:`spawner`,spawnerKey:t,instancePublicKey:n},r)}async function ee(e,t,n={}){return Q(e,{mode:`owner`,handover:t},n)}exports.DEFAULT_HANDSHAKE_TIMEOUT_MS=1e4,exports.DEFAULT_OPEN_TIMEOUT_MS=1e4,exports.LINK_PROTOCOL=1,exports.LINK_REQUEST_LINE=r,exports.OwnerLinkClosedError=s,exports.OwnerRequestRefusedError=o,exports.isLinkRequest=i,exports.openOwnerLink=ee,exports.openSpawnerLink=$,exports.serveRuntimeLink=q;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Duplex } from "node:stream";
|
|
2
|
+
import { ILinkBinding, IOwnerHandover, IReadKeys, IRuntimeIdentity, ITrustRoot, SpawnerKey, TLinkMode } from "@robota-sdk/agent-keyring";
|
|
3
|
+
import { TClientMessage, TServerMessage } from "@robota-sdk/agent-transport";
|
|
4
|
+
//#region src/link-request.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The request line that opens an owner link, in the control endpoint's existing request shape so
|
|
7
|
+
* the endpoint routes it like any other command. It names no runtime and no start: the binding is
|
|
8
|
+
* proven inside the handshake, never taken from the request.
|
|
9
|
+
*/
|
|
10
|
+
declare const LINK_PROTOCOL = 1;
|
|
11
|
+
/** Exactly what a client writes first, newline included. */
|
|
12
|
+
declare const LINK_REQUEST_LINE: string;
|
|
13
|
+
/** Whether a parsed request line is exactly the link request; anything more or less is not. */
|
|
14
|
+
declare function isLinkRequest(value: unknown): boolean;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/link-ports.d.ts
|
|
17
|
+
/** What a client may ask a runtime to lock for; idle and last-link-closed are the runtime's own. */
|
|
18
|
+
type TRequestedLockReason = 'explicit' | 'screen-lock' | 'suspend';
|
|
19
|
+
/**
|
|
20
|
+
* How a read grant went. `mismatch`: the keyring file's sealing public key is not the granted one;
|
|
21
|
+
* the runtime seals to the granted key, and the owner is told to run `secure verify`.
|
|
22
|
+
*/
|
|
23
|
+
type TGrantOutcome = 'verified' | 'mismatch';
|
|
24
|
+
/** What every linked client is told, once admitted and whenever it changes. */
|
|
25
|
+
interface IRuntimeCustodyState {
|
|
26
|
+
readonly unlocked: boolean;
|
|
27
|
+
/** Whether owner links are possible: a spawner installed a trust root. */
|
|
28
|
+
readonly certified: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The runtime's answer to "may I open this session?". `accept` asks the owner, on a surface that
|
|
32
|
+
* shows the fixed `notice` and `question` and never the record, and `digest` binds the answer to
|
|
33
|
+
* the bytes the runtime read.
|
|
34
|
+
*/
|
|
35
|
+
type TOwnerSessionQuestion = {
|
|
36
|
+
readonly kind: 'open';
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: 'accept';
|
|
39
|
+
readonly digest: string;
|
|
40
|
+
readonly notice: string;
|
|
41
|
+
readonly question: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly kind: 'refused';
|
|
44
|
+
readonly message: string;
|
|
45
|
+
};
|
|
46
|
+
/** One of the runtime's existing control commands, carried inside the link. */
|
|
47
|
+
interface IOwnerControlRequest {
|
|
48
|
+
readonly command: string;
|
|
49
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
50
|
+
}
|
|
51
|
+
type TOwnerControlValue = null | boolean | number | string | readonly TOwnerControlValue[] | {
|
|
52
|
+
readonly [key: string]: TOwnerControlValue;
|
|
53
|
+
};
|
|
54
|
+
/** What a control command answered. */
|
|
55
|
+
type TOwnerControlResult = {
|
|
56
|
+
readonly [key: string]: TOwnerControlValue;
|
|
57
|
+
};
|
|
58
|
+
/** The runtime's custody answers for one admitted link. */
|
|
59
|
+
interface IOwnerLinkDesk {
|
|
60
|
+
/** The state the link announces as soon as it is admitted. */
|
|
61
|
+
custody(): IRuntimeCustodyState;
|
|
62
|
+
installTrustRoot(root: ITrustRoot): void | Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Installs the granted read keys. The desk owns them once this returns; when it refuses, by
|
|
65
|
+
* throwing, the carrier locks them.
|
|
66
|
+
*/
|
|
67
|
+
unlock(keys: IReadKeys): TGrantOutcome | Promise<TGrantOutcome>;
|
|
68
|
+
lock(reason: TRequestedLockReason): void | Promise<void>;
|
|
69
|
+
control(request: IOwnerControlRequest): TOwnerControlResult | Promise<TOwnerControlResult>;
|
|
70
|
+
/** The link ended, for whatever reason. Called once. */
|
|
71
|
+
closed(): void;
|
|
72
|
+
}
|
|
73
|
+
/** The runtime's session decisions for one admitted link. */
|
|
74
|
+
interface IOwnerLinkSessions {
|
|
75
|
+
question(sessionId: string): TOwnerSessionQuestion | Promise<TOwnerSessionQuestion>;
|
|
76
|
+
openAccepted(sessionId: string, acceptedDigest: string): void | Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/** Where the session protocol of an admitted link goes: one serialized client message at a time. */
|
|
79
|
+
interface ILinkSessionSink {
|
|
80
|
+
receive(data: string): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A request the runtime refused, by decision: a desk throws it with a fixed code and fixed text, and
|
|
84
|
+
* the client rejects with it. Any other failure reaches the client as `failed`, never with its text.
|
|
85
|
+
*/
|
|
86
|
+
declare class OwnerRequestRefusedError extends Error {
|
|
87
|
+
readonly code: string;
|
|
88
|
+
constructor(code: string, message: string);
|
|
89
|
+
}
|
|
90
|
+
/** The link closed, or never opened: the other end does not say why. */
|
|
91
|
+
declare class OwnerLinkClosedError extends Error {
|
|
92
|
+
constructor();
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/serve-runtime-link.d.ts
|
|
96
|
+
/** A runtime gives up on a handshake that has not finished by then. */
|
|
97
|
+
declare const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000;
|
|
98
|
+
/** An admitted link, as the runtime holds it. */
|
|
99
|
+
interface IAdmittedRuntimeLink {
|
|
100
|
+
/** How the client was admitted: `spawner` by the spawner key, `owner` by the attach key. */
|
|
101
|
+
readonly standing: TLinkMode;
|
|
102
|
+
readonly binding: ILinkBinding;
|
|
103
|
+
readonly closed: boolean;
|
|
104
|
+
/** Sends one serialized session-protocol message; throws `RangeError` for one over the frame bound. */
|
|
105
|
+
sendSession(data: string): void;
|
|
106
|
+
/** Tells the client its runtime's custody state changed. */
|
|
107
|
+
notifyCustody(state: IRuntimeCustodyState): void;
|
|
108
|
+
close(): void;
|
|
109
|
+
}
|
|
110
|
+
/** The runtime's answers for one admitted link. */
|
|
111
|
+
interface IRuntimeLinkPorts {
|
|
112
|
+
readonly desk: IOwnerLinkDesk;
|
|
113
|
+
readonly sessions: IOwnerLinkSessions;
|
|
114
|
+
readonly session: ILinkSessionSink;
|
|
115
|
+
}
|
|
116
|
+
interface IRuntimeLinkDeps {
|
|
117
|
+
readonly identity: IRuntimeIdentity;
|
|
118
|
+
/** This runtime start, as its registration names it. */
|
|
119
|
+
readonly binding: ILinkBinding;
|
|
120
|
+
/** The bytes after the request line that arrived with it. */
|
|
121
|
+
readonly rest?: Uint8Array;
|
|
122
|
+
readonly handshakeTimeoutMs?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Called once the handshake admitted the client; the ports it returns serve that link. The opening
|
|
125
|
+
* custody notice is always the first frame the client sees: whatever `admit` sends through the link
|
|
126
|
+
* goes out right after it, in the order it was sent.
|
|
127
|
+
*/
|
|
128
|
+
readonly admit: (link: IAdmittedRuntimeLink) => IRuntimeLinkPorts;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Serves one owner link on `stream`, which the control endpoint has read the request line from and
|
|
132
|
+
* paused. Returns at once; the link lives until the stream closes.
|
|
133
|
+
*/
|
|
134
|
+
declare function serveRuntimeLink(stream: Duplex, deps: IRuntimeLinkDeps): void;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/open-runtime-link.d.ts
|
|
137
|
+
/** A client gives up on a runtime that has not admitted it by then. */
|
|
138
|
+
declare const DEFAULT_OPEN_TIMEOUT_MS = 10000;
|
|
139
|
+
interface IOpenRuntimeLinkOptions {
|
|
140
|
+
readonly timeoutMs?: number;
|
|
141
|
+
}
|
|
142
|
+
/** The session protocol over the link: what an attach client or a desktop relay sends and hears. */
|
|
143
|
+
interface ILinkSessionChannel {
|
|
144
|
+
send(message: TClientMessage): void;
|
|
145
|
+
onMessage(listener: (message: TServerMessage) => void): () => void;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One open link, as its client holds it. Only so many requests (read grants included) may wait for
|
|
149
|
+
* the runtime's answer at once; one more is refused at once with an `OwnerRequestRefusedError` coded
|
|
150
|
+
* `busy`, is not sent, and leaves the link open.
|
|
151
|
+
*/
|
|
152
|
+
interface IRuntimeLinkClient {
|
|
153
|
+
readonly standing: TLinkMode;
|
|
154
|
+
/** The runtime start the handshake proved. */
|
|
155
|
+
readonly binding: ILinkBinding;
|
|
156
|
+
/** The runtime's instance key: the exchange's (spawner) or the certified one (owner). */
|
|
157
|
+
readonly instancePublicKey: Uint8Array;
|
|
158
|
+
/** The custody state the runtime announced last. */
|
|
159
|
+
readonly custody: IRuntimeCustodyState;
|
|
160
|
+
onCustody(listener: (state: IRuntimeCustodyState) => void): () => void;
|
|
161
|
+
installTrustRoot(root: ITrustRoot): Promise<void>;
|
|
162
|
+
lock(reason: TRequestedLockReason): Promise<void>;
|
|
163
|
+
question(sessionId: string): Promise<TOwnerSessionQuestion>;
|
|
164
|
+
openAccepted(sessionId: string, acceptedDigest: string): Promise<void>;
|
|
165
|
+
control(request: IOwnerControlRequest): Promise<TOwnerControlResult>;
|
|
166
|
+
readonly session: ILinkSessionChannel;
|
|
167
|
+
/** Settles once the link has closed, for whatever reason. */
|
|
168
|
+
readonly closed: Promise<void>;
|
|
169
|
+
close(): void;
|
|
170
|
+
}
|
|
171
|
+
/** An owner link, which alone can unlock the runtime. */
|
|
172
|
+
interface IOwnerRuntimeLinkClient extends IRuntimeLinkClient {
|
|
173
|
+
/** Seals the owner's read keys for this runtime and sends them. */
|
|
174
|
+
unlock(): Promise<TGrantOutcome>;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Opens a spawner link on `stream`, already connected to the runtime's control endpoint, with the
|
|
178
|
+
* key and instance key the spawner exchange gave. Rejects with `LinkRefusedError` when the runtime
|
|
179
|
+
* is not the one the exchange started, before sending anything secret-derived, and with
|
|
180
|
+
* `OwnerLinkClosedError` when the runtime closes or does not admit the link in time. Listeners
|
|
181
|
+
* attached in the same tick the open resolves receive everything the runtime sends after its
|
|
182
|
+
* custody notice.
|
|
183
|
+
*/
|
|
184
|
+
declare function openSpawnerLink(stream: Duplex, spawnerKey: SpawnerKey, instancePublicKey: Uint8Array, options?: IOpenRuntimeLinkOptions): Promise<IRuntimeLinkClient>;
|
|
185
|
+
/**
|
|
186
|
+
* Opens an owner link on `stream` with the owner's handover. Rejects with `LinkRefusedError` when the
|
|
187
|
+
* runtime shows no certificate the handover's attach key made for it (an impostor, a runtime nobody
|
|
188
|
+
* certified, or one another keyring certified), before sending anything secret-derived. Listeners
|
|
189
|
+
* attached in the same tick the open resolves receive everything the runtime sends after its
|
|
190
|
+
* custody notice.
|
|
191
|
+
*/
|
|
192
|
+
declare function openOwnerLink(stream: Duplex, handover: IOwnerHandover, options?: IOpenRuntimeLinkOptions): Promise<IOwnerRuntimeLinkClient>;
|
|
193
|
+
//#endregion
|
|
194
|
+
export { DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_OPEN_TIMEOUT_MS, type IAdmittedRuntimeLink, type ILinkSessionChannel, type ILinkSessionSink, type IOpenRuntimeLinkOptions, type IOwnerControlRequest, type IOwnerLinkDesk, type IOwnerLinkSessions, type IOwnerRuntimeLinkClient, type IRuntimeCustodyState, type IRuntimeLinkClient, type IRuntimeLinkDeps, type IRuntimeLinkPorts, LINK_PROTOCOL, LINK_REQUEST_LINE, OwnerLinkClosedError, OwnerRequestRefusedError, type TGrantOutcome, type TOwnerControlResult, type TOwnerControlValue, type TOwnerSessionQuestion, type TRequestedLockReason, isLinkRequest, openOwnerLink, openSpawnerLink, serveRuntimeLink };
|
|
195
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/link-request.ts","../../src/link-ports.ts","../../src/serve-runtime-link.ts","../../src/open-runtime-link.ts"],"mappings":";;;;;;;;;cAKa;;cAGA;;iBAGG,cAAc;;;;KCElB;;;;;KAMA;;UAGK;WACN;;WAEA;;;;;;;KAQC;WACG;;WAEA;WACA;WACA;WACA;;WAEA;WAA0B;;;UAGxB;WACN;WACA,QAAQ,SAAS;;KAGhB,iEAKC;YACG,cAAc;;;KAGlB;YAAkC,cAAc;;;UAG3C;;EAEf,WAAW;EACX,iBAAiB,MAAM,oBAAoB;;;;;EAK3C,OAAO,MAAM,YAAY,gBAAgB,QAAQ;EACjD,KAAK,QAAQ,8BAA8B;EAC3C,QAAQ,SAAS,uBAAuB,sBAAsB,QAAQ;;EAEtE;;;UAIe;EACf,SAAS,oBAAoB,wBAAwB,QAAQ;EAC7D,aAAa,mBAAmB,gCAAgC;;;UAIjD;EACf,QAAQ;;;;;;cAUG,iCAAiC;WACnC;EAEG,YAAA,cAAc;;;cAef,6BAA6B;EAAA;;;;;cC9D7B;;UAGI;;WAEN,UAAU;WACV,SAAS;WACT;;EAET,YAAY;;EAEZ,cAAc,OAAO;EACrB;;;UAIe;WACN,MAAM;WACN,UAAU;WACV,SAAS;;UAGH;WACN,UAAU;;WAEV,SAAS;;WAET,OAAO;WACP;;;;;;WAMA,QAAQ,MAAM,yBAAyB;;;;;;iBAgBlC,iBAAiB,QAAQ,QAAQ,MAAM;;;;cCzC1C;UAEI;WACN;;;UAIM;EACf,KAAK,SAAS;EACd,UAAU,WAAW,SAAS;;;;;;;UAQf;WACN,UAAU;;WAEV,SAAS;;WAET,mBAAmB;;WAEnB,SAAS;EAClB,UAAU,WAAW,OAAO;EAC5B,iBAAiB,MAAM,aAAa;EACpC,KAAK,QAAQ,uBAAuB;EACpC,SAAS,oBAAoB,QAAQ;EACrC,aAAa,mBAAmB,yBAAyB;EACzD,QAAQ,SAAS,uBAAuB,QAAQ;WACvC,SAAS;;WAET,QAAQ;EACjB;;;UAIe,gCAAgC;;EAE/C,UAAU,QAAQ;;;;;;;;;;iBA+TE,gBACpB,QAAQ,QACR,YAAY,YACZ,mBAAmB,YACnB,UAAS,0BACR,QAAQ;;;;;;;;iBAWW,cACpB,QAAQ,QACR,UAAU,gBACV,UAAS,0BACR,QAAQ"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { ILinkBinding, IOwnerHandover, IReadKeys, IRuntimeIdentity, ITrustRoot, SpawnerKey, TLinkMode } from "@robota-sdk/agent-keyring";
|
|
2
|
+
import { TClientMessage, TServerMessage } from "@robota-sdk/agent-transport";
|
|
3
|
+
import { Duplex } from "node:stream";
|
|
4
|
+
//#region src/link-request.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The request line that opens an owner link, in the control endpoint's existing request shape so
|
|
7
|
+
* the endpoint routes it like any other command. It names no runtime and no start: the binding is
|
|
8
|
+
* proven inside the handshake, never taken from the request.
|
|
9
|
+
*/
|
|
10
|
+
declare const LINK_PROTOCOL = 1;
|
|
11
|
+
/** Exactly what a client writes first, newline included. */
|
|
12
|
+
declare const LINK_REQUEST_LINE: string;
|
|
13
|
+
/** Whether a parsed request line is exactly the link request; anything more or less is not. */
|
|
14
|
+
declare function isLinkRequest(value: unknown): boolean;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/link-ports.d.ts
|
|
17
|
+
/** What a client may ask a runtime to lock for; idle and last-link-closed are the runtime's own. */
|
|
18
|
+
type TRequestedLockReason = 'explicit' | 'screen-lock' | 'suspend';
|
|
19
|
+
/**
|
|
20
|
+
* How a read grant went. `mismatch`: the keyring file's sealing public key is not the granted one;
|
|
21
|
+
* the runtime seals to the granted key, and the owner is told to run `secure verify`.
|
|
22
|
+
*/
|
|
23
|
+
type TGrantOutcome = 'verified' | 'mismatch';
|
|
24
|
+
/** What every linked client is told, once admitted and whenever it changes. */
|
|
25
|
+
interface IRuntimeCustodyState {
|
|
26
|
+
readonly unlocked: boolean;
|
|
27
|
+
/** Whether owner links are possible: a spawner installed a trust root. */
|
|
28
|
+
readonly certified: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The runtime's answer to "may I open this session?". `accept` asks the owner, on a surface that
|
|
32
|
+
* shows the fixed `notice` and `question` and never the record, and `digest` binds the answer to
|
|
33
|
+
* the bytes the runtime read.
|
|
34
|
+
*/
|
|
35
|
+
type TOwnerSessionQuestion = {
|
|
36
|
+
readonly kind: 'open';
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: 'accept';
|
|
39
|
+
readonly digest: string;
|
|
40
|
+
readonly notice: string;
|
|
41
|
+
readonly question: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly kind: 'refused';
|
|
44
|
+
readonly message: string;
|
|
45
|
+
};
|
|
46
|
+
/** One of the runtime's existing control commands, carried inside the link. */
|
|
47
|
+
interface IOwnerControlRequest {
|
|
48
|
+
readonly command: string;
|
|
49
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
50
|
+
}
|
|
51
|
+
type TOwnerControlValue = null | boolean | number | string | readonly TOwnerControlValue[] | {
|
|
52
|
+
readonly [key: string]: TOwnerControlValue;
|
|
53
|
+
};
|
|
54
|
+
/** What a control command answered. */
|
|
55
|
+
type TOwnerControlResult = {
|
|
56
|
+
readonly [key: string]: TOwnerControlValue;
|
|
57
|
+
};
|
|
58
|
+
/** The runtime's custody answers for one admitted link. */
|
|
59
|
+
interface IOwnerLinkDesk {
|
|
60
|
+
/** The state the link announces as soon as it is admitted. */
|
|
61
|
+
custody(): IRuntimeCustodyState;
|
|
62
|
+
installTrustRoot(root: ITrustRoot): void | Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Installs the granted read keys. The desk owns them once this returns; when it refuses, by
|
|
65
|
+
* throwing, the carrier locks them.
|
|
66
|
+
*/
|
|
67
|
+
unlock(keys: IReadKeys): TGrantOutcome | Promise<TGrantOutcome>;
|
|
68
|
+
lock(reason: TRequestedLockReason): void | Promise<void>;
|
|
69
|
+
control(request: IOwnerControlRequest): TOwnerControlResult | Promise<TOwnerControlResult>;
|
|
70
|
+
/** The link ended, for whatever reason. Called once. */
|
|
71
|
+
closed(): void;
|
|
72
|
+
}
|
|
73
|
+
/** The runtime's session decisions for one admitted link. */
|
|
74
|
+
interface IOwnerLinkSessions {
|
|
75
|
+
question(sessionId: string): TOwnerSessionQuestion | Promise<TOwnerSessionQuestion>;
|
|
76
|
+
openAccepted(sessionId: string, acceptedDigest: string): void | Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/** Where the session protocol of an admitted link goes: one serialized client message at a time. */
|
|
79
|
+
interface ILinkSessionSink {
|
|
80
|
+
receive(data: string): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A request the runtime refused, by decision: a desk throws it with a fixed code and fixed text, and
|
|
84
|
+
* the client rejects with it. Any other failure reaches the client as `failed`, never with its text.
|
|
85
|
+
*/
|
|
86
|
+
declare class OwnerRequestRefusedError extends Error {
|
|
87
|
+
readonly code: string;
|
|
88
|
+
constructor(code: string, message: string);
|
|
89
|
+
}
|
|
90
|
+
/** The link closed, or never opened: the other end does not say why. */
|
|
91
|
+
declare class OwnerLinkClosedError extends Error {
|
|
92
|
+
constructor();
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/serve-runtime-link.d.ts
|
|
96
|
+
/** A runtime gives up on a handshake that has not finished by then. */
|
|
97
|
+
declare const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000;
|
|
98
|
+
/** An admitted link, as the runtime holds it. */
|
|
99
|
+
interface IAdmittedRuntimeLink {
|
|
100
|
+
/** How the client was admitted: `spawner` by the spawner key, `owner` by the attach key. */
|
|
101
|
+
readonly standing: TLinkMode;
|
|
102
|
+
readonly binding: ILinkBinding;
|
|
103
|
+
readonly closed: boolean;
|
|
104
|
+
/** Sends one serialized session-protocol message; throws `RangeError` for one over the frame bound. */
|
|
105
|
+
sendSession(data: string): void;
|
|
106
|
+
/** Tells the client its runtime's custody state changed. */
|
|
107
|
+
notifyCustody(state: IRuntimeCustodyState): void;
|
|
108
|
+
close(): void;
|
|
109
|
+
}
|
|
110
|
+
/** The runtime's answers for one admitted link. */
|
|
111
|
+
interface IRuntimeLinkPorts {
|
|
112
|
+
readonly desk: IOwnerLinkDesk;
|
|
113
|
+
readonly sessions: IOwnerLinkSessions;
|
|
114
|
+
readonly session: ILinkSessionSink;
|
|
115
|
+
}
|
|
116
|
+
interface IRuntimeLinkDeps {
|
|
117
|
+
readonly identity: IRuntimeIdentity;
|
|
118
|
+
/** This runtime start, as its registration names it. */
|
|
119
|
+
readonly binding: ILinkBinding;
|
|
120
|
+
/** The bytes after the request line that arrived with it. */
|
|
121
|
+
readonly rest?: Uint8Array;
|
|
122
|
+
readonly handshakeTimeoutMs?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Called once the handshake admitted the client; the ports it returns serve that link. The opening
|
|
125
|
+
* custody notice is always the first frame the client sees: whatever `admit` sends through the link
|
|
126
|
+
* goes out right after it, in the order it was sent.
|
|
127
|
+
*/
|
|
128
|
+
readonly admit: (link: IAdmittedRuntimeLink) => IRuntimeLinkPorts;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Serves one owner link on `stream`, which the control endpoint has read the request line from and
|
|
132
|
+
* paused. Returns at once; the link lives until the stream closes.
|
|
133
|
+
*/
|
|
134
|
+
declare function serveRuntimeLink(stream: Duplex, deps: IRuntimeLinkDeps): void;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/open-runtime-link.d.ts
|
|
137
|
+
/** A client gives up on a runtime that has not admitted it by then. */
|
|
138
|
+
declare const DEFAULT_OPEN_TIMEOUT_MS = 10000;
|
|
139
|
+
interface IOpenRuntimeLinkOptions {
|
|
140
|
+
readonly timeoutMs?: number;
|
|
141
|
+
}
|
|
142
|
+
/** The session protocol over the link: what an attach client or a desktop relay sends and hears. */
|
|
143
|
+
interface ILinkSessionChannel {
|
|
144
|
+
send(message: TClientMessage): void;
|
|
145
|
+
onMessage(listener: (message: TServerMessage) => void): () => void;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One open link, as its client holds it. Only so many requests (read grants included) may wait for
|
|
149
|
+
* the runtime's answer at once; one more is refused at once with an `OwnerRequestRefusedError` coded
|
|
150
|
+
* `busy`, is not sent, and leaves the link open.
|
|
151
|
+
*/
|
|
152
|
+
interface IRuntimeLinkClient {
|
|
153
|
+
readonly standing: TLinkMode;
|
|
154
|
+
/** The runtime start the handshake proved. */
|
|
155
|
+
readonly binding: ILinkBinding;
|
|
156
|
+
/** The runtime's instance key: the exchange's (spawner) or the certified one (owner). */
|
|
157
|
+
readonly instancePublicKey: Uint8Array;
|
|
158
|
+
/** The custody state the runtime announced last. */
|
|
159
|
+
readonly custody: IRuntimeCustodyState;
|
|
160
|
+
onCustody(listener: (state: IRuntimeCustodyState) => void): () => void;
|
|
161
|
+
installTrustRoot(root: ITrustRoot): Promise<void>;
|
|
162
|
+
lock(reason: TRequestedLockReason): Promise<void>;
|
|
163
|
+
question(sessionId: string): Promise<TOwnerSessionQuestion>;
|
|
164
|
+
openAccepted(sessionId: string, acceptedDigest: string): Promise<void>;
|
|
165
|
+
control(request: IOwnerControlRequest): Promise<TOwnerControlResult>;
|
|
166
|
+
readonly session: ILinkSessionChannel;
|
|
167
|
+
/** Settles once the link has closed, for whatever reason. */
|
|
168
|
+
readonly closed: Promise<void>;
|
|
169
|
+
close(): void;
|
|
170
|
+
}
|
|
171
|
+
/** An owner link, which alone can unlock the runtime. */
|
|
172
|
+
interface IOwnerRuntimeLinkClient extends IRuntimeLinkClient {
|
|
173
|
+
/** Seals the owner's read keys for this runtime and sends them. */
|
|
174
|
+
unlock(): Promise<TGrantOutcome>;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Opens a spawner link on `stream`, already connected to the runtime's control endpoint, with the
|
|
178
|
+
* key and instance key the spawner exchange gave. Rejects with `LinkRefusedError` when the runtime
|
|
179
|
+
* is not the one the exchange started, before sending anything secret-derived, and with
|
|
180
|
+
* `OwnerLinkClosedError` when the runtime closes or does not admit the link in time. Listeners
|
|
181
|
+
* attached in the same tick the open resolves receive everything the runtime sends after its
|
|
182
|
+
* custody notice.
|
|
183
|
+
*/
|
|
184
|
+
declare function openSpawnerLink(stream: Duplex, spawnerKey: SpawnerKey, instancePublicKey: Uint8Array, options?: IOpenRuntimeLinkOptions): Promise<IRuntimeLinkClient>;
|
|
185
|
+
/**
|
|
186
|
+
* Opens an owner link on `stream` with the owner's handover. Rejects with `LinkRefusedError` when the
|
|
187
|
+
* runtime shows no certificate the handover's attach key made for it (an impostor, a runtime nobody
|
|
188
|
+
* certified, or one another keyring certified), before sending anything secret-derived. Listeners
|
|
189
|
+
* attached in the same tick the open resolves receive everything the runtime sends after its
|
|
190
|
+
* custody notice.
|
|
191
|
+
*/
|
|
192
|
+
declare function openOwnerLink(stream: Duplex, handover: IOwnerHandover, options?: IOpenRuntimeLinkOptions): Promise<IOwnerRuntimeLinkClient>;
|
|
193
|
+
//#endregion
|
|
194
|
+
export { DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_OPEN_TIMEOUT_MS, type IAdmittedRuntimeLink, type ILinkSessionChannel, type ILinkSessionSink, type IOpenRuntimeLinkOptions, type IOwnerControlRequest, type IOwnerLinkDesk, type IOwnerLinkSessions, type IOwnerRuntimeLinkClient, type IRuntimeCustodyState, type IRuntimeLinkClient, type IRuntimeLinkDeps, type IRuntimeLinkPorts, LINK_PROTOCOL, LINK_REQUEST_LINE, OwnerLinkClosedError, OwnerRequestRefusedError, type TGrantOutcome, type TOwnerControlResult, type TOwnerControlValue, type TOwnerSessionQuestion, type TRequestedLockReason, isLinkRequest, openOwnerLink, openSpawnerLink, serveRuntimeLink };
|
|
195
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/link-request.ts","../../src/link-ports.ts","../../src/serve-runtime-link.ts","../../src/open-runtime-link.ts"],"mappings":";;;;;;;;;cAKa;;cAGA;;iBAGG,cAAc;;;;KCElB;;;;;KAMA;;UAGK;WACN;;WAEA;;;;;;;KAQC;WACG;;WAEA;WACA;WACA;WACA;;WAEA;WAA0B;;;UAGxB;WACN;WACA,QAAQ,SAAS;;KAGhB,iEAKC;YACG,cAAc;;;KAGlB;YAAkC,cAAc;;;UAG3C;;EAEf,WAAW;EACX,iBAAiB,MAAM,oBAAoB;;;;;EAK3C,OAAO,MAAM,YAAY,gBAAgB,QAAQ;EACjD,KAAK,QAAQ,8BAA8B;EAC3C,QAAQ,SAAS,uBAAuB,sBAAsB,QAAQ;;EAEtE;;;UAIe;EACf,SAAS,oBAAoB,wBAAwB,QAAQ;EAC7D,aAAa,mBAAmB,gCAAgC;;;UAIjD;EACf,QAAQ;;;;;;cAUG,iCAAiC;WACnC;EAEG,YAAA,cAAc;;;cAef,6BAA6B;EAAA;;;;;cC9D7B;;UAGI;;WAEN,UAAU;WACV,SAAS;WACT;;EAET,YAAY;;EAEZ,cAAc,OAAO;EACrB;;;UAIe;WACN,MAAM;WACN,UAAU;WACV,SAAS;;UAGH;WACN,UAAU;;WAEV,SAAS;;WAET,OAAO;WACP;;;;;;WAMA,QAAQ,MAAM,yBAAyB;;;;;;iBAgBlC,iBAAiB,QAAQ,QAAQ,MAAM;;;;cCzC1C;UAEI;WACN;;;UAIM;EACf,KAAK,SAAS;EACd,UAAU,WAAW,SAAS;;;;;;;UAQf;WACN,UAAU;;WAEV,SAAS;;WAET,mBAAmB;;WAEnB,SAAS;EAClB,UAAU,WAAW,OAAO;EAC5B,iBAAiB,MAAM,aAAa;EACpC,KAAK,QAAQ,uBAAuB;EACpC,SAAS,oBAAoB,QAAQ;EACrC,aAAa,mBAAmB,yBAAyB;EACzD,QAAQ,SAAS,uBAAuB,QAAQ;WACvC,SAAS;;WAET,QAAQ;EACjB;;;UAIe,gCAAgC;;EAE/C,UAAU,QAAQ;;;;;;;;;;iBA+TE,gBACpB,QAAQ,QACR,YAAY,YACZ,mBAAmB,YACnB,UAAS,0BACR,QAAQ;;;;;;;;iBAWW,cACpB,QAAQ,QACR,UAAU,gBACV,UAAS,0BACR,QAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{KeyringError as e,LINK_CHALLENGE_MAX_BYTES as t,LINK_FRAME_OVERHEAD_BYTES as n,LINK_GRANT_FRAME_BYTES as r,LINK_HELLO_BYTES as i,LINK_PROOF_MAX_BYTES as a,createLinkInitiator as o,createLinkResponder as s,linkFrameKind as c}from"@robota-sdk/agent-keyring";import{Buffer as l}from"node:buffer";import{MAX_INBOUND_FRAME_BYTES as u,decodeFrame as d,decodeServerMessage as f}from"@robota-sdk/agent-transport";const p=1,m=`${JSON.stringify({command:`link`,protocol:1})}\n`;function h(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=e;return Object.keys(t).length===2&&t.command===`link`&&t.protocol===1}const g=/^[a-z][a-z0-9-]{0,63}$/u;var _=class extends Error{code;constructor(e,t){if(!g.test(e)||t.length===0||t.length>1024)throw RangeError(`A refusal has a lower-case code and a message of 1-1024 characters.`);super(t),this.name=`OwnerRequestRefusedError`,this.code=e}},v=class extends Error{constructor(){super(`The owner link closed.`),this.name=`OwnerLinkClosedError`}};function y(e){let t=l.alloc(4);return t.writeUInt32BE(e.length),l.concat([t,e])}function b(e){return l.concat([l.from(m,`utf8`),y(e)])}var x=class{#e=[];#t=0;push(e){e.length!==0&&(this.#e.push(l.from(e.buffer,e.byteOffset,e.byteLength)),this.#t+=e.length)}next(e){if(this.#t<4)return;this.#e[0].length<4&&this.#n();let t=this.#e[0].readUInt32BE(0);if(t>e)throw RangeError(`A frame was announced over its bound.`);if(this.#t<4+t)return;let n=this.#n(),r=4+t,i=l.from(n.subarray(4,r)),a=n.subarray(r);return this.#e=a.length>0?[a]:[],this.#t=a.length,i}#n(){let e=this.#e.length===1?this.#e[0]:l.concat(this.#e,this.#t);return this.#e=[e],e}};function S(e,t){let n=new x,r=!1,i=()=>{r||(r=!0,t.onEnd())},a=!1,o=()=>{if(!a)try{for(let r=n.next(t.maxBytes());r!==void 0;r=n.next(t.maxBytes())){if(e.destroyed)return;let n=t.onFrame(r);if(e.destroyed)return;if(n===`yield`){a=!0,e.pause(),setImmediate(()=>{a=!1,o(),a||e.resume()});return}}}catch{e.destroy()}};e.on(`data`,t=>{if(typeof t==`string`){e.destroy();return}n.push(t),o()}),e.on(`error`,()=>e.destroy()),e.once(`close`,i),e.once(`end`,()=>e.destroy()),t.rest!==void 0&&n.push(t.rest),o(),e.resume(),e.destroyed&&process.nextTick(i)}const C=u,w=1024*1024,T=Math.max(C,w)+n,E=/^r[1-9][0-9]{0,15}$/u,D=/^g[1-9][0-9]{0,15}$/u,O=/^[A-Za-z0-9._:-]{1,256}$/u,k=/^[A-Za-z0-9._:+/=-]{1,256}$/u,A=/^[a-z][a-z0-9-]{0,31}$/u,ee=/^[A-Za-z][A-Za-z0-9]{0,31}$/u,te=/^[a-z][a-z0-9-]{0,63}$/u,ne=/^[A-Za-z0-9_-]+$/u,j=/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/u,M=/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/u,N=4096,P=new Set([`explicit`,`screen-lock`,`suspend`]),F=new Set([`verified`,`mismatch`]),re=new TextDecoder(`utf-8`,{fatal:!0});function I(e){try{return JSON.parse(re.decode(e))}catch{return}}function L(e,t){if(!(typeof e!=`object`||!e||Array.isArray(e))&&!(Object.keys(e).length!==t.length||!t.every(t=>Object.hasOwn(e,t))))return e}const R=(e,t)=>typeof e==`string`&&t.test(e),z=(e,t)=>typeof e==`string`&&e.length>0&&e.length<=t&&!j.test(e);function B(e,t){if(!R(e,ne))return;let n=l.from(e,`base64url`);return n.length===t&&n.toString(`base64url`)===e?Uint8Array.from(n):void 0}function V(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=Object.entries(e);if(t.length>16)return;let n={};for(let[e,r]of t){if(!ee.test(e)||typeof r!=`string`||r.length>4096||M.test(r))return;n[e]=r}return n}function H(e){let t=0,n=(e,r)=>{if(t+=1,t>4096)return;if(e===null||typeof e==`boolean`)return e;if(typeof e==`number`)return Number.isFinite(e)?e:void 0;if(typeof e==`string`)return e.length<=65536?e:void 0;if(r>=8||typeof e!=`object`)return;if(Array.isArray(e)){let t=[];for(let i of e){let e=n(i,r+1);if(e===void 0)return;t.push(e)}return t}let i={};for(let[t,a]of Object.entries(e)){if(t===`__proto__`||t.length>256)return;let e=n(a,r+1);if(e===void 0)return;i[t]=e}return i};if(typeof e!=`object`||!e||Array.isArray(e))return;let r=n(e,0);return r===void 0?void 0:r}function U(e){let t=typeof e==`object`&&e?e.kind:void 0;if(t===`open`)return L(e,[`kind`])?{kind:`open`}:void 0;if(t===`accept`){let t=L(e,[`kind`,`digest`,`notice`,`question`]);return t===void 0||!R(t.digest,k)||!z(t.notice,N)||!z(t.question,N)?void 0:{kind:`accept`,digest:t.digest,notice:t.notice,question:t.question}}if(t===`refused`){let t=L(e,[`kind`,`message`]);return t!==void 0&&z(t.message,N)?{kind:`refused`,message:t.message}:void 0}}function W(e){let t=L(e,[`unlocked`,`certified`]);return t!==void 0&&typeof t.unlocked==`boolean`&&typeof t.certified==`boolean`?{unlocked:t.unlocked,certified:t.certified}:void 0}function G(e){let t=I(e),n=typeof t==`object`&&t?t.op:void 0;switch(n){case`install-trust-root`:{let e=L(t,[`op`,`requestId`,`attachPublicKey`,`certificate`]),r=B(e?.attachPublicKey,32),i=B(e?.certificate,64);return!R(e?.requestId,E)||!r||!i?void 0:{op:n,requestId:e.requestId,root:{attachPublicKey:r,certificate:i}}}case`lock`:{let e=L(t,[`op`,`requestId`,`reason`]),r=e?.reason;return!R(e?.requestId,E)||typeof r!=`string`||!P.has(r)?void 0:{op:n,requestId:e.requestId,reason:r}}case`question`:{let e=L(t,[`op`,`requestId`,`sessionId`]);return!R(e?.requestId,E)||!R(e.sessionId,O)?void 0:{op:n,requestId:e.requestId,sessionId:e.sessionId}}case`open-accepted`:{let e=L(t,[`op`,`requestId`,`sessionId`,`acceptedDigest`]);return!R(e?.requestId,E)||!R(e.sessionId,O)||!R(e.acceptedDigest,k)?void 0:{op:n,requestId:e.requestId,sessionId:e.sessionId,acceptedDigest:e.acceptedDigest}}case`control`:{let e=L(t,[`op`,`requestId`,`command`,`fields`]),r=V(e?.fields);return!R(e?.requestId,E)||!R(e.command,A)||!r?void 0:{op:n,requestId:e.requestId,request:{command:e.command,fields:r}}}default:return}}function K(e){let t=I(e),n=typeof t==`object`&&t?t.op:void 0;switch(n){case`done`:{let e=L(t,[`op`,`requestId`]);return R(e?.requestId,E)?{op:n,requestId:e.requestId}:void 0}case`unlocked`:{let e=L(t,[`op`,`requestId`,`sealingKey`]),r=e?.sealingKey;return!R(e?.requestId,D)||typeof r!=`string`||!F.has(r)?void 0:{op:n,requestId:e.requestId,sealingKey:r}}case`question`:{let e=L(t,[`op`,`requestId`,`question`]),r=U(e?.question);return R(e?.requestId,E)&&r?{op:n,requestId:e.requestId,question:r}:void 0}case`refused`:{let e=L(t,[`op`,`requestId`,`code`,`message`]),r=e?.requestId;return!(R(r,E)||R(r,D))||!R(e?.code,te)||!z(e.message,1024)?void 0:{op:n,requestId:r,code:e.code,message:e.message}}case`control`:{let e=L(t,[`op`,`requestId`,`result`]),r=H(e?.result);return R(e?.requestId,E)&&r?{op:n,requestId:e.requestId,result:r}:void 0}case`custody`:{let e=W(L(t,[`op`,`state`])?.state);return e?{op:n,state:e}:void 0}default:return}}function q(e){switch(e.op){case`install-trust-root`:return{op:e.op,requestId:e.requestId,attachPublicKey:l.from(e.root.attachPublicKey).toString(`base64url`),certificate:l.from(e.root.certificate).toString(`base64url`)};case`control`:return{op:e.op,requestId:e.requestId,command:e.request.command,fields:{...e.request.fields}};default:return{...e}}}function J(e,t){if(t(e)===void 0)throw RangeError(`The message is outside the owner vocabulary.`);return e}function Y(e){return J(l.from(JSON.stringify(q(e)),`utf8`),G)}function ie(e){return J(l.from(JSON.stringify(e),`utf8`),K)}const ae=1e4,oe=new TextDecoder(`utf-8`,{fatal:!0}),se=new TextEncoder;function X(e){return e instanceof _?{code:e.code,message:e.message}:{code:`failed`,message:`The runtime could not do this.`}}function ce(e,t){let o;try{o=s(t.identity,t.binding)}catch(t){throw e.destroy(),t}let l=`hello`,u,d,f=0,p=0,m=setTimeout(()=>e.destroy(),t.handshakeTimeoutMs??1e4);m.unref?.();let h,g=(t,n)=>{let r=()=>{u===void 0||u.closed||e.destroyed||e.write(y(u.seal(t,n)))};h===void 0?r():h.push(r)},_=t=>{u===void 0||u.closed||e.destroyed||g(`owner`,ie(t))},v=()=>p<64?!0:(e.destroy(),!1),b=(t,n)=>{p+=1,n().then(e=>e,e=>({op:`refused`,requestId:t,...X(e)})).then(e=>{--p;try{_(e)}catch{_({op:`refused`,requestId:t,...X(void 0)})}}).catch(()=>e.destroy())},x=(e,t)=>{let{requestId:n}=e;switch(e.op){case`install-trust-root`:return b(n,async()=>(await t.desk.installTrustRoot(e.root),{op:`done`,requestId:n}));case`lock`:return b(n,async()=>(await t.desk.lock(e.reason),{op:`done`,requestId:n}));case`question`:return b(n,async()=>({op:`question`,requestId:n,question:await t.sessions.question(e.sessionId)}));case`open-accepted`:return b(n,async()=>(await t.sessions.openAccepted(e.sessionId,e.acceptedDigest),{op:`done`,requestId:n}));case`control`:return b(n,async()=>({op:`control`,requestId:n,result:await t.desk.control(e.request)}))}},E=(e,t)=>{f+=1;let n=`g${f}`;b(n,async()=>{try{return{op:`unlocked`,requestId:n,sealingKey:await t.desk.unlock(e)}}catch(t){throw e.lock(),t}})},D=(e,t,i)=>{let a=c(e);if(a===`grant`){if(e.length!==r)throw RangeError(`A grant has one size.`);if(!v())return;E(i.openReadGrant(e),t);return}let o=a===`owner`?w:C;if(e.length>o+n)throw RangeError(`A frame over its bound.`);let{plaintext:s}=i.open(e);if(a===`session`){t.session.receive(oe.decode(s));return}let l=G(s);if(l===void 0)throw RangeError(`A request outside the vocabulary.`);v()&&x(l,t)},O={get standing(){return u.standing},binding:t.binding,get closed(){return u===void 0||u.closed||e.destroyed},sendSession(t){let n=se.encode(t);if(n.length>C)throw RangeError(`A session message over the frame bound.`);u===void 0||u.closed||e.destroyed||g(`session`,n)},notifyCustody(e){_({op:`custody`,state:{unlocked:e.unlocked,certified:e.certified}})},close(){e.destroy()}};S(e,{...t.rest===void 0?{}:{rest:t.rest},maxBytes:()=>l===`hello`?i:l===`proof`?a:T,onFrame:n=>{try{if(l===`hello`){e.write(y(o.challenge(n))),l=`proof`;return}if(l===`proof`){u=o.accept(n).link,clearTimeout(m),l=`linked`;let e=[];h=e,d=t.admit(O);let r=d.desk.custody();h=void 0,O.notifyCustody(r);for(let t of e)t();return}D(n,d,u)}catch{e.destroy()}},onEnd:()=>{if(clearTimeout(m),u?.close(),d!==void 0)try{d.desk.closed()}catch{}}})}const Z=1e4,le=new TextDecoder(`utf-8`,{fatal:!0}),ue=new TextEncoder;function Q(e,t){for(let n of[...e])try{n(t)}catch{}}var de=class{closed;session;#e;#t;#n;#r=new Map;#i=new Set;#a=new Set;#o;#s=0;#c=0;#l=!1;#u;constructor(e,t,n,r){this.#e=e,this.#t=t,this.#o=n,this.#n=r,this.closed=new Promise(e=>{this.#u=e}),this.session={send:e=>this.#d(e),onMessage:e=>(this.#a.add(e),()=>{this.#a.delete(e)})}}get standing(){return this.#t.standing}get binding(){return this.#t.binding}get instancePublicKey(){return this.#t.instancePublicKey}get custody(){return this.#o}onCustody(e){return this.#i.add(e),()=>{this.#i.delete(e)}}async installTrustRoot(e){await this.#f(t=>({op:`install-trust-root`,requestId:t,root:e}),`done`)}async lock(e){await this.#f(t=>({op:`lock`,requestId:t,reason:e}),`done`)}async question(e){return(await this.#f(t=>({op:`question`,requestId:t,sessionId:e}),`question`)).question}async openAccepted(e,t){await this.#f(n=>({op:`open-accepted`,requestId:n,sessionId:e,acceptedDigest:t}),`done`)}async control(e){return(await this.#f(t=>({op:`control`,requestId:t,request:e}),`control`)).result}async unlock(){if(this.#n===void 0)throw new _(`not-owner`,`Only an owner link can unlock the runtime.`);let e=this.#n,t=`g${this.#c+1}`;return(await this.#p(t,`unlocked`,()=>{this.#c+=1,this.#m(this.#t.sealReadGrant(e))})).sealingKey}close(){this.#e.destroy()}receive(e){let t=c(e),r=t===`owner`?w:C;if(t===`grant`||e.length>r+n)throw RangeError(`The runtime sent a frame it may not.`);let{plaintext:i}=this.#t.open(e);if(t===`session`){let e=d(le.decode(i),f);if(!e.ok)throw RangeError(`The runtime sent a session message that does not decode.`);Q(this.#a,e.message);return}let a=K(i);if(a===void 0)throw RangeError(`The runtime sent a message outside the vocabulary.`);if(a.op===`custody`){this.#o=a.state,Q(this.#i,a.state);return}let o=this.#r.get(a.requestId);if(o===void 0||a.op!==o.expects&&a.op!==`refused`)throw RangeError(`The runtime answered a request that was not asked.`);this.#r.delete(a.requestId),a.op===`refused`?o.reject(new _(a.code,a.message)):o.resolve(a)}end(){if(!this.#l){this.#l=!0,this.#t.close();for(let e of this.#r.values())e.reject(new v);this.#r.clear(),this.#u()}}#d(e){let t=ue.encode(JSON.stringify(e));if(t.length>C)throw RangeError(`A session message over the frame bound.`);if(this.#l)throw new v;this.#m(this.#t.seal(`session`,t))}#f(e,t){let n=`r${this.#s+1}`,r=Y(e(n));return this.#p(n,t,()=>{this.#s+=1,this.#m(this.#t.seal(`owner`,r))})}#p(e,t,n){return this.#l?Promise.reject(new v):this.#r.size>=64?Promise.reject(new _(`busy`,`The link has as many requests waiting as it allows.`)):new Promise((r,i)=>{this.#r.set(e,{expects:t,resolve:r,reject:i});try{n()}catch(t){this.#r.delete(e),i(t)}})}#m(e){this.#e.write(y(e))}};function $(n,r,i){return new Promise((a,s)=>{let c;try{c=o(r)}catch(e){n.destroy(),s(e);return}let l,u,d,f=()=>{n.destroy()},p=setTimeout(()=>{f(),l?.close(),s(new v)},i.timeoutMs??1e4);p.unref?.(),S(n,{maxBytes:()=>l===void 0?t:T,onFrame:e=>{if(u!==void 0){try{u.receive(e)}catch{n.destroy()}return}if(l===void 0){try{let t=c.answer(e);l=t.link,n.write(y(t.proof))}catch(e){d=e,f()}return}try{let t=l.open(e),i=t.kind===`owner`?K(t.plaintext):void 0;if(i?.op!==`custody`)throw RangeError(`The runtime did not announce its custody.`);return clearTimeout(p),u=new de(n,l,i.state,r.mode===`owner`?r.handover:void 0),a(u),`yield`}catch{f()}},onEnd:()=>{if(clearTimeout(p),u!==void 0){u.end();return}l?.close(),s(d instanceof e?d:new v)}}),n.write(b(c.hello()))})}async function fe(e,t,n,r={}){return $(e,{mode:`spawner`,spawnerKey:t,instancePublicKey:n},r)}async function pe(e,t,n={}){return $(e,{mode:`owner`,handover:t},n)}export{ae as DEFAULT_HANDSHAKE_TIMEOUT_MS,Z as DEFAULT_OPEN_TIMEOUT_MS,p as LINK_PROTOCOL,m as LINK_REQUEST_LINE,v as OwnerLinkClosedError,_ as OwnerRequestRefusedError,h as isLinkRequest,pe as openOwnerLink,fe as openSpawnerLink,ce as serveRuntimeLink};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|