@api.global/typedsocket 5.1.2 → 6.0.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.
package/readme.md CHANGED
@@ -11,18 +11,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
11
11
  - 🔒 **Full Type Safety** - Leverages TypeScript for compile-time checking of all request/response payloads
12
12
  - 🔄 **Bi-directional Communication** - Both server and client can initiate requests
13
13
  - 🔌 **Auto-reconnect** - Client automatically reconnects on connection loss
14
- - 🏷️ **Connection Tagging** - Tag and filter connections for targeted messaging
14
+ - 🏷️ **Policy-Gated Connection Tagging** - Default-deny client tags and protected server metadata
15
15
  - 🌐 **Browser Compatible** - Works in both Node.js and browser environments
16
16
  - 🚀 **SmartServe Integration** - Native support for SmartServe's WebSocket handling
17
17
 
18
18
  ## Install
19
19
 
20
- ```bash
21
- npm install @api.global/typedsocket
22
- ```
23
-
24
- Or with pnpm:
25
-
26
20
  ```bash
27
21
  pnpm add @api.global/typedsocket
28
22
  ```
@@ -34,6 +28,11 @@ pnpm add @api.global/typedsocket
34
28
  - TypeScript project setup
35
29
  - Basic understanding of async/await patterns
36
30
  - Familiarity with `@api.global/typedrequest` concepts
31
+ - `@api.global/typedrequest` 5 and `@api.global/typedrequest-interfaces` 5
32
+ - `@push.rocks/smartserve` 4.2.1 or newer within major 4 for server integrations
33
+
34
+ TypedSocket 6, SmartServe 4.2.1, and TypedRequest 5.2.1 share one `TypedRouter` contract. Do not
35
+ mix older router contracts or bridge the mismatch with casts.
37
36
 
38
37
  ### Define Your Request Interface
39
38
 
@@ -82,6 +81,7 @@ const smartServe = new SmartServe({
82
81
  port: 3000,
83
82
  websocket: {
84
83
  typedRouter,
84
+ transportOwner: server.webSocketTransportOwner,
85
85
  },
86
86
  });
87
87
  server.attachSmartServe(smartServe);
@@ -114,9 +114,10 @@ const smartServe = new SmartServe({
114
114
  port: 3000,
115
115
  websocket: {
116
116
  typedRouter,
117
+ transportOwner: typedSocket.webSocketTransportOwner,
117
118
  onConnectionOpen: (peer) => {
118
- // Tag connections for later filtering
119
- peer.tags.add('client');
119
+ // Server metadata is protected from client overwrite/removal.
120
+ typedSocket.setServerTag(peer, 'client');
120
121
  }
121
122
  }
122
123
  });
@@ -148,6 +149,7 @@ const smartServe = new SmartServe({
148
149
  if (context.url.hostname === 'admin.example.com') return adminRouter;
149
150
  return undefined;
150
151
  },
152
+ transportOwner: typedSocket.webSocketTransportOwner,
151
153
  },
152
154
  });
153
155
  typedSocket.attachSmartServe(smartServe);
@@ -157,15 +159,287 @@ await smartServe.start();
157
159
  TypedSocket adds a private, one-way fallback router containing its protocol
158
160
  handlers to every application router. Duplicate protocol method names are
159
161
  rejected during composition and on later router mutations; `stop()` releases
160
- the owned fallback edges. `fromSmartServe()` remains available as a
161
- compatibility shortcut that delegates to `createServer()` and
162
- `attachSmartServe()`.
162
+ the owned fallback edges. `fromSmartServe()` remains available as a JSON-only
163
+ attachment helper that delegates to `createServer()` and `attachSmartServe()`.
163
164
 
164
165
  Connection lookup and implicit single-peer targeting span every peer on the
165
166
  attached SmartServe transport, regardless of the surface router that accepted
166
167
  the peer. Multi-surface servers should tag or filter peers by surface and pass
167
168
  an explicit target to `createTypedRequest()`.
168
169
 
