{"_id":"@avatarsd-llc/strawberry-client","name":"@avatarsd-llc/strawberry-client","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@avatarsd-llc/strawberry-client","version":"0.1.0","description":"Open-source, framework-free TypeScript client for the Strawberry (Gorshok-v4) grow controller WS+protobuf interface. One library for the web UI SPA, the Pulumi deploy provider, the strawberry-cli, and a Claude Code agent-skill.","license":"Apache-2.0","author":{"name":"Avatars LLC"},"homepage":"https://github.com/avatarsd-llc/strawberry-cli#readme","repository":{"type":"git","url":"git+https://github.com/avatarsd-llc/strawberry-cli.git","directory":"packages/strawberry-client"},"bugs":{"url":"https://github.com/avatarsd-llc/strawberry-cli/issues"},"keywords":["gorshok","strawberry","websocket","protobuf","grow-controller","strawberry-client","esp32"],"type":"module","main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./design":{"types":"./dist/design/index.d.ts","import":"./dist/design/index.mjs","require":"./dist/design/index.cjs"},"./node":{"types":"./dist/node.d.ts","import":"./dist/node.mjs","require":"./dist/node.cjs"},"./proto":{"types":"./dist/proto/index.d.ts","import":"./dist/proto/index.mjs","require":"./dist/proto/index.cjs"}},"sideEffects":false,"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"scripts":{"build":"tsup","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","lint:purity":"node scripts/check-purity.mjs","proto":"node scripts/gen-proto.mjs"},"dependencies":{"@protobuf-ts/runtime":"^2.9.4"},"peerDependencies":{"ws":"^8"},"peerDependenciesMeta":{"ws":{"optional":true}},"engines":{"node":">=22"},"_id":"@avatarsd-llc/strawberry-client@0.1.0","gitHead":"502b4d89bcb2e66e027e5e1c6c33ab925a0013a3","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-bR5bUh7kQvmpA81QFl8fJC9rqsgVH7aZehogS9RsH4W9KPQEQo5u8LH/5sQK/UyCY4fxed7XCOEpJUSRfX6O+w==","shasum":"da24269ffa6274910354999d236a7deb5f28e128","tarball":"https://registry.npmjs.org/@avatarsd-llc/strawberry-client/-/strawberry-client-0.1.0.tgz","fileCount":34,"unpackedSize":8293741,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCDxSNioJn4sYA6Um3YaqLqLe6/NVNpBTC2ebNJ629U3wIhAKJsf/Gn0j1ve8Y0JtXUz8QE/AY/bPv+hnhiFyMsPYWL"}]},"_npmUser":{"name":"avatarsd","email":"avatarsd2@gmail.com"},"directories":{},"maintainers":[{"name":"avatarsd","email":"avatarsd2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/strawberry-client_0.1.0_1781955833567_0.5674650702419286"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-20T11:43:53.402Z","0.1.0":"2026-06-20T11:43:53.759Z","modified":"2026-06-20T11:43:54.003Z"},"maintainers":[{"name":"avatarsd","email":"avatarsd2@gmail.com"}],"description":"Open-source, framework-free TypeScript client for the Strawberry (Gorshok-v4) grow controller WS+protobuf interface. One library for the web UI SPA, the Pulumi deploy provider, the strawberry-cli, and a Claude Code agent-skill.","homepage":"https://github.com/avatarsd-llc/strawberry-cli#readme","keywords":["gorshok","strawberry","websocket","protobuf","grow-controller","strawberry-client","esp32"],"repository":{"type":"git","url":"git+https://github.com/avatarsd-llc/strawberry-cli.git","directory":"packages/strawberry-client"},"author":{"name":"Avatars LLC"},"bugs":{"url":"https://github.com/avatarsd-llc/strawberry-cli/issues"},"license":"Apache-2.0","readme":"# @avatarsd-llc/strawberry-client\n\nOpen-source, **framework-free** TypeScript WebSocket + protobuf client for the\n**Strawberry** (Gorshok-v4) ESP32-C6 grow controller: `DeviceClient`, a generated\nprotobuf-ts codec, and a pure-JS HMAC. Runs unchanged in a browser, in Node, and in a\nPulumi provider host — no Angular, no RxJS, no framework lock-in.\n\nIt is the shared core consumed by the firmware's web-ui SPA, a Pulumi deploy provider, and\nthe [`@avatarsd-llc/strawberry-cli`](../strawberry-cli) CLI, and is embedded in\n[`avatarsd-llc/strawberry-fw`](https://github.com/avatarsd-llc/strawberry-fw) as a git\n**submodule** at `packages/strawberry-client`.\n\n## Install\n\n```bash\nnpm i @avatarsd-llc/strawberry-client\n```\n\n**Node 22+** is the baseline: Node ships a global `WebSocket` from v22, so the library\nworks with no extra dependency. The [`ws`](https://www.npmjs.com/package/ws) package is an\n**optional** peer dependency, dynamically imported only by the `./node` transport for\nolder runtimes or when you want to inject a WebSocket implementation. In a browser the\nglobal `WebSocket` is used automatically.\n\n## Quick start\n\n`DeviceClient` drives a `{ transport, codec }` pair and never names a WebSocket or a wire\nformat directly (the [transport + codec seam](#transport--codec-seam)). The static\n`forWsHost` helper wires the default `WsTransport` + protobuf codec for you. `--host` may\nbe a bare host, `host:port`, or a full `ws(s)://…/ws` URL.\n\n```ts\nimport { DeviceClient, Query_What, Topic } from '@avatarsd-llc/strawberry-client';\nimport { commands } from '@avatarsd-llc/strawberry-client';\n\nconst client = DeviceClient.forWsHost('192.168.1.117', {\n  requestMode: 'sequential', // 'concurrent' (default, rid Map) | 'sequential' (one in-flight)\n});\n\nawait client.connect();                 // opens the transport; auto-resumes a stored token\nawait client.login('strawberry');       // HMAC; plaintext password NEVER hits the wire\n\nconst caps = await client.query(Query_What.CAPABILITIES); // typed one-shot pull\nawait client.sendExpectAck(commands.growUnitSet({ id: 'grow.1', name: 'Basil', active: true }));\n\nclient.push.on('stats', (s) => console.log('min_free', s.minFreeHeap)); // push topics\nawait client.subscribe(Topic.STATS);\n\nclient.disconnect();\n```\n\n`requestMode` selects request/reply discipline: `'concurrent'` tracks replies by\n`request_id` and lets them overlap (the SPA default); `'sequential'` is one-in-flight\n(what the CLI and the Pulumi provider use). On Node without a global WebSocket, build the\ntransport from the `./node` subpath:\n\n```ts\nimport { NodeWsTransport, FileTokenStore } from '@avatarsd-llc/strawberry-client/node';\nimport { DeviceClient, wsUrlForHost } from '@avatarsd-llc/strawberry-client';\n\nconst transport = await NodeWsTransport.create(wsUrlForHost('192.168.1.117'));\nconst client = new DeviceClient({ transport, tokenStore: new FileTokenStore('/path/to.token') });\n```\n\n## WS protocol surface\n\nThe wire protocol is `proto/messages.proto` (a oneof + `request_id` envelope in both\ndirections; `request_id == 0` means an unsolicited push). The current surface:\n\n| Element | Count |\n|---------|-------|\n| `ClientMessage` commands | **64** |\n| `ServerMessage` types | **38** |\n| `Query.What` pullable states | **17** |\n| push `Topic`s | **13** |\n\nAuth is **HMAC** HMAC challenge-response: `AuthChallengeReq` -> `AuthChallenge{nonce}`\n-> `HMAC-SHA256(password, nonce)` computed in **pure JS** (the device serves over plain\n`http`, where `crypto.subtle` is `undefined`, so there is no `crypto.subtle` /\n`node:crypto` dependency) -> `AuthLogin{hmac}` -> `AuthOk{token}`. The plaintext password\nnever crosses the wire.\n\nFull reference: [`docs/protocol.md`](./docs/protocol.md) and [`docs/library.md`](./docs/library.md).\n\n### Firmware quirks\n\nVerified against real hardware (see [`../../HIL-FINDINGS.md`](../../HIL-FINDINGS.md)):\n\n- **`stats` and `snapshot` are push-only.** The firmware has no `query` case for\n  `WHAT_STATS` (returns `unknown query`); `WHAT_SNAPSHOT` broadcasts and then replies with\n  a bare `Ack`. Read both by subscribing to `TOPIC_STATS` / `TOPIC_SNAPSHOT` via\n  `client.push`, not through `query`. The library enforces this — `query()` throws on a\n  push-only `WHAT` rather than handing back a mistyped payload.\n- **Session resume is single-connection-only.** The firmware binds each auth token to the\n  socket that created it and revokes it on socket close (the 8-slot-leak fix). So a token\n  cannot be resumed across separate process invocations or after a reconnect — `resume` on\n  a new socket gets `419 token expired`. Hold a token only within one long-lived\n  connection; otherwise log in fresh.\n\n## Transport + codec seam\n\n`DeviceClient` exchanges decoded `ClientMessage` / `ServerMessage` with a\n`{ transport, codec }` pair and never names a `WebSocket` or the protobuf runtime. The\nshipping pair is `WsTransport` + `ProtobufWsCodec`. A future **libtracer (TLV)** transport\nis a drop-in `Transport` + `Codec` implementation — nothing in `commands/*`, the CLI, the\nSPA, or the Pulumi provider changes. The one invariant TLV must honor: `request_id` echo,\nwith `request_id == 0` meaning an unsolicited push.\n\n```ts\nexport interface Transport {\n  connect(): Promise<void>;\n  send(data: Uint8Array): void;\n  onMessage(cb: (data: Uint8Array) => void): void;\n  onClose(cb: () => void): void;\n  isOpen(): boolean;\n  close(): void;\n}\n```\n\nSubpath exports keep the SPA graph clean:\n\n| Subpath    | Contains | Consumer |\n|------------|----------|----------|\n| `.`        | full client: `DeviceClient` + `commands/*` builders + proto + design models | SPA, CLI, Pulumi |\n| `./design` | browser-safe pure models only (zero transport deps) | SPA tree-shake |\n| `./node`   | `FileTokenStore` (0600 token persistence) + `NodeWsTransport` (`ws`) | CLI, agent-skill |\n| `./proto`  | the raw protobuf-ts message vocabulary (`ClientMessage`/`ServerMessage`/…) | CLI command modules |\n\n## Build & test\n\nFrom the monorepo root (workspace-aware):\n\n```bash\nnpm install\nnpm run proto -w @avatarsd-llc/strawberry-client   # regenerate src/proto/messages.ts (gitignored)\nnpm run build -w @avatarsd-llc/strawberry-client   # tsup -> dist/ (ESM + CJS + .d.ts), four subpaths\nnpm run test  -w @avatarsd-llc/strawberry-client   # vitest: HMAC vector + codec round-trips + protocol matrix\n```\n\nOr from this package dir: `npm run proto`, `npm run build`, `npm test`.\n\nValidation is layered:\n\n- **Protocol matrix** ([`test/ws-protocol-matrix.test.ts`](./test/ws-protocol-matrix.test.ts))\n  — data-driven over the generated codec's reflection: every one of the 64 `ClientMessage`\n  commands and 38 `ServerMessage` types round-trips encode→decode, and the\n  `Query.What`/`Topic` counts are asserted. Exhaustive by construction.\n- **HIL** (hardware-in-the-loop) — [`../../HIL-FINDINGS.md`](../../HIL-FINDINGS.md) is the\n  running catalog of behavior against a live Gorshok-v4 board.\n\nPinned cross-impl HMAC test vector (shared with the firmware's `ws_hmac.c`):\n\n```\nHMAC-SHA256(\"strawberry\", 0x00..0x0f) =\n  880e5c19ec51b5646794e768dd50f6ec6f7961b9de89dd79852d00d7482bfaed\n```\n\n## Proto sync\n\n`proto/messages.proto` is **vendored from `strawberry-fw`** (`components/proto`). It is the\nsingle source of the wire vocabulary; the generated codec (`src/proto/messages.ts`) is\ngitignored and rebuilt by `npm run proto` (which defaults `PROTO_DIR` to `./proto`, with an\nenv override). When the firmware changes the protocol, re-vendor `messages.proto` and\nregenerate. The web-ui SPA uses byte-identical generation options so the SPA and this\nlibrary share the same codec.\n\n## License\n\n[Apache-2.0](./LICENSE), (c) Avatars LLC.\n","readmeFilename":"README.md","_rev":"1-46dc2b5f3592972c6f62c4e50dbff8b5"}