arcane-os 0.52.1 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/NOTICE +4 -1
  3. package/README.md +1 -1
  4. package/bin/arcane-test.mjs +2 -1
  5. package/browser-runtime/ai/browser-decisions.mjs +405 -0
  6. package/browser-runtime/ai/decision-runtime.mjs +281 -0
  7. package/browser-runtime/ai/decision-worker.mjs +67 -0
  8. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/index.js +1151 -0
  9. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/licence +21 -0
  10. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/package.json +61 -0
  11. package/browser-runtime/dependencies/event-pubsub/index.js +2 -2
  12. package/browser-runtime/dependencies/event-pubsub/package.json +11 -4
  13. package/browser-runtime/dependencies/ws-share/WS.js +128 -0
  14. package/browser-runtime/dependencies/ws-share/licence.md +27 -0
  15. package/browser-runtime/dependencies/ws-share/package.json +64 -0
  16. package/browser-runtime/websocket-client.mjs +1 -0
  17. package/docs/event-manager.md +7 -4
  18. package/docs/reference/ai/browser-decisions.md +184 -0
  19. package/docs/reference/inventory/package-api.json +44 -2
  20. package/docs/reference/inventory/runtime-modules.json +17 -4
  21. package/docs/reference/protocols.md +76 -1
  22. package/docs/reference/runtime-modules.md +45 -2
  23. package/docs/reference/sdk-api.md +280 -6
  24. package/docs/reference/websocket-client.md +132 -0
  25. package/node_modules/event-pubsub/CHANGELOG.md +12 -1
  26. package/node_modules/event-pubsub/MIGRATION.md +8 -5
  27. package/node_modules/event-pubsub/README.md +75 -19
  28. package/node_modules/event-pubsub/SECURITY.md +3 -3
  29. package/node_modules/event-pubsub/index.js +2 -2
  30. package/node_modules/event-pubsub/package.json +11 -4
  31. package/package.json +9 -5
  32. package/runtime/arcane/modules/HTMLImport.js +81 -26
  33. package/runtime/arcane/modules/HTMLImportScript.js +3 -0
  34. package/src/import-map.mjs +416 -59
  35. package/src/index.mjs +2 -0
  36. package/src/packager/core.mjs +24 -6
  37. package/src/packager/native-modules.mjs +458 -0
  38. package/src/testing-loader.mjs +41 -20
  39. package/src/websocket.mjs +479 -0