170
+ ### Native Byte Streams
171
+
172
+ TypedSocket 6 negotiates `native-byte-v1` on each physical WebSocket before
173
+ restoring tags or publishing the client as connected. Native bytes require the
174
+ SmartServe 4.2.1 raw-frame owner to be selected when SmartServe is constructed. The
175
+ server construction order is strict:
176
+
177
+ 1. Create the application `TypedRouter` instances.
178
+ 2. Call `TypedSocket.createServer(routerOrRouters, options?)`.
179
+ 3. Construct `SmartServe` with the composed router and
180
+ `transportOwner: typedSocket.webSocketTransportOwner`.
181
+ 4. Call `typedSocket.attachSmartServe(smartServe)`.
182
+ 5. Start SmartServe.
183
+
184
+ `fromSmartServe()` supports JSON-only attachment to an existing server. It cannot add a
185
+ raw-frame owner to a SmartServe instance that has already selected transports,
186
+ so native negotiation succeeds only for peers already bound to the exact
187
+ `webSocketTransportOwner` object.
188
+
189
+ Client and server options accept `nativeByteCapabilityMode`:
190
+
191
+ - `optional` is the default. JSON RPC remains available if native negotiation is unavailable.
192
+ - `required` fails client startup when negotiation does not return `native-byte-v1`. On servers it closes peers that do not negotiate the capability.
193
+ - `disabled` advertises no native capability and keeps JSON RPC available.
194
+
195
+ #### Preferred VirtualStream Facade
196
+
197
+ For bounded byte payloads, application code uses
198
+ `TypedSocket.createVirtualStream()` and passes the returned
199
+ `VirtualStream<Uint8Array>` facade in its typed DTO. TypedRequest transfers the
200
+ opaque transport descriptor automatically. The receiving application drains
201
+ and durably commits the stream before confirmation; the sender sees only the
202
+ reversed `send` facade and its durable completion receipt.
203
+
204
+ ```typescript
205
+ import * as typedrequest from '@api.global/typedrequest';
206
+ import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
207
+ import { SmartServe } from '@push.rocks/smartserve';
208
+ import { createSha256Hasher } from '@push.rocks/smarthash/web';
209
+ import {
210
+ TypedSocket,
211
+ type TNativeByteAuthorityOperation,
212
+ } from '@api.global/typedsocket';
213
+
214
+ interface IUploadRequest extends typedrequestInterfaces.implementsTR<
215
+ typedrequestInterfaces.ITypedRequest,
216
+ IUploadRequest
217
+ > {
218
+ method: 'uploadBytes';
219
+ request: {
220
+ uploadId: string;
221
+ byteLength: number;
222
+ sha256: string;
223
+ contentType: string;
224
+ };
225
+ response: {
226
+ stream: typedrequestInterfaces.INativeByteVirtualStream<Uint8Array>;
227
+ };
228
+ }
229
+
230
+ interface IUploadAuthorization {
231
+ uploadId: string;
232
+ principalId: string;
233
+ credentialRevision: string;
234
+ configRevision: string;
235
+ bindingRevision: string;
236
+ }
237
+
238
+ interface IDurableUpload {
239
+ writable: WritableStream<Uint8Array>;
240
+ commit(): Promise<void>;
241
+ abort(reasonArg: unknown): Promise<void>;
242
+ }
243
+
244
+ // These hooks belong to the application and its durable storage layer.
245
+ declare function bindUploadAuthorization(uploadIdArg: string): IUploadAuthorization;
246
+ declare function revalidateUpload(
247
+ authorizationArg: IUploadAuthorization,
248
+ manifestArg: typedrequestInterfaces.INativeByteStreamManifest,
249
+ operationArg: TNativeByteAuthorityOperation,
250
+ ): Promise<boolean>;
251
+ declare function openDurableUpload(uploadIdArg: string): Promise<IDurableUpload>;
252
+
253
+ function parseUploadAuthorization(valueArg: unknown): IUploadAuthorization {
254
+ if (typeof valueArg !== 'object' || valueArg === null) {
255
+ throw new Error('Upload authorization is invalid');
256
+ }
257
+ const uploadId = Reflect.get(valueArg, 'uploadId');
258
+ const principalId = Reflect.get(valueArg, 'principalId');
259
+ const credentialRevision = Reflect.get(valueArg, 'credentialRevision');
260
+ const configRevision = Reflect.get(valueArg, 'configRevision');
261
+ const bindingRevision = Reflect.get(valueArg, 'bindingRevision');
262
+ if (
263
+ typeof uploadId !== 'string'
264
+ || typeof principalId !== 'string'
265
+ || typeof credentialRevision !== 'string'
266
+ || typeof configRevision !== 'string'
267
+ || typeof bindingRevision !== 'string'
268
+ ) {
269
+ throw new Error('Upload authorization is invalid');
270
+ }
271
+ return { uploadId, principalId, credentialRevision, configRevision, bindingRevision };
272
+ }
273
+
274
+ const serverRouter = new typedrequest.TypedRouter();
275
+ let typedSocketServer!: TypedSocket;
276
+
277
+ serverRouter.addTypedHandler(new typedrequest.TypedHandler<IUploadRequest>(
278
+ 'uploadBytes',
279
+ async (requestArg, metaArg) => {
280
+ const authorization = bindUploadAuthorization(requestArg.uploadId);
281
+ const connection = typedSocketServer.getServerConnectionForRequest(metaArg);
282
+ const stream = typedSocketServer.createVirtualStream({
283
+ protocol: 'native-byte-v1',
284
+ direction: 'receive',
285
+ target: connection,
286
+ byteLength: requestArg.byteLength,
287
+ sha256: requestArg.sha256,
288
+ contentType: requestArg.contentType,
289
+ authorization,
290
+ });
291
+
292
+ void (async () => {
293
+ let durableUpload: IDurableUpload | undefined;
294
+ try {
295
+ durableUpload = await openDurableUpload(authorization.uploadId);
296
+ await stream.writeToWebstream(durableUpload.writable);
297
+ await durableUpload.commit();
298
+ await stream.confirmDurable();
299
+ } catch (errorArg) {
300
+ await durableUpload?.abort(errorArg).catch(() => {});
301
+ await stream.reject(errorArg).catch(() => {});
302
+ }
303
+ })();
304
+
305
+ return { stream };
306
+ },
307
+ ));
308
+
309
+ typedSocketServer = TypedSocket.createServer(serverRouter, {
310
+ nativeByteCapabilityMode: 'required',
311
+ nativeByteAuthorizationAdapter: {
312
+ bind: (authorizationArg, contextArg) => {
313
+ const authorization = parseUploadAuthorization(authorizationArg);
314
+ return {
315
+ principalId: authorization.principalId,
316
+ credentialRevision: authorization.credentialRevision,
317
+ configRevision: authorization.configRevision,
318
+ bindingRevision: authorization.bindingRevision,
319
+ revalidate: async (revalidationArg) =>
320
+ revalidationArg.connection.side === 'server'
321
+ && revalidationArg.connection.peer === contextArg.target
322
+ && await revalidateUpload(
323
+ authorization,
324
+ contextArg.manifest,
325
+ revalidationArg.operation,
326
+ ),
327
+ };
328
+ },
329
+ },
330
+ });
331
+
332
+ const smartServe = new SmartServe({
333
+ port: 3000,
334
+ websocket: {
335
+ typedRouter: serverRouter,
336
+ transportOwner: typedSocketServer.webSocketTransportOwner,
337
+ },
338
+ });
339
+ typedSocketServer.attachSmartServe(smartServe);
340
+ await smartServe.start();
341
+
342
+ const client = await TypedSocket.createClient(
343
+ new typedrequest.TypedRouter(),
344
+ 'http://127.0.0.1:3000',
345
+ { nativeByteCapabilityMode: 'required', autoReconnect: false },
346
+ );
347
+ try {
348
+ const payload = new TextEncoder().encode('authorized payload');
349
+ const sha256 = `sha256:${createSha256Hasher().update(payload).digest()}`;
350
+ const response = await client.createTypedRequest<IUploadRequest>('uploadBytes').fire({
351
+ uploadId: 'upload-1',
352
+ byteLength: payload.byteLength,
353
+ sha256,
354
+ contentType: 'text/plain',
355
+ });
356
+ await response.stream.readFromWebstream(new ReadableStream<Uint8Array>({
357
+ start: (controllerArg) => {
358
+ controllerArg.enqueue(payload);
359
+ controllerArg.close();
360
+ },
361
+ }));
362
+ const receipt = await response.stream.completion;
363
+ if (receipt.sha256 !== sha256 || !receipt.durable) {
364
+ throw new Error('Upload receipt did not match the authorized manifest');
365
+ }
366
+ } finally {
367
+ await client.stop();
368
+ await typedSocketServer.stop();
369
+ await smartServe.stop();
370
+ }
371
+ ```
372
+
373
+ `nativeByteAuthorizationAdapter.bind()` is synchronous so descriptor publication
374
+ cannot race authority capture. Its `revalidate()` callback may be asynchronous;
375
+ OPEN, DATA, FIN, and durable confirmation are followed by an exact
376
+ peer/generation ownership check. Rejection attempts one final best-effort
377
+ revalidation before local cleanup and RESET.
378
+
379
+ #### Advanced Transport API
380
+
381
+ `nativeBytes.createReceiveGrant()`, `nativeBytes.openSender()`, and opaque native
382
+ descriptors are protocol-integration APIs. Normal application DTOs should carry
383
+ the `VirtualStream` facade shown above, not a descriptor. Transport integrations
384
+ that cannot use the facade must still provide a non-negative safe-integer
385
+ `byteLength`, exact `sha256:<64 lowercase hex>`, and a normalized media-type
386
+ `contentType` of at most 255 UTF-8 bytes. They must also provide all three
387
+ nonempty authority revisions and mandatory `revalidate(context)`.
388
+
389
+ The application must drain the stream, commit it durably, and only then call
390
+ `confirmDurable()`. The transport can enforce validated FIN, complete drain, and
391
+ explicit confirmation, but the application owns the storage durability claim.
392
+ `FIN_ACK` is not emitted before confirmation. The sender's `close()` and
393
+ `completion` remain pending until then.
394
+ Accepted nonempty DATA calls are copied and admitted against the authorized
395
+ length and queue limits before asynchronous execution, so caller mutation,
396
+ large backing buffers, and non-awaited calls cannot escape transport accounting.
397
+
398
+ Native byte transport deliberately provides no business idempotency, reconnect
399
+ resume, HTTP fallback, or legacy-byte fallback. A disconnect or generation
400
+ change fails all affected grants and streams. Capabilities and stream IDs are
401
+ one-use on one exact physical peer.
402
+
403
+ #### Legacy VirtualStream Removal
404
+
405
+ TypedSocket 6 unconditionally rejects generic `##VirtualStream##` descriptors and
406
+ control requests. There is no client option, server option, fallback, or migration
407
+ opt-in. Byte DTOs must use the exact native facade returned by
408
+ `TypedSocket.createVirtualStream()`.
409
+
410
+ #### Native Limits
411
+
412
+ | Limit | Value |
413
+ |---|---:|
414
+ | Complete binary message | 32 KiB |
415
+ | DATA payload per message | 32,720 bytes |
416
+ | Maximum stream length | 140,531,329,925,120 bytes (127.8125 TiB) |
417
+ | Default receive window | 256 KiB |
418
+ | Maximum receive window / queued payload per stream | 1 MiB |
419
+ | Queued payload per connection | 8 MiB |
420
+ | Queued receive chunks per stream | 4,096 |
421
+ | Grants plus active streams per connection | 32 |
422
+ | Pending admitted DATA operations per connection | 64 |
423
+ | Raw inbound queue | 64 frames / 2 MiB |
424
+ | Grant and OPEN timeout | 10 seconds |
425
+ | Progress, ACK, frame settlement, FIN_ACK, durable confirmation | 30 seconds |
426
+ | Closed-stream tombstones | 64, oldest-first, 60 seconds |
427
+ | Principal and each authority revision | 256 UTF-8 bytes |
428
+ | Retained revalidation callbacks | 4 per peer / 16 per principal / 128 per server |
429
+ | Server connections / streams | 1,024 / 1,024 |
430
+ | Server retained bytes / receive reservations | 64 MiB / 64 MiB |
431
+
432
+ SmartServe owner sends prioritize control frames over DATA and pull one binary
433
+ frame per requested turn. Browser clients likewise send one binary frame per
434
+ macrotask, allowing direct JSON text traffic to run before the next binary turn.
435
+ Server frame accounting remains retained after stream or connection cleanup
436
+ until SmartServe settles the exact frame object returned by `pullBinaryFrame()`;
437
+ late, cloned, or duplicate callbacks cannot settle newer work. Tombstone
438
+ admission evicts oldest entries first and closes the connection if the fixed
439
+ retained-byte budget still cannot hold the required replay fence.
440
+ `getStats()` exposes connection/negotiation status, counts, and byte totals;
441
+ descriptors, capability tokens, digests, and payloads are not included.
442
+
169
443
  ### Client Setup
