@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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner vocabulary's decoders are strict on both ends: each message has exactly its fields,
|
|
3
|
+
* each field its type and bounds, and anything else is not a message at all. An encoded message
|
|
4
|
+
* decodes back to what was encoded.
|
|
5
|
+
*/
|
|
6
|
+
import { Buffer } from 'node:buffer';
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
decodeOwnerRequest,
|
|
12
|
+
decodeRuntimeMessage,
|
|
13
|
+
encodeOwnerRequest,
|
|
14
|
+
encodeRuntimeMessage,
|
|
15
|
+
} from '../owner-vocabulary.js';
|
|
16
|
+
|
|
17
|
+
const utf8 = (value: unknown): Uint8Array => Buffer.from(JSON.stringify(value), 'utf8');
|
|
18
|
+
const b64 = (length: number, fill = 7): string => Buffer.alloc(length, fill).toString('base64url');
|
|
19
|
+
|
|
20
|
+
const REQUESTS = [
|
|
21
|
+
{ op: 'install-trust-root', requestId: 'r1', attachPublicKey: b64(32), certificate: b64(64) },
|
|
22
|
+
{ op: 'lock', requestId: 'r2', reason: 'screen-lock' },
|
|
23
|
+
{ op: 'question', requestId: 'r3', sessionId: 'sess_01-a.b:c' },
|
|
24
|
+
{ op: 'open-accepted', requestId: 'r4', sessionId: 's', acceptedDigest: 'ab'.repeat(32) },
|
|
25
|
+
{ op: 'control', requestId: 'r5', command: 'rename', fields: { name: 'My session' } },
|
|
26
|
+
{ op: 'control', requestId: 'r6', command: 'events-list', fields: {} },
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
const RUNTIME_MESSAGES = [
|
|
30
|
+
{ op: 'done', requestId: 'r1' },
|
|
31
|
+
{ op: 'unlocked', requestId: 'g1', sealingKey: 'mismatch' },
|
|
32
|
+
{ op: 'question', requestId: 'r3', question: { kind: 'open' } },
|
|
33
|
+
{
|
|
34
|
+
op: 'question',
|
|
35
|
+
requestId: 'r3',
|
|
36
|
+
question: {
|
|
37
|
+
kind: 'accept',
|
|
38
|
+
digest: 'ab'.repeat(32),
|
|
39
|
+
notice: 'Not verified.\nCheck it.',
|
|
40
|
+
question: 'Resume?',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{ op: 'question', requestId: 'r3', question: { kind: 'refused', message: 'No such session.' } },
|
|
44
|
+
{
|
|
45
|
+
op: 'refused',
|
|
46
|
+
requestId: 'r9',
|
|
47
|
+
code: 'not-allowed',
|
|
48
|
+
message: 'A spawner link cannot do this.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
op: 'control',
|
|
52
|
+
requestId: 'r5',
|
|
53
|
+
result: { status: 'events', grants: [{ id: 'g', count: 2, live: true, note: null }] },
|
|
54
|
+
},
|
|
55
|
+
{ op: 'custody', state: { unlocked: true, certified: false } },
|
|
56
|
+
] as const;
|
|
57
|
+
|
|
58
|
+
describe('owner requests', () => {
|
|
59
|
+
it('decode every request the vocabulary has, and encode back to the same bytes', () => {
|
|
60
|
+
for (const wire of REQUESTS) {
|
|
61
|
+
const decoded = decodeOwnerRequest(utf8(wire));
|
|
62
|
+
expect(decoded).toBeDefined();
|
|
63
|
+
expect(JSON.parse(Buffer.from(encodeOwnerRequest(decoded!)).toString())).toEqual(wire);
|
|
64
|
+
}
|
|
65
|
+
const root = decodeOwnerRequest(utf8(REQUESTS[0]));
|
|
66
|
+
expect(root?.op === 'install-trust-root' && root.root.certificate).toHaveLength(64);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('refuse anything that is not exactly one of them', () => {
|
|
70
|
+
const refused: unknown[] = [
|
|
71
|
+
null,
|
|
72
|
+
[],
|
|
73
|
+
'lock',
|
|
74
|
+
{ op: 'unlock', requestId: 'r1' },
|
|
75
|
+
{ op: 'lock', requestId: 'r1', reason: 'idle' },
|
|
76
|
+
{ op: 'lock', requestId: 'r1', reason: 'explicit', extra: 1 },
|
|
77
|
+
{ op: 'lock', requestId: 'g1', reason: 'explicit' },
|
|
78
|
+
{ op: 'lock', requestId: 'r0', reason: 'explicit' },
|
|
79
|
+
{ op: 'lock', requestId: `r${'1'.repeat(17)}`, reason: 'explicit' },
|
|
80
|
+
{ op: 'lock', reason: 'explicit' },
|
|
81
|
+
{ ...REQUESTS[0], attachPublicKey: b64(31) },
|
|
82
|
+
{ ...REQUESTS[0], certificate: b64(64) + 'A' },
|
|
83
|
+
{ ...REQUESTS[0], certificate: `${b64(64).slice(0, -1)}=` },
|
|
84
|
+
{ ...REQUESTS[0], attachPublicKey: 'A'.repeat(42) + 'B' },
|
|
85
|
+
{ ...REQUESTS[2], sessionId: '' },
|
|
86
|
+
{ ...REQUESTS[2], sessionId: 'a b' },
|
|
87
|
+
{ ...REQUESTS[2], sessionId: 's'.repeat(257) },
|
|
88
|
+
{ ...REQUESTS[3], acceptedDigest: 'ab\n' },
|
|
89
|
+
{ ...REQUESTS[4], command: 'Rename' },
|
|
90
|
+
{ ...REQUESTS[4], fields: { name: 7 } },
|
|
91
|
+
{ ...REQUESTS[4], fields: { 'bad key': 'x' } },
|
|
92
|
+
{ ...REQUESTS[4], fields: { name: 'x\u0007' } },
|
|
93
|
+
{
|
|
94
|
+
...REQUESTS[4],
|
|
95
|
+
fields: Object.fromEntries(Array.from({ length: 17 }, (_, i) => [`k${i}`, 'v'])),
|
|
96
|
+
},
|
|
97
|
+
{ ...REQUESTS[4], fields: ['name'] },
|
|
98
|
+
];
|
|
99
|
+
for (const value of refused) expect(decodeOwnerRequest(utf8(value))).toBeUndefined();
|
|
100
|
+
expect(decodeOwnerRequest(Buffer.from('{"op":"lock",'))).toBeUndefined();
|
|
101
|
+
expect(decodeOwnerRequest(Buffer.from([0xff, 0xfe]))).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('runtime replies and notices', () => {
|
|
106
|
+
it('decode every reply and notice, and encode back to the same bytes', () => {
|
|
107
|
+
for (const wire of RUNTIME_MESSAGES) {
|
|
108
|
+
const decoded = decodeRuntimeMessage(utf8(wire));
|
|
109
|
+
expect(decoded).toBeDefined();
|
|
110
|
+
expect(JSON.parse(Buffer.from(encodeRuntimeMessage(decoded!)).toString())).toEqual(wire);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('refuse anything that is not exactly one of them', () => {
|
|
115
|
+
const refused: unknown[] = [
|
|
116
|
+
{ op: 'done', requestId: 'r1', extra: true },
|
|
117
|
+
{ op: 'unlocked', requestId: 'r1', sealingKey: 'verified' },
|
|
118
|
+
{ op: 'unlocked', requestId: 'g1', sealingKey: 'repaired' },
|
|
119
|
+
{ op: 'question', requestId: 'r3', question: { kind: 'accept', digest: 'd', notice: 'n' } },
|
|
120
|
+
{ op: 'question', requestId: 'r3', question: { kind: 'open', extra: 1 } },
|
|
121
|
+
{ op: 'question', requestId: 'r3', question: { kind: 'refused', message: 'x\u001b[2J' } },
|
|
122
|
+
{ op: 'question', requestId: 'r3', question: { kind: 'refused', message: 'x\u202e' } },
|
|
123
|
+
{ op: 'question', requestId: 'r3', question: { kind: 'refused', message: 'm'.repeat(4097) } },
|
|
124
|
+
{ op: 'refused', requestId: 'r9', code: 'Not Allowed', message: 'x' },
|
|
125
|
+
{ op: 'refused', requestId: 'r9', code: 'x', message: '' },
|
|
126
|
+
{ op: 'control', requestId: 'r5', result: [] },
|
|
127
|
+
{
|
|
128
|
+
op: 'control',
|
|
129
|
+
requestId: 'r5',
|
|
130
|
+
result: { a: { b: { c: { d: { e: { f: { g: { h: { i: 1 } } } } } } } } },
|
|
131
|
+
},
|
|
132
|
+
{ op: 'control', requestId: 'r5', result: JSON.parse('{"__proto__":{"polluted":true}}') },
|
|
133
|
+
{ op: 'custody', state: { unlocked: 'yes', certified: false } },
|
|
134
|
+
{ op: 'custody', state: { unlocked: true } },
|
|
135
|
+
{ op: 'custody', requestId: 'r1', state: { unlocked: true, certified: true } },
|
|
136
|
+
];
|
|
137
|
+
for (const value of refused) expect(decodeRuntimeMessage(utf8(value))).toBeUndefined();
|
|
138
|
+
expect(({} as Record<string, unknown>)['polluted']).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
});
|