@@ -0,0 +1,479 @@
1
+ import {createHash} from 'node:crypto';
2
+ import Is from 'strong-type';
3
+
4
+ const is=new Is(false);
5
+ const OPEN=1;
6
+ const CLOSING=2;
7
+ const CLOSED=3;
8
+ const TOKEN=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u;
9
+
10
+ function protocolError(code,message,details={}){
11
+ const error=new Error(message);
12
+ error.code=`ARCANE_WEBSOCKET_${code}`;
13
+ return Object.assign(error,details);
14
+ }
15
+
16
+ function handshake(request,protocol){
17
+ const headers=request?.headers??{};
18
+ function tokens(value){
19
+ return is.string(value)?value.split(',').map(function trimToken(token){
20
+ return token.trim();
21
+ }):[];
22
+ }
23
+ function hasToken(value,expected){
24
+ return tokens(value).some(function matchesToken(token){
25
+ return token.toLowerCase()===expected;
26
+ });
27
+ }
28
+ function invalid(message){
29
+ return protocolError('HANDSHAKE_INVALID',message,{statusCode:400,headers:{}});
30
+ }
31
+ if(request?.method!=='GET'
32
+ ||request.httpVersionMajor!==1||request.httpVersionMinor<1
33
+ ||!is.string(headers.host)||!headers.host.trim()
34
+ ||!hasToken(headers.upgrade,'websocket')
35
+ ||!hasToken(headers.connection,'upgrade')){
36
+ throw invalid('WebSocket requires an HTTP/1.1 GET Upgrade request with Host.');
37
+ }
38
+ if(headers['sec-websocket-version']!=='13'){
39
+ throw protocolError('VERSION_UNSUPPORTED','WebSocket version 13 is required.',{
40
+ statusCode:426,
41
+ headers:{'Sec-WebSocket-Version':'13'}
42
+ });
43
+ }
44
+ const key=headers['sec-websocket-key'];
45
+ // The key syntax and SHA-1 accept value are required HTTP Upgrade framing,
46
+ // not a content identity, admission receipt, or application security policy.
47
+ if(!is.string(key)||!/^[A-Za-z0-9+/]{22}==$/u.test(key)
48
+ ||Buffer.from(key,'base64').length!==16){
49
+ throw invalid('Sec-WebSocket-Key must encode the RFC6455 handshake nonce.');
50
+ }
51
+ const offered=tokens(headers['sec-websocket-protocol']);
52
+ if(offered.some(function invalidToken(token){return !TOKEN.test(token);})
53
+ ||new Set(offered).size!==offered.length){
54
+ throw invalid('Sec-WebSocket-Protocol must contain distinct protocol tokens.');
55
+ }
56
+ if(!is.string(protocol)||(protocol!==''&&(!TOKEN.test(protocol)||!offered.includes(protocol)))){
57
+ throw invalid('The selected WebSocket protocol must be offered by the client.');
58
+ }
59
+ const accept=createHash('sha1')
60
+ .update(key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
61
+ .digest('base64');
62
+ return Buffer.from([
63
+ 'HTTP/1.1 101 Switching Protocols',
64
+ 'Upgrade: websocket',
65
+ 'Connection: Upgrade',
66
+ `Sec-WebSocket-Accept: ${accept}`,
67
+ ...(protocol?[`Sec-WebSocket-Protocol: ${protocol}`]:[]),
68
+ '',
69
+ ''
70
+ ].join('\r\n'));
71
+ }
72
+
73
+ function binaryData(data){
74
+ if(data instanceof ArrayBuffer)return Buffer.from(data);
75
+ if(ArrayBuffer.isView(data))return Buffer.from(data.buffer,data.byteOffset,data.byteLength);
76
+ throw new TypeError('WebSocket data must be text, an ArrayBuffer, or an ArrayBuffer view.');
77
+ }
78
+
79
+ function framePacket(opcode,payload){
80
+ // Length fields belong only to the wire format. No product payload limit is imposed.
81
+ const length=payload.length;
82
+ const header=Buffer.alloc(length<126?2:length<=65535?4:10);
83
+ header[0]=0x80|opcode;
84
+ if(length<126)header[1]=length;
85
+ else if(length<=65535){
86
+ header[1]=126;
87
+ header.writeUInt16BE(length,2);
88
+ }else{
89
+ header[1]=127;
90
+ header.writeBigUInt64BE(BigInt(length),2);
91
+ }
92
+ // Snapshot the selected view at submission. Queued callers may reuse their input.
93
+ return Buffer.concat([header,payload]);
94
+ }
95
+
96
+ function validCloseCode(code){
97
+ return is.integer(code)&&(
98
+ (code>=1000&&code<=1014&&![1004,1005,1006].includes(code))
99
+ ||(code>=3000&&code<=4999)
100
+ );
101
+ }
102
+
103
+ function encodeText(text){
104
+ if(!text.isWellFormed()){
105
+ throw protocolError('TEXT_INVALID','WebSocket UTF-8 cannot represent an unpaired UTF-16 surrogate.');
106
+ }
107
+ return Buffer.from(text);
108
+ }
109
+
110
+ function closePayload(code,reason){
111
+ if(!validCloseCode(code))throw new RangeError('Invalid WebSocket close status code.');
112
+ if(!is.string(reason))throw new TypeError('WebSocket close reason must be text.');
113
+ const text=encodeText(reason);
114
+ // RFC6455 control frames permit 125 octets, including the two-octet status.
115
+ if(text.length>123)throw new RangeError('WebSocket close reason exceeds its protocol frame.');
116
+ const payload=Buffer.alloc(2+text.length);
117
+ payload.writeUInt16BE(code);
118
+ text.copy(payload,2);
119
+ return payload;
120
+ }
121
+
122
+ function abortError(reason){
123
+ const error=protocolError('ABORTED','WebSocket connection was aborted.',{cause:reason});
124
+ error.name='AbortError';
125
+ return error;
126
+ }
127
+
128
+ /** Accept one caller-selected HTTP/1.1 Upgrade. Owns no listener, route or storage. */
129
+ export function acceptWebSocket({request,socket,head=Buffer.alloc(0),protocol='',signal}={}){
130
+ if(signal!=null&&(!is.function(signal.addEventListener)||!is.function(signal.removeEventListener))){
131
+ throw new TypeError('WebSocket signal must provide the AbortSignal event interface.');
132
+ }
133
+ if(signal?.aborted)throw abortError(signal.reason);
134
+ const response=handshake(request,protocol);
135
+ for(const method of ['on','off','pause','resume','write','end','destroy']){
136
+ if(!is.function(socket?.[method]))throw new TypeError(`Upgrade socket requires ${method}().`);
137
+ }
138
+ if(socket.destroyed||socket.readableEncoding){
139
+ throw new TypeError('Upgrade socket must be open and retain binary transport data.');
140
+ }
141
+ let initial=Buffer.from(binaryData(head));
142
+ const connection=new EventTarget();
143
+ let state=OPEN;
144
+ let started=false;
145
+ let failure=null;
146
+ let ending=false;
147
+ let failedProtocol=false;
148
+ let peerClose=null;
149
+ let closeQueued=false;
150
+ let closeWritten=false;
151
+ let activeWrite=null;
152
+ let incomingFrame=null;
153
+ let messageOpcode=0;
154
+ let messageParts=[];
155
+ const pendingInput=[];
156
+ const writes=[];
157
+ const header=Buffer.alloc(14);
158
+ let headerUsed=0;
159
+ let headerNeeded=2;
160
+ let settleClosed;
161
+ const closed=new Promise(function retainCloseSettlement(resolve){settleClosed=resolve;});
162
+
163
+ function closedError(){
164
+ return failure??protocolError('CONNECTION_CLOSED','WebSocket connection is closing or closed.');
165
+ }
166
+
167
+ function reportError(error){
168
+ if(failure)return;
169
+ failure=error;
170
+ connection.dispatchEvent(new CustomEvent('error',{detail:error}));
171
+ }
172
+
173
+ function rejectQueued(error,dataOnly=false){
174
+ for(const entry of writes.splice(0)){
175
+ if(dataOnly&&entry.kind!=='data')writes.push(entry);
176
+ else entry.reject(error);
177
+ }
178
+ }
179
+
180
+ function disposeInput(){
181
+ initial=null;
182
+ pendingInput.length=0;
183
+ messageParts=[];
184
+ incomingFrame=null;
185
+ messageOpcode=0;
186
+ headerUsed=0;
187
+ }
188
+
189
+ function finish(){
190
+ if(state===CLOSED)return;
191
+ state=CLOSED;
192
+ activeWrite?.finish(closedError());
193
+ rejectQueued(closedError());
194
+ disposeInput();
195
+ socket.off('data',receive);
196
+ socket.off('end',onEnd);
197
+ socket.off('error',onError);
198
+ socket.off('close',onClose);
199
+ signal?.removeEventListener('abort',onAbort);
200
+ const detail={
201
+ code:peerClose?.code??1006,
202
+ reason:peerClose?.reason??'',
203
+ wasClean:Boolean(peerClose&&closeWritten&&!failure),
204
+ error:failure
205
+ };
206
+ settleClosed(detail);
207
+ connection.dispatchEvent(new CustomEvent('close',{detail}));
208
+ }
209
+
210
+ function terminate(reason){
211
+ if(state===CLOSED)return closed;
212
+ state=CLOSING;
213
+ if(reason!==undefined)reportError(reason instanceof Error?reason:new Error('WebSocket terminated.',{cause:reason}));
214
+ activeWrite?.finish(closedError());
215
+ rejectQueued(closedError());
216
+ disposeInput();
217
+ // Keep the error/close observers installed until the actual stream closes.
218
+ socket.destroy();
219
+ return closed;
220
+ }
221
+
222
+ function onError(error){terminate(error);}
223
+ function onAbort(){terminate(abortError(signal.reason));}
224
+ function onClose(){finish();}
225
+ function onEnd(){
226
+ if(peerClose&&closeQueued){
227
+ if(closeWritten)endTransport();
228
+ }else{
229
+ terminate(protocolError('CONNECTION_CLOSED','WebSocket transport ended without a close exchange.'));
230
+ }
231
+ }
232
+
233
+ function endTransport(){
234
+ if(ending||state===CLOSED)return;
235
+ ending=true;
236
+ try{socket.end(function transportFlushed(){socket.destroy();});}
237
+ catch(error){terminate(error);}
238
+ }
239
+
240
+ function pumpWrites(){
241
+ if(!started||activeWrite||state===CLOSED||socket.destroyed||ending)return;
242
+ const entry=writes.shift();
243
+ if(!entry)return;
244
+ let callbackDone=false;
245
+ let drained=false;
246
+ let returned=false;
247
+ let settled=false;
248
+ function finishWrite(error){
249
+ if(settled)return;
250
+ if(!error&&(!callbackDone||!drained||!returned))return;
251
+ settled=true;
252
+ socket.off('drain',onDrain);
253
+ activeWrite=null;
254
+ if(error)entry.reject(error);
255
+ else entry.resolve();
256
+ queueMicrotask(pumpWrites);
257
+ }
258
+ function onDrain(){drained=true;finishWrite();}
259
+ function onWritten(error){
260
+ if(error){
261
+ finishWrite(error);
262
+ terminate(error);
263
+ return;
264
+ }
265
+ callbackDone=true;
266
+ finishWrite();
267
+ }
268
+ activeWrite={finish:finishWrite};
269
+ socket.on('drain',onDrain);
270
+ try{
271
+ // false means already accepted: await drain, never resubmit this packet.
272
+ if(socket.write(entry.packet,onWritten))drained=true;
273
+ returned=true;
274
+ finishWrite();
275
+ }catch(error){
276
+ finishWrite(error);
277
+ terminate(error);
278
+ }
279
+ }
280
+
281
+ function enqueue(packet,kind='data'){
282
+ const promise=new Promise(function retainWriteSettlement(resolve,reject){
283
+ writes.push({packet,kind,resolve,reject});
284
+ });
285
+ pumpWrites();
286
+ return promise;
287
+ }
288
+
289
+ function send(data){
290
+ if(state!==OPEN)return Promise.reject(closedError());
291
+ try{
292
+ const text=is.string(data);
293
+ return enqueue(framePacket(text?1:2,text?encodeText(data):binaryData(data)));
294
+ }catch(error){return Promise.reject(error);}
295
+ }
296
+
297
+ function queueClose(payload){
298
+ if(closeQueued)return;
299
+ closeQueued=true;
300
+ enqueue(framePacket(8,payload),'close').then(function closeSent(){
301
+ closeWritten=true;
302
+ if(peerClose||failedProtocol)endTransport();
303
+ },function closeSendFailed(error){terminate(error);});
304
+ }
305
+
306
+ function close(code=1000,reason=''){
307
+ if(state!==OPEN)return closed;
308
+ const payload=closePayload(code,reason);
309
+ state=CLOSING;
310
+ queueClose(payload);
311
+ return closed;
312
+ }
313
+
314
+ function failProtocol(error){
315
+ if(failedProtocol||state===CLOSED)return;
316
+ failedProtocol=true;
317
+ state=CLOSING;
318
+ socket.pause();
319
+ disposeInput();
320
+ reportError(error);
321
+ rejectQueued(error,true);
322
+ // A packet already accepted by write must finish before a close frame.
323
+ if(!closeQueued){
324
+ queueClose(closePayload(error.closeCode??1011,''));
325
+ }else if(closeWritten){endTransport();}
326
+ }
327
+
328
+ function invalidFrame(message){
329
+ throw protocolError('PROTOCOL_INVALID',message,{closeCode:1002});
330
+ }
331
+
332
+ function decodeText(payload){
333
+ try{
334
+ return new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(payload);
335
+ }catch(cause){
336
+ throw protocolError('TEXT_INVALID','WebSocket text is not valid UTF-8.',{closeCode:1007,cause});
337
+ }
338
+ }
339
+
340
+ function validateBaseHeader(){
341
+ const opcode=header[0]&15;
342
+ const final=Boolean(header[0]&128);
343
+ const control=opcode>=8;
344
+ const encodedLength=header[1]&127;
345
+ if(header[0]&112)invalidFrame('WebSocket extensions were not negotiated.');
346
+ if(!(header[1]&128))invalidFrame('Client WebSocket frames must be masked.');
347
+ if(![0,1,2,8,9,10].includes(opcode))invalidFrame('Unknown WebSocket opcode.');
348
+ if(control&&(!final||encodedLength>125))invalidFrame('Invalid WebSocket control frame.');
349
+ if(!control){
350
+ if(opcode===0&&!messageOpcode)invalidFrame('Unexpected WebSocket continuation.');
351
+ if(opcode!==0&&messageOpcode)invalidFrame('Interleaved WebSocket messages.');
352
+ }
353
+ }
354
+
355
+ function beginFrame(){
356
+ const opcode=header[0]&15;
357
+ const final=Boolean(header[0]&128);
358
+ const control=opcode>=8;
359
+ const encodedLength=header[1]&127;
360
+ let length=BigInt(encodedLength);
361
+ if(encodedLength===126){
362
+ length=BigInt(header.readUInt16BE(2));
363
+ if(length<126n)invalidFrame('Nonminimal WebSocket length encoding.');
364
+ }else if(encodedLength===127){
365
+ length=header.readBigUInt64BE(2);
366
+ if(length<65536n||length>>63n)invalidFrame('Invalid WebSocket extended length.');
367
+ }
368
+ if(!control){
369
+ if(opcode!==0)messageOpcode=opcode;
370
+ }
371
+ incomingFrame={
372
+ opcode,final,control,remaining:length,
373
+ mask:Buffer.from(header.subarray(headerNeeded-4,headerNeeded)),
374
+ maskIndex:0,parts:[]
375
+ };
376
+ headerUsed=0;
377
+ headerNeeded=2;
378
+ }
379
+
380
+ function completeFrame(){
381
+ const frame=incomingFrame;
382
+ incomingFrame=null;
383
+ if(frame.control){
384
+ const payload=Buffer.concat(frame.parts);
385
+ if(frame.opcode===8){
386
+ if(payload.length===1)invalidFrame('Incomplete WebSocket close status.');
387
+ const code=payload.length?payload.readUInt16BE(0):1005;
388
+ if(payload.length&&!validCloseCode(code))invalidFrame('Invalid WebSocket close status.');
389
+ const reason=payload.length?decodeText(payload.subarray(2)):'';
390
+ peerClose={code,reason};
391
+ state=CLOSING;
392
+ rejectQueued(closedError(),true);
393
+ if(!closeQueued){
394
+ queueClose(payload);
395
+ }
396
+ if(closeWritten)endTransport();
397
+ }else if(frame.opcode===9&&!peerClose){
398
+ enqueue(framePacket(10,payload),'control').catch(function pongFailed(error){terminate(error);});
399
+ }
400
+ }else if(frame.final){
401
+ const opcode=messageOpcode;
402
+ const parts=messageParts;
403
+ messageParts=[];
404
+ messageOpcode=0;
405
+ if(state===OPEN){
406
+ const payload=Buffer.concat(parts);
407
+ const data=opcode===1?decodeText(payload):new Uint8Array(payload.buffer,payload.byteOffset,payload.length);
408
+ connection.dispatchEvent(new MessageEvent('message',{data}));
409
+ }
410
+ }
411
+ }
412
+
413
+ function consume(chunk){
414
+ let offset=0;
415
+ while(offset<chunk.length&&!failedProtocol&&state!==CLOSED&&!socket.destroyed&&!peerClose){
416
+ if(!incomingFrame){
417
+ const count=Math.min(headerNeeded-headerUsed,chunk.length-offset);
418
+ chunk.copy(header,headerUsed,offset,offset+count);
419
+ offset+=count;
420
+ headerUsed+=count;
421
+ if(headerUsed<headerNeeded)continue;
422
+ if(headerNeeded===2){
423
+ validateBaseHeader();
424
+ const encodedLength=header[1]&127;
425
+ headerNeeded=2+(encodedLength===126?2:encodedLength===127?8:0)+4;
426
+ continue;
427
+ }
428
+ beginFrame();
429
+ }
430
+ const frame=incomingFrame;
431
+ const available=chunk.length-offset;
432
+ const count=frame.remaining<BigInt(available)?Number(frame.remaining):available;
433
+ if(count){
434
+ const payload=Buffer.allocUnsafe(count);
435
+ for(let index=0;index<count;index++){
436
+ payload[index]=chunk[offset+index]^frame.mask[frame.maskIndex];
437
+ frame.maskIndex=(frame.maskIndex+1)&3;
438
+ }
439
+ offset+=count;
440
+ frame.remaining-=BigInt(count);
441
+ if(frame.control)frame.parts.push(payload);
442
+ else if(state===OPEN)messageParts.push(payload);
443
+ }
444
+ if(frame.remaining===0n)completeFrame();
445
+ }
446
+ }
447
+
448
+ function receive(chunk){
449
+ if(failedProtocol||state===CLOSED||socket.destroyed||peerClose)return;
450
+ if(!started){pendingInput.push(chunk);return;}
451
+ try{consume(chunk);}
452
+ catch(error){failProtocol(error);}
453
+ }
454
+
455
+ Object.defineProperties(connection,{
456
+ readyState:{get:function readState(){return state;},enumerable:true},
457
+ protocol:{value:protocol,enumerable:true},
458
+ closed:{value:closed,enumerable:true}
459
+ });
460
+ Object.assign(connection,{send,close,terminate});
461
+ socket.pause();
462
+ socket.on('data',receive);
463
+ socket.on('end',onEnd);
464
+ socket.on('error',onError);
465
+ socket.on('close',onClose);
466
+ signal?.addEventListener('abort',onAbort,{once:true});
467
+ enqueue(response,'handshake').catch(function handshakeWriteFailed(error){terminate(error);});
468
+ queueMicrotask(function startConnection(){
469
+ started=true;
470
+ if(signal?.aborted){onAbort();return;}
471
+ if(state===CLOSED||socket.destroyed)return;
472
+ pumpWrites();
473
+ receive(initial);
474
+ initial=null;
475
+ for(const chunk of pendingInput.splice(0))receive(chunk);
476
+ if(!failedProtocol&&!peerClose&&state!==CLOSED&&!socket.destroyed)socket.resume();
477
+ });
478
+ return connection;
479
+ }