@camada/sveltekit 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,176 @@
1
+ # @camada/sveltekit
2
+
3
+ camada for [SvelteKit](https://svelte.dev/docs/kit) as a `handle` hook: enforces the tenant
4
+ snapshot inline (your ordered custom rules, then block, allow, challenge), serves a first-party
5
+ proof-of-work challenge page and beacon, records the outcomes your actions know (`track()`), and
6
+ ships wire events off the response path — through `waitUntil` on Cloudflare, in the background
7
+ on Node. 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.
10
+
11
+ ## Quickstart
12
+
13
+ ```ts
14
+ // src/hooks.server.ts
15
+ import { camada } from '@camada/sveltekit';
16
+
17
+ export const handle = camada(); // reads CAMADA_KEY / CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL from the env
18
+ ```
19
+
20
+ With other hooks, put it first so a blocked request never reaches them:
21
+
22
+ ```ts
23
+ import { sequence } from '@sveltejs/kit/hooks';
24
+ export const handle = sequence(camada(), auth, i18n);
25
+ ```
26
+
27
+ Env (printed by camada onboarding / `npm run seed` in dev):
28
+
29
+ ```
30
+ CAMADA_KEY=<ingest_token>.<snap_token>
31
+ CAMADA_INGEST_URL=http://localhost:8787 # dev only; defaults to production ingest
32
+ ```
33
+
34
+ On adapter-node that is `process.env`; on adapter-cloudflare it is the Worker bindings
35
+ (`event.platform.env`), read per request. Both are merged, and an app that reads its own config
36
+ can pass the values instead:
37
+
38
+ ```ts
39
+ export const handle = camada({ key: MY_KEY, ingestUrl: MY_INGEST });
40
+ ```
41
+
42
+ Call `camada()` once, at module scope. Engines are cached per resolved configuration inside the
43
+ handle, so a Worker isolate serving several tenants never enforces one tenant's snapshot on
44
+ another, nor signs its cookies with another's secret. Without `CAMADA_KEY` the handle is inert
45
+ (one log line, no requests, no enforcement), so an unprovisioned environment behaves exactly as
46
+ if camada were not installed.
47
+
48
+ ## What it does per request
49
+
50
+ 1. Refreshes the snapshot off-path (lazy mode by default — no interval timers, so the handle is
51
+ safe on edge and serverless adapters; `mode: 'timer'` polls on an unref'd interval for a
52
+ long-lived adapter-node process). Every poll and event batch carries
53
+ `x-camada-sdk: @camada/sveltekit/<version>`, and polls ask for snapshot v5 — the container
54
+ that carries your ordered custom rules.
55
+ 2. Resolves the client from `event.getClientAddress()` — the address your adapter vouches for
56
+ (adapter-node honours its `ADDRESS_HEADER` / `XFF_DEPTH`, adapter-cloudflare reads
57
+ `cf-connecting-ip`) — falling back to `X-Forwarded-For` only under your tenant's
58
+ trusted-proxy config. A client header alone is never the ip. When the adapter has no address
59
+ (prerendering) the ip is null: ip rules and the challenge stand down, the event still ships —
60
+ so a `vite build` with prerendered routes ships one first-visit page event per route from the
61
+ build machine; set `CAMADA_DISABLED=1` in the build env to suppress them. A server-side
62
+ `event.fetch` to the app's own routes (`isSubRequest`) is left alone: one page view is one
63
+ event, and the page-level verdict is not re-applied to your own `load()`.
64
+ 3. Runs your ordered custom rules, then the allow, block and challenge lists.
65
+ 4. **Block** → `403` with `x-block-reason` before `resolve()`; the event still ships, with
66
+ `st: 403` and `blk: <reason>` so the analyst counts SDK blocks apart from your own 403s.
67
+ 5. **Skip** → a skip rule or the allow list wins over a wider block.
68
+ 6. **Challenge** → a `403` proof-of-work page (HTML navigations) or `403 {"error":"challenge_required"}`
69
+ (everything else); `POST /__camada/challenge` verifies the solution, sets `_cch` and 302s back.
70
+ 7. Otherwise `resolve(event)` runs, and the settled response ships one batched, redacted event
71
+ with its real status (Authorization and Cookie values never leave the process; credential-looking
72
+ query values are scrubbed — see `@camada/core`).
73
+
74
+ A first visit gets the shared `_sfp` session cookie through `event.cookies.set()` — SvelteKit
75
+ adds the header to whatever `resolve()` returns, so a `redirect()` or a raw `Response` passes
76
+ through untouched.
77
+
78
+ ## Options
79
+
80
+ | option | default | meaning |
81
+ |---|---|---|
82
+ | `key` | `env.CAMADA_KEY` | `<ingest_token>.<snap_token>`; without it the handle is inert |
83
+ | `ingestUrl` | `env.CAMADA_INGEST_URL` | ingest base; batches go to `<ingestUrl>/e` |
84
+ | `snapshotUrl` | `<ingestUrl>/snapshot` | snapshot endpoint |
85
+ | `trustedProxy` | server config | `none` / `vercel` / `hops:N` / `cidrs:a,b`, or the parsed object |
86
+ | `challenge` | `true` | serve the proof-of-work page for `challenge` verdicts |
87
+ | `challengePath` | `/__camada/challenge` | where that page posts its solution |
88
+ | `snapshotVersion` | `5` | `4` drops the custom rules, `3` the allow/challenge sides too |
89
+ | `scriptPath` | `/_cam/b.js` | where the first-party beacon script is served |
90
+ | `fpPath` | `/_cam/fp` | where that script posts the beacon; keep it in `scriptPath`'s directory |
91
+ | `mode` | `lazy` (or `timer`) | `timer` polls the snapshot on an unref'd interval (long-lived process); `lazy` refreshes it per request off-path. `CAMADA_SERVERLESS=1` forces `lazy` |
92
+ | `env` | `process.env` + `platform.env` | overrides the host env (tests, and apps that read config themselves) |
93
+
94
+ `CAMADA_CHALLENGE=0` in the env switches the challenge off without a code change.
95
+
96
+ `CAMADA_DISABLED=1` in the env switches everything off, checked per request.
97
+
98
+ ## The first-party beacon
99
+
100
+ Bots that never run JavaScript are the cheapest to catch. Return the tag from a server `load`
101
+ and put it in the page's `<head>`; the handle does the rest:
102
+
103
+ ```ts
104
+ // src/routes/+page.server.ts
105
+ import { scriptTag } from '@camada/sveltekit';
106
+ export const load = (event) => ({ camada: scriptTag(event) });
107
+ ```
108
+
109
+ ```svelte
110
+ <!-- src/routes/+page.svelte -->
111
+ <script>
112
+ let { data } = $props();
113
+ </script>
114
+ <svelte:head>{@html data.camada}</svelte:head>
115
+ ```
116
+
117
+ `scriptTag(event)` returns `<script src="/_cam/b.js?r=<rid>" async></script>` — the `rid` is this
118
+ request's event id, so the analyst joins the beacon to the page view. The handle serves the
119
+ script at `GET /_cam/b.js` (cacheable, 1 h) and relays `POST /_cam/fp` (≤ 32 KB, answers 204)
120
+ onto the event batch as a `sig: 1` row stamped with the client ip camada resolved — never the
121
+ one the body claims. Both endpoints sit behind the verdict: a blocked client gets 403 there too.
122
+ The tag is `''` when camada is off for the request or the project turned the beacon off in its
123
+ settings, and the endpoints stand down with it.
124
+
125
+ ## App-context events
126
+
127
+ The wire shows a `POST /login`; only your action knows whether it failed. Tell camada:
128
+
129
+ ```ts
130
+ // src/routes/login/+page.server.ts
131
+ import { fail, redirect } from '@sveltejs/kit';
132
+ import { track } from '@camada/sveltekit';
133
+
134
+ export const actions = {
135
+ default: async (event) => {
136
+ const form = await event.request.formData();
137
+ const ok = await signIn(form);
138
+ if (!ok) { track(event, 'login_failed', { user: String(form.get('email')) }); return fail(401); } // await optional
139
+ redirect(303, '/');
140
+ },
141
+ };
142
+ ```
143
+
144
+ `track(event, name, { user? })` ships `{ et, uid, rid, sid, ip, ts }` joined to this request's
145
+ event. The user identifier is HMAC-hashed in-process with the ingest token — the raw value never
146
+ leaves the process. It never throws and is a no-op where the handle did not run. The event name
147
+ is free-form; the analyst's rules read this vocabulary:
148
+
149
+ | event | when |
150
+ |---|---|
151
+ | `login_failed` / `login_succeeded` | a credential check settled |
152
+ | `signup` | an account was created |
153
+ | `password_reset` | a reset was requested |
154
+ | `mfa_failed` | a second factor was rejected |
155
+ | `payment_failed` / `payment_succeeded` | a charge settled |
156
+ | `coupon_failed` | a promo code was rejected |
157
+
158
+ ## What this tap can see
159
+
160
+ This is the in-app position: the beacon, the client hints the browser sends, the real status
161
+ your routes answered, the `_sfp` session, and the app context `track()` adds. What SvelteKit
162
+ vouches for depends on the adapter. `getClientAddress()` is the one address the handle trusts;
163
+ on adapter-cloudflare `event.platform.cf` also supplies `asn`, `country`,
164
+ `tlsClientExtensionsSha1` and `httpProtocol`: the handle evaluates ASN, country and
165
+ TLS-fingerprint rules on them locally and stamps `asn`, `cc`, `tlsx` and the visitor's own
166
+ protocol on the event, but the analyst's capability mask for `sdk-sveltekit` does not credit
167
+ them (it is one mask for every adapter), so rules on those conditions are reported as not
168
+ enforceable by this SDK and the shipped facts are enrichment only. On adapter-node and the
169
+ other adapters none of those exist — the request arrives as a `Request` with no connection
170
+ facts, no client protocol, and headers already normalised — so those rules do not fire and the
171
+ analyst never scores their absence as evidence. No forwarded header is ever read for any of them.
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.
package/dist/index.cjs ADDED
@@ -0,0 +1,115 @@
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/sveltekit";
47
+ var version = "0.1.0";
48
+
49
+ // src/version.ts
50
+ var SDK_ID = `${name}/${version}`;
51
+
52
+ // src/camada.ts
53
+ var VAR = "__camada";
54
+ var instances = /* @__PURE__ */ new Set();
55
+ var slotOf = (event) => (0, import_core.guarded)(() => event.locals[VAR], void 0);
56
+ function contextOf(event) {
57
+ const platform = event.platform;
58
+ const env = (0, import_core.guarded)(() => ({ ...globalThis.process?.env, ...platform?.env }), {});
59
+ let peer = null;
60
+ try {
61
+ peer = event.getClientAddress();
62
+ } catch {
63
+ }
64
+ const exec = platform?.context;
65
+ const cf = platform?.cf;
66
+ return {
67
+ peer,
68
+ env,
69
+ waitUntil: exec ? (p) => exec.waitUntil(p) : void 0,
70
+ asn: cf?.asn ?? null,
71
+ country: cf?.country ?? null,
72
+ tlsx: cf?.tlsClientExtensionsSha1 ?? null,
73
+ // The connection terminates at Cloudflare, so `cf.httpProtocol` is the visitor's own hop.
74
+ httpVersion: cf?.httpProtocol ? cf.httpProtocol.replace(/^HTTP\//i, "") : null
75
+ };
76
+ }
77
+ function camada(opts = {}) {
78
+ const cam = (0, import_fetch.createFetchCamada)({ tap: import_core.TAP_SVELTEKIT, sdk: SDK_ID, iife: import_iife_string.default }, opts);
79
+ instances.add(cam);
80
+ return async ({ event, resolve }) => {
81
+ if (event.isSubRequest) return resolve(event);
82
+ const r = await cam.before(event.request, contextOf(event));
83
+ if (!r) return resolve(event);
84
+ if (r.response) return r.response;
85
+ const vars = r.vars;
86
+ (0, import_core.guarded)(() => {
87
+ event.locals[VAR] = vars;
88
+ if (vars.sessionCookie && vars.sid) {
89
+ event.cookies.set(import_fetch.SESSION_COOKIE, vars.sid, {
90
+ path: "/",
91
+ maxAge: import_fetch.SESSION_MAX_AGE,
92
+ httpOnly: true,
93
+ sameSite: "lax",
94
+ secure: new URL(event.request.url).protocol === "https:"
95
+ });
96
+ }
97
+ }, void 0);
98
+ const res = await resolve(event);
99
+ cam.after(event.request, vars, res.status);
100
+ return res;
101
+ };
102
+ }
103
+ var track = (event, et, data) => (0, import_fetch.track)(slotOf(event), et, data);
104
+ var scriptTag = (event) => (0, import_fetch.scriptTag)(slotOf(event));
105
+ function resetCamada() {
106
+ for (const cam of instances) cam.reset();
107
+ }
108
+ // Annotate the CommonJS export names for ESM import in node:
109
+ 0 && (module.exports = {
110
+ camada,
111
+ resetCamada,
112
+ scriptTag,
113
+ track
114
+ });
115
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/camada.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/sveltekit — the one-line install for a SvelteKit app, in src/hooks.server.ts:\n// export const handle = camada(); // env: CAMADA_KEY (+ CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL in dev)\n// scriptTag(event) // the first-party beacon, returned from a load() into the page\n// track(event, 'login_failed', { user }) // an outcome the wire cannot show\nexport { camada, track, scriptTag, resetCamada, type CamadaSvelteKitOptions, type CamadaSvelteKitVars } from './camada.js';\n","// @camada/sveltekit — the `handle` hook that binds @camada/core/fetch to a SvelteKit app. The\n// pipeline (verdict, block, challenge, beacon, the event) lives in core; this file only says what\n// SvelteKit knows: the adapter-resolved client address, the per-request env and execution context\n// an edge adapter hands over, the cookie API, and `event.locals` as the per-request slot.\nimport type { Handle, RequestEvent } from '@sveltejs/kit';\nimport { guarded, TAP_SVELTEKIT } from '@camada/core';\nimport { createFetchCamada, SESSION_COOKIE, SESSION_MAX_AGE, track as coreTrack, scriptTag as coreScriptTag, type FetchCamada, type FetchCamadaOptions, type FetchRequestContext, type FetchVars } from '@camada/core/fetch';\nimport iife from '@camada/browser/iife-string';\nimport { SDK_ID } from './version.js';\n\nexport type CamadaSvelteKitOptions = FetchCamadaOptions;\nexport type CamadaSvelteKitVars = FetchVars;\n\nconst VAR = '__camada'; // private: track() and scriptTag() are the API, not event.locals\nconst instances = new Set<FetchCamada>();\n\ntype Env = Record<string, string | undefined>;\n\n/** What adapter-cloudflare puts on `event.platform`; every field is optional because other adapters put nothing there. */\ninterface CfPlatform {\n env?: Env;\n context?: { waitUntil(p: Promise<unknown>): void };\n cf?: { asn?: number; country?: string; tlsClientExtensionsSha1?: string; httpProtocol?: string };\n}\n\nconst slotOf = (event: RequestEvent): FetchVars | undefined =>\n guarded(() => (event.locals as Record<string, unknown>)[VAR] as FetchVars | undefined, undefined);\n\n/** Everything the runtime vouches for. Nothing here is read from a client header. */\nfunction contextOf(event: RequestEvent): FetchRequestContext {\n const platform = event.platform as CfPlatform | undefined;\n // process.env on adapter-node (and nodejs_compat Workers), the bindings on adapter-cloudflare;\n // a binding can be a KV namespace, so the merge is guarded and core reads only CAMADA_* keys.\n const env = guarded<Env>(() => ({ ...(globalThis as { process?: { env?: Env } }).process?.env, ...platform?.env }), {});\n let peer: string | null = null;\n try { peer = event.getClientAddress(); } catch { /* prerendering, or an adapter with no socket: nothing vouched for */ }\n const exec = platform?.context;\n const cf = platform?.cf;\n return {\n peer, env,\n waitUntil: exec ? (p) => exec.waitUntil(p) : undefined,\n asn: cf?.asn ?? null,\n country: cf?.country ?? null,\n tlsx: cf?.tlsClientExtensionsSha1 ?? null,\n // The connection terminates at Cloudflare, so `cf.httpProtocol` is the visitor's own hop.\n httpVersion: cf?.httpProtocol ? cf.httpProtocol.replace(/^HTTP\\//i, '') : null,\n };\n}\n\n/**\n * The `handle` for `src/hooks.server.ts`: `export const handle = camada()`, or first in a\n * `sequence()`. Reads `CAMADA_KEY` (+ `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL`) from\n * `process.env` or the Cloudflare bindings; inert without a key. One pipeline per call.\n */\nexport function camada(opts: CamadaSvelteKitOptions = {}): Handle {\n const cam = createFetchCamada({ tap: TAP_SVELTEKIT, sdk: SDK_ID, iife }, opts); // mode defaults to lazy in core: the handle may run on edge/serverless\n instances.add(cam);\n return async ({ event, resolve }) => {\n // A server-side `event.fetch` to the app's own routes runs the handle again on a fresh event\n // with no user-agent or accept: one page view is one event, and the page-level verdict must\n // not be re-applied to the server's own load().\n if (event.isSubRequest) return resolve(event);\n const r = await cam.before(event.request, contextOf(event));\n if (!r) return resolve(event);\n if (r.response) return r.response;\n const vars = r.vars;\n guarded(() => {\n (event.locals as Record<string, unknown>)[VAR] = vars;\n // The framework cookie API: SvelteKit adds the header to whatever resolve() returns, so a\n // redirect or an immutable Response needs no rebuilding here.\n if (vars.sessionCookie && vars.sid) {\n event.cookies.set(SESSION_COOKIE, vars.sid, {\n path: '/', maxAge: SESSION_MAX_AGE, httpOnly: true, sameSite: 'lax', secure: new URL(event.request.url).protocol === 'https:',\n });\n }\n }, undefined);\n const res = await resolve(event);\n cam.after(event.request, vars, res.status);\n return res;\n };\n}\n\n/**\n * Records an outcome the app knows and the wire cannot show (`login_failed`, `signup`,\n * `payment_failed`, ...), joined to this request's event by rid and session. The user identifier\n * is HMAC-hashed in-process. Never throws; a no-op where the handle did not run for this event.\n */\nexport const track = (event: RequestEvent, et: string, data?: { user?: string }): Promise<void> => coreTrack(slotOf(event), et, data);\n\n/** The beacon `<script>` tag for this request's page; `''` where the handle did not run or the tenant turned the beacon off. */\nexport const scriptTag = (event: RequestEvent): string => coreScriptTag(slotOf(event));\n\n/** Test/reset hook: stops and drops every engine of every handle this module created. */\nexport function resetCamada(): void {\n for (const cam of instances) cam.reset();\n}\n","{\n \"name\": \"@camada/sveltekit\",\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 SvelteKit (hooks.server.ts handle): snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping on Cloudflare\",\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 \"peerDependencies\": {\n \"@sveltejs/kit\": \">=2\"\n },\n \"devDependencies\": {\n \"@sveltejs/kit\": \"^2.0.0\",\n \"@types/node\": \"^22.10.0\",\n \"svelte\": \"^5.0.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vite\": \"^8.0.0\",\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;;;ACKA,kBAAuC;AACvC,mBAAwM;AACxM,yBAAiB;;;ACNf,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFUxC,IAAM,MAAM;AACZ,IAAM,YAAY,oBAAI,IAAiB;AAWvC,IAAM,SAAS,CAAC,cACd,qBAAQ,MAAO,MAAM,OAAmC,GAAG,GAA4B,MAAS;AAGlG,SAAS,UAAU,OAA0C;AAC3D,QAAM,WAAW,MAAM;AAGvB,QAAM,UAAM,qBAAa,OAAO,EAAE,GAAI,WAA2C,SAAS,KAAK,GAAG,UAAU,IAAI,IAAI,CAAC,CAAC;AACtH,MAAI,OAAsB;AAC1B,MAAI;AAAE,WAAO,MAAM,iBAAiB;AAAA,EAAG,QAAQ;AAAA,EAAwE;AACvH,QAAM,OAAO,UAAU;AACvB,QAAM,KAAK,UAAU;AACrB,SAAO;AAAA,IACL;AAAA,IAAM;AAAA,IACN,WAAW,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI;AAAA,IAC7C,KAAK,IAAI,OAAO;AAAA,IAChB,SAAS,IAAI,WAAW;AAAA,IACxB,MAAM,IAAI,2BAA2B;AAAA;AAAA,IAErC,aAAa,IAAI,eAAe,GAAG,aAAa,QAAQ,YAAY,EAAE,IAAI;AAAA,EAC5E;AACF;AAOO,SAAS,OAAO,OAA+B,CAAC,GAAW;AAChE,QAAM,UAAM,gCAAkB,EAAE,KAAK,2BAAe,KAAK,QAAQ,yBAAAA,QAAK,GAAG,IAAI;AAC7E,YAAU,IAAI,GAAG;AACjB,SAAO,OAAO,EAAE,OAAO,QAAQ,MAAM;AAInC,QAAI,MAAM,aAAc,QAAO,QAAQ,KAAK;AAC5C,UAAM,IAAI,MAAM,IAAI,OAAO,MAAM,SAAS,UAAU,KAAK,CAAC;AAC1D,QAAI,CAAC,EAAG,QAAO,QAAQ,KAAK;AAC5B,QAAI,EAAE,SAAU,QAAO,EAAE;AACzB,UAAM,OAAO,EAAE;AACf,6BAAQ,MAAM;AACZ,MAAC,MAAM,OAAmC,GAAG,IAAI;AAGjD,UAAI,KAAK,iBAAiB,KAAK,KAAK;AAClC,cAAM,QAAQ,IAAI,6BAAgB,KAAK,KAAK;AAAA,UAC1C,MAAM;AAAA,UAAK,QAAQ;AAAA,UAAiB,UAAU;AAAA,UAAM,UAAU;AAAA,UAAO,QAAQ,IAAI,IAAI,MAAM,QAAQ,GAAG,EAAE,aAAa;AAAA,QACvH,CAAC;AAAA,MACH;AAAA,IACF,GAAG,MAAS;AACZ,UAAM,MAAM,MAAM,QAAQ,KAAK;AAC/B,QAAI,MAAM,MAAM,SAAS,MAAM,IAAI,MAAM;AACzC,WAAO;AAAA,EACT;AACF;AAOO,IAAM,QAAQ,CAAC,OAAqB,IAAY,aAA4C,aAAAC,OAAU,OAAO,KAAK,GAAG,IAAI,IAAI;AAG7H,IAAM,YAAY,CAAC,cAAgC,aAAAC,WAAc,OAAO,KAAK,CAAC;AAG9E,SAAS,cAAoB;AAClC,aAAW,OAAO,UAAW,KAAI,MAAM;AACzC;","names":["iife","coreTrack","coreScriptTag"]}
@@ -0,0 +1,25 @@
1
+ import { Handle, RequestEvent } from '@sveltejs/kit';
2
+ import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
3
+
4
+ type CamadaSvelteKitOptions = FetchCamadaOptions;
5
+ type CamadaSvelteKitVars = FetchVars;
6
+ /**
7
+ * The `handle` for `src/hooks.server.ts`: `export const handle = camada()`, or first in a
8
+ * `sequence()`. Reads `CAMADA_KEY` (+ `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL`) from
9
+ * `process.env` or the Cloudflare bindings; inert without a key. One pipeline per call.
10
+ */
11
+ declare function camada(opts?: CamadaSvelteKitOptions): Handle;
12
+ /**
13
+ * Records an outcome the app knows and the wire cannot show (`login_failed`, `signup`,
14
+ * `payment_failed`, ...), joined to this request's event by rid and session. The user identifier
15
+ * is HMAC-hashed in-process. Never throws; a no-op where the handle did not run for this event.
16
+ */
17
+ declare const track: (event: RequestEvent, et: string, data?: {
18
+ user?: string;
19
+ }) => Promise<void>;
20
+ /** The beacon `<script>` tag for this request's page; `''` where the handle did not run or the tenant turned the beacon off. */
21
+ declare const scriptTag: (event: RequestEvent) => string;
22
+ /** Test/reset hook: stops and drops every engine of every handle this module created. */
23
+ declare function resetCamada(): void;
24
+
25
+ export { type CamadaSvelteKitOptions, type CamadaSvelteKitVars, camada, resetCamada, scriptTag, track };
@@ -0,0 +1,25 @@
1
+ import { Handle, RequestEvent } from '@sveltejs/kit';
2
+ import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
3
+
4
+ type CamadaSvelteKitOptions = FetchCamadaOptions;
5
+ type CamadaSvelteKitVars = FetchVars;
6
+ /**
7
+ * The `handle` for `src/hooks.server.ts`: `export const handle = camada()`, or first in a
8
+ * `sequence()`. Reads `CAMADA_KEY` (+ `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL`) from
9
+ * `process.env` or the Cloudflare bindings; inert without a key. One pipeline per call.
10
+ */
11
+ declare function camada(opts?: CamadaSvelteKitOptions): Handle;
12
+ /**
13
+ * Records an outcome the app knows and the wire cannot show (`login_failed`, `signup`,
14
+ * `payment_failed`, ...), joined to this request's event by rid and session. The user identifier
15
+ * is HMAC-hashed in-process. Never throws; a no-op where the handle did not run for this event.
16
+ */
17
+ declare const track: (event: RequestEvent, et: string, data?: {
18
+ user?: string;
19
+ }) => Promise<void>;
20
+ /** The beacon `<script>` tag for this request's page; `''` where the handle did not run or the tenant turned the beacon off. */
21
+ declare const scriptTag: (event: RequestEvent) => string;
22
+ /** Test/reset hook: stops and drops every engine of every handle this module created. */
23
+ declare function resetCamada(): void;
24
+
25
+ export { type CamadaSvelteKitOptions, type CamadaSvelteKitVars, camada, resetCamada, scriptTag, track };
package/dist/index.js ADDED
@@ -0,0 +1,75 @@
1
+ // src/camada.ts
2
+ import { guarded, TAP_SVELTEKIT } from "@camada/core";
3
+ import { createFetchCamada, SESSION_COOKIE, SESSION_MAX_AGE, 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/sveltekit";
8
+ var version = "0.1.0";
9
+
10
+ // src/version.ts
11
+ var SDK_ID = `${name}/${version}`;
12
+
13
+ // src/camada.ts
14
+ var VAR = "__camada";
15
+ var instances = /* @__PURE__ */ new Set();
16
+ var slotOf = (event) => guarded(() => event.locals[VAR], void 0);
17
+ function contextOf(event) {
18
+ const platform = event.platform;
19
+ const env = guarded(() => ({ ...globalThis.process?.env, ...platform?.env }), {});
20
+ let peer = null;
21
+ try {
22
+ peer = event.getClientAddress();
23
+ } catch {
24
+ }
25
+ const exec = platform?.context;
26
+ const cf = platform?.cf;
27
+ return {
28
+ peer,
29
+ env,
30
+ waitUntil: exec ? (p) => exec.waitUntil(p) : void 0,
31
+ asn: cf?.asn ?? null,
32
+ country: cf?.country ?? null,
33
+ tlsx: cf?.tlsClientExtensionsSha1 ?? null,
34
+ // The connection terminates at Cloudflare, so `cf.httpProtocol` is the visitor's own hop.
35
+ httpVersion: cf?.httpProtocol ? cf.httpProtocol.replace(/^HTTP\//i, "") : null
36
+ };
37
+ }
38
+ function camada(opts = {}) {
39
+ const cam = createFetchCamada({ tap: TAP_SVELTEKIT, sdk: SDK_ID, iife }, opts);
40
+ instances.add(cam);
41
+ return async ({ event, resolve }) => {
42
+ if (event.isSubRequest) return resolve(event);
43
+ const r = await cam.before(event.request, contextOf(event));
44
+ if (!r) return resolve(event);
45
+ if (r.response) return r.response;
46
+ const vars = r.vars;
47
+ guarded(() => {
48
+ event.locals[VAR] = vars;
49
+ if (vars.sessionCookie && vars.sid) {
50
+ event.cookies.set(SESSION_COOKIE, vars.sid, {
51
+ path: "/",
52
+ maxAge: SESSION_MAX_AGE,
53
+ httpOnly: true,
54
+ sameSite: "lax",
55
+ secure: new URL(event.request.url).protocol === "https:"
56
+ });
57
+ }
58
+ }, void 0);
59
+ const res = await resolve(event);
60
+ cam.after(event.request, vars, res.status);
61
+ return res;
62
+ };
63
+ }
64
+ var track = (event, et, data) => coreTrack(slotOf(event), et, data);
65
+ var scriptTag = (event) => coreScriptTag(slotOf(event));
66
+ function resetCamada() {
67
+ for (const cam of instances) cam.reset();
68
+ }
69
+ export {
70
+ camada,
71
+ resetCamada,
72
+ scriptTag,
73
+ track
74
+ };
75
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/camada.ts","../package.json","../src/version.ts"],"sourcesContent":["// @camada/sveltekit — the `handle` hook that binds @camada/core/fetch to a SvelteKit app. The\n// pipeline (verdict, block, challenge, beacon, the event) lives in core; this file only says what\n// SvelteKit knows: the adapter-resolved client address, the per-request env and execution context\n// an edge adapter hands over, the cookie API, and `event.locals` as the per-request slot.\nimport type { Handle, RequestEvent } from '@sveltejs/kit';\nimport { guarded, TAP_SVELTEKIT } from '@camada/core';\nimport { createFetchCamada, SESSION_COOKIE, SESSION_MAX_AGE, track as coreTrack, scriptTag as coreScriptTag, type FetchCamada, type FetchCamadaOptions, type FetchRequestContext, type FetchVars } from '@camada/core/fetch';\nimport iife from '@camada/browser/iife-string';\nimport { SDK_ID } from './version.js';\n\nexport type CamadaSvelteKitOptions = FetchCamadaOptions;\nexport type CamadaSvelteKitVars = FetchVars;\n\nconst VAR = '__camada'; // private: track() and scriptTag() are the API, not event.locals\nconst instances = new Set<FetchCamada>();\n\ntype Env = Record<string, string | undefined>;\n\n/** What adapter-cloudflare puts on `event.platform`; every field is optional because other adapters put nothing there. */\ninterface CfPlatform {\n env?: Env;\n context?: { waitUntil(p: Promise<unknown>): void };\n cf?: { asn?: number; country?: string; tlsClientExtensionsSha1?: string; httpProtocol?: string };\n}\n\nconst slotOf = (event: RequestEvent): FetchVars | undefined =>\n guarded(() => (event.locals as Record<string, unknown>)[VAR] as FetchVars | undefined, undefined);\n\n/** Everything the runtime vouches for. Nothing here is read from a client header. */\nfunction contextOf(event: RequestEvent): FetchRequestContext {\n const platform = event.platform as CfPlatform | undefined;\n // process.env on adapter-node (and nodejs_compat Workers), the bindings on adapter-cloudflare;\n // a binding can be a KV namespace, so the merge is guarded and core reads only CAMADA_* keys.\n const env = guarded<Env>(() => ({ ...(globalThis as { process?: { env?: Env } }).process?.env, ...platform?.env }), {});\n let peer: string | null = null;\n try { peer = event.getClientAddress(); } catch { /* prerendering, or an adapter with no socket: nothing vouched for */ }\n const exec = platform?.context;\n const cf = platform?.cf;\n return {\n peer, env,\n waitUntil: exec ? (p) => exec.waitUntil(p) : undefined,\n asn: cf?.asn ?? null,\n country: cf?.country ?? null,\n tlsx: cf?.tlsClientExtensionsSha1 ?? null,\n // The connection terminates at Cloudflare, so `cf.httpProtocol` is the visitor's own hop.\n httpVersion: cf?.httpProtocol ? cf.httpProtocol.replace(/^HTTP\\//i, '') : null,\n };\n}\n\n/**\n * The `handle` for `src/hooks.server.ts`: `export const handle = camada()`, or first in a\n * `sequence()`. Reads `CAMADA_KEY` (+ `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL`) from\n * `process.env` or the Cloudflare bindings; inert without a key. One pipeline per call.\n */\nexport function camada(opts: CamadaSvelteKitOptions = {}): Handle {\n const cam = createFetchCamada({ tap: TAP_SVELTEKIT, sdk: SDK_ID, iife }, opts); // mode defaults to lazy in core: the handle may run on edge/serverless\n instances.add(cam);\n return async ({ event, resolve }) => {\n // A server-side `event.fetch` to the app's own routes runs the handle again on a fresh event\n // with no user-agent or accept: one page view is one event, and the page-level verdict must\n // not be re-applied to the server's own load().\n if (event.isSubRequest) return resolve(event);\n const r = await cam.before(event.request, contextOf(event));\n if (!r) return resolve(event);\n if (r.response) return r.response;\n const vars = r.vars;\n guarded(() => {\n (event.locals as Record<string, unknown>)[VAR] = vars;\n // The framework cookie API: SvelteKit adds the header to whatever resolve() returns, so a\n // redirect or an immutable Response needs no rebuilding here.\n if (vars.sessionCookie && vars.sid) {\n event.cookies.set(SESSION_COOKIE, vars.sid, {\n path: '/', maxAge: SESSION_MAX_AGE, httpOnly: true, sameSite: 'lax', secure: new URL(event.request.url).protocol === 'https:',\n });\n }\n }, undefined);\n const res = await resolve(event);\n cam.after(event.request, vars, res.status);\n return res;\n };\n}\n\n/**\n * Records an outcome the app knows and the wire cannot show (`login_failed`, `signup`,\n * `payment_failed`, ...), joined to this request's event by rid and session. The user identifier\n * is HMAC-hashed in-process. Never throws; a no-op where the handle did not run for this event.\n */\nexport const track = (event: RequestEvent, et: string, data?: { user?: string }): Promise<void> => coreTrack(slotOf(event), et, data);\n\n/** The beacon `<script>` tag for this request's page; `''` where the handle did not run or the tenant turned the beacon off. */\nexport const scriptTag = (event: RequestEvent): string => coreScriptTag(slotOf(event));\n\n/** Test/reset hook: stops and drops every engine of every handle this module created. */\nexport function resetCamada(): void {\n for (const cam of instances) cam.reset();\n}\n","{\n \"name\": \"@camada/sveltekit\",\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 SvelteKit (hooks.server.ts handle): snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping on Cloudflare\",\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 \"peerDependencies\": {\n \"@sveltejs/kit\": \">=2\"\n },\n \"devDependencies\": {\n \"@sveltejs/kit\": \"^2.0.0\",\n \"@types/node\": \"^22.10.0\",\n \"svelte\": \"^5.0.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"vite\": \"^8.0.0\",\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":";AAKA,SAAS,SAAS,qBAAqB;AACvC,SAAS,mBAAmB,gBAAgB,iBAAiB,SAAS,WAAW,aAAa,qBAA0G;AACxM,OAAO,UAAU;;;ACNf,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFUxC,IAAM,MAAM;AACZ,IAAM,YAAY,oBAAI,IAAiB;AAWvC,IAAM,SAAS,CAAC,UACd,QAAQ,MAAO,MAAM,OAAmC,GAAG,GAA4B,MAAS;AAGlG,SAAS,UAAU,OAA0C;AAC3D,QAAM,WAAW,MAAM;AAGvB,QAAM,MAAM,QAAa,OAAO,EAAE,GAAI,WAA2C,SAAS,KAAK,GAAG,UAAU,IAAI,IAAI,CAAC,CAAC;AACtH,MAAI,OAAsB;AAC1B,MAAI;AAAE,WAAO,MAAM,iBAAiB;AAAA,EAAG,QAAQ;AAAA,EAAwE;AACvH,QAAM,OAAO,UAAU;AACvB,QAAM,KAAK,UAAU;AACrB,SAAO;AAAA,IACL;AAAA,IAAM;AAAA,IACN,WAAW,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI;AAAA,IAC7C,KAAK,IAAI,OAAO;AAAA,IAChB,SAAS,IAAI,WAAW;AAAA,IACxB,MAAM,IAAI,2BAA2B;AAAA;AAAA,IAErC,aAAa,IAAI,eAAe,GAAG,aAAa,QAAQ,YAAY,EAAE,IAAI;AAAA,EAC5E;AACF;AAOO,SAAS,OAAO,OAA+B,CAAC,GAAW;AAChE,QAAM,MAAM,kBAAkB,EAAE,KAAK,eAAe,KAAK,QAAQ,KAAK,GAAG,IAAI;AAC7E,YAAU,IAAI,GAAG;AACjB,SAAO,OAAO,EAAE,OAAO,QAAQ,MAAM;AAInC,QAAI,MAAM,aAAc,QAAO,QAAQ,KAAK;AAC5C,UAAM,IAAI,MAAM,IAAI,OAAO,MAAM,SAAS,UAAU,KAAK,CAAC;AAC1D,QAAI,CAAC,EAAG,QAAO,QAAQ,KAAK;AAC5B,QAAI,EAAE,SAAU,QAAO,EAAE;AACzB,UAAM,OAAO,EAAE;AACf,YAAQ,MAAM;AACZ,MAAC,MAAM,OAAmC,GAAG,IAAI;AAGjD,UAAI,KAAK,iBAAiB,KAAK,KAAK;AAClC,cAAM,QAAQ,IAAI,gBAAgB,KAAK,KAAK;AAAA,UAC1C,MAAM;AAAA,UAAK,QAAQ;AAAA,UAAiB,UAAU;AAAA,UAAM,UAAU;AAAA,UAAO,QAAQ,IAAI,IAAI,MAAM,QAAQ,GAAG,EAAE,aAAa;AAAA,QACvH,CAAC;AAAA,MACH;AAAA,IACF,GAAG,MAAS;AACZ,UAAM,MAAM,MAAM,QAAQ,KAAK;AAC/B,QAAI,MAAM,MAAM,SAAS,MAAM,IAAI,MAAM;AACzC,WAAO;AAAA,EACT;AACF;AAOO,IAAM,QAAQ,CAAC,OAAqB,IAAY,SAA4C,UAAU,OAAO,KAAK,GAAG,IAAI,IAAI;AAG7H,IAAM,YAAY,CAAC,UAAgC,cAAc,OAAO,KAAK,CAAC;AAG9E,SAAS,cAAoB;AAClC,aAAW,OAAO,UAAW,KAAI,MAAM;AACzC;","names":[]}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@camada/sveltekit",
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 SvelteKit (hooks.server.ts handle): snapshot enforcement, first-party challenge and beacon, track() outcomes, waitUntil-flushed event shipping on Cloudflare",
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
+ "peerDependencies": {
30
+ "@sveltejs/kit": ">=2"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/camada-app/camada-sveltekit.git"
35
+ },
36
+ "homepage": "https://github.com/camada-app/camada-sveltekit#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/camada-app/camada-sveltekit/issues"
39
+ }
40
+ }