feat(sdk): @punktfunk/host — the Effect TypeScript SDK (M3)
apple / swift (push) Successful in 1m15s
apple / screenshots (push) Successful in 4m21s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 1m0s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 18s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m42s
docker / deploy-docs (push) Successful in 26s
apple / swift (push) Successful in 1m15s
apple / screenshots (push) Successful in 4m21s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 1m0s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 18s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m42s
docker / deploy-docs (push) Successful in 26s
New top-level sdk/ package (RFC §7): a typed management-API client plus the lifecycle event stream, built on Effect, two surfaces over one core: - @punktfunk/host — the Promise facade front door: connect() resolves URL/token/TLS pin from the host's own files (zero config on the box), fails fast on bad credentials, pf.events.on() with typed callbacks (exact kinds, domain.* prefixes, "*", "dropped", "unknown"), pf.request() for the REST surface. Effect never required. - @punktfunk/host/effect — the PunktfunkHost service + PunktfunkHostLive layer, Stream-based events()/eventsRaw(), typed errors (AuthError | ApiError | TransportError | VersionSkew — a 2xx that fails its schema is a typed skew, not undefined later), and every wire shape as an effect/Schema: REST generated via orval client:'effect' from api/openapi.json (S3 spike: works well; the text/event-stream payload is out of its reach), events hand-mirrored from the host's snapshot-tested wire format as a kind-discriminated union. One reconnecting SSE core under both surfaces: spec-shaped parser, exponential+jittered backoff (capped, resets after a healthy connection), Last-Event-ID resume, 401 terminal. Default is LIVE tail only — a fresh notify script must not re-fire on the host's replayed ring (since: 0 opts into full replay). TLS: the pin trusts exactly the host's self-signed identity cert (chain-verified; hostname check waived — the cert is deliberately CN-only for fingerprint pinning). Bun via fetch tls, Node via an undici dispatcher (optionalDependency). definePlugin() accepts both main shapes (async fn | Effect requiring PunktfunkHost). Examples in both styles; README carries the compat contract + systemd/Task Scheduler templates. 11 bun tests green (wire decode against the Rust snapshot strings, SSE parser/reconnect/Last-Event-ID/401, both surfaces vs a mock host). Live-verified against a real host on Bun AND Node through the pinned loopback hop: connect → REST mutate → live event received → resume cursor advanced; a wrong CA is rejected. npm publish + CI wiring deferred (npm org = RFC open question 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
# @punktfunk/host
|
||||
|
||||
TypeScript SDK for the [punktfunk](https://git.unom.io/unom/punktfunk) streaming host: a typed
|
||||
management-API client plus the host's lifecycle **event stream** (client connect/disconnect,
|
||||
stream start/stop, pairing, displays, library) — built on [Effect](https://effect.website).
|
||||
|
||||
Two surfaces, one core:
|
||||
|
||||
- **`@punktfunk/host`** — the Promise facade, the front door. `connect()`, `await`, `.on()`.
|
||||
You never need to know Effect exists.
|
||||
- **`@punktfunk/host/effect`** — the Effect-native surface for plugins and composed programs:
|
||||
the `PunktfunkHost` service + layer, `Stream`-based events, typed errors
|
||||
(`AuthError | ApiError | TransportError | VersionSkew`), and every wire shape as an
|
||||
`effect/Schema` (REST shapes generated from the host's OpenAPI spec; event shapes mirroring
|
||||
the host's snapshot-tested wire format).
|
||||
|
||||
## Quickstart
|
||||
|
||||
```ts
|
||||
import { connect } from "@punktfunk/host";
|
||||
|
||||
const pf = await connect(); // zero config on the host box
|
||||
|
||||
pf.events.on("stream.started", (e) => {
|
||||
console.log(`${e.stream.client} started ${e.stream.mode}${e.stream.hdr ? " HDR" : ""}`);
|
||||
});
|
||||
pf.events.on("pairing.pending", async (e) => {
|
||||
// notify your phone, then decide through the API:
|
||||
// await pf.request("POST", `/native/pending/${id}/approve`);
|
||||
});
|
||||
```
|
||||
|
||||
The same, Effect-native:
|
||||
|
||||
```ts
|
||||
import { Effect, Stream } from "effect";
|
||||
import { events, PunktfunkHostLive } from "@punktfunk/host/effect";
|
||||
|
||||
const program = events().pipe(
|
||||
Stream.filter((e) => e.kind === "stream.started"),
|
||||
Stream.runForEach((e) => Effect.log(`stream: ${e.stream.mode}`)),
|
||||
);
|
||||
Effect.runPromise(program.pipe(Effect.provide(PunktfunkHostLive())));
|
||||
```
|
||||
|
||||
## Connection resolution
|
||||
|
||||
`connect()` / `PunktfunkHostLive()` resolve, in order:
|
||||
|
||||
| What | Source |
|
||||
|---|---|
|
||||
| URL | `{ url }` → `PUNKTFUNK_MGMT_URL` → `https://127.0.0.1:47990` |
|
||||
| Token | `{ token }` → `PUNKTFUNK_MGMT_TOKEN` → `<config_dir>/mgmt-token` |
|
||||
| TLS pin | `{ ca }` → `PUNKTFUNK_MGMT_CA` (path) → `<config_dir>/cert.pem` |
|
||||
|
||||
`<config_dir>` is `~/.config/punktfunk` (Linux/macOS) or `%ProgramData%\punktfunk` (Windows) —
|
||||
so a script running on the host box needs **zero configuration**. The TLS pin trusts exactly
|
||||
the host's self-signed identity cert (chain-verified; the hostname check is waived — the cert
|
||||
is deliberately CN-only, native clients pin its fingerprint). Bun and Node are first-class;
|
||||
other runtimes fall back to system trust (point your runtime's CA option at `cert.pem`).
|
||||
|
||||
The bearer token is the host's **admin** credential and is honored from loopback only — run
|
||||
scripts on the host box (or through an SSH tunnel).
|
||||
|
||||
## Events
|
||||
|
||||
- Reconnects automatically (exponential backoff + jitter, capped) and resumes with
|
||||
`Last-Event-ID` — the host replays what you missed from its ring.
|
||||
- Default is **live tail only** (a fresh notify script must not re-fire on history);
|
||||
pass `{ since: 0 }` on the Effect surface to replay the host's full ring, or `since: N`
|
||||
to resume after a seq you persisted.
|
||||
- `on()` patterns: exact kinds (`"stream.started"`, typed callback), `"domain.*"` prefixes,
|
||||
`"*"`, plus `"dropped"` (your cursor fell off the ring — resync via REST) and `"unknown"`
|
||||
(an event kind newer than this SDK — the additive-only wire at work).
|
||||
- Effect surface: `events()` is a `Stream<HostEvent, EventStreamError>`; `eventsRaw()` carries
|
||||
every SSE frame verbatim.
|
||||
|
||||
## Plugins (`punktfunk-plugin-*`)
|
||||
|
||||
```ts
|
||||
import { definePlugin } from "@punktfunk/host";
|
||||
import { Effect } from "effect";
|
||||
import { PunktfunkHost } from "@punktfunk/host/effect";
|
||||
|
||||
export default definePlugin({
|
||||
name: "romm-library",
|
||||
main: Effect.gen(function* () {
|
||||
const pf = yield* PunktfunkHost;
|
||||
// subscribe, sync, reconcile — scoped finalizers run on shutdown/interruption
|
||||
}),
|
||||
// …or the simple shape: main: async (pf) => { … }
|
||||
});
|
||||
```
|
||||
|
||||
In v1 a plugin is a script you run (see below); the managed runner package is a later step.
|
||||
|
||||
## Running as a service
|
||||
|
||||
systemd user unit (`~/.config/systemd/user/punktfunk-myscript.service`):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=punktfunk automation: myscript
|
||||
After=punktfunk-host.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/bun /home/me/punktfunk-scripts/myscript.ts
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Windows Task Scheduler: a task triggered *At log on* running
|
||||
`bun C:\Users\me\punktfunk-scripts\myscript.ts` (the SDK reads
|
||||
`%ProgramData%\punktfunk\mgmt-token` — run the task as an account that can).
|
||||
|
||||
## Compatibility
|
||||
|
||||
- SDK **majors** track the management-API major; an event `schema` bump or an `effect` major
|
||||
is an SDK major too.
|
||||
- The wire is **additive-only** within a major: an older SDK keeps working against a newer
|
||||
host (unknown response keys are ignored; unknown event kinds ride the `"unknown"` channel).
|
||||
- A 2xx response that doesn't match its schema surfaces as `VersionSkew` on the Effect
|
||||
surface — a typed nudge to update, not an `undefined` three frames later.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
bun install
|
||||
bun run gen # regenerate src/gen/schemas.ts from ../api/openapi.json
|
||||
bun run typecheck
|
||||
bun test
|
||||
```
|
||||
Reference in New Issue
Block a user