170
444
 
171
445
  Connect to the WebSocket server from a client:
@@ -184,6 +458,68 @@ const client = await TypedSocket.createClient(
184
458
  );
185
459
  ```
186
460
 
461
+ Remote connections must use `https:` or `wss:`. Plain `http:` and `ws:` are
462
+ accepted only for loopback hosts. Credentials and URL fragments are rejected;
463
+ connection logs include only the protocol and authority, never paths or queries.
464
+
465
+ Client options can lower, but never raise, package ceilings for text-frame bytes,
466
+ queued text frames/bytes, concurrent handlers, retained callbacks, pending
467
+ requests, outbound WebSocket buffering, request timeouts, and connection
468
+ restoration timeouts. `restoreConnection(context)` runs after capability
469
+ negotiation and before desired tags are reconciled or `connected` is published.
470
+ Its `context.createTypedRequest<T>(method)` uses the new physical connection and
471
+ inherits the restoration abort signal and remaining deadline. The factory is
472
+ invalidated when restoration succeeds, fails, times out, or is aborted, so it
473
+ must not be retained for later application traffic.
474
+
475
+ ```typescript
476
+ import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
477
+
478
+ interface IRestoreSessionRequest extends typedrequestInterfaces.implementsTR<
479
+ typedrequestInterfaces.ITypedRequest,
480
+ IRestoreSessionRequest
481
+ > {
482
+ method: 'restoreSession';
483
+ request: { token: string; connectionId: string };
484
+ response: { restored: true };
485
+ }
486
+
487
+ const client = await TypedSocket.createClient(clientRouter, serverUrl, {
488
+ autoReconnect: true,
489
+ restoreConnection: async ({
490
+ connectionId,
491
+ abortSignal,
492
+ deadline,
493
+ createTypedRequest,
494
+ }) => {
495
+ if (abortSignal.aborted || Date.now() >= deadline) {
496
+ throw new Error('Connection restoration expired');
497
+ }
498
+ await createTypedRequest<IRestoreSessionRequest>('restoreSession').fire({
499
+ token: sessionToken,
500
+ connectionId,
501
+ });
502
+ },
503
+ });
504
+ ```
505
+
506
+ | Client limit | Package ceiling |
507
+ |---|---:|
508
+ | Complete text frame | 1 MiB |
509
+ | Queued text frames / bytes | 64 / 4 MiB |
510
+ | Concurrent handlers / retained callbacks | 16 / 64 |
511
+ | Pending client requests | 1,024 |
512
+ | Outbound WebSocket buffered bytes | 4 MiB |
513
+ | Method name / correlation ID | 256 UTF-8 bytes each |
514
+ | Request timeout | 5 minutes |
515
+ | Connection restoration timeout | 10 seconds |
516
+ | Reconnect attempts | 100 |
517
+ | Initial / maximum reconnect backoff | 60 seconds |
518
+
519
+ `maxRetries` is a non-negative safe integer no greater than 100.
520
+ `initialBackoffMs` and `maxBackoffMs` are positive safe integers no greater
521
+ than 60,000, and the initial value cannot exceed the maximum.
522
+
187
523
  #### Abortable Startup
188
524
 
189
525
  Pass an `AbortSignal` when startup or reconnect attempts must be cancellable. Aborting stops the in-flight WebSocket and prevents queued reconnect attempts from continuing.
@@ -281,57 +617,120 @@ const responsePromise = request.fire(
281
617
  const response = await responsePromise;
282
618
  ```
