@akanjs/cli 3.0.0-alpha.62 → 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
- d6bf8d5dde2b2ae1de7b48fd33e2631928f688849e22150658c732e283b86011
1
+ 91051910ed4a6b677266a1e02acdb28b3dd899f6d24b642112cf0de3dcb7c1f2
@@ -118,7 +118,7 @@ back.
118
118
  error; it falls back to discovery and aborts on whatever nested config the walk finds. Rename the file to
119
119
  `biome.jsonc` to document a disabled rule; `akan lint` pins the config path either way, so it reports the parse
120
120
  error on the offending line.
121
- - **`akan lint` prints up to 200 diagnostics** (`--maxDiagnostics <n>`, `0` for no limit). Biome's own default is 20
121
+ - **`akan lint` prints up to 200 diagnostics** (`--max-diagnostics <n>`, `0` for no limit). Biome's own default is 20
122
122
  with no count, which reads as progress when the mix of findings merely changed.
123
123
 
124
124
  ## Coding Style (`**/*.{ts,tsx}`)
@@ -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
 
@@ -418,6 +449,13 @@ workflow changes.
418
449
  ref model's store — that store is a singleton, and loading into it would overwrite whatever listing of the same
419
450
  model is already on screen. `ref` is the only UI hint a filter arg carries; there is no render callback, because a
420
451
  function cannot cross to the client.
452
+ - **A summary counter names the listing it counts, on the field itself.** `field(Int).meta(getQueryMeta("user")
453
+ .query("byStatuses").args([["prepare"]]))` is what makes the dashboard tile clickable: clicking it applies that
454
+ filter to the listing in place — one request, no navigation — and fills the arg controls under the toolbar, so
455
+ the filter is visible and editable rather than opaque. `args` may be a thunk (`() => [dayjs().subtract(1,
456
+ "hour")]`), read at the click so a relative time is current. A meta naming a different model counts a different
457
+ listing, so its tile stays inert here. `AdminPanel`'s `queryMap` still overrides per column, and is now only for
458
+ a column whose field cannot declare one.
421
459
  - **A picker row is labelled by `labelOf`, which reads `Light<Model>.label()` first.** Then the field carrying
422
460
  `text: "title"`, then `title` / `name`, with the id as the floor. Write the method on the Light class — that is
423
461
  where display logic belongs, and the same label is what `Load.Units` and the in-page agent's screen scope show.
@@ -627,10 +665,18 @@ commands, transcript compaction, and the built-in tool semantics: `get_guideline
627
665
 
628
666
  ### Scalar & Field Type Reference
629
667
 
630
- - **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.
631
669
  - **Use the JS globals directly (no import needed)**: `String`, `Boolean`, `Date`. They are monkey-patched to behave like scalars, so `field(String)` typechecks.
632
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.
633
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`.
634
680
 
635
681
  ### Text Search Fields — the `text` role
636
682
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "3.0.0-alpha.62",
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.62",
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",