@camada/bun 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 camada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @camada/bun
2
+
3
+ camada for [Bun.serve](https://bun.sh/docs/api/http): enforces the tenant snapshot inline (your
4
+ ordered custom rules, then block, allow, challenge), serves a first-party proof-of-work challenge
5
+ page and beacon, records the outcomes your handlers know (`track()`), and ships wire events in
6
+ batches off the response path. A thin binding over `@camada/core/fetch`: the socket peer comes
7
+ from `server.requestIP`, the env from `Bun.env`, and the snapshot polls on a timer because a Bun
8
+ server lives long enough to have one. Fails open by design — a camada outage or bug never 5xxes
9
+ your app.
10
+
11
+ Not yet on npm — consumed via a `file:` dependency from a sibling checkout.
12
+
13
+ ## Quickstart
14
+
15
+ ```ts
16
+ import { camada } from '@camada/bun';
17
+
18
+ Bun.serve({
19
+ fetch: camada()((req, server) => new Response('hello')), // reads CAMADA_KEY / CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL from Bun.env
20
+ });
21
+ ```
22
+
23
+ `camada()` builds one instance and returns `wrap`; call it once and wrap every handler the
24
+ server runs with the same `wrap`. With Bun's `routes` the fallback `fetch` and each route
25
+ handler are separate entry points, so each one is wrapped:
26
+
27
+ ```ts
28
+ import { camada, scriptTag } from '@camada/bun';
29
+
30
+ const guard = camada();
31
+
32
+ Bun.serve({
33
+ routes: {
34
+ '/': guard((req) => new Response(`<html><head>${scriptTag(req)}</head><body>home</body></html>`, { headers: { 'content-type': 'text/html' } })),
35
+ '/api/health': () => new Response('ok'), // unwrapped: answers whatever the snapshot says
36
+ },
37
+ fetch: guard((req) => new Response('not found', { status: 404 })),
38
+ });
39
+ ```
40
+
41
+ Env (printed by camada onboarding / `npm run seed` in dev):
42
+
43
+ ```
44
+ CAMADA_KEY=<ingest_token>.<snap_token>
45
+ CAMADA_INGEST_URL=http://localhost:8787 # dev only; defaults to production ingest
46
+ ```
47
+
48
+ The engine is built from `Bun.env` (or `process.env` under Node) on the first request, merged
49
+ with whatever the app passes in code:
50
+
51
+ ```ts
52
+ const guard = camada({ key: MY_KEY, ingestUrl: MY_INGEST });
53
+ ```
54
+
55
+ Without `CAMADA_KEY` the wrapper is inert (one log line, no requests, no enforcement), so an
56
+ unprovisioned environment behaves exactly as if camada were not installed.
57
+
58
+ ## What it does per request
59
+
60
+ 1. Keeps the snapshot fresh on an unref'd 30 s timer (the server steers the cadence per tenant)
61
+ and drains the event queue on `beforeExit`, `SIGTERM` and `SIGINT`. `mode: 'lazy'` or
62
+ `CAMADA_SERVERLESS=1` refreshes per request instead, for a Bun that is not a long-lived
63
+ process. Every poll and event batch carries `x-camada-sdk: @camada/bun/<version>`, and polls
64
+ ask for snapshot v5 (`x-camada-snapshot: 5`) — the container that carries your ordered
65
+ custom rules.
66
+ 2. Resolves the client from the socket Bun vouches for (`server.requestIP(req)`), then
67
+ `X-Forwarded-For` under your tenant's trusted-proxy config. No header is ever trusted on its
68
+ own: without a trusted-proxy config a forwarded header is just another header the caller
69
+ controls, and `cf-connecting-ip` / `x-real-ip` are never read.
70
+ 3. Runs your ordered custom rules, then the allow, block and challenge lists.
71
+ 4. **Block** → `403` with `x-block-reason` before your handler; the event still ships, with
72
+ `st: 403` and `blk: <reason>` so the analyst counts SDK blocks apart from your own 403s.
73
+ 5. **Skip** → a skip rule or the allow list wins over a wider block.
74
+ 6. **Challenge** → a `403` proof-of-work page: no external assets, `no-store`, an inline SHA-256
75
+ solver that posts to `POST /__camada/challenge`, which sets `_cch` (an HMAC token bound to
76
+ the client ip, 1 h) and 302s back. Non-HTML requests get `403 {"error":"challenge_required"}`.
77
+ 7. Otherwise your handler runs, the `_sfp` session cookie is appended on a first visit (also
78
+ onto a `Response.redirect()`, whose headers are immutable — the response is rebuilt around
79
+ the same body), and the settled response ships one batched, redacted event with its real
80
+ status (Authorization and Cookie values never leave the process — see `@camada/core`). A
81
+ handler that throws ships the event with `st: 500` — what Bun answers for it — and the error
82
+ goes on to Bun's `error` callback (a callback that answers with another status is not seen
83
+ here); a handler that returns nothing after `server.upgrade(req)` (Bun's WebSocket pattern)
84
+ is passed through and ships `st: 101`, with no Response to carry a cookie.
85
+
86
+ ## Options
87
+
88
+ | option | default | meaning |
89
+ |---|---|---|
90
+ | `key` | `env.CAMADA_KEY` | `<ingest_token>.<snap_token>`; without it the wrapper is inert |
91
+ | `ingestUrl` | `env.CAMADA_INGEST_URL` | ingest base; batches go to `<ingestUrl>/e` |
92
+ | `snapshotUrl` | `<ingestUrl>/snapshot` | snapshot endpoint |
93
+ | `trustedProxy` | server config | `none` / `vercel` / `hops:N` / `cidrs:a,b`, or the parsed object |
94
+ | `challenge` | `true` | serve the proof-of-work page for `challenge` verdicts |
95
+ | `challengePath` | `/__camada/challenge` | where that page posts its solution |
96
+ | `snapshotVersion` | `5` | `4` drops the custom rules, `3` the allow/challenge sides too |
97
+ | `scriptPath` | `/_cam/b.js` | where the first-party beacon script is served |
98
+ | `fpPath` | `/_cam/fp` | where that script posts the beacon; keep it in `scriptPath`'s directory |
99
+ | `mode` | `timer` (or `lazy`) | `timer` polls the snapshot on an unref'd interval (long-lived process); `lazy` refreshes it per request off-path. `CAMADA_SERVERLESS=1` forces `lazy` |
100
+ | `env` | `Bun.env` | overrides the host env (tests, and apps that read config themselves) |
101
+
102
+ `CAMADA_CHALLENGE=0` in the env switches the challenge off without a code change.
103
+
104
+ `CAMADA_DISABLED=1` in the env switches everything off, checked per request.
105
+
106
+ ## The first-party beacon
107
+
108
+ Bots that never run JavaScript are the cheapest to catch. Put the tag in the `<head>` of the
109
+ pages you render and the wrapper does the rest:
110
+
111
+ ```ts
112
+ import { camada, scriptTag } from '@camada/bun';
113
+
114
+ Bun.serve({
115
+ fetch: camada()((req) => new Response(`<html><head>${scriptTag(req)}</head><body>…</body></html>`, { headers: { 'content-type': 'text/html' } })),
116
+ });
117
+ ```
118
+
119
+ `scriptTag(req)` returns `<script src="/_cam/b.js?r=<rid>" async></script>` — the `rid` is this
120
+ request's event id, so the analyst joins the beacon to the page view. `scriptTag` and `track`
121
+ find their request by the `Request` object Bun handed you — pass that one, not a `clone()`. The wrapper serves the
122
+ script at `GET /_cam/b.js` (cacheable, 1 h) and relays `POST /_cam/fp` (≤ 32 KB, answers 204)
123
+ onto the event batch as a `sig: 1` row stamped with the client ip camada resolved — never the
124
+ one the body claims. Both endpoints sit behind the verdict: a blocked client gets 403 there
125
+ too. The tag is `''` when camada is off for the request or the project turned the beacon off
126
+ in its settings, and the endpoints stand down with it.
127
+
128
+ With `routes`, the two paths are answered by whichever wrapped handler Bun dispatches to — the
129
+ fallback `fetch` in the example above — so wrap the fallback even when every route is wrapped.
130
+ Moving them (`camada({ scriptPath: '/api/_cam/b.js', fpPath: '/api/_cam/fp' })`) keeps the
131
+ same rule: the script derives the post path from its own `src`, so the two must share a directory.
132
+
133
+ ## App-context events
134
+
135
+ The wire shows a `POST /login`; only your handler knows whether it failed. Tell camada:
136
+
137
+ ```ts
138
+ import { camada, track } from '@camada/bun';
139
+
140
+ Bun.serve({
141
+ fetch: camada()(async (req) => {
142
+ const ok = await signIn(req);
143
+ if (!ok) track(req, 'login_failed', { user: email }); // await optional — it never throws
144
+ return ok ? Response.redirect('/') : new Response('Invalid credentials', { status: 401 });
145
+ }),
146
+ });
147
+ ```
148
+
149
+ `track(req, event, { user? })` ships `{ et, uid, rid, sid, ip, ts }` joined to this request's
150
+ event. The user identifier is HMAC-hashed in-process with the ingest token — the raw value never
151
+ leaves the process. It never throws and is a no-op where the wrapper did not run. The event name
152
+ is free-form; the analyst's rules read this vocabulary:
153
+
154
+ | event | when |
155
+ |---|---|
156
+ | `login_failed` / `login_succeeded` | a credential check settled |
157
+ | `signup` | an account was created |
158
+ | `password_reset` | a reset was requested |
159
+ | `mfa_failed` | a second factor was rejected |
160
+ | `payment_failed` / `payment_succeeded` | a charge settled |
161
+ | `coupon_failed` | a promo code was rejected |
162
+
163
+ ## What this tap can see
164
+
165
+ This is the in-app position: the beacon, the client hints the browser sends, the real response
166
+ status, the `_sfp` session and the app context from `track()`. Bun vouches for one connection
167
+ fact — the socket address from `server.requestIP` — and nothing else: no ASN, country or TLS
168
+ fingerprint (those rules never match here), no client protocol (the event's `proto` is null; a
169
+ forwarded protocol header is never read for it), and `Headers` normalises order, so the
170
+ raw-wire-order signal is not available either. The analyst knows all of that from the tap's
171
+ capability mask (`sdk-bun`) and never scores an absence as evidence.
172
+
173
+ ## Fail open
174
+
175
+ Every entry point runs inside camada's guard. A dead ingest, a corrupt snapshot, a bug in this
176
+ package: telemetry is lost, the request is not.
177
+
178
+ ## Tests
179
+
180
+ `npm test` runs the suite under vitest in Node with a stub `server` (the two snapshot-mode tests
181
+ use vitest's fake timers, which `bun test` does not provide).
package/dist/index.cjs ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ camada: () => camada,
34
+ resetCamada: () => resetCamada,
35
+ scriptTag: () => scriptTag,
36
+ track: () => track
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/camada.ts
41
+ var import_core = require("@camada/core");
42
+ var import_fetch = require("@camada/core/fetch");
43
+ var import_iife_string = __toESM(require("@camada/browser/iife-string"), 1);
44
+
45
+ // package.json
46
+ var name = "@camada/bun";
47
+ var version = "0.1.0";
48
+
49
+ // src/version.ts
50
+ var SDK_ID = `${name}/${version}`;
51
+
52
+ // src/camada.ts
53
+ var slots = /* @__PURE__ */ new WeakMap();
54
+ var instances = /* @__PURE__ */ new Set();
55
+ function camada(opts = {}) {
56
+ const cam = (0, import_fetch.createFetchCamada)({ tap: import_core.TAP_BUN, sdk: SDK_ID, iife: import_iife_string.default }, { ...opts, mode: opts.mode ?? "timer" });
57
+ instances.add(cam);
58
+ return function wrap(handler) {
59
+ return async (req, server) => {
60
+ const peer = (0, import_core.guarded)(() => server?.requestIP?.(req)?.address ?? null, null);
61
+ const r = await cam.before(req, { peer, env: globalThis.Bun?.env ?? globalThis.process?.env });
62
+ if (!r) return handler(req, server);
63
+ if (r.response) return r.response;
64
+ slots.set(req, r.vars);
65
+ let res;
66
+ try {
67
+ res = await handler(req, server);
68
+ } catch (err) {
69
+ cam.after(req, r.vars, 500);
70
+ throw err;
71
+ }
72
+ if (!res) {
73
+ cam.after(req, r.vars, 101);
74
+ return res;
75
+ }
76
+ cam.after(req, r.vars, res.status);
77
+ return r.vars.sessionCookie ? (0, import_fetch.withSetCookie)(res, r.vars.sessionCookie) : res;
78
+ };
79
+ };
80
+ }
81
+ var track = (req, event, data) => (0, import_fetch.track)(slots.get(req), event, data);
82
+ var scriptTag = (req) => (0, import_fetch.scriptTag)(slots.get(req));
83
+ function resetCamada() {
84
+ for (const cam of instances) cam.reset();
85
+ }
86
+ // Annotate the CommonJS export names for ESM import in node:
87
+ 0 && (module.exports = {
88
+ camada,
89
+ resetCamada,
90
+ scriptTag,
91
+ track
92
+ });
93
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/camada.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/bun — the one-line install for a Bun.serve app:\n// Bun.serve({ fetch: camada()(handler) }); // env: CAMADA_KEY (+ CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL in dev)\n// new Response(`<head>${scriptTag(req)}</head>…`, { headers: { 'content-type': 'text/html' } }) // the first-party beacon\n// track(req, 'login_failed', { user }) // an outcome the wire cannot show\nexport { camada, track, scriptTag, resetCamada, type CamadaBunOptions, type CamadaBunVars, type BunServer, type BunHandler } from './camada.js';\n","// @camada/bun — the Bun.serve binding for @camada/core/fetch. `wrap` sits around the app's fetch\n// handler: the socket peer comes from `server.requestIP` (the one address Bun vouches for), the\n// env from Bun.env, and there is no waitUntil — a long-lived process polls on a timer and drains\n// on exit instead. Bun has no per-request context object, so the Request itself keys the slot.\nimport { TAP_BUN, guarded } from '@camada/core';\nimport { createFetchCamada, withSetCookie, track as coreTrack, scriptTag as coreScriptTag, type FetchCamada, type FetchCamadaOptions, type FetchVars } from '@camada/core/fetch';\nimport iife from '@camada/browser/iife-string';\nimport { SDK_ID } from './version.js';\n\n/** The one thing the wrapper needs from Bun's server, typed structurally so the emitted types need no `bun` import; any `Bun.Server` satisfies it. */\nexport interface BunServer { requestIP(req: Request): { address: string } | null }\n/** A Bun fetch handler. `undefined`/`void` is what Bun's own websocket pattern returns after `server.upgrade(req)`. */\nexport type BunHandler<S extends BunServer = BunServer, R extends Response | undefined | void = Response> = (req: Request, server: S) => R | Promise<R>;\nexport type CamadaBunOptions = FetchCamadaOptions;\nexport type CamadaBunVars = FetchVars;\n\nconst slots = new WeakMap<Request, FetchVars>(); // set only when the app is about to run; camada's own answers leave nothing\nconst instances = new Set<FetchCamada>();\n\n/**\n * Builds one camada instance and returns `wrap`, which turns a Bun fetch handler into a guarded\n * one: `Bun.serve({ fetch: camada()(handler) })`. Timer mode by default (a server process lives\n * long enough to poll); `mode: 'lazy'` or CAMADA_SERVERLESS=1 switches to per-request refresh.\n */\nexport function camada(opts: CamadaBunOptions = {}) {\n const cam = createFetchCamada({ tap: TAP_BUN, sdk: SDK_ID, iife }, { ...opts, mode: opts.mode ?? 'timer' });\n instances.add(cam);\n return function wrap<S extends BunServer, R extends Response | undefined | void>(handler: BunHandler<S, R>): (req: Request, server: S) => Promise<Response | R> {\n return async (req, server) => {\n // Guarded: a runtime that is not Bun (or a Bun without a socket for this request) hands us\n // no peer rather than a throw. Client headers are core's to judge under the trusted-proxy rules.\n const peer = guarded(() => server?.requestIP?.(req)?.address ?? null, null);\n const r = await cam.before(req, { peer, env: globalThis.Bun?.env ?? globalThis.process?.env }); // Bun.env is process.env under Bun; the fallback is this suite under Node\n if (!r) return handler(req, server);\n if (r.response) return r.response;\n slots.set(req, r.vars);\n let res: R;\n try {\n res = await handler(req, server);\n } catch (err) {\n cam.after(req, r.vars, 500); // Bun answers 500 for a thrown handler (an `error` callback returning its own status is not visible here)\n throw err;\n }\n if (!res) { cam.after(req, r.vars, 101); return res; } // a websocket upgrade: Bun answers 101 itself, so there is no Response to carry a cookie\n cam.after(req, r.vars, res.status);\n return r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res;\n };\n };\n}\n\n/** An outcome the wire cannot show (`login_failed`, `signup`, …) joined to this request's event;\n * the user is HMAC-hashed in-process. A silent no-op where the wrapper did not run. */\nexport const track = (req: Request, event: string, data?: { user?: string }): Promise<void> => coreTrack(slots.get(req), event, data);\n\n/** The beacon `<script>` tag for an HTML response — `''` where the wrapper did not run or the tenant turned the beacon off. */\nexport const scriptTag = (req: Request): string => coreScriptTag(slots.get(req));\n\n/** Test/reset hook: stops every poller and exit handler of every instance this module created. */\nexport function resetCamada(): void {\n for (const cam of instances) cam.reset();\n}\n","{\n \"name\": \"@camada/bun\",\n \"version\": \"0.1.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"description\": \"camada SDK for Bun.serve: snapshot enforcement, first-party challenge and beacon, track() outcomes, the socket peer from server.requestIP, timer-polled snapshot with exit flush\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"dependencies\": {\n \"@camada/core\": \"file:../camada-core\",\n \"@camada/browser\": \"file:../camada-browser\"\n },\n \"devDependencies\": {\n \"@types/bun\": \"^1.2.0\",\n \"@types/node\": \"^22.10.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\n }\n}\n","// The SDK's wire identity (SDK-03): x-camada-sdk: <package>/<version>. Named JSON imports are\n// inlined and tree-shaken by tsup at build, so dist carries two literals — no runtime fs read.\nimport { name, version } from '../package.json';\nexport const SDK_ID = `${name}/${version}`;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,kBAAiC;AACjC,mBAA4J;AAC5J,yBAAiB;;;ACLf,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFaxC,IAAM,QAAQ,oBAAI,QAA4B;AAC9C,IAAM,YAAY,oBAAI,IAAiB;AAOhC,SAAS,OAAO,OAAyB,CAAC,GAAG;AAClD,QAAM,UAAM,gCAAkB,EAAE,KAAK,qBAAS,KAAK,QAAQ,yBAAAA,QAAK,GAAG,EAAE,GAAG,MAAM,MAAM,KAAK,QAAQ,QAAQ,CAAC;AAC1G,YAAU,IAAI,GAAG;AACjB,SAAO,SAAS,KAAiE,SAA+E;AAC9J,WAAO,OAAO,KAAK,WAAW;AAG5B,YAAM,WAAO,qBAAQ,MAAM,QAAQ,YAAY,GAAG,GAAG,WAAW,MAAM,IAAI;AAC1E,YAAM,IAAI,MAAM,IAAI,OAAO,KAAK,EAAE,MAAM,KAAK,WAAW,KAAK,OAAO,WAAW,SAAS,IAAI,CAAC;AAC7F,UAAI,CAAC,EAAG,QAAO,QAAQ,KAAK,MAAM;AAClC,UAAI,EAAE,SAAU,QAAO,EAAE;AACzB,YAAM,IAAI,KAAK,EAAE,IAAI;AACrB,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,QAAQ,KAAK,MAAM;AAAA,MACjC,SAAS,KAAK;AACZ,YAAI,MAAM,KAAK,EAAE,MAAM,GAAG;AAC1B,cAAM;AAAA,MACR;AACA,UAAI,CAAC,KAAK;AAAE,YAAI,MAAM,KAAK,EAAE,MAAM,GAAG;AAAG,eAAO;AAAA,MAAK;AACrD,UAAI,MAAM,KAAK,EAAE,MAAM,IAAI,MAAM;AACjC,aAAO,EAAE,KAAK,oBAAgB,4BAAc,KAAK,EAAE,KAAK,aAAa,IAAI;AAAA,IAC3E;AAAA,EACF;AACF;AAIO,IAAM,QAAQ,CAAC,KAAc,OAAe,aAA4C,aAAAC,OAAU,MAAM,IAAI,GAAG,GAAG,OAAO,IAAI;AAG7H,IAAM,YAAY,CAAC,YAAyB,aAAAC,WAAc,MAAM,IAAI,GAAG,CAAC;AAGxE,SAAS,cAAoB;AAClC,aAAW,OAAO,UAAW,KAAI,MAAM;AACzC;","names":["iife","coreTrack","coreScriptTag"]}
@@ -0,0 +1,29 @@
1
+ import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
2
+
3
+ /** The one thing the wrapper needs from Bun's server, typed structurally so the emitted types need no `bun` import; any `Bun.Server` satisfies it. */
4
+ interface BunServer {
5
+ requestIP(req: Request): {
6
+ address: string;
7
+ } | null;
8
+ }
9
+ /** A Bun fetch handler. `undefined`/`void` is what Bun's own websocket pattern returns after `server.upgrade(req)`. */
10
+ type BunHandler<S extends BunServer = BunServer, R extends Response | undefined | void = Response> = (req: Request, server: S) => R | Promise<R>;
11
+ type CamadaBunOptions = FetchCamadaOptions;
12
+ type CamadaBunVars = FetchVars;
13
+ /**
14
+ * Builds one camada instance and returns `wrap`, which turns a Bun fetch handler into a guarded
15
+ * one: `Bun.serve({ fetch: camada()(handler) })`. Timer mode by default (a server process lives
16
+ * long enough to poll); `mode: 'lazy'` or CAMADA_SERVERLESS=1 switches to per-request refresh.
17
+ */
18
+ declare function camada(opts?: CamadaBunOptions): <S extends BunServer, R extends Response | undefined | void>(handler: BunHandler<S, R>) => (req: Request, server: S) => Promise<Response | R>;
19
+ /** An outcome the wire cannot show (`login_failed`, `signup`, …) joined to this request's event;
20
+ * the user is HMAC-hashed in-process. A silent no-op where the wrapper did not run. */
21
+ declare const track: (req: Request, event: string, data?: {
22
+ user?: string;
23
+ }) => Promise<void>;
24
+ /** The beacon `<script>` tag for an HTML response — `''` where the wrapper did not run or the tenant turned the beacon off. */
25
+ declare const scriptTag: (req: Request) => string;
26
+ /** Test/reset hook: stops every poller and exit handler of every instance this module created. */
27
+ declare function resetCamada(): void;
28
+
29
+ export { type BunHandler, type BunServer, type CamadaBunOptions, type CamadaBunVars, camada, resetCamada, scriptTag, track };
@@ -0,0 +1,29 @@
1
+ import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
2
+
3
+ /** The one thing the wrapper needs from Bun's server, typed structurally so the emitted types need no `bun` import; any `Bun.Server` satisfies it. */
4
+ interface BunServer {
5
+ requestIP(req: Request): {
6
+ address: string;
7
+ } | null;
8
+ }
9
+ /** A Bun fetch handler. `undefined`/`void` is what Bun's own websocket pattern returns after `server.upgrade(req)`. */
10
+ type BunHandler<S extends BunServer = BunServer, R extends Response | undefined | void = Response> = (req: Request, server: S) => R | Promise<R>;
11
+ type CamadaBunOptions = FetchCamadaOptions;
12
+ type CamadaBunVars = FetchVars;
13
+ /**
14
+ * Builds one camada instance and returns `wrap`, which turns a Bun fetch handler into a guarded
15
+ * one: `Bun.serve({ fetch: camada()(handler) })`. Timer mode by default (a server process lives
16
+ * long enough to poll); `mode: 'lazy'` or CAMADA_SERVERLESS=1 switches to per-request refresh.
17
+ */
18
+ declare function camada(opts?: CamadaBunOptions): <S extends BunServer, R extends Response | undefined | void>(handler: BunHandler<S, R>) => (req: Request, server: S) => Promise<Response | R>;
19
+ /** An outcome the wire cannot show (`login_failed`, `signup`, …) joined to this request's event;
20
+ * the user is HMAC-hashed in-process. A silent no-op where the wrapper did not run. */
21
+ declare const track: (req: Request, event: string, data?: {
22
+ user?: string;
23
+ }) => Promise<void>;
24
+ /** The beacon `<script>` tag for an HTML response — `''` where the wrapper did not run or the tenant turned the beacon off. */
25
+ declare const scriptTag: (req: Request) => string;
26
+ /** Test/reset hook: stops every poller and exit handler of every instance this module created. */
27
+ declare function resetCamada(): void;
28
+
29
+ export { type BunHandler, type BunServer, type CamadaBunOptions, type CamadaBunVars, camada, resetCamada, scriptTag, track };
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ // src/camada.ts
2
+ import { TAP_BUN, guarded } from "@camada/core";
3
+ import { createFetchCamada, withSetCookie, track as coreTrack, scriptTag as coreScriptTag } from "@camada/core/fetch";
4
+ import iife from "@camada/browser/iife-string";
5
+
6
+ // package.json
7
+ var name = "@camada/bun";
8
+ var version = "0.1.0";
9
+
10
+ // src/version.ts
11
+ var SDK_ID = `${name}/${version}`;
12
+
13
+ // src/camada.ts
14
+ var slots = /* @__PURE__ */ new WeakMap();
15
+ var instances = /* @__PURE__ */ new Set();
16
+ function camada(opts = {}) {
17
+ const cam = createFetchCamada({ tap: TAP_BUN, sdk: SDK_ID, iife }, { ...opts, mode: opts.mode ?? "timer" });
18
+ instances.add(cam);
19
+ return function wrap(handler) {
20
+ return async (req, server) => {
21
+ const peer = guarded(() => server?.requestIP?.(req)?.address ?? null, null);
22
+ const r = await cam.before(req, { peer, env: globalThis.Bun?.env ?? globalThis.process?.env });
23
+ if (!r) return handler(req, server);
24
+ if (r.response) return r.response;
25
+ slots.set(req, r.vars);
26
+ let res;
27
+ try {
28
+ res = await handler(req, server);
29
+ } catch (err) {
30
+ cam.after(req, r.vars, 500);
31
+ throw err;
32
+ }
33
+ if (!res) {
34
+ cam.after(req, r.vars, 101);
35
+ return res;
36
+ }
37
+ cam.after(req, r.vars, res.status);
38
+ return r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res;
39
+ };
40
+ };
41
+ }
42
+ var track = (req, event, data) => coreTrack(slots.get(req), event, data);
43
+ var scriptTag = (req) => coreScriptTag(slots.get(req));
44
+ function resetCamada() {
45
+ for (const cam of instances) cam.reset();
46
+ }
47
+ export {
48
+ camada,
49
+ resetCamada,
50
+ scriptTag,
51
+ track
52
+ };
53
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/camada.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/bun — the Bun.serve binding for @camada/core/fetch. `wrap` sits around the app's fetch\n// handler: the socket peer comes from `server.requestIP` (the one address Bun vouches for), the\n// env from Bun.env, and there is no waitUntil — a long-lived process polls on a timer and drains\n// on exit instead. Bun has no per-request context object, so the Request itself keys the slot.\nimport { TAP_BUN, guarded } from '@camada/core';\nimport { createFetchCamada, withSetCookie, track as coreTrack, scriptTag as coreScriptTag, type FetchCamada, type FetchCamadaOptions, type FetchVars } from '@camada/core/fetch';\nimport iife from '@camada/browser/iife-string';\nimport { SDK_ID } from './version.js';\n\n/** The one thing the wrapper needs from Bun's server, typed structurally so the emitted types need no `bun` import; any `Bun.Server` satisfies it. */\nexport interface BunServer { requestIP(req: Request): { address: string } | null }\n/** A Bun fetch handler. `undefined`/`void` is what Bun's own websocket pattern returns after `server.upgrade(req)`. */\nexport type BunHandler<S extends BunServer = BunServer, R extends Response | undefined | void = Response> = (req: Request, server: S) => R | Promise<R>;\nexport type CamadaBunOptions = FetchCamadaOptions;\nexport type CamadaBunVars = FetchVars;\n\nconst slots = new WeakMap<Request, FetchVars>(); // set only when the app is about to run; camada's own answers leave nothing\nconst instances = new Set<FetchCamada>();\n\n/**\n * Builds one camada instance and returns `wrap`, which turns a Bun fetch handler into a guarded\n * one: `Bun.serve({ fetch: camada()(handler) })`. Timer mode by default (a server process lives\n * long enough to poll); `mode: 'lazy'` or CAMADA_SERVERLESS=1 switches to per-request refresh.\n */\nexport function camada(opts: CamadaBunOptions = {}) {\n const cam = createFetchCamada({ tap: TAP_BUN, sdk: SDK_ID, iife }, { ...opts, mode: opts.mode ?? 'timer' });\n instances.add(cam);\n return function wrap<S extends BunServer, R extends Response | undefined | void>(handler: BunHandler<S, R>): (req: Request, server: S) => Promise<Response | R> {\n return async (req, server) => {\n // Guarded: a runtime that is not Bun (or a Bun without a socket for this request) hands us\n // no peer rather than a throw. Client headers are core's to judge under the trusted-proxy rules.\n const peer = guarded(() => server?.requestIP?.(req)?.address ?? null, null);\n const r = await cam.before(req, { peer, env: globalThis.Bun?.env ?? globalThis.process?.env }); // Bun.env is process.env under Bun; the fallback is this suite under Node\n if (!r) return handler(req, server);\n if (r.response) return r.response;\n slots.set(req, r.vars);\n let res: R;\n try {\n res = await handler(req, server);\n } catch (err) {\n cam.after(req, r.vars, 500); // Bun answers 500 for a thrown handler (an `error` callback returning its own status is not visible here)\n throw err;\n }\n if (!res) { cam.after(req, r.vars, 101); return res; } // a websocket upgrade: Bun answers 101 itself, so there is no Response to carry a cookie\n cam.after(req, r.vars, res.status);\n return r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res;\n };\n };\n}\n\n/** An outcome the wire cannot show (`login_failed`, `signup`, …) joined to this request's event;\n * the user is HMAC-hashed in-process. A silent no-op where the wrapper did not run. */\nexport const track = (req: Request, event: string, data?: { user?: string }): Promise<void> => coreTrack(slots.get(req), event, data);\n\n/** The beacon `<script>` tag for an HTML response — `''` where the wrapper did not run or the tenant turned the beacon off. */\nexport const scriptTag = (req: Request): string => coreScriptTag(slots.get(req));\n\n/** Test/reset hook: stops every poller and exit handler of every instance this module created. */\nexport function resetCamada(): void {\n for (const cam of instances) cam.reset();\n}\n","{\n \"name\": \"@camada/bun\",\n \"version\": \"0.1.0\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"check\": \"tsc --noEmit\"\n },\n \"description\": \"camada SDK for Bun.serve: snapshot enforcement, first-party challenge and beacon, track() outcomes, the socket peer from server.requestIP, timer-polled snapshot with exit flush\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n }\n },\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"dependencies\": {\n \"@camada/core\": \"file:../camada-core\",\n \"@camada/browser\": \"file:../camada-browser\"\n },\n \"devDependencies\": {\n \"@types/bun\": \"^1.2.0\",\n \"@types/node\": \"^22.10.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vitest\": \"^3.0.0\"\n }\n}\n","// The SDK's wire identity (SDK-03): x-camada-sdk: <package>/<version>. Named JSON imports are\n// inlined and tree-shaken by tsup at build, so dist carries two literals — no runtime fs read.\nimport { name, version } from '../package.json';\nexport const SDK_ID = `${name}/${version}`;\n"],"mappings":";AAIA,SAAS,SAAS,eAAe;AACjC,SAAS,mBAAmB,eAAe,SAAS,WAAW,aAAa,qBAAgF;AAC5J,OAAO,UAAU;;;ACLf,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFaxC,IAAM,QAAQ,oBAAI,QAA4B;AAC9C,IAAM,YAAY,oBAAI,IAAiB;AAOhC,SAAS,OAAO,OAAyB,CAAC,GAAG;AAClD,QAAM,MAAM,kBAAkB,EAAE,KAAK,SAAS,KAAK,QAAQ,KAAK,GAAG,EAAE,GAAG,MAAM,MAAM,KAAK,QAAQ,QAAQ,CAAC;AAC1G,YAAU,IAAI,GAAG;AACjB,SAAO,SAAS,KAAiE,SAA+E;AAC9J,WAAO,OAAO,KAAK,WAAW;AAG5B,YAAM,OAAO,QAAQ,MAAM,QAAQ,YAAY,GAAG,GAAG,WAAW,MAAM,IAAI;AAC1E,YAAM,IAAI,MAAM,IAAI,OAAO,KAAK,EAAE,MAAM,KAAK,WAAW,KAAK,OAAO,WAAW,SAAS,IAAI,CAAC;AAC7F,UAAI,CAAC,EAAG,QAAO,QAAQ,KAAK,MAAM;AAClC,UAAI,EAAE,SAAU,QAAO,EAAE;AACzB,YAAM,IAAI,KAAK,EAAE,IAAI;AACrB,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,QAAQ,KAAK,MAAM;AAAA,MACjC,SAAS,KAAK;AACZ,YAAI,MAAM,KAAK,EAAE,MAAM,GAAG;AAC1B,cAAM;AAAA,MACR;AACA,UAAI,CAAC,KAAK;AAAE,YAAI,MAAM,KAAK,EAAE,MAAM,GAAG;AAAG,eAAO;AAAA,MAAK;AACrD,UAAI,MAAM,KAAK,EAAE,MAAM,IAAI,MAAM;AACjC,aAAO,EAAE,KAAK,gBAAgB,cAAc,KAAK,EAAE,KAAK,aAAa,IAAI;AAAA,IAC3E;AAAA,EACF;AACF;AAIO,IAAM,QAAQ,CAAC,KAAc,OAAe,SAA4C,UAAU,MAAM,IAAI,GAAG,GAAG,OAAO,IAAI;AAG7H,IAAM,YAAY,CAAC,QAAyB,cAAc,MAAM,IAAI,GAAG,CAAC;AAGxE,SAAS,cAAoB;AAClC,aAAW,OAAO,UAAW,KAAI,MAAM;AACzC;","names":[]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@camada/bun",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "description": "camada SDK for Bun.serve: snapshot enforcement, first-party challenge and beacon, track() outcomes, the socket peer from server.requestIP, timer-polled snapshot with exit flush",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "dependencies": {
26
+ "@camada/core": "^0.3.0",
27
+ "@camada/browser": "^0.2.0"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/camada-app/camada-bun.git"
32
+ },
33
+ "homepage": "https://github.com/camada-app/camada-bun#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/camada-app/camada-bun/issues"
36
+ }
37
+ }