@camada/nuxt 0.1.3 → 0.1.4

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/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var import_iife_string = __toESM(require("@camada/browser/iife-string"), 1);
46
46
 
47
47
  // package.json
48
48
  var name = "@camada/nuxt";
49
- var version = "0.1.3";
49
+ var version = "0.1.4";
50
50
 
51
51
  // src/version.ts
52
52
  var SDK_ID = `${name}/${version}`;
@@ -76,7 +76,16 @@ function camada(opts = {}) {
76
76
  };
77
77
  const r = await cam.before(req, ctx);
78
78
  if (!r) return;
79
- if (r.response) return r.response;
79
+ if (r.response) {
80
+ const out = r.response;
81
+ if (out.headers.has("content-security-policy")) {
82
+ (0, import_core.guarded)(() => {
83
+ event.node?.res?.removeHeader("content-security-policy");
84
+ event.node?.res?.removeHeader("content-security-policy-report-only");
85
+ }, void 0);
86
+ }
87
+ return out;
88
+ }
80
89
  const vars = r.vars;
81
90
  event.context[VAR] = vars;
82
91
  const res = event.node?.res;
@@ -1 +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, setResponseHeader, 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 // x-rid, set up front like the cookie: h3 keeps node.res headers through the app's answer,\n // a returned web Response included. Camada's own answers returned above never get it.\n if (res) guarded(() => setResponseHeader(event, 'x-rid', vars.rid), undefined);\n // Ship with the real status once the response has settled — a Node response, or the mock a\n // web preset provides. 'close' alone means the client left mid-response (an aborted SSE):\n // the status set so far and dur up to the abort, the fetch adapters' cancel semantics; it\n // also follows every 'finish', so a guard keeps it to one event. An event with no response\n // object at all ships now, with st and dur null: nothing here can see when its response\n // settles, and a dur taken now would be ~0.\n if (typeof res?.once === 'function') {\n let shipped = false;\n const ship = () => { if (!shipped) { shipped = true; cam.after(req, vars, res.statusCode); } };\n res.once('finish', ship);\n res.once('close', ship);\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.3\",\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,gBAA8H;AAC9H,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,IAAK,0BAAQ,UAAM,6BAAkB,OAAO,SAAS,KAAK,GAAG,GAAG,MAAS;AAO7E,QAAI,OAAO,KAAK,SAAS,YAAY;AACnC,UAAI,UAAU;AACd,YAAM,OAAO,MAAM;AAAE,YAAI,CAAC,SAAS;AAAE,oBAAU;AAAM,cAAI,MAAM,KAAK,MAAM,IAAI,UAAU;AAAA,QAAG;AAAA,MAAE;AAC7F,UAAI,KAAK,UAAU,IAAI;AACvB,UAAI,KAAK,SAAS,IAAI;AAAA,IACxB,MAAO,KAAI,MAAM,KAAK,MAAM,IAAI;AAAA,EAClC,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;;;AG3FO,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"]}
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, setResponseHeader, 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) {\n // h3 keeps headers an earlier middleware put on node.res and lays the Response's over them: the\n // challenge page's own CSP replaces an app policy, but an app's Report-Only policy would still ride\n // along and report the solver as a violation. Drop both first (camada-all-mwpo).\n const out = r.response;\n if (out.headers.has('content-security-policy')) {\n guarded(() => {\n event.node?.res?.removeHeader('content-security-policy');\n event.node?.res?.removeHeader('content-security-policy-report-only');\n }, undefined);\n }\n return out;\n }\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 // x-rid, set up front like the cookie: h3 keeps node.res headers through the app's answer,\n // a returned web Response included. Camada's own answers returned above never get it.\n if (res) guarded(() => setResponseHeader(event, 'x-rid', vars.rid), undefined);\n // Ship with the real status once the response has settled — a Node response, or the mock a\n // web preset provides. 'close' alone means the client left mid-response (an aborted SSE):\n // the status set so far and dur up to the abort, the fetch adapters' cancel semantics; it\n // also follows every 'finish', so a guard keeps it to one event. An event with no response\n // object at all ships now, with st and dur null: nothing here can see when its response\n // settles, and a dur taken now would be ~0.\n if (typeof res?.once === 'function') {\n let shipped = false;\n const ship = () => { if (!shipped) { shipped = true; cam.after(req, vars, res.statusCode); } };\n res.once('finish', ship);\n res.once('close', ship);\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.4\",\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 \"import\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.cts\",\n \"default\": \"./dist/index.cjs\"\n }\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":"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","names":["iife","coreTrack","coreScriptTag"]}
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import iife from "@camada/browser/iife-string";
6
6
 
7
7
  // package.json
8
8
  var name = "@camada/nuxt";
9
- var version = "0.1.3";
9
+ var version = "0.1.4";
10
10
 
11
11
  // src/version.ts
12
12
  var SDK_ID = `${name}/${version}`;
@@ -36,7 +36,16 @@ function camada(opts = {}) {
36
36
  };
37
37
  const r = await cam.before(req, ctx);
38
38
  if (!r) return;
39
- if (r.response) return r.response;
39
+ if (r.response) {
40
+ const out = r.response;
41
+ if (out.headers.has("content-security-policy")) {
42
+ guarded(() => {
43
+ event.node?.res?.removeHeader("content-security-policy");
44
+ event.node?.res?.removeHeader("content-security-policy-report-only");
45
+ }, void 0);
46
+ }
47
+ return out;
48
+ }
40
49
  const vars = r.vars;
41
50
  event.context[VAR] = vars;
42
51
  const res = event.node?.res;
package/dist/index.js.map CHANGED
@@ -1 +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, setResponseHeader, 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 // x-rid, set up front like the cookie: h3 keeps node.res headers through the app's answer,\n // a returned web Response included. Camada's own answers returned above never get it.\n if (res) guarded(() => setResponseHeader(event, 'x-rid', vars.rid), undefined);\n // Ship with the real status once the response has settled — a Node response, or the mock a\n // web preset provides. 'close' alone means the client left mid-response (an aborted SSE):\n // the status set so far and dur up to the abort, the fetch adapters' cancel semantics; it\n // also follows every 'finish', so a guard keeps it to one event. An event with no response\n // object at all ships now, with st and dur null: nothing here can see when its response\n // settles, and a dur taken now would be ~0.\n if (typeof res?.once === 'function') {\n let shipped = false;\n const ship = () => { if (!shipped) { shipped = true; cam.after(req, vars, res.statusCode); } };\n res.once('finish', ship);\n res.once('close', ship);\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.3\",\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,mBAAmB,oBAAqD;AAC9H,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,IAAK,SAAQ,MAAM,kBAAkB,OAAO,SAAS,KAAK,GAAG,GAAG,MAAS;AAO7E,QAAI,OAAO,KAAK,SAAS,YAAY;AACnC,UAAI,UAAU;AACd,YAAM,OAAO,MAAM;AAAE,YAAI,CAAC,SAAS;AAAE,oBAAU;AAAM,cAAI,MAAM,KAAK,MAAM,IAAI,UAAU;AAAA,QAAG;AAAA,MAAE;AAC7F,UAAI,KAAK,UAAU,IAAI;AACvB,UAAI,KAAK,SAAS,IAAI;AAAA,IACxB,MAAO,KAAI,MAAM,KAAK,MAAM,IAAI;AAAA,EAClC,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;;;AG3FO,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":[]}
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, setResponseHeader, 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) {\n // h3 keeps headers an earlier middleware put on node.res and lays the Response's over them: the\n // challenge page's own CSP replaces an app policy, but an app's Report-Only policy would still ride\n // along and report the solver as a violation. Drop both first (camada-all-mwpo).\n const out = r.response;\n if (out.headers.has('content-security-policy')) {\n guarded(() => {\n event.node?.res?.removeHeader('content-security-policy');\n event.node?.res?.removeHeader('content-security-policy-report-only');\n }, undefined);\n }\n return out;\n }\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 // x-rid, set up front like the cookie: h3 keeps node.res headers through the app's answer,\n // a returned web Response included. Camada's own answers returned above never get it.\n if (res) guarded(() => setResponseHeader(event, 'x-rid', vars.rid), undefined);\n // Ship with the real status once the response has settled — a Node response, or the mock a\n // web preset provides. 'close' alone means the client left mid-response (an aborted SSE):\n // the status set so far and dur up to the abort, the fetch adapters' cancel semantics; it\n // also follows every 'finish', so a guard keeps it to one event. An event with no response\n // object at all ships now, with st and dur null: nothing here can see when its response\n // settles, and a dur taken now would be ~0.\n if (typeof res?.once === 'function') {\n let shipped = false;\n const ship = () => { if (!shipped) { shipped = true; cam.after(req, vars, res.statusCode); } };\n res.once('finish', ship);\n res.once('close', ship);\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.4\",\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 \"import\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.cts\",\n \"default\": \"./dist/index.cjs\"\n }\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":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camada/nuxt",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,17 +14,22 @@
14
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
15
  "exports": {
16
16
  ".": {
17
- "types": "./dist/index.d.ts",
18
- "import": "./dist/index.js",
19
- "require": "./dist/index.cjs"
17
+ "import": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ }
20
25
  }
21
26
  },
22
27
  "main": "./dist/index.cjs",
23
28
  "module": "./dist/index.js",
24
29
  "types": "./dist/index.d.ts",
25
30
  "dependencies": {
26
- "@camada/core": "^0.5.1",
27
- "@camada/browser": "^0.3.0"
31
+ "@camada/core": "^0.5.2",
32
+ "@camada/browser": "^0.3.2"
28
33
  },
29
34
  "peerDependencies": {
30
35
  "h3": "^1.12.0"