283
619
 
620
+ Server-initiated requests retain at most 64 pending requests per peer and 1,024
621
+ per TypedSocket server. Asynchronous `addInterest()` registration retains at
622
+ most 8 operations per peer and 64 per server. Cancellation removes the pending
623
+ request immediately, while a non-settling registration remains charged until
624
+ its underlying promise actually settles.
625
+
284
626
  ### Connection Tagging
285
627
 
286
- Tag connections for organized, targeted communication:
628
+ Client tag mutation is disabled by default. A server must opt in each exact name, choose whether
629
+ an accepted proposal remains client-owned or becomes server-owned, and validate both payload shape
630
+ and connection authorization inside the private protocol boundary.
287
631
 
288
632
  ```typescript
289
- // Client side: add a tag
290
- interface IUserTag extends typedrequestInterfaces.ITag {
291
- name: 'userRole';
292
- payload: 'admin' | 'user' | 'guest';
633
+ interface IProgressTag extends typedrequestInterfaces.ITag {
634
+ name: 'progressSubscription';
635
+ payload: { channel: 'scan-progress' };
293
636
  }
294
637
 
295
- await client.setTag<IUserTag>('userRole', 'admin');
638
+ declare const authenticatedAuthorities: ReadonlySet<string>;
639
+
640
+ const server = TypedSocket.createServer(typedRouter, {
641
+ clientTagPolicy: {
642
+ rules: [{
643
+ name: 'progressSubscription',
644
+ owner: 'client',
645
+ validateAndAuthorize: ({ operation, payload, authority, abortSignal }) => {
646
+ // Both IDs are opaque exact-object identities. The callback receives no
647
+ // mutable peer.tags, peer.data, peer, or router object access.
648
+ const authorityKey = `${authority.connectionId}:${authority.routingSurfaceId}`;
649
+ if (abortSignal.aborted || !authenticatedAuthorities.has(authorityKey)) return false;
650
+ return operation === 'remove'
651
+ || (
652
+ typeof payload === 'object'
653
+ && payload !== null
654
+ && Reflect.get(payload, 'channel') === 'scan-progress'
655
+ );
656
+ },
657
+ }],
658
+ },
659
+ });
660
+
661
+ await client.setTag<IProgressTag>(
662
+ 'progressSubscription',
663
+ { channel: 'scan-progress' },
664
+ );
296
665
 
297
- // On reconnect, stored tags are restored before statusSubject emits
298
- // "connected". A failed restoration keeps the client unready and is retried
299
- // through the normal reconnect policy.
666
+ // On reconnect, desired tags are reconciled before statusSubject emits
667
+ // "connected". Acknowledgements are scoped to one physical generation.
300
668
 
301
- // Removed tags are not restored after reconnect, even if this rejects because
302
- // the server is unavailable while the removal request is attempted.
303
- await client.removeTag('userRole');
669
+ // A policy denial discards only that FIFO intent and recomputes desired state
670
+ // from any later pending intent or the stable baseline established by earlier
671
+ // settlements. Transport failure retains the canonical desired removal tombstone;
672
+ // accepted removal, including successful reconnect replay, clears it.
673
+ await client.removeTag('progressSubscription');
304
674
  ```
