@akanjs/cli 3.0.0-alpha.63 → 3.0.0-alpha.64

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/.build-stamp CHANGED
@@ -1 +1 @@
1
- 766fec76fb1eaf8c5b0fe9ce5f5368d31232118a41ead868a0c1b0b30ed68570
1
+ 91051910ed4a6b677266a1e02acdb28b3dd899f6d24b642112cf0de3dcb7c1f2
@@ -369,6 +369,15 @@ workflow changes.
369
369
  - Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is unguarded unless it declares its own `guards` in its signal option.
370
370
  - A pubsub room is authorized once, at subscribe. When a socket's credential changes the framework re-runs each room's guards and unsubscribes the ones that now fail (`SignalResolver.revalidateWsRooms`), so guards must stay side-effect free and safe to re-run.
371
371
  - A websocket carries its credential in the handshake snapshot on `ws.data` (`AppWsData`); clients that hold the token in memory send it with `fetch.setJwt(...)`, which forwards an auth frame over the socket.
372
+ - **Never read the caller's IP off the socket or the request peer — take `.with(Ip)`.** Every app runs behind
373
+ the federation gateway, so `ws.remoteAddress` and the child's own peer are the *gateway* (`127.0.0.1`) for
374
+ every caller and for the whole life of every socket. `Ip` reads what the gateway recorded
375
+ (`x-real-ip`, then the first `x-forwarded-for` entry) and falls back to the peer only when nothing proxied
376
+ the call; `context.getClientIp()` is the same answer inside a guard or middleware. It arrives unwrapped from
377
+ its `::ffff:` IPv4-mapped form, so it can address a `udp4` socket as well as identify a caller, and it is
378
+ `null` rather than a placeholder when genuinely unknown — a loopback-looking address for an unknown caller
379
+ is the failure this replaced. The gateway also forwards the client port (`x-forwarded-port`, read with
380
+ `context.getClientPort()`) along with host and protocol.
372
381
  - **Every socket carries a `socketId`, and only the framework mints one.** `AppWsData` assigns it at the handshake, and a `message` / `pubsub` handler reads it off the `Ws` internal arg — `.with(Ws)` hands `{ ws, socketId, subscribe, on, off }`. Never mint your own from `ws.data`: the room bookkeeping keys on the framework's id, and a second one fails to match it silently. It identifies a **connection**, not a caller — a reconnect gets a new id and the federation gateway's own socket is a different one — so per-user state keys on the account, never on this.
373
382
 
374
383
  ### Authorization Defaults
@@ -389,6 +398,28 @@ workflow changes.
389
398
  - `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
390
399
  - Numbers must use `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
391
400
  - `Upload` is valid only inside a mutation flagged for file upload: `mutation([cnst.File], { fileUpload: true }).body("files", [Upload])`, as the `file` module does. It is not a model field type.
401
+ - Bytes are `Binary`, never `Any` — see Scalar & Field Type Reference.
402
+
403
+ ### Binary Pubsub
404
+
405
+ - **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
406
+ envelope and the base64 a JSON wire would need. The client's subscribe callback receives a `Uint8Array`.
407
+ Nothing else is declared: the return type is the whole switch, and text and binary frames coexist on one
408
+ socket, so every JSON endpoint is untouched. The optimization applies only when the **whole** return is
409
+ `Binary` — `[Binary]`, or a `Binary` inside a model, falls back to base64.
410
+ - **A declared `Binary` room coalesces under backpressure**: while a socket cannot keep up the room keeps only
411
+ its newest frame and drops the rest, which is what a telemetry or video stream wants. Declare
412
+ `pubsub(Binary, { backpressure: "queue" })` when the frames are a sequence a subscriber must see in full, such
413
+ as deltas against a base it already holds; the send buffer then grows with the slowest subscriber. Coalescing
414
+ is keyed by the endpoint, not carried with the frame, so this process, an IPC deliver and a Redis fan-out all
415
+ reach the same answer. Coalesced frames are counted in `pubsubCoalesceCount` on the replica's metrics report.
416
+ - **A `pubsub(Any)` that happens to carry bytes is framed too, and warns once.** `Any` passes a `Buffer` through
417
+ untouched, so the transport sees bytes and sends them whole rather than as a number array — an undeclared
418
+ publisher is fixed rather than left corrupt. It is still not the contract: it queues rather than coalescing
419
+ (nobody declared lossy delivery for that room), and a Redis fan-out still JSON-stringifies it. Declare
420
+ `Binary`.
421
+ - The federation gateway relays a binary frame unchanged, and Bun IPC carries bytes as bytes, so the only hop
422
+ that needed teaching was the socket itself. A cross-server (Redis) fan-out encodes it as protobuf `bytes`.
392
423
 
393
424
  ### Mutation HTTP Verb
394
425
 
@@ -634,10 +665,18 @@ commands, transcript compaction, and the built-in tool semantics: `get_guideline
634
665
 
635
666
  ### Scalar & Field Type Reference
636
667
 
637
- - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Upload`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible payloads.
668
+ - **Import from `akanjs/base`** (real classes/helpers, not globals): `Int`, `Float`, `ID`, `Any`, `Binary`, `Upload`, `enumOf`, and the `dayjs` factory. There is **no `JSON` scalar** — use `Any` for open/flexible payloads.
638
669
  - **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to behave like scalars, so `field(String)` typechecks.
639
670
  - **`Number` is not a valid field/body type.** `NumberConstructor` is intentionally not augmented, so `field(Number)` / `.body("x", Number)` fails to typecheck. Use `Int` or `Float` instead.
640
671
  - Runtime resolution of every scalar (globals included) goes through `PrimitiveRegistry` by `refName` (`pkgs/akanjs/base/primitiveRegistry.ts`).
672
+ - **`Binary` is raw bytes on the wire, and never a model field.** It is `Uint8Array` on both sides — a Node
673
+ `Buffer` is one, so a server handler may pass one straight in — and it accepts base64 in either direction, so
674
+ the same declaration serves a JSON body and a binary frame. It is **not storable**: the class build refuses
675
+ `field(Binary)` naming the `File` model instead, because every non-base field lives in the `_doc` JSON column
676
+ and bytes would sit there as base64 and ride every read of the row. Never send bytes as `Any` — `Any` passes a
677
+ `Buffer` through untouched and `JSON.stringify` then spells it `{ type: "Buffer", data: number[] }`, which
678
+ `JSON.parse` never restores; the result is 3.6x the wire and a shape that only breaks at the first byte-offset
679
+ read. MCP refuses a `Binary` return for the same reason it refuses `Any`.
641
680
 
642
681
  ### Text Search Fields — the `text` role
643
682
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "3.0.0-alpha.63",
3
+ "version": "3.0.0-alpha.64",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "3.0.0-alpha.63",
37
+ "akanjs": "3.0.0-alpha.64",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "dayjs": "^1.11.20",