@camada/nuxt 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 +21 -0
- package/README.md +158 -0
- package/dist/index.cjs +120 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
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,158 @@
|
|
|
1
|
+
# @camada/nuxt
|
|
2
|
+
|
|
3
|
+
camada for [Nuxt](https://nuxt.com) as h3 server middleware: enforces the tenant snapshot inline
|
|
4
|
+
(your ordered custom rules, then block, allow, challenge), serves a first-party proof-of-work
|
|
5
|
+
challenge page and beacon, records the outcomes your server routes know (`track()`), and ships
|
|
6
|
+
one wire event per request with the status your app really answered. Fails open by design — a
|
|
7
|
+
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
|
+
Two files under `server/`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// server/middleware/camada.ts
|
|
17
|
+
import { camada } from '@camada/nuxt';
|
|
18
|
+
export default camada(); // reads CAMADA_KEY / CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL from process.env
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// server/plugins/camada.ts
|
|
23
|
+
import { camadaNitroPlugin } from '@camada/nuxt';
|
|
24
|
+
export default camadaNitroPlugin(); // puts the beacon tag in every rendered <head>
|
|
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
|
+
An app that reads its own config can pass the values instead:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export default camada({ key: MY_KEY, ingestUrl: MY_INGEST });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The middleware is built once per `camada()` call and keeps one engine per resolved
|
|
41
|
+
configuration. Without `CAMADA_KEY` it is inert (one log line, no requests, no enforcement),
|
|
42
|
+
so an unprovisioned environment behaves exactly as if camada were not installed.
|
|
43
|
+
|
|
44
|
+
## What it does per request
|
|
45
|
+
|
|
46
|
+
1. Refreshes the snapshot off-path (lazy mode by default: no interval timers, so the same
|
|
47
|
+
build runs on a Node server and on an edge preset). Every poll and event batch carries
|
|
48
|
+
`x-camada-sdk: @camada/nuxt/<version>`, and polls ask for snapshot v5 — the container that
|
|
49
|
+
carries your ordered custom rules.
|
|
50
|
+
2. Resolves the client from the peer h3 vouches for — `getRequestIP(event)`: the socket
|
|
51
|
+
address, or the `clientAddress` a Nitro preset stamped on the context — combined with
|
|
52
|
+
`X-Forwarded-For` only under your tenant's trusted-proxy config. A bare header
|
|
53
|
+
(`x-forwarded-for`, `cf-connecting-ip`, `x-real-ip`) never becomes the client on its own.
|
|
54
|
+
3. Runs your ordered custom rules, then the allow, block and challenge lists.
|
|
55
|
+
4. **Block** → `403` with `x-block-reason` before your route; the event still ships, with
|
|
56
|
+
`st: 403` and `blk: <reason>` so the analyst counts SDK blocks apart from your own 403s.
|
|
57
|
+
5. **Challenge** → a `403` proof-of-work page for HTML navigations, `403 {"error":
|
|
58
|
+
"challenge_required"}` for anything else; the solution posts to `/__camada/challenge`,
|
|
59
|
+
which sets `_cch` and 302s back.
|
|
60
|
+
6. Otherwise the request falls through to your routes and pages. The event ships on the
|
|
61
|
+
response's `finish` with the real `statusCode` — a Node response, or the mock a web preset
|
|
62
|
+
provides; `st` is null only for an event without a response object at all. Your own
|
|
63
|
+
`readBody()` still sees the request body (the middleware hands h3 the stream it built), and
|
|
64
|
+
Nitro's internal `event.$fetch` / `useFetch` during SSR is left to the outer request (found on the
|
|
65
|
+
inner request's `__unenv__` context; a bare `$fetch` with no event is a request of its own): one page view
|
|
66
|
+
is one event.
|
|
67
|
+
|
|
68
|
+
A first visit is given the shared `_sfp` session cookie through h3's `setCookie` before your
|
|
69
|
+
handler runs (`HttpOnly; SameSite=Lax; Path=/; Max-Age=30d`, `Secure` when the request URL is
|
|
70
|
+
https), so a redirect from your own handler carries it too. An existing session is never
|
|
71
|
+
overwritten.
|
|
72
|
+
|
|
73
|
+
## Options
|
|
74
|
+
|
|
75
|
+
| option | default | meaning |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `key` | `env.CAMADA_KEY` | `<ingest_token>.<snap_token>`; without it the middleware 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` | `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` |
|
|
87
|
+
| `env` | `process.env` | overrides the process env (tests, and apps that read config themselves) |
|
|
88
|
+
|
|
89
|
+
`CAMADA_CHALLENGE=0` switches the challenge off without a code change.
|
|
90
|
+
|
|
91
|
+
`CAMADA_DISABLED=1` switches everything off, checked per request.
|
|
92
|
+
|
|
93
|
+
## The first-party beacon
|
|
94
|
+
|
|
95
|
+
Bots that never run JavaScript are the cheapest to catch. The Nitro plugin above pushes the tag
|
|
96
|
+
into the `<head>` Nuxt renders; a server route that writes its own HTML uses the helper:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { scriptTag } from '@camada/nuxt';
|
|
100
|
+
|
|
101
|
+
export default defineEventHandler((event) => `<html><head>${scriptTag(event)}</head><body>…</body></html>`);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`scriptTag(event)` returns `<script src="/_cam/b.js?r=<rid>" async></script>` — the `rid` is this
|
|
105
|
+
request's event id, so the analyst joins the beacon to the page view. The middleware serves the
|
|
106
|
+
script at `GET /_cam/b.js` (cacheable, 1 h) and relays `POST /_cam/fp` (≤ 32 KB, answers 204)
|
|
107
|
+
onto the event batch as a `sig: 1` row stamped with the client ip camada resolved — never the
|
|
108
|
+
one the body claims. Both endpoints sit behind the verdict: a blocked client gets 403 there
|
|
109
|
+
too. The tag is `''` where the middleware did not run or the project turned the beacon off in
|
|
110
|
+
its settings, and the endpoints stand down with it.
|
|
111
|
+
|
|
112
|
+
## App-context events
|
|
113
|
+
|
|
114
|
+
The wire shows a `POST /api/login`; only your route knows whether it failed. Tell camada:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// server/api/login.post.ts
|
|
118
|
+
import { track } from '@camada/nuxt';
|
|
119
|
+
|
|
120
|
+
export default defineEventHandler(async (event) => {
|
|
121
|
+
const { email, password } = await readBody(event);
|
|
122
|
+
const ok = await signIn(email, password);
|
|
123
|
+
if (!ok) track(event, 'login_failed', { user: email }); // await optional
|
|
124
|
+
setResponseStatus(event, ok ? 200 : 401);
|
|
125
|
+
return ok ? { ok } : 'Invalid credentials';
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`track(event, name, { user? })` ships `{ et, uid, rid, sid, ip, ts }` joined to this request's
|
|
130
|
+
event. The user identifier is HMAC-hashed in-process with the ingest token — the raw value never
|
|
131
|
+
leaves the process. It never throws and is a no-op where the middleware did not run. The event
|
|
132
|
+
name is free-form; the analyst's rules read this vocabulary:
|
|
133
|
+
|
|
134
|
+
| event | when |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `login_failed` / `login_succeeded` | a credential check settled |
|
|
137
|
+
| `signup` | an account was created |
|
|
138
|
+
| `password_reset` | a reset was requested |
|
|
139
|
+
| `mfa_failed` | a second factor was rejected |
|
|
140
|
+
| `payment_failed` / `payment_succeeded` | a charge settled |
|
|
141
|
+
| `coupon_failed` | a promo code was rejected |
|
|
142
|
+
|
|
143
|
+
## What this tap can see
|
|
144
|
+
|
|
145
|
+
This is the in-app position: the beacon, the client hints the browser sends, the status your
|
|
146
|
+
app answered, the session, and the outcomes your routes report. h3 vouches for the peer
|
|
147
|
+
address (the socket, or a preset's `clientAddress`) and for nothing about the connection
|
|
148
|
+
beyond it: no ASN, country or TLS fingerprint, so rules on those conditions do not enforce here
|
|
149
|
+
(the analyst knows that from the tap's capability mask, `sdk-nuxt`, and never scores their
|
|
150
|
+
absence as evidence). The protocol on the event is `null` — h3 does not see the client's own —
|
|
151
|
+
and the one forwarded header h3 does honour is `x-forwarded-proto`, which decides only whether
|
|
152
|
+
the session cookie is marked `Secure`. Node normalises header order, so the raw-wire-order
|
|
153
|
+
signal is not available either.
|
|
154
|
+
|
|
155
|
+
## Fail open
|
|
156
|
+
|
|
157
|
+
Every entry point runs inside camada's guard. A dead ingest, a corrupt snapshot, a bug in this
|
|
158
|
+
package: telemetry is lost, the request is not.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
camadaNitroPlugin: () => camadaNitroPlugin,
|
|
35
|
+
resetCamada: () => resetCamada,
|
|
36
|
+
scriptTag: () => scriptTag,
|
|
37
|
+
track: () => track
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/camada.ts
|
|
42
|
+
var import_h3 = require("h3");
|
|
43
|
+
var import_core = require("@camada/core");
|
|
44
|
+
var import_fetch = require("@camada/core/fetch");
|
|
45
|
+
var import_iife_string = __toESM(require("@camada/browser/iife-string"), 1);
|
|
46
|
+
|
|
47
|
+
// package.json
|
|
48
|
+
var name = "@camada/nuxt";
|
|
49
|
+
var version = "0.1.0";
|
|
50
|
+
|
|
51
|
+
// src/version.ts
|
|
52
|
+
var SDK_ID = `${name}/${version}`;
|
|
53
|
+
|
|
54
|
+
// src/camada.ts
|
|
55
|
+
var VAR = "__camada";
|
|
56
|
+
var instances = /* @__PURE__ */ new Set();
|
|
57
|
+
var slotOf = (event) => (0, import_core.guarded)(() => {
|
|
58
|
+
const own = event.context[VAR];
|
|
59
|
+
if (own) return own;
|
|
60
|
+
const outer = event.node?.req?.__unenv__;
|
|
61
|
+
return outer?.[VAR];
|
|
62
|
+
}, void 0);
|
|
63
|
+
function camada(opts = {}) {
|
|
64
|
+
const cam = (0, import_fetch.createFetchCamada)({ tap: import_core.TAP_NUXT, sdk: SDK_ID, iife: import_iife_string.default }, opts);
|
|
65
|
+
instances.add(cam);
|
|
66
|
+
return (0, import_h3.defineEventHandler)(async (event) => {
|
|
67
|
+
if (slotOf(event)) return;
|
|
68
|
+
const req = (0, import_core.guarded)(() => (0, import_h3.toWebRequest)(event), null);
|
|
69
|
+
if (!req) return;
|
|
70
|
+
if (req.body) event._requestBody = req.body;
|
|
71
|
+
const ctx = {
|
|
72
|
+
// The socket address (or what the host preset stamped as clientAddress); never
|
|
73
|
+
// X-Forwarded-For here, core resolves that under the trusted-proxy rules.
|
|
74
|
+
peer: (0, import_core.guarded)(() => (0, import_h3.getRequestIP)(event) ?? null, null),
|
|
75
|
+
env: globalThis.process?.env
|
|
76
|
+
};
|
|
77
|
+
const r = await cam.before(req, ctx);
|
|
78
|
+
if (!r) return;
|
|
79
|
+
if (r.response) return r.response;
|
|
80
|
+
const vars = r.vars;
|
|
81
|
+
event.context[VAR] = vars;
|
|
82
|
+
const res = event.node?.res;
|
|
83
|
+
if (vars.sessionCookie && vars.sid && res) {
|
|
84
|
+
const sid = vars.sid;
|
|
85
|
+
(0, import_core.guarded)(() => (0, import_h3.setCookie)(event, import_fetch.SESSION_COOKIE, sid, {
|
|
86
|
+
path: "/",
|
|
87
|
+
maxAge: import_fetch.SESSION_MAX_AGE,
|
|
88
|
+
httpOnly: true,
|
|
89
|
+
sameSite: "lax",
|
|
90
|
+
secure: new URL(req.url).protocol === "https:"
|
|
91
|
+
}), void 0);
|
|
92
|
+
}
|
|
93
|
+
if (typeof res?.once === "function") res.once("finish", () => cam.after(req, vars, res.statusCode));
|
|
94
|
+
else cam.after(req, vars, null);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
var track = (event, et, data) => (0, import_fetch.track)(slotOf(event), et, data);
|
|
98
|
+
var scriptTag = (event) => (0, import_fetch.scriptTag)(slotOf(event));
|
|
99
|
+
function resetCamada() {
|
|
100
|
+
for (const c of instances) c.reset();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/nitro.ts
|
|
104
|
+
function camadaNitroPlugin() {
|
|
105
|
+
return (nitroApp) => {
|
|
106
|
+
nitroApp.hooks.hook("render:html", (html, { event }) => {
|
|
107
|
+
const tag = scriptTag(event);
|
|
108
|
+
if (tag) html.head.push(tag);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
113
|
+
0 && (module.exports = {
|
|
114
|
+
camada,
|
|
115
|
+
camadaNitroPlugin,
|
|
116
|
+
resetCamada,
|
|
117
|
+
scriptTag,
|
|
118
|
+
track
|
|
119
|
+
});
|
|
120
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/camada.ts","../package.json","../src/version.ts","../src/nitro.ts"],"sourcesContent":["// @camada/nuxt — the two-file install for a Nuxt app (any Nitro preset with an h3 event):\n// server/middleware/camada.ts: export default camada(); // env: CAMADA_KEY (+ CAMADA_INGEST_URL / CAMADA_SNAPSHOT_URL in dev)\n// server/plugins/camada.ts: export default camadaNitroPlugin(); // the first-party beacon in every rendered <head>\n// track(event, 'login_failed', { user }) // an outcome the wire cannot show, from a server route\nexport { camada, track, scriptTag, resetCamada, type CamadaNuxtOptions, type CamadaNuxtVars } from './camada.js';\nexport { camadaNitroPlugin, type NitroAppLike } from './nitro.js';\n","// @camada/nuxt — the h3 binding over @camada/core/fetch. The pipeline (verdict, block,\n// challenge, beacon endpoints, the wire event) lives in core; this file only says what h3 knows:\n// the Web Request, the socket peer, the process env, and when the response has settled.\nimport { defineEventHandler, getRequestIP, setCookie, toWebRequest, type EventHandler, type H3Event } from 'h3';\nimport { guarded, TAP_NUXT } 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 CamadaNuxtOptions = FetchCamadaOptions;\nexport type CamadaNuxtVars = FetchVars;\n\nconst VAR = '__camada'; // private: track() and scriptTag() are the API, not event.context\n\n// Every instance this module built, so a test or a hot reload can stop them all at once.\nconst instances = new Set<FetchCamada>();\n\n/** Reads the slot without trusting the event shape: a stub or a foreign event must not throw.\n * An inner event Nitro made for `event.$fetch` / SSR `useFetch` carries the outer event's context\n * on its mock request (`req.__unenv__`; Nitro copies only `_platform` and `waitUntil` into the\n * inner context), so the outer request's vars are found there — the page's own rid and session. */\nconst slotOf = (event: H3Event): FetchVars | undefined =>\n guarded(() => {\n const own = event.context[VAR] as FetchVars | undefined;\n if (own) return own;\n const outer = (event.node?.req as { __unenv__?: Record<string, unknown> } | undefined)?.__unenv__;\n return outer?.[VAR] as FetchVars | undefined;\n }, undefined);\n\n/**\n * The server middleware: `export default camada()` in `server/middleware/camada.ts`. Reads\n * `CAMADA_KEY` / `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL` from `process.env`; without a key\n * it is inert. Returns a web Response where camada answers (block, challenge, the beacon\n * endpoints) and falls through otherwise.\n */\nexport function camada(opts: CamadaNuxtOptions = {}): EventHandler {\n const cam = createFetchCamada({ tap: TAP_NUXT, sdk: SDK_ID, iife }, opts); // mode defaults to lazy in core: the middleware may run on an edge preset\n instances.add(cam);\n return defineEventHandler(async (event) => {\n // Nitro's own `event.$fetch` / SSR `useFetch` re-enters the h3 app for the same page view,\n // already captured on the outer request (a bare `$fetch` with no event is a request of its own).\n if (slotOf(event)) return;\n const req = guarded(() => toWebRequest(event), null);\n if (!req) return; // an event h3 cannot express as a Request is not ours to break\n // Building the Request puts a Node request body into a web stream; hand that stream back to\n // h3 so the app's readBody() reads the same bytes rather than an already-drained socket.\n if (req.body) event._requestBody = req.body;\n const ctx: FetchRequestContext = {\n // The socket address (or what the host preset stamped as clientAddress); never\n // X-Forwarded-For here, core resolves that under the trusted-proxy rules.\n peer: guarded(() => getRequestIP(event) ?? null, null),\n env: globalThis.process?.env,\n };\n const r = await cam.before(req, ctx);\n if (!r) return;\n if (r.response) return r.response;\n const vars = r.vars;\n event.context[VAR] = vars;\n const res = event.node?.res;\n // Set before the app runs so a handler's own redirect carries the session too; core only\n // hands a cookie for a new session, so an existing `_sfp` is never overwritten. h3 1.x\n // writes cookies through node.res, so an event without one gets no session, quietly.\n if (vars.sessionCookie && vars.sid && res) {\n const sid = vars.sid;\n guarded(() => setCookie(event, SESSION_COOKIE, sid, {\n path: '/', maxAge: SESSION_MAX_AGE, httpOnly: true, sameSite: 'lax', secure: new URL(req.url).protocol === 'https:',\n }), undefined);\n }\n // Ship with the real status once the response has settled — a Node response, or the mock a\n // web preset provides. An event with no response object at all ships now, with st null.\n if (typeof res?.once === 'function') res.once('finish', () => cam.after(req, vars, res.statusCode));\n else cam.after(req, vars, null);\n });\n}\n\n/**\n * Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,\n * `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`\n * (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event\n * through its rid and session; the user identifier is HMAC-hashed in-process. Never throws, and\n * a silent no-op where the middleware did not run.\n */\nexport const track = (event: H3Event, et: string, data?: { user?: string }): Promise<void> => coreTrack(slotOf(event), et, data);\n\n/** The `<script>` tag for an HTML response — `''` where the middleware did not run or the tenant turned the beacon off. */\nexport const scriptTag = (event: H3Event): string => coreScriptTag(slotOf(event));\n\n/** Test/reset hook: stops and drops every engine behind every `camada()` this module created. */\nexport function resetCamada(): void {\n for (const c of instances) c.reset();\n}\n","{\n \"name\": \"@camada/nuxt\",\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 Nuxt (h3 server middleware): snapshot enforcement, first-party challenge and beacon, track() outcomes, events shipped with the settled status\",\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 \"h3\": \"^1.12.0\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.10.0\",\n \"h3\": \"^1.15.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","// The Nitro side of the beacon: Nuxt renders `<head>` through the `render:html` hook, so the\n// tag goes in there rather than in every page. Typed structurally so this package never\n// depends on nitropack — the plugin file is `export default camadaNitroPlugin()`.\nimport type { H3Event } from 'h3';\nimport { scriptTag } from './camada.js';\n\nexport interface NitroAppLike { hooks: { hook(name: string, fn: (...a: any[]) => void): void } }\ninterface RenderHtml { head: string[] }\n\n/** `server/plugins/camada.ts`: pushes this request's beacon tag into the rendered `<head>`; nothing where the middleware did not run. */\nexport function camadaNitroPlugin(): (nitroApp: NitroAppLike) => void {\n return (nitroApp) => {\n nitroApp.hooks.hook('render:html', (html: RenderHtml, { event }: { event: H3Event }) => {\n const tag = scriptTag(event);\n if (tag) html.head.push(tag);\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,gBAA2G;AAC3G,kBAAkC;AAClC,mBAAwM;AACxM,yBAAiB;;;ACLf,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFSxC,IAAM,MAAM;AAGZ,IAAM,YAAY,oBAAI,IAAiB;AAMvC,IAAM,SAAS,CAAC,cACd,qBAAQ,MAAM;AACZ,QAAM,MAAM,MAAM,QAAQ,GAAG;AAC7B,MAAI,IAAK,QAAO;AAChB,QAAM,QAAS,MAAM,MAAM,KAA6D;AACxF,SAAO,QAAQ,GAAG;AACpB,GAAG,MAAS;AAQP,SAAS,OAAO,OAA0B,CAAC,GAAiB;AACjE,QAAM,UAAM,gCAAkB,EAAE,KAAK,sBAAU,KAAK,QAAQ,yBAAAA,QAAK,GAAG,IAAI;AACxE,YAAU,IAAI,GAAG;AACjB,aAAO,8BAAmB,OAAO,UAAU;AAGzC,QAAI,OAAO,KAAK,EAAG;AACnB,UAAM,UAAM,qBAAQ,UAAM,wBAAa,KAAK,GAAG,IAAI;AACnD,QAAI,CAAC,IAAK;AAGV,QAAI,IAAI,KAAM,OAAM,eAAe,IAAI;AACvC,UAAM,MAA2B;AAAA;AAAA;AAAA,MAG/B,UAAM,qBAAQ,UAAM,wBAAa,KAAK,KAAK,MAAM,IAAI;AAAA,MACrD,KAAK,WAAW,SAAS;AAAA,IAC3B;AACA,UAAM,IAAI,MAAM,IAAI,OAAO,KAAK,GAAG;AACnC,QAAI,CAAC,EAAG;AACR,QAAI,EAAE,SAAU,QAAO,EAAE;AACzB,UAAM,OAAO,EAAE;AACf,UAAM,QAAQ,GAAG,IAAI;AACrB,UAAM,MAAM,MAAM,MAAM;AAIxB,QAAI,KAAK,iBAAiB,KAAK,OAAO,KAAK;AACzC,YAAM,MAAM,KAAK;AACjB,+BAAQ,UAAM,qBAAU,OAAO,6BAAgB,KAAK;AAAA,QAClD,MAAM;AAAA,QAAK,QAAQ;AAAA,QAAiB,UAAU;AAAA,QAAM,UAAU;AAAA,QAAO,QAAQ,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa;AAAA,MAC7G,CAAC,GAAG,MAAS;AAAA,IACf;AAGA,QAAI,OAAO,KAAK,SAAS,WAAY,KAAI,KAAK,UAAU,MAAM,IAAI,MAAM,KAAK,MAAM,IAAI,UAAU,CAAC;AAAA,QAC7F,KAAI,MAAM,KAAK,MAAM,IAAI;AAAA,EAChC,CAAC;AACH;AASO,IAAM,QAAQ,CAAC,OAAgB,IAAY,aAA4C,aAAAC,OAAU,OAAO,KAAK,GAAG,IAAI,IAAI;AAGxH,IAAM,YAAY,CAAC,cAA2B,aAAAC,WAAc,OAAO,KAAK,CAAC;AAGzE,SAAS,cAAoB;AAClC,aAAW,KAAK,UAAW,GAAE,MAAM;AACrC;;;AGhFO,SAAS,oBAAsD;AACpE,SAAO,CAAC,aAAa;AACnB,aAAS,MAAM,KAAK,eAAe,CAAC,MAAkB,EAAE,MAAM,MAA0B;AACtF,YAAM,MAAM,UAAU,KAAK;AAC3B,UAAI,IAAK,MAAK,KAAK,KAAK,GAAG;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;","names":["iife","coreTrack","coreScriptTag"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EventHandler, H3Event } from 'h3';
|
|
2
|
+
import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
|
|
3
|
+
|
|
4
|
+
type CamadaNuxtOptions = FetchCamadaOptions;
|
|
5
|
+
type CamadaNuxtVars = FetchVars;
|
|
6
|
+
/**
|
|
7
|
+
* The server middleware: `export default camada()` in `server/middleware/camada.ts`. Reads
|
|
8
|
+
* `CAMADA_KEY` / `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL` from `process.env`; without a key
|
|
9
|
+
* it is inert. Returns a web Response where camada answers (block, challenge, the beacon
|
|
10
|
+
* endpoints) and falls through otherwise.
|
|
11
|
+
*/
|
|
12
|
+
declare function camada(opts?: CamadaNuxtOptions): EventHandler;
|
|
13
|
+
/**
|
|
14
|
+
* Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,
|
|
15
|
+
* `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`
|
|
16
|
+
* (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event
|
|
17
|
+
* through its rid and session; the user identifier is HMAC-hashed in-process. Never throws, and
|
|
18
|
+
* a silent no-op where the middleware did not run.
|
|
19
|
+
*/
|
|
20
|
+
declare const track: (event: H3Event, et: string, data?: {
|
|
21
|
+
user?: string;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
/** The `<script>` tag for an HTML response — `''` where the middleware did not run or the tenant turned the beacon off. */
|
|
24
|
+
declare const scriptTag: (event: H3Event) => string;
|
|
25
|
+
/** Test/reset hook: stops and drops every engine behind every `camada()` this module created. */
|
|
26
|
+
declare function resetCamada(): void;
|
|
27
|
+
|
|
28
|
+
interface NitroAppLike {
|
|
29
|
+
hooks: {
|
|
30
|
+
hook(name: string, fn: (...a: any[]) => void): void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** `server/plugins/camada.ts`: pushes this request's beacon tag into the rendered `<head>`; nothing where the middleware did not run. */
|
|
34
|
+
declare function camadaNitroPlugin(): (nitroApp: NitroAppLike) => void;
|
|
35
|
+
|
|
36
|
+
export { type CamadaNuxtOptions, type CamadaNuxtVars, type NitroAppLike, camada, camadaNitroPlugin, resetCamada, scriptTag, track };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EventHandler, H3Event } from 'h3';
|
|
2
|
+
import { FetchCamadaOptions, FetchVars } from '@camada/core/fetch';
|
|
3
|
+
|
|
4
|
+
type CamadaNuxtOptions = FetchCamadaOptions;
|
|
5
|
+
type CamadaNuxtVars = FetchVars;
|
|
6
|
+
/**
|
|
7
|
+
* The server middleware: `export default camada()` in `server/middleware/camada.ts`. Reads
|
|
8
|
+
* `CAMADA_KEY` / `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL` from `process.env`; without a key
|
|
9
|
+
* it is inert. Returns a web Response where camada answers (block, challenge, the beacon
|
|
10
|
+
* endpoints) and falls through otherwise.
|
|
11
|
+
*/
|
|
12
|
+
declare function camada(opts?: CamadaNuxtOptions): EventHandler;
|
|
13
|
+
/**
|
|
14
|
+
* Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,
|
|
15
|
+
* `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`
|
|
16
|
+
* (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event
|
|
17
|
+
* through its rid and session; the user identifier is HMAC-hashed in-process. Never throws, and
|
|
18
|
+
* a silent no-op where the middleware did not run.
|
|
19
|
+
*/
|
|
20
|
+
declare const track: (event: H3Event, et: string, data?: {
|
|
21
|
+
user?: string;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
/** The `<script>` tag for an HTML response — `''` where the middleware did not run or the tenant turned the beacon off. */
|
|
24
|
+
declare const scriptTag: (event: H3Event) => string;
|
|
25
|
+
/** Test/reset hook: stops and drops every engine behind every `camada()` this module created. */
|
|
26
|
+
declare function resetCamada(): void;
|
|
27
|
+
|
|
28
|
+
interface NitroAppLike {
|
|
29
|
+
hooks: {
|
|
30
|
+
hook(name: string, fn: (...a: any[]) => void): void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** `server/plugins/camada.ts`: pushes this request's beacon tag into the rendered `<head>`; nothing where the middleware did not run. */
|
|
34
|
+
declare function camadaNitroPlugin(): (nitroApp: NitroAppLike) => void;
|
|
35
|
+
|
|
36
|
+
export { type CamadaNuxtOptions, type CamadaNuxtVars, type NitroAppLike, camada, camadaNitroPlugin, resetCamada, scriptTag, track };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/camada.ts
|
|
2
|
+
import { defineEventHandler, getRequestIP, setCookie, toWebRequest } from "h3";
|
|
3
|
+
import { guarded, TAP_NUXT } from "@camada/core";
|
|
4
|
+
import { createFetchCamada, SESSION_COOKIE, SESSION_MAX_AGE, track as coreTrack, scriptTag as coreScriptTag } from "@camada/core/fetch";
|
|
5
|
+
import iife from "@camada/browser/iife-string";
|
|
6
|
+
|
|
7
|
+
// package.json
|
|
8
|
+
var name = "@camada/nuxt";
|
|
9
|
+
var version = "0.1.0";
|
|
10
|
+
|
|
11
|
+
// src/version.ts
|
|
12
|
+
var SDK_ID = `${name}/${version}`;
|
|
13
|
+
|
|
14
|
+
// src/camada.ts
|
|
15
|
+
var VAR = "__camada";
|
|
16
|
+
var instances = /* @__PURE__ */ new Set();
|
|
17
|
+
var slotOf = (event) => guarded(() => {
|
|
18
|
+
const own = event.context[VAR];
|
|
19
|
+
if (own) return own;
|
|
20
|
+
const outer = event.node?.req?.__unenv__;
|
|
21
|
+
return outer?.[VAR];
|
|
22
|
+
}, void 0);
|
|
23
|
+
function camada(opts = {}) {
|
|
24
|
+
const cam = createFetchCamada({ tap: TAP_NUXT, sdk: SDK_ID, iife }, opts);
|
|
25
|
+
instances.add(cam);
|
|
26
|
+
return defineEventHandler(async (event) => {
|
|
27
|
+
if (slotOf(event)) return;
|
|
28
|
+
const req = guarded(() => toWebRequest(event), null);
|
|
29
|
+
if (!req) return;
|
|
30
|
+
if (req.body) event._requestBody = req.body;
|
|
31
|
+
const ctx = {
|
|
32
|
+
// The socket address (or what the host preset stamped as clientAddress); never
|
|
33
|
+
// X-Forwarded-For here, core resolves that under the trusted-proxy rules.
|
|
34
|
+
peer: guarded(() => getRequestIP(event) ?? null, null),
|
|
35
|
+
env: globalThis.process?.env
|
|
36
|
+
};
|
|
37
|
+
const r = await cam.before(req, ctx);
|
|
38
|
+
if (!r) return;
|
|
39
|
+
if (r.response) return r.response;
|
|
40
|
+
const vars = r.vars;
|
|
41
|
+
event.context[VAR] = vars;
|
|
42
|
+
const res = event.node?.res;
|
|
43
|
+
if (vars.sessionCookie && vars.sid && res) {
|
|
44
|
+
const sid = vars.sid;
|
|
45
|
+
guarded(() => setCookie(event, SESSION_COOKIE, sid, {
|
|
46
|
+
path: "/",
|
|
47
|
+
maxAge: SESSION_MAX_AGE,
|
|
48
|
+
httpOnly: true,
|
|
49
|
+
sameSite: "lax",
|
|
50
|
+
secure: new URL(req.url).protocol === "https:"
|
|
51
|
+
}), void 0);
|
|
52
|
+
}
|
|
53
|
+
if (typeof res?.once === "function") res.once("finish", () => cam.after(req, vars, res.statusCode));
|
|
54
|
+
else cam.after(req, vars, null);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
var track = (event, et, data) => coreTrack(slotOf(event), et, data);
|
|
58
|
+
var scriptTag = (event) => coreScriptTag(slotOf(event));
|
|
59
|
+
function resetCamada() {
|
|
60
|
+
for (const c of instances) c.reset();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/nitro.ts
|
|
64
|
+
function camadaNitroPlugin() {
|
|
65
|
+
return (nitroApp) => {
|
|
66
|
+
nitroApp.hooks.hook("render:html", (html, { event }) => {
|
|
67
|
+
const tag = scriptTag(event);
|
|
68
|
+
if (tag) html.head.push(tag);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
camada,
|
|
74
|
+
camadaNitroPlugin,
|
|
75
|
+
resetCamada,
|
|
76
|
+
scriptTag,
|
|
77
|
+
track
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/camada.ts","../package.json","../src/version.ts","../src/nitro.ts"],"sourcesContent":["// @camada/nuxt — the h3 binding over @camada/core/fetch. The pipeline (verdict, block,\n// challenge, beacon endpoints, the wire event) lives in core; this file only says what h3 knows:\n// the Web Request, the socket peer, the process env, and when the response has settled.\nimport { defineEventHandler, getRequestIP, setCookie, toWebRequest, type EventHandler, type H3Event } from 'h3';\nimport { guarded, TAP_NUXT } 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 CamadaNuxtOptions = FetchCamadaOptions;\nexport type CamadaNuxtVars = FetchVars;\n\nconst VAR = '__camada'; // private: track() and scriptTag() are the API, not event.context\n\n// Every instance this module built, so a test or a hot reload can stop them all at once.\nconst instances = new Set<FetchCamada>();\n\n/** Reads the slot without trusting the event shape: a stub or a foreign event must not throw.\n * An inner event Nitro made for `event.$fetch` / SSR `useFetch` carries the outer event's context\n * on its mock request (`req.__unenv__`; Nitro copies only `_platform` and `waitUntil` into the\n * inner context), so the outer request's vars are found there — the page's own rid and session. */\nconst slotOf = (event: H3Event): FetchVars | undefined =>\n guarded(() => {\n const own = event.context[VAR] as FetchVars | undefined;\n if (own) return own;\n const outer = (event.node?.req as { __unenv__?: Record<string, unknown> } | undefined)?.__unenv__;\n return outer?.[VAR] as FetchVars | undefined;\n }, undefined);\n\n/**\n * The server middleware: `export default camada()` in `server/middleware/camada.ts`. Reads\n * `CAMADA_KEY` / `CAMADA_INGEST_URL` / `CAMADA_SNAPSHOT_URL` from `process.env`; without a key\n * it is inert. Returns a web Response where camada answers (block, challenge, the beacon\n * endpoints) and falls through otherwise.\n */\nexport function camada(opts: CamadaNuxtOptions = {}): EventHandler {\n const cam = createFetchCamada({ tap: TAP_NUXT, sdk: SDK_ID, iife }, opts); // mode defaults to lazy in core: the middleware may run on an edge preset\n instances.add(cam);\n return defineEventHandler(async (event) => {\n // Nitro's own `event.$fetch` / SSR `useFetch` re-enters the h3 app for the same page view,\n // already captured on the outer request (a bare `$fetch` with no event is a request of its own).\n if (slotOf(event)) return;\n const req = guarded(() => toWebRequest(event), null);\n if (!req) return; // an event h3 cannot express as a Request is not ours to break\n // Building the Request puts a Node request body into a web stream; hand that stream back to\n // h3 so the app's readBody() reads the same bytes rather than an already-drained socket.\n if (req.body) event._requestBody = req.body;\n const ctx: FetchRequestContext = {\n // The socket address (or what the host preset stamped as clientAddress); never\n // X-Forwarded-For here, core resolves that under the trusted-proxy rules.\n peer: guarded(() => getRequestIP(event) ?? null, null),\n env: globalThis.process?.env,\n };\n const r = await cam.before(req, ctx);\n if (!r) return;\n if (r.response) return r.response;\n const vars = r.vars;\n event.context[VAR] = vars;\n const res = event.node?.res;\n // Set before the app runs so a handler's own redirect carries the session too; core only\n // hands a cookie for a new session, so an existing `_sfp` is never overwritten. h3 1.x\n // writes cookies through node.res, so an event without one gets no session, quietly.\n if (vars.sessionCookie && vars.sid && res) {\n const sid = vars.sid;\n guarded(() => setCookie(event, SESSION_COOKIE, sid, {\n path: '/', maxAge: SESSION_MAX_AGE, httpOnly: true, sameSite: 'lax', secure: new URL(req.url).protocol === 'https:',\n }), undefined);\n }\n // Ship with the real status once the response has settled — a Node response, or the mock a\n // web preset provides. An event with no response object at all ships now, with st null.\n if (typeof res?.once === 'function') res.once('finish', () => cam.after(req, vars, res.statusCode));\n else cam.after(req, vars, null);\n });\n}\n\n/**\n * Records an outcome the app knows and the wire cannot show: `login_failed`, `login_succeeded`,\n * `signup`, `password_reset`, `mfa_failed`, `payment_failed`, `payment_succeeded`, `coupon_failed`\n * (free-form; that vocabulary is what the analyst's rules read). Joined to this request's event\n * through its rid and session; the user identifier is HMAC-hashed in-process. Never throws, and\n * a silent no-op where the middleware did not run.\n */\nexport const track = (event: H3Event, et: string, data?: { user?: string }): Promise<void> => coreTrack(slotOf(event), et, data);\n\n/** The `<script>` tag for an HTML response — `''` where the middleware did not run or the tenant turned the beacon off. */\nexport const scriptTag = (event: H3Event): string => coreScriptTag(slotOf(event));\n\n/** Test/reset hook: stops and drops every engine behind every `camada()` this module created. */\nexport function resetCamada(): void {\n for (const c of instances) c.reset();\n}\n","{\n \"name\": \"@camada/nuxt\",\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 Nuxt (h3 server middleware): snapshot enforcement, first-party challenge and beacon, track() outcomes, events shipped with the settled status\",\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 \"h3\": \"^1.12.0\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.10.0\",\n \"h3\": \"^1.15.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","// The Nitro side of the beacon: Nuxt renders `<head>` through the `render:html` hook, so the\n// tag goes in there rather than in every page. Typed structurally so this package never\n// depends on nitropack — the plugin file is `export default camadaNitroPlugin()`.\nimport type { H3Event } from 'h3';\nimport { scriptTag } from './camada.js';\n\nexport interface NitroAppLike { hooks: { hook(name: string, fn: (...a: any[]) => void): void } }\ninterface RenderHtml { head: string[] }\n\n/** `server/plugins/camada.ts`: pushes this request's beacon tag into the rendered `<head>`; nothing where the middleware did not run. */\nexport function camadaNitroPlugin(): (nitroApp: NitroAppLike) => void {\n return (nitroApp) => {\n nitroApp.hooks.hook('render:html', (html: RenderHtml, { event }: { event: H3Event }) => {\n const tag = scriptTag(event);\n if (tag) html.head.push(tag);\n });\n };\n}\n"],"mappings":";AAGA,SAAS,oBAAoB,cAAc,WAAW,oBAAqD;AAC3G,SAAS,SAAS,gBAAgB;AAClC,SAAS,mBAAmB,gBAAgB,iBAAiB,SAAS,WAAW,aAAa,qBAA0G;AACxM,OAAO,UAAU;;;ACLf,WAAQ;AACR,cAAW;;;ACCN,IAAM,SAAS,GAAG,IAAI,IAAI,OAAO;;;AFSxC,IAAM,MAAM;AAGZ,IAAM,YAAY,oBAAI,IAAiB;AAMvC,IAAM,SAAS,CAAC,UACd,QAAQ,MAAM;AACZ,QAAM,MAAM,MAAM,QAAQ,GAAG;AAC7B,MAAI,IAAK,QAAO;AAChB,QAAM,QAAS,MAAM,MAAM,KAA6D;AACxF,SAAO,QAAQ,GAAG;AACpB,GAAG,MAAS;AAQP,SAAS,OAAO,OAA0B,CAAC,GAAiB;AACjE,QAAM,MAAM,kBAAkB,EAAE,KAAK,UAAU,KAAK,QAAQ,KAAK,GAAG,IAAI;AACxE,YAAU,IAAI,GAAG;AACjB,SAAO,mBAAmB,OAAO,UAAU;AAGzC,QAAI,OAAO,KAAK,EAAG;AACnB,UAAM,MAAM,QAAQ,MAAM,aAAa,KAAK,GAAG,IAAI;AACnD,QAAI,CAAC,IAAK;AAGV,QAAI,IAAI,KAAM,OAAM,eAAe,IAAI;AACvC,UAAM,MAA2B;AAAA;AAAA;AAAA,MAG/B,MAAM,QAAQ,MAAM,aAAa,KAAK,KAAK,MAAM,IAAI;AAAA,MACrD,KAAK,WAAW,SAAS;AAAA,IAC3B;AACA,UAAM,IAAI,MAAM,IAAI,OAAO,KAAK,GAAG;AACnC,QAAI,CAAC,EAAG;AACR,QAAI,EAAE,SAAU,QAAO,EAAE;AACzB,UAAM,OAAO,EAAE;AACf,UAAM,QAAQ,GAAG,IAAI;AACrB,UAAM,MAAM,MAAM,MAAM;AAIxB,QAAI,KAAK,iBAAiB,KAAK,OAAO,KAAK;AACzC,YAAM,MAAM,KAAK;AACjB,cAAQ,MAAM,UAAU,OAAO,gBAAgB,KAAK;AAAA,QAClD,MAAM;AAAA,QAAK,QAAQ;AAAA,QAAiB,UAAU;AAAA,QAAM,UAAU;AAAA,QAAO,QAAQ,IAAI,IAAI,IAAI,GAAG,EAAE,aAAa;AAAA,MAC7G,CAAC,GAAG,MAAS;AAAA,IACf;AAGA,QAAI,OAAO,KAAK,SAAS,WAAY,KAAI,KAAK,UAAU,MAAM,IAAI,MAAM,KAAK,MAAM,IAAI,UAAU,CAAC;AAAA,QAC7F,KAAI,MAAM,KAAK,MAAM,IAAI;AAAA,EAChC,CAAC;AACH;AASO,IAAM,QAAQ,CAAC,OAAgB,IAAY,SAA4C,UAAU,OAAO,KAAK,GAAG,IAAI,IAAI;AAGxH,IAAM,YAAY,CAAC,UAA2B,cAAc,OAAO,KAAK,CAAC;AAGzE,SAAS,cAAoB;AAClC,aAAW,KAAK,UAAW,GAAE,MAAM;AACrC;;;AGhFO,SAAS,oBAAsD;AACpE,SAAO,CAAC,aAAa;AACnB,aAAS,MAAM,KAAK,eAAe,CAAC,MAAkB,EAAE,MAAM,MAA0B;AACtF,YAAM,MAAM,UAAU,KAAK;AAC3B,UAAI,IAAK,MAAK,KAAK,KAAK,GAAG;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@camada/nuxt",
|
|
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 Nuxt (h3 server middleware): snapshot enforcement, first-party challenge and beacon, track() outcomes, events shipped with the settled status",
|
|
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
|
+
"h3": "^1.12.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/camada-app/camada-nuxt.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/camada-app/camada-nuxt#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/camada-app/camada-nuxt/issues"
|
|
39
|
+
}
|
|
40
|
+
}
|