305
675
 
306
676
  ```typescript
307
- // Server side: find connections by tag
308
- const adminConnections = await server.findAllTargetConnectionsByTag<IUserTag>(
309
- 'userRole',
310
- 'admin'
677
+ const progressConnections = await server.findAllTargetConnectionsByTag<IProgressTag>(
678
+ 'progressSubscription',
679
+ { channel: 'scan-progress' }
311
680
  );
312
681
 
313
- // Send to all admins
314
- for (const conn of adminConnections) {
682
+ for (const conn of progressConnections) {
315
683
  const request = server.createTypedRequest<IGreetingRequest>('greet', conn);
316
- await request.fire({ name: 'admin' });
684
+ await request.fire({ name: 'subscriber' });
317
685
  }
686
+ ```
687
+
688
+ Authentication, roles, service registration, and other authoritative metadata must be assigned by
689
+ the server after application-level verification:
318
690
 
319
- // Find a single connection
320
- const firstAdmin = await server.findTargetConnectionByTag<IUserTag>('userRole', 'admin');
691
+ ```typescript
692
+ const connection = server.getServerConnectionForRequest(typedToolsArg);
693
+ server.setServerTag(connection, 'authenticatedUser', { userId });
694
+
695
+ // Clients cannot set, overwrite, or remove this name. Removal remains server-owned.
696
+ server.removeServerTag(connection, 'authenticatedUser');
321
697
  ```
