@camada/deno 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,155 @@
1
+ # @camada/deno
2
+
3
+ camada for [Deno](https://deno.com) `Deno.serve`: 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 handler knows (`track()`), and ships wire events in
6
+ batches off the response path. A thin binding over `@camada/core/fetch`, the pipeline every
7
+ Web-fetch adapter shares. Fails open by design — a camada outage or bug never 5xxes your app.
8
+
9
+ Not yet on npm — consumed via a `file:` dependency from a sibling checkout. Built with tsup like
10
+ the rest of the workspace; there is no `deno.json` this round. From Deno, once published it is
11
+ `npm:@camada/deno`; locally, an import-map entry pointing at the sibling build works:
12
+
13
+ ```json
14
+ { "imports": { "@camada/deno": "../camada-deno/dist/index.js" } }
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```ts
20
+ import { camada } from '@camada/deno';
21
+
22
+ Deno.serve(camada()((req, info) => new Response('hello')));
23
+ ```
24
+
25
+ Env (printed by camada onboarding / `npm run seed` in dev), read key by key through
26
+ `Deno.env.get` — run with `--allow-env`, or the granular
27
+ `--allow-env=CAMADA_KEY,CAMADA_INGEST_URL,CAMADA_SNAPSHOT_URL` (a key outside the grant simply
28
+ reads as unset):
29
+
30
+ ```
31
+ CAMADA_KEY=<ingest_token>.<snap_token>
32
+ CAMADA_INGEST_URL=http://localhost:8787 # dev only; defaults to production ingest
33
+ ```
34
+
35
+ `camada()` reads the env on each request and builds its engine on the first configured one; a
36
+ denied env read (no `--allow-env` at all) is caught, logged as denied at most once a minute, and
37
+ leaves the wrapper inert — the app still answers. An app that reads its own config can pass the values instead:
38
+
39
+ ```ts
40
+ Deno.serve(camada({ key: MY_KEY, ingestUrl: MY_INGEST })(handler));
41
+ ```
42
+
43
+ Call `camada()` once and reuse the wrapper. Engines are cached per resolved configuration, so
44
+ wrapping two handlers with the same config shares one snapshot poller and one event queue,
45
+ while two wrappers with different keys or URLs each get their own — a process hosting several
46
+ apps never enforces one tenant's snapshot on another, nor signs its cookies with another's
47
+ secret. Without `CAMADA_KEY` the wrapper is inert (one log line, no requests, no enforcement).
48
+
49
+ ## What it does per request
50
+
51
+ 1. Keeps the snapshot fresh. Deno is a long-lived process, so the default is `mode: 'timer'`: an
52
+ interval poll at the cadence your tenant config sets. `CAMADA_SERVERLESS=1` (or
53
+ `mode: 'lazy'`) switches to a per-request staleness check for Deno Deploy, where an isolate
54
+ may be frozen between requests. Every poll and event batch carries
55
+ `x-camada-sdk: @camada/deno/<version>`, and polls ask for snapshot v5
56
+ (`x-camada-snapshot: 5`) — the container that carries your ordered custom rules.
57
+ 2. Resolves the client from `info.remoteAddr` — the TCP peer Deno vouches for — combined with
58
+ `X-Forwarded-For` only under your tenant's trusted-proxy config. A client header on its own
59
+ (`x-forwarded-for`, `x-real-ip`, `cf-connecting-ip`) is never the ip: any caller can set it.
60
+ Over a unix socket there is no peer, so the ip is null: the request is still captured, but
61
+ ip rules cannot match it and it is never challenged.
62
+ 3. Runs your ordered custom rules, then the allow, block and challenge lists.
63
+ 4. **Block** → `403` with `x-block-reason` before your handler; the event still ships, with
64
+ `st: 403` and `blk: <reason>` so the analyst counts SDK blocks apart from your own 403s.
65
+ 5. **Skip** → a skip rule or the allow list wins over a wider block.
66
+ 6. **Challenge** → a `403` proof-of-work page (HTML navigations) or `403 {"error":"challenge_required"}`
67
+ (everything else), verified at `POST /__camada/challenge`, which sets `_cch` and 302s back.
68
+ 7. Otherwise your handler runs; the settled response ships one batched, redacted event with its
69
+ real status. A handler that throws ships `st: 500` — what `Deno.serve` answers for it — and
70
+ rethrows (an `onError` answering with another status is not seen here). A first visit is given the `_sfp` session
71
+ cookie (appended even to a `Response.redirect()`, whose headers are immutable).
72
+
73
+ ## Options
74
+
75
+ | option | default | meaning |
76
+ |---|---|---|
77
+ | `key` | `env.CAMADA_KEY` | `<ingest_token>.<snap_token>`; without it the wrapper is inert |
78
+ | `ingestUrl` | `env.CAMADA_INGEST_URL` | ingest base; batches go to `<ingestUrl>/e` |
79
+ | `snapshotUrl` | `<ingestUrl>/snapshot` | snapshot endpoint |
80
+ | `trustedProxy` | server config | `none` / `vercel` / `hops:N` / `cidrs:a,b`, or the parsed object |
81
+ | `challenge` | `true` | serve the proof-of-work page for `challenge` verdicts |
82
+ | `challengePath` | `/__camada/challenge` | where that page posts its solution |
83
+ | `snapshotVersion` | `5` | `4` drops the custom rules, `3` the allow/challenge sides too |
84
+ | `scriptPath` | `/_cam/b.js` | where the first-party beacon script is served |
85
+ | `fpPath` | `/_cam/fp` | where that script posts the beacon; keep it in `scriptPath`'s directory |
86
+ | `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` |
87
+ | `env` | `Deno.env` | overrides the process env (tests, and apps that read config themselves) |
88
+
89
+ `CAMADA_CHALLENGE=0` in the env switches the challenge off without a code change.
90
+
91
+ `CAMADA_DISABLED=1` in the env switches everything off, checked per request.
92
+
93
+ ## The first-party beacon
94
+
95
+ Bots that never run JavaScript are the cheapest to catch. Put the tag in the `<head>` of the
96
+ pages you render and the wrapper does the rest:
97
+
98
+ ```ts
99
+ import { camada, scriptTag } from '@camada/deno';
100
+
101
+ Deno.serve(camada()((req) =>
102
+ new Response(`<html><head>${scriptTag(req)}</head><body>…</body></html>`, { headers: { 'content-type': 'text/html' } })));
103
+ ```
104
+
105
+ `scriptTag(req)` returns `<script src="/_cam/b.js?r=<rid>" async></script>` — the `rid` is this
106
+ request's event id, so the analyst joins the beacon to the page view. The wrapper serves the
107
+ script at `GET /_cam/b.js` (cacheable, 1 h) and relays `POST /_cam/fp` (≤ 32 KB, answers 204)
108
+ onto the event batch as a `sig: 1` row stamped with the client ip camada resolved — never the
109
+ one the body claims. Both endpoints sit behind the verdict: a blocked client gets 403 there
110
+ too. The tag is `''` when camada is off for the request or the project turned the beacon off
111
+ in its settings, and the endpoints stand down with it. `scriptTag` and `track` find their
112
+ request by the `Request` object `Deno.serve` handed you, so pass that one, not a clone.
113
+
114
+ ## App-context events
115
+
116
+ The wire shows a `POST /login`; only your handler knows whether it failed. Tell camada:
117
+
118
+ ```ts
119
+ import { camada, track } from '@camada/deno';
120
+
121
+ Deno.serve(camada()(async (req) => {
122
+ const ok = await signIn(req);
123
+ if (!ok) track(req, 'login_failed', { user: email }); // await optional — the flush is off-path
124
+ return ok ? Response.redirect('/', 302) : new Response('Invalid credentials', { status: 401 });
125
+ }));
126
+ ```
127
+
128
+ `track(req, event, { user? })` ships `{ et, uid, rid, sid, ip, ts }` joined to this request's event.
129
+ The user identifier is HMAC-hashed in-process with the ingest token — the raw value never leaves
130
+ the process. It never throws and is a no-op where the wrapper did not run. The event name is
131
+ free-form; the analyst's rules read this vocabulary:
132
+
133
+ | event | when |
134
+ |---|---|
135
+ | `login_failed` / `login_succeeded` | a credential check settled |
136
+ | `signup` | an account was created |
137
+ | `password_reset` | a reset was requested |
138
+ | `mfa_failed` | a second factor was rejected |
139
+ | `payment_failed` / `payment_succeeded` | a charge settled |
140
+ | `coupon_failed` | a promo code was rejected |
141
+
142
+ ## What this tap can see
143
+
144
+ The in-app position: the beacon, the client hints and headers, the settled status, the `_sfp`
145
+ session and the app context from `track()`. Deno vouches for the socket peer (`info.remoteAddr`)
146
+ and nothing else about the connection: no ASN, no country, no TLS fingerprint and no client
147
+ protocol reach this tap, so `asn`, `country` and `tlsx` conditions cannot match here and `proto`
148
+ is null on the event (camada never reads a forwarded protocol header for it). Header order is normalised by the `Headers` object, so the raw-wire-order signal is not
149
+ available either. The analyst knows all of this from the tap's capability mask (`sdk-deno`) and
150
+ never scores an absence as evidence.
151
+
152
+ ## Fail open
153
+
154
+ Every entry point runs inside camada's guard. A dead ingest, a corrupt snapshot, a denied env
155
+ read, a bug in this package: telemetry is lost, the request is not.
package/dist/index.cjs ADDED
@@ -0,0 +1,103 @@
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_iife_string = __toESM(require("@camada/browser/iife-string"), 1);
42
+ var import_core = require("@camada/core");
43
+ var import_fetch = require("@camada/core/fetch");
44
+
45
+ // package.json
46
+ var name = "@camada/deno";
47
+ var version = "0.1.0";
48
+
49
+ // src/version.ts
50
+ var SDK_ID = `${name}/${version}`;
51
+
52
+ // src/camada.ts
53
+ var instances = /* @__PURE__ */ new Set();
54
+ var slots = /* @__PURE__ */ new WeakMap();
55
+ var ENV_KEYS = ["CAMADA_KEY", "CAMADA_TOKEN", "CAMADA_SNAPSHOT_TOKEN", "CAMADA_INGEST_URL", "CAMADA_SNAPSHOT_URL", "CAMADA_TRUSTED_PROXY", "CAMADA_CHALLENGE", "CAMADA_DISABLED", "CAMADA_SERVERLESS"];
56
+ function hostEnv() {
57
+ const deno = globalThis.Deno;
58
+ if (!deno) return globalThis.process?.env;
59
+ const env = {};
60
+ let denied = 0;
61
+ for (const k of ENV_KEYS) {
62
+ try {
63
+ env[k] = deno.env.get(k);
64
+ } catch {
65
+ denied++;
66
+ }
67
+ }
68
+ if (denied === ENV_KEYS.length) (0, import_core.logRateLimited)(new Error("Deno.env access denied \u2014 run with --allow-env (or --allow-env=CAMADA_KEY,CAMADA_INGEST_URL,CAMADA_SNAPSHOT_URL); camada is inactive"));
69
+ return env;
70
+ }
71
+ var peerOf = (info) => info?.remoteAddr?.transport === "tcp" ? info.remoteAddr.hostname ?? null : null;
72
+ function camada(opts = {}) {
73
+ const cam = (0, import_fetch.createFetchCamada)({ tap: import_core.TAP_DENO, sdk: SDK_ID, iife: import_iife_string.default }, { ...opts, mode: opts.mode ?? "timer" });
74
+ instances.add(cam);
75
+ return (handler) => async (req, info) => {
76
+ const r = await cam.before(req, { peer: peerOf(info), env: hostEnv() });
77
+ if (!r) return handler(req, info);
78
+ if (r.response) return r.response;
79
+ slots.set(req, r.vars);
80
+ let res;
81
+ try {
82
+ res = await handler(req, info);
83
+ } catch (err) {
84
+ cam.after(req, r.vars, 500);
85
+ throw err;
86
+ }
87
+ cam.after(req, r.vars, res.status);
88
+ return r.vars.sessionCookie ? (0, import_fetch.withSetCookie)(res, r.vars.sessionCookie) : res;
89
+ };
90
+ }
91
+ var track = (req, event, data) => (0, import_fetch.track)(slots.get(req), event, data);
92
+ var scriptTag = (req) => (0, import_fetch.scriptTag)(slots.get(req));
93
+ function resetCamada() {
94
+ for (const cam of instances) cam.reset();
95
+ }
96
+ // Annotate the CommonJS export names for ESM import in node:
97
+ 0 && (module.exports = {
98
+ camada,
99
+ resetCamada,
100
+ scriptTag,
101
+ track
102
+ });
103
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/camada.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/deno — the one-line install for a Deno.serve app:\n// Deno.serve(camada()(handler)); // env: CAMADA_KEY (+ CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL in dev)\n// new Response(`<head>${scriptTag(req)}</head>…`) // the first-party beacon\n// track(req, 'login_failed', { user }) // an outcome the wire cannot show\nexport {\n camada, track, scriptTag, resetCamada,\n type CamadaDenoOptions, type CamadaDenoVars, type ServeHandlerInfo, type ServeHandler, type WrappedHandler,\n} from './camada.js';\n","// @camada/deno — the Deno.serve wrapper over @camada/core/fetch (SDK-G04).\n// Deno.serve(camada()(handler));\n// The runtime vouches for exactly one thing here: `info.remoteAddr`, the TCP socket peer. It goes\n// in as `ctx.peer` and core combines it with X-Forwarded-For under the trusted-proxy rules; a\n// client header on its own never becomes the ip. Deno is a long-lived process, so the snapshot\n// polls on a timer by default (CAMADA_SERVERLESS=1 or `mode: 'lazy'` for Deno Deploy), and there\n// is no `waitUntil`: the event flush is a fire-and-forget promise the process keeps alive.\nimport iife from '@camada/browser/iife-string';\nimport { TAP_DENO, logRateLimited } from '@camada/core';\nimport { createFetchCamada, withSetCookie, track as coreTrack, scriptTag as coreScriptTag, type FetchCamada, type FetchCamadaOptions, type FetchVars } from '@camada/core/fetch';\nimport { SDK_ID } from './version.js';\n\nexport type CamadaDenoOptions = FetchCamadaOptions;\nexport type CamadaDenoVars = FetchVars;\n\n/** The second argument Deno.serve hands a handler, typed structurally so this package needs no Deno types. */\nexport interface ServeHandlerInfo {\n remoteAddr: { transport: 'tcp' | 'udp' | 'unix' | 'unixpacket'; hostname?: string; port?: number };\n}\nexport type ServeHandler = (req: Request, info: ServeHandlerInfo) => Response | Promise<Response>;\nexport type WrappedHandler = (req: Request, info: ServeHandlerInfo) => Promise<Response>;\n\ntype DenoGlobal = { env: { get(name: string): string | undefined } };\ntype Env = Record<string, string | undefined>;\n\n// Every camada() call owns one pipeline; resetCamada() must reach all of them.\nconst instances = new Set<FetchCamada>();\n// The per-request slot: Deno hands the same Request object to the handler, so track() and\n// scriptTag() find their vars by it without the app threading anything through.\nconst slots = new WeakMap<Request, FetchVars>();\n\n/** Every key core reads, fetched one by one: `Deno.env.get` works under a granular\n * `--allow-env=CAMADA_KEY,…` grant, where `toObject()` (and enumerating `process.env`) throws. */\nconst ENV_KEYS = ['CAMADA_KEY', 'CAMADA_TOKEN', 'CAMADA_SNAPSHOT_TOKEN', 'CAMADA_INGEST_URL', 'CAMADA_SNAPSHOT_URL', 'CAMADA_TRUSTED_PROXY', 'CAMADA_CHALLENGE', 'CAMADA_DISABLED', 'CAMADA_SERVERLESS'];\n\n/** Deno.env where there is a Deno (a Node test process has only process.env). A key the grant does\n * not cover throws NotCapable; that costs enforcement, never the response, and is logged as what it is. */\nfunction hostEnv(): Env | undefined {\n const deno = (globalThis as { Deno?: DenoGlobal }).Deno;\n if (!deno) return globalThis.process?.env;\n const env: Env = {};\n let denied = 0;\n for (const k of ENV_KEYS) {\n try { env[k] = deno.env.get(k); } catch { denied++; }\n }\n if (denied === ENV_KEYS.length) logRateLimited(new Error('Deno.env access denied — run with --allow-env (or --allow-env=CAMADA_KEY,CAMADA_INGEST_URL,CAMADA_SNAPSHOT_URL); camada is inactive'));\n return env;\n}\n\n/** Only a TCP peer is an address; a unix socket has none, and camada then sees no client ip. */\nconst peerOf = (info: ServeHandlerInfo | undefined): string | null =>\n info?.remoteAddr?.transport === 'tcp' ? info.remoteAddr.hostname ?? null : null;\n\n/**\n * Builds the wrapper: `Deno.serve(camada()(handler))`. Mode defaults to `timer` (a long-lived\n * process polls the snapshot on an unref'd interval); `opts.mode` overrides and CAMADA_SERVERLESS=1\n * forces lazy. Deno's setInterval returns a number, so that unref is a no-op there and the poll\n * keeps the process alive until resetCamada() or exit — under Node (vitest) resetCamada() is what\n * lets a test process end.\n */\nexport function camada(opts: CamadaDenoOptions = {}): (handler: ServeHandler) => WrappedHandler {\n const cam = createFetchCamada({ tap: TAP_DENO, sdk: SDK_ID, iife }, { ...opts, mode: opts.mode ?? 'timer' });\n instances.add(cam);\n return (handler) => async (req, info) => {\n const r = await cam.before(req, { peer: peerOf(info), env: hostEnv() });\n if (!r) return handler(req, info);\n if (r.response) return r.response;\n slots.set(req, r.vars);\n let res: Response;\n try {\n res = await handler(req, info);\n } catch (err) {\n cam.after(req, r.vars, 500); // Deno.serve answers 500 for a thrown handler (an onError returning its own status is not visible here)\n throw err;\n }\n cam.after(req, r.vars, res.status);\n return r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res;\n };\n}\n\n/** Records an outcome the handler knows (`login_failed`, `signup`, ...), joined to this request's\n * event. Never throws; a silent no-op where the wrapper did not run. See @camada/core/fetch. */\nexport const track = (req: Request, event: string, data?: { user?: string }): Promise<void> => coreTrack(slots.get(req), event, data);\n\n/** The `<script>` tag for an HTML response; `''` where the wrapper did not run or the beacon is off. */\nexport const scriptTag = (req: Request): string => coreScriptTag(slots.get(req));\n\n/** Test/reset hook: stops every poller and queue this module created and drops their engines; the wrappers stay wired and rebuild lazily. */\nexport function resetCamada(): void {\n for (const cam of instances) cam.reset();\n}\n","{\n \"name\": \"@camada/deno\",\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 Deno.serve: snapshot enforcement, first-party challenge and beacon, track() outcomes, timer-polled snapshot and batched event shipping\",\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/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;;;ACOA,yBAAiB;AACjB,kBAAyC;AACzC,mBAA4J;;;ACR1J,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFuBxC,IAAM,YAAY,oBAAI,IAAiB;AAGvC,IAAM,QAAQ,oBAAI,QAA4B;AAI9C,IAAM,WAAW,CAAC,cAAc,gBAAgB,yBAAyB,qBAAqB,uBAAuB,wBAAwB,oBAAoB,mBAAmB,mBAAmB;AAIvM,SAAS,UAA2B;AAClC,QAAM,OAAQ,WAAqC;AACnD,MAAI,CAAC,KAAM,QAAO,WAAW,SAAS;AACtC,QAAM,MAAW,CAAC;AAClB,MAAI,SAAS;AACb,aAAW,KAAK,UAAU;AACxB,QAAI;AAAE,UAAI,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC;AAAA,IAAG,QAAQ;AAAE;AAAA,IAAU;AAAA,EACtD;AACA,MAAI,WAAW,SAAS,OAAQ,iCAAe,IAAI,MAAM,0IAAqI,CAAC;AAC/L,SAAO;AACT;AAGA,IAAM,SAAS,CAAC,SACd,MAAM,YAAY,cAAc,QAAQ,KAAK,WAAW,YAAY,OAAO;AAStE,SAAS,OAAO,OAA0B,CAAC,GAA8C;AAC9F,QAAM,UAAM,gCAAkB,EAAE,KAAK,sBAAU,KAAK,QAAQ,yBAAAA,QAAK,GAAG,EAAE,GAAG,MAAM,MAAM,KAAK,QAAQ,QAAQ,CAAC;AAC3G,YAAU,IAAI,GAAG;AACjB,SAAO,CAAC,YAAY,OAAO,KAAK,SAAS;AACvC,UAAM,IAAI,MAAM,IAAI,OAAO,KAAK,EAAE,MAAM,OAAO,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;AACtE,QAAI,CAAC,EAAG,QAAO,QAAQ,KAAK,IAAI;AAChC,QAAI,EAAE,SAAU,QAAO,EAAE;AACzB,UAAM,IAAI,KAAK,EAAE,IAAI;AACrB,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,QAAQ,KAAK,IAAI;AAAA,IAC/B,SAAS,KAAK;AACZ,UAAI,MAAM,KAAK,EAAE,MAAM,GAAG;AAC1B,YAAM;AAAA,IACR;AACA,QAAI,MAAM,KAAK,EAAE,MAAM,IAAI,MAAM;AACjC,WAAO,EAAE,KAAK,oBAAgB,4BAAc,KAAK,EAAE,KAAK,aAAa,IAAI;AAAA,EAC3E;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,33 @@
1
+ import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
2
+
3
+ type CamadaDenoOptions = FetchCamadaOptions;
4
+ type CamadaDenoVars = FetchVars;
5
+ /** The second argument Deno.serve hands a handler, typed structurally so this package needs no Deno types. */
6
+ interface ServeHandlerInfo {
7
+ remoteAddr: {
8
+ transport: 'tcp' | 'udp' | 'unix' | 'unixpacket';
9
+ hostname?: string;
10
+ port?: number;
11
+ };
12
+ }
13
+ type ServeHandler = (req: Request, info: ServeHandlerInfo) => Response | Promise<Response>;
14
+ type WrappedHandler = (req: Request, info: ServeHandlerInfo) => Promise<Response>;
15
+ /**
16
+ * Builds the wrapper: `Deno.serve(camada()(handler))`. Mode defaults to `timer` (a long-lived
17
+ * process polls the snapshot on an unref'd interval); `opts.mode` overrides and CAMADA_SERVERLESS=1
18
+ * forces lazy. Deno's setInterval returns a number, so that unref is a no-op there and the poll
19
+ * keeps the process alive until resetCamada() or exit — under Node (vitest) resetCamada() is what
20
+ * lets a test process end.
21
+ */
22
+ declare function camada(opts?: CamadaDenoOptions): (handler: ServeHandler) => WrappedHandler;
23
+ /** Records an outcome the handler knows (`login_failed`, `signup`, ...), joined to this request's
24
+ * event. Never throws; a silent no-op where the wrapper did not run. See @camada/core/fetch. */
25
+ declare const track: (req: Request, event: string, data?: {
26
+ user?: string;
27
+ }) => Promise<void>;
28
+ /** The `<script>` tag for an HTML response; `''` where the wrapper did not run or the beacon is off. */
29
+ declare const scriptTag: (req: Request) => string;
30
+ /** Test/reset hook: stops every poller and queue this module created and drops their engines; the wrappers stay wired and rebuild lazily. */
31
+ declare function resetCamada(): void;
32
+
33
+ export { type CamadaDenoOptions, type CamadaDenoVars, type ServeHandler, type ServeHandlerInfo, type WrappedHandler, camada, resetCamada, scriptTag, track };
@@ -0,0 +1,33 @@
1
+ import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
2
+
3
+ type CamadaDenoOptions = FetchCamadaOptions;
4
+ type CamadaDenoVars = FetchVars;
5
+ /** The second argument Deno.serve hands a handler, typed structurally so this package needs no Deno types. */
6
+ interface ServeHandlerInfo {
7
+ remoteAddr: {
8
+ transport: 'tcp' | 'udp' | 'unix' | 'unixpacket';
9
+ hostname?: string;
10
+ port?: number;
11
+ };
12
+ }
13
+ type ServeHandler = (req: Request, info: ServeHandlerInfo) => Response | Promise<Response>;
14
+ type WrappedHandler = (req: Request, info: ServeHandlerInfo) => Promise<Response>;
15
+ /**
16
+ * Builds the wrapper: `Deno.serve(camada()(handler))`. Mode defaults to `timer` (a long-lived
17
+ * process polls the snapshot on an unref'd interval); `opts.mode` overrides and CAMADA_SERVERLESS=1
18
+ * forces lazy. Deno's setInterval returns a number, so that unref is a no-op there and the poll
19
+ * keeps the process alive until resetCamada() or exit — under Node (vitest) resetCamada() is what
20
+ * lets a test process end.
21
+ */
22
+ declare function camada(opts?: CamadaDenoOptions): (handler: ServeHandler) => WrappedHandler;
23
+ /** Records an outcome the handler knows (`login_failed`, `signup`, ...), joined to this request's
24
+ * event. Never throws; a silent no-op where the wrapper did not run. See @camada/core/fetch. */
25
+ declare const track: (req: Request, event: string, data?: {
26
+ user?: string;
27
+ }) => Promise<void>;
28
+ /** The `<script>` tag for an HTML response; `''` where the wrapper did not run or the beacon is off. */
29
+ declare const scriptTag: (req: Request) => string;
30
+ /** Test/reset hook: stops every poller and queue this module created and drops their engines; the wrappers stay wired and rebuild lazily. */
31
+ declare function resetCamada(): void;
32
+
33
+ export { type CamadaDenoOptions, type CamadaDenoVars, type ServeHandler, type ServeHandlerInfo, type WrappedHandler, camada, resetCamada, scriptTag, track };
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ // src/camada.ts
2
+ import iife from "@camada/browser/iife-string";
3
+ import { TAP_DENO, logRateLimited } from "@camada/core";
4
+ import { createFetchCamada, withSetCookie, track as coreTrack, scriptTag as coreScriptTag } from "@camada/core/fetch";
5
+
6
+ // package.json
7
+ var name = "@camada/deno";
8
+ var version = "0.1.0";
9
+
10
+ // src/version.ts
11
+ var SDK_ID = `${name}/${version}`;
12
+
13
+ // src/camada.ts
14
+ var instances = /* @__PURE__ */ new Set();
15
+ var slots = /* @__PURE__ */ new WeakMap();
16
+ var ENV_KEYS = ["CAMADA_KEY", "CAMADA_TOKEN", "CAMADA_SNAPSHOT_TOKEN", "CAMADA_INGEST_URL", "CAMADA_SNAPSHOT_URL", "CAMADA_TRUSTED_PROXY", "CAMADA_CHALLENGE", "CAMADA_DISABLED", "CAMADA_SERVERLESS"];
17
+ function hostEnv() {
18
+ const deno = globalThis.Deno;
19
+ if (!deno) return globalThis.process?.env;
20
+ const env = {};
21
+ let denied = 0;
22
+ for (const k of ENV_KEYS) {
23
+ try {
24
+ env[k] = deno.env.get(k);
25
+ } catch {
26
+ denied++;
27
+ }
28
+ }
29
+ if (denied === ENV_KEYS.length) logRateLimited(new Error("Deno.env access denied \u2014 run with --allow-env (or --allow-env=CAMADA_KEY,CAMADA_INGEST_URL,CAMADA_SNAPSHOT_URL); camada is inactive"));
30
+ return env;
31
+ }
32
+ var peerOf = (info) => info?.remoteAddr?.transport === "tcp" ? info.remoteAddr.hostname ?? null : null;
33
+ function camada(opts = {}) {
34
+ const cam = createFetchCamada({ tap: TAP_DENO, sdk: SDK_ID, iife }, { ...opts, mode: opts.mode ?? "timer" });
35
+ instances.add(cam);
36
+ return (handler) => async (req, info) => {
37
+ const r = await cam.before(req, { peer: peerOf(info), env: hostEnv() });
38
+ if (!r) return handler(req, info);
39
+ if (r.response) return r.response;
40
+ slots.set(req, r.vars);
41
+ let res;
42
+ try {
43
+ res = await handler(req, info);
44
+ } catch (err) {
45
+ cam.after(req, r.vars, 500);
46
+ throw err;
47
+ }
48
+ cam.after(req, r.vars, res.status);
49
+ return r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res;
50
+ };
51
+ }
52
+ var track = (req, event, data) => coreTrack(slots.get(req), event, data);
53
+ var scriptTag = (req) => coreScriptTag(slots.get(req));
54
+ function resetCamada() {
55
+ for (const cam of instances) cam.reset();
56
+ }
57
+ export {
58
+ camada,
59
+ resetCamada,
60
+ scriptTag,
61
+ track
62
+ };
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/camada.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/deno — the Deno.serve wrapper over @camada/core/fetch (SDK-G04).\n// Deno.serve(camada()(handler));\n// The runtime vouches for exactly one thing here: `info.remoteAddr`, the TCP socket peer. It goes\n// in as `ctx.peer` and core combines it with X-Forwarded-For under the trusted-proxy rules; a\n// client header on its own never becomes the ip. Deno is a long-lived process, so the snapshot\n// polls on a timer by default (CAMADA_SERVERLESS=1 or `mode: 'lazy'` for Deno Deploy), and there\n// is no `waitUntil`: the event flush is a fire-and-forget promise the process keeps alive.\nimport iife from '@camada/browser/iife-string';\nimport { TAP_DENO, logRateLimited } from '@camada/core';\nimport { createFetchCamada, withSetCookie, track as coreTrack, scriptTag as coreScriptTag, type FetchCamada, type FetchCamadaOptions, type FetchVars } from '@camada/core/fetch';\nimport { SDK_ID } from './version.js';\n\nexport type CamadaDenoOptions = FetchCamadaOptions;\nexport type CamadaDenoVars = FetchVars;\n\n/** The second argument Deno.serve hands a handler, typed structurally so this package needs no Deno types. */\nexport interface ServeHandlerInfo {\n remoteAddr: { transport: 'tcp' | 'udp' | 'unix' | 'unixpacket'; hostname?: string; port?: number };\n}\nexport type ServeHandler = (req: Request, info: ServeHandlerInfo) => Response | Promise<Response>;\nexport type WrappedHandler = (req: Request, info: ServeHandlerInfo) => Promise<Response>;\n\ntype DenoGlobal = { env: { get(name: string): string | undefined } };\ntype Env = Record<string, string | undefined>;\n\n// Every camada() call owns one pipeline; resetCamada() must reach all of them.\nconst instances = new Set<FetchCamada>();\n// The per-request slot: Deno hands the same Request object to the handler, so track() and\n// scriptTag() find their vars by it without the app threading anything through.\nconst slots = new WeakMap<Request, FetchVars>();\n\n/** Every key core reads, fetched one by one: `Deno.env.get` works under a granular\n * `--allow-env=CAMADA_KEY,…` grant, where `toObject()` (and enumerating `process.env`) throws. */\nconst ENV_KEYS = ['CAMADA_KEY', 'CAMADA_TOKEN', 'CAMADA_SNAPSHOT_TOKEN', 'CAMADA_INGEST_URL', 'CAMADA_SNAPSHOT_URL', 'CAMADA_TRUSTED_PROXY', 'CAMADA_CHALLENGE', 'CAMADA_DISABLED', 'CAMADA_SERVERLESS'];\n\n/** Deno.env where there is a Deno (a Node test process has only process.env). A key the grant does\n * not cover throws NotCapable; that costs enforcement, never the response, and is logged as what it is. */\nfunction hostEnv(): Env | undefined {\n const deno = (globalThis as { Deno?: DenoGlobal }).Deno;\n if (!deno) return globalThis.process?.env;\n const env: Env = {};\n let denied = 0;\n for (const k of ENV_KEYS) {\n try { env[k] = deno.env.get(k); } catch { denied++; }\n }\n if (denied === ENV_KEYS.length) logRateLimited(new Error('Deno.env access denied — run with --allow-env (or --allow-env=CAMADA_KEY,CAMADA_INGEST_URL,CAMADA_SNAPSHOT_URL); camada is inactive'));\n return env;\n}\n\n/** Only a TCP peer is an address; a unix socket has none, and camada then sees no client ip. */\nconst peerOf = (info: ServeHandlerInfo | undefined): string | null =>\n info?.remoteAddr?.transport === 'tcp' ? info.remoteAddr.hostname ?? null : null;\n\n/**\n * Builds the wrapper: `Deno.serve(camada()(handler))`. Mode defaults to `timer` (a long-lived\n * process polls the snapshot on an unref'd interval); `opts.mode` overrides and CAMADA_SERVERLESS=1\n * forces lazy. Deno's setInterval returns a number, so that unref is a no-op there and the poll\n * keeps the process alive until resetCamada() or exit — under Node (vitest) resetCamada() is what\n * lets a test process end.\n */\nexport function camada(opts: CamadaDenoOptions = {}): (handler: ServeHandler) => WrappedHandler {\n const cam = createFetchCamada({ tap: TAP_DENO, sdk: SDK_ID, iife }, { ...opts, mode: opts.mode ?? 'timer' });\n instances.add(cam);\n return (handler) => async (req, info) => {\n const r = await cam.before(req, { peer: peerOf(info), env: hostEnv() });\n if (!r) return handler(req, info);\n if (r.response) return r.response;\n slots.set(req, r.vars);\n let res: Response;\n try {\n res = await handler(req, info);\n } catch (err) {\n cam.after(req, r.vars, 500); // Deno.serve answers 500 for a thrown handler (an onError returning its own status is not visible here)\n throw err;\n }\n cam.after(req, r.vars, res.status);\n return r.vars.sessionCookie ? withSetCookie(res, r.vars.sessionCookie) : res;\n };\n}\n\n/** Records an outcome the handler knows (`login_failed`, `signup`, ...), joined to this request's\n * event. Never throws; a silent no-op where the wrapper did not run. See @camada/core/fetch. */\nexport const track = (req: Request, event: string, data?: { user?: string }): Promise<void> => coreTrack(slots.get(req), event, data);\n\n/** The `<script>` tag for an HTML response; `''` where the wrapper did not run or the beacon is off. */\nexport const scriptTag = (req: Request): string => coreScriptTag(slots.get(req));\n\n/** Test/reset hook: stops every poller and queue this module created and drops their engines; the wrappers stay wired and rebuild lazily. */\nexport function resetCamada(): void {\n for (const cam of instances) cam.reset();\n}\n","{\n \"name\": \"@camada/deno\",\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 Deno.serve: snapshot enforcement, first-party challenge and beacon, track() outcomes, timer-polled snapshot and batched event shipping\",\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/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":";AAOA,OAAO,UAAU;AACjB,SAAS,UAAU,sBAAsB;AACzC,SAAS,mBAAmB,eAAe,SAAS,WAAW,aAAa,qBAAgF;;;ACR1J,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFuBxC,IAAM,YAAY,oBAAI,IAAiB;AAGvC,IAAM,QAAQ,oBAAI,QAA4B;AAI9C,IAAM,WAAW,CAAC,cAAc,gBAAgB,yBAAyB,qBAAqB,uBAAuB,wBAAwB,oBAAoB,mBAAmB,mBAAmB;AAIvM,SAAS,UAA2B;AAClC,QAAM,OAAQ,WAAqC;AACnD,MAAI,CAAC,KAAM,QAAO,WAAW,SAAS;AACtC,QAAM,MAAW,CAAC;AAClB,MAAI,SAAS;AACb,aAAW,KAAK,UAAU;AACxB,QAAI;AAAE,UAAI,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC;AAAA,IAAG,QAAQ;AAAE;AAAA,IAAU;AAAA,EACtD;AACA,MAAI,WAAW,SAAS,OAAQ,gBAAe,IAAI,MAAM,0IAAqI,CAAC;AAC/L,SAAO;AACT;AAGA,IAAM,SAAS,CAAC,SACd,MAAM,YAAY,cAAc,QAAQ,KAAK,WAAW,YAAY,OAAO;AAStE,SAAS,OAAO,OAA0B,CAAC,GAA8C;AAC9F,QAAM,MAAM,kBAAkB,EAAE,KAAK,UAAU,KAAK,QAAQ,KAAK,GAAG,EAAE,GAAG,MAAM,MAAM,KAAK,QAAQ,QAAQ,CAAC;AAC3G,YAAU,IAAI,GAAG;AACjB,SAAO,CAAC,YAAY,OAAO,KAAK,SAAS;AACvC,UAAM,IAAI,MAAM,IAAI,OAAO,KAAK,EAAE,MAAM,OAAO,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;AACtE,QAAI,CAAC,EAAG,QAAO,QAAQ,KAAK,IAAI;AAChC,QAAI,EAAE,SAAU,QAAO,EAAE;AACzB,UAAM,IAAI,KAAK,EAAE,IAAI;AACrB,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,QAAQ,KAAK,IAAI;AAAA,IAC/B,SAAS,KAAK;AACZ,UAAI,MAAM,KAAK,EAAE,MAAM,GAAG;AAC1B,YAAM;AAAA,IACR;AACA,QAAI,MAAM,KAAK,EAAE,MAAM,IAAI,MAAM;AACjC,WAAO,EAAE,KAAK,gBAAgB,cAAc,KAAK,EAAE,KAAK,aAAa,IAAI;AAAA,EAC3E;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/deno",
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 Deno.serve: snapshot enforcement, first-party challenge and beacon, track() outcomes, timer-polled snapshot and batched event shipping",
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-deno.git"
32
+ },
33
+ "homepage": "https://github.com/camada-app/camada-deno#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/camada-app/camada-deno/issues"
36
+ }
37
+ }