322
698
 
699
+ Do not mutate `peer.tags` or TypedSocket's prefixed `peer.data` entries directly. The protected
700
+ server methods reconcile ownership with in-flight client proposals.
701
+
702
+ Fixed limits cannot be raised by policy:
703
+
704
+ - Tag name: 128 UTF-8 bytes.
705
+ - SmartJSON payload envelope: 4,096 bytes.
706
+ - Client-originated retained tags: 16 per peer.
707
+ - Client-originated cumulative name and payload storage: 16,384 bytes per peer.
708
+ - Client-side retained mutation work: 8 per exact name and 64 per client.
709
+ - Retained mutation/authorization work: 8 per peer, 32 per exact rule, and 256 per TypedSocket server.
710
+ - Protected server tag state: 64 names and 65,536 cumulative name/payload bytes per peer.
711
+ - Authorization callback deadline: at most 5 seconds; policy may only lower it.
712
+
713
+ Same-name mutations execute FIFO. Timed-out callbacks receive an aborted signal and remain charged
714
+ against authorization budgets until they actually settle. Disconnect and server stop abort active
715
+ callbacks and prevent late commits.
716
+
717
+ #### TypedServer Integration Contract
718
+
719
+ TypedServer 9 passes `clientTagPolicy` through, exposes
720
+ `webSocketTransportOwner`, resolves handler-local peers through
721
+ `getServerConnectionForRequest(typedTools)`, and assigns infrastructure tags
722
+ through `setServerTag()`. Authentication, roles, frontend registration, and
723
+ service-worker registration are server-owned application state; v6 provides no
724
+ legacy tag migration mode.
725
+
323
726
  ### Event Handling
324
727
 
325
- Subscribe to connection status events:
728
+ Client instances publish connection status events:
326
729
 
327
730
  ```typescript
328
731
  client.statusSubject.subscribe((status) => {
329
732
  console.log('Connection status:', status);
330
733
  });
331
-
332
- server.statusSubject.subscribe((status) => {
333
- console.log('Server connection event:', status);
334
- });
335
734
  ```
336
735
 
337
736
  ### Cleanup
@@ -355,9 +754,9 @@ await smartServe.stop();
355
754
 
356
755
  | Method | Description |
357
756
  |--------|-------------|
358
- | `createClient(router, serverUrl, options?)` | Creates a WebSocket client that connects to the specified server URL. Options include `autoReconnect`, `maxRetries`, `initialBackoffMs`, `maxBackoffMs`, and `abortSignal`. |
359
- | `createServer(routerOrRouters)` | Synchronously composes TypedSocket protocol handling into one or more isolated application routers. |
360
- | `fromSmartServe(smartServe, routerOrRouters)` | Compatibility shortcut that creates and attaches a server-side TypedSocket. |
757
+ | `createClient(router, serverUrl, options?)` | Creates a WebSocket client. Options include reconnect controls, `abortSignal`, `nativeByteCapabilityMode`, lowering-only `limits`, and `restoreConnection`. |
758
+ | `createServer(routerOrRouters, options?)` | Synchronously composes protocol handling. Options include `nativeByteCapabilityMode`, `nativeByteAuthorizationAdapter`, and the default-deny `clientTagPolicy`. |
759
+ | `fromSmartServe(smartServe, routerOrRouters, options?)` | Creates and attaches a JSON-only server-side TypedSocket to an existing SmartServe instance. |
361
760
  | `useWindowLocationOriginUrl()` | Returns the current window location origin (browser only). |
362
761
 
363
762
  #### Instance Properties
@@ -366,25 +765,41 @@ await smartServe.stop();
366
765
  |----------|-------------|
367
766
  | `side` | Whether this instance is a `'server'` or `'client'`. |
368
767
  | `typedrouter` | The TypedRouter instance handling requests. |
369
- | `statusSubject` | RxJS Subject for connection status events. |
768
+ | `nativeBytes` | Advanced native-byte grant, sender, capability, and statistics API for transport integrations. |
769
+ | `webSocketTransportOwner` | Stable SmartServe 4 raw-frame owner selected during SmartServe construction. |
770
+ | `statusSubject` | RxJS Subject for client connection status events. Server instances do not publish lifecycle transitions here. |
370
771
 
371
772
  #### Instance Methods
372
773
 
373
774
  | Method | Description |
374
775
  |--------|-------------|
375
776
  | `attachSmartServe(smartServe)` | Attaches one SmartServe transport to a composed server-side TypedSocket before listening. |
777
+ | `createVirtualStream(options)` | Creates the preferred exact, authorized native-byte receive facade for one server peer. TypedRequest transfers its descriptor automatically. |
376
778
  | `createTypedRequest(method, targetConnection?, options?)` | Creates a typed request. Options include the transport `timeoutMs` and `abortSignal`; per-call `fire()` deadlines are also forwarded to the transport. |
377
- | `setTag(name, payload)` | Sets a tag on the client connection (client-side only). |
378
- | `removeTag(name)` | Immediately removes a tag from local reconnect state, then removes it from the server connection; rejects if the server removal fails. |
779
+ | `getServerConnectionForRequest(typedTools)` | Resolves the exact transport connection for an incoming server handler without assertions. |
780
+ | `getStatus()` | Returns the client connection lifecycle status. Server instances remain in the initial `new` state. |
781
+ | `setTag(name, payload)` | Requests one exact policy-authorized tag and records reconnect state only after an ordered server acknowledgement. |
782
+ | `removeTag(name)` | Requests removal of a client-owned tag. Policy denial discards that FIFO intent and recomputes from any later pending intent or the stable settled baseline. Transport failure retains the canonical desired removal tombstone; accepted removal, including successful reconnect replay, clears it. |
783
+ | `setServerTag(connection, name, payload?)` | Assigns protected server-owned metadata after application verification. |
784
+ | `removeServerTag(connection, name)` | Removes server-owned metadata while keeping the name protected from client mutation for that peer. |
379
785
  | `findAllTargetConnections(filterFn)` | Finds all connections matching the filter (server-side only). |
380
786
  | `findTargetConnection(filterFn)` | Finds the first connection matching the filter (server-side only). |
381
787
  | `findAllTargetConnectionsByTag(key, payload?)` | Finds all connections with the specified tag. |
382
788
  | `findTargetConnectionByTag(key, payload?)` | Finds the first connection with the specified tag. |
383
789
  | `stop()` | On clients, closes the WebSocket and rejects pending requests. On servers, cancels pending requests, cleans their interests, unsubscribes from SmartServe, and releases protocol-router composition without stopping SmartServe itself. |
384
790
 
791
+ #### Advanced Native Methods
792
+
793
+ | Method | Description |
794
+ |--------|-------------|
795
+ | `nativeBytes.createReceiveGrant(options)` | Creates an explicit exact-manifest receive grant for a protocol integration. |
796
+ | `nativeBytes.openSender(descriptor, options?)` | Opens an explicit sender for an opaque descriptor on the exact target connection. |
797
+ | `nativeBytes.getCapability(target?)` | Reports negotiated native-byte capability without exposing tokens or descriptors. |
798
+ | `nativeBytes.getStats(target?)` | Reports bounded connection, stream, queue, and tombstone counts. |
799
+
385
800
  ## License and Legal Information
386
801
 
387
- This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
802
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
388
803
 
389
804
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
390
805
 
@@ -396,7 +811,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
396
811
 
397
812
  ### Company Information
398
813
 
399
- Task Venture Capital GmbH
814
+ Task Venture Capital GmbH<br>
400
815
  Registered at District Court Bremen HRB 35230 HB, Germany
401
816
 
402
817
  For any legal inquiries or further information, please contact us via email at hello@task.vc.
package/npmextra.json DELETED
@@ -1,35 +0,0 @@
1
- {
2
- "@git.zone/cli": {
3
- "projectType": "npm",
4
- "module": {
5
- "githost": "code.foss.global",
6
- "gitscope": "api.global",
7
- "gitrepo": "typedsocket",
8
- "description": "A library for creating typed WebSocket connections, supporting bi-directional communication with type safety.",
9
- "npmPackagename": "@api.global/typedsocket",
10
- "license": "MIT",
11
- "projectDomain": "api.global",
12
- "keywords": [
13
- "WebSocket",
14
- "Type Safety",
15
- "Real-time Communication",
16
- "Client-Server Architecture",
17
- "TypeScript",
18
- "Networking"
19
- ]
20
- },
21
- "release": {
22
- "registries": [
23
- "https://verdaccio.lossless.digital",
24
- "https://registry.npmjs.org"
25
- ],
26
- "accessLevel": "public"
27
- }
28
- },
29
- "@git.zone/tsdoc": {
30
- "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
31
- },
32
- "@ship.zone/szci": {
33
- "npmGlobalTools": []
34
- }
35
- }