@quatronic/sdk 0.7.0-rc.6 → 0.7.0-rc.7

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.
Files changed (35) hide show
  1. package/dist/auth/index.js +1 -1
  2. package/dist/{chunk-G7VPNC6M.js → chunk-5MNPBJYS.js} +11 -5
  3. package/dist/{chunk-G7VPNC6M.js.map → chunk-5MNPBJYS.js.map} +1 -1
  4. package/dist/{chunk-MET4SIMB.js → chunk-EK6TGWWT.js} +8 -2
  5. package/dist/chunk-EK6TGWWT.js.map +1 -0
  6. package/dist/{chunk-TEBDKNUB.js → chunk-ROHIN74F.js} +2 -2
  7. package/dist/{chunk-HQT57ITV.js → chunk-ZZTO5Z3N.js} +2 -2
  8. package/dist/eslint/index.js +22 -2
  9. package/dist/eslint/index.js.map +1 -1
  10. package/dist/next/auth/index.js +1 -1
  11. package/dist/next/auth/index.js.map +1 -1
  12. package/dist/next/better-auth/index.js +1 -1
  13. package/dist/next/connections/index.js +3 -3
  14. package/dist/next/security/index.js +3 -3
  15. package/dist/next/sentry/client.d.ts +1 -0
  16. package/dist/next/sentry/client.js +2 -2
  17. package/dist/next/sentry/register.js +2 -2
  18. package/dist/node/connections/index.js +3 -3
  19. package/dist/node/identity/index.js +2 -2
  20. package/dist/node/sentry/register.js +2 -2
  21. package/dist/observability/index.d.ts +10 -0
  22. package/dist/observability/index.js +7 -0
  23. package/dist/observability/index.js.map +1 -0
  24. package/migrations/no-raw-fetch/codemod.mjs +97 -0
  25. package/migrations/no-raw-fetch/instructions.md +68 -0
  26. package/migrations/no-raw-fetch/meta.json +6 -0
  27. package/migrations/report-error/codemod.mjs +94 -0
  28. package/migrations/report-error/instructions.md +46 -0
  29. package/migrations/report-error/meta.json +6 -0
  30. package/package.json +12 -3
  31. package/dist/chunk-MET4SIMB.js.map +0 -1
  32. package/dist/next/auth/migrations/0001_init.sql +0 -31
  33. package/dist/next/auth/migrations/0002_id_token.sql +0 -1
  34. /package/dist/{chunk-TEBDKNUB.js.map → chunk-ROHIN74F.js.map} +0 -0
  35. /package/dist/{chunk-HQT57ITV.js.map → chunk-ZZTO5Z3N.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  setUserContext
3
- } from "../chunk-MET4SIMB.js";
3
+ } from "../chunk-EK6TGWWT.js";
4
4
 
5
5
  // src/auth/PortalProvider.tsx
6
6
  import { useEffect as useEffect2, useState } from "react";
@@ -1,9 +1,9 @@
1
- import {
2
- setTelemetryHooks
3
- } from "./chunk-MET4SIMB.js";
4
1
  import {
5
2
  makeDevSinkTransport
6
3
  } from "./chunk-7ASQHY7O.js";
4
+ import {
5
+ setTelemetryHooks
6
+ } from "./chunk-EK6TGWWT.js";
7
7
 
8
8
  // src/sentry-core/noise.ts
9
9
  var NOISE_EXACT = /* @__PURE__ */ new Set(["/favicon.ico", "/api/auth/me"]);
@@ -180,11 +180,17 @@ function initSentry(sentry, runtime, cfg, extra = {}) {
180
180
  console.error("[qore] Sentry init failed", err);
181
181
  return false;
182
182
  }
183
- setTelemetryHooks({ log: sentry.logger, setUser: (u) => sentry.setUser(u) });
183
+ setTelemetryHooks({
184
+ log: sentry.logger,
185
+ setUser: (u) => sentry.setUser(u),
186
+ captureException: (err, hint) => {
187
+ sentry.captureException(err, hint);
188
+ }
189
+ });
184
190
  return true;
185
191
  }
186
192
 
187
193
  export {
188
194
  initSentry
189
195
  };
190
- //# sourceMappingURL=chunk-G7VPNC6M.js.map
196
+ //# sourceMappingURL=chunk-5MNPBJYS.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sentry-core/noise.ts","../src/sentry-core/sampler.ts","../src/sentry-core/scrub.ts","../src/sentry-core/options.ts","../src/sentry-core/init.ts"],"sourcesContent":["// Pure predicate deciding whether an HTTP request is infrastructural noise that\n// should never become a span: a `_rsc` query param, `/favicon.ico`, the SDK's\n// session poll, source maps, `/_next/*`, plus the caller's prefixes and health path.\n\nexport interface NoiseFilterOptions {\n healthPath?: string\n noisePrefixes?: string[]\n}\n\n// `/api/auth/me` is the SDK's own session poll (PortalProvider fires it on every\n// page mount): a heartbeat, not user activity.\nconst NOISE_EXACT = new Set([\"/favicon.ico\", \"/api/auth/me\"])\n\n// React Server Components navigations/prefetches carry a `?_rsc=` query param;\n// they look like real routes, so a path-prefix filter misses them.\nconst RSC_QUERY_RE = /[?&]_rsc=/\n\nexport function isNoisePath(rawUrl: string, opts: NoiseFilterOptions = {}): boolean {\n if (RSC_QUERY_RE.test(rawUrl)) return true\n const path = (rawUrl.split(\"?\")[0] ?? \"\").trim()\n if (!path) return false\n if (NOISE_EXACT.has(path)) return true\n if (path.endsWith(\".map\")) return true\n if (path.startsWith(\"/_next/\")) return true\n if ((opts.noisePrefixes ?? []).some((p) => path.startsWith(p))) return true\n if (opts.healthPath && path === opts.healthPath) return true\n return false\n}\n","import { isNoisePath } from \"./noise\"\n\nexport interface SamplingContextLike {\n name: string\n attributes?: Record<string, unknown>\n inheritOrSampleWith: (fallback: number) => number\n}\n\nconst MUTATIONS = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"])\n\nconst str = (v: unknown): string | undefined => (typeof v === \"string\" && v ? v : undefined)\n\nfunction pathOf(ctx: SamplingContextLike): string | undefined {\n const a = ctx.attributes ?? {}\n return str(a[\"url.path\"]) ?? str(a[\"http.target\"]) ?? ctx.name.split(\" \")[1]\n}\n\nexport function createTracesSampler(policy: { production: boolean; readRate: number; healthPath: string }) {\n return (ctx: SamplingContextLike): number => {\n const path = pathOf(ctx)\n if (path && isNoisePath(path, { healthPath: policy.healthPath })) return 0\n const a = ctx.attributes ?? {}\n const method = (str(a[\"http.request.method\"]) ?? str(a[\"http.method\"]) ?? \"\").toUpperCase()\n if (MUTATIONS.has(method)) return 1\n return ctx.inheritOrSampleWith(policy.production ? policy.readRate : 1)\n }\n}\n","export const REDACTED = \"●●●\"\n\n// Bounded and anchored (`(?<![...])` + `{1,64}` local part, no `.` in the\n// domain-char class) so it cannot backtrack catastrophically on long runs of\n// email-charset bytes — see the fix-round-1 report for the O(n²) it replaced.\nconst EMAIL = /(?<![A-Z0-9._%+-])[A-Z0-9._%+-]{1,64}@[A-Z0-9-]+(?:\\.[A-Z0-9-]+)*\\.[A-Z]{2,}/gi\nconst JWT = /eyJ[\\w-]+\\.[\\w-]+\\.[\\w-]+/g\nconst AUTH_SCHEME = /\\b(Bearer|Basic)\\s+\\S+/gi\nconst PARAMS_TAIL = /\\bparams:\\s.*$/s\nexport const SECRET_PARAMS: ReadonlySet<string> = new Set([\"token\", \"code\", \"state\", \"password\", \"secret\", \"key\", \"session\", \"access_token\", \"refresh_token\", \"id_token\"])\nconst SECRET_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-qdc-connection-token\",\n \"x-forwarded-for\",\n \"x-real-ip\",\n \"forwarded\",\n \"user-agent\",\n])\nconst DROP_LOG_ATTRS = new Set([\"ip\", \"ip_address\", \"user_agent\", \"email\", \"user_email\", \"server.address\"])\nconst REDACT_KEYS = new Set([\"db.connection_string\", \"db.user\"])\nconst REDACT_PREFIXES = [\"net.peer.\", \"net.sock.\", \"net.host.\", \"network.peer.\", \"network.local.\"]\nconst DROP_SPAN_KEYS = new Set([\"user_agent.original\", \"http.user_agent\", \"client.address\", \"http.client_ip\"])\nconst DROP_SPAN_PREFIXES = [\"http.request.header.\", \"http.response.header.\"]\nconst URL_SPAN_KEYS = new Set([\"url.full\", \"http.url\", \"url\", \"http.target\", \"url.query\", \"http.query\"])\n\nexport function redactText(s: string): string {\n return s\n .replace(PARAMS_TAIL, \"params: [redacted]\")\n .replace(AUTH_SCHEME, \"$1 [redacted]\")\n .replace(JWT, \"[jwt]\")\n .replace(EMAIL, \"[email]\")\n}\n\nfunction redactQuery(query: string): string {\n return query\n .split(\"&\")\n .map((pair) => {\n const [k, v] = pair.split(\"=\", 2)\n return v !== undefined && SECRET_PARAMS.has(k!.toLowerCase()) ? `${k}=[redacted]` : pair\n })\n .join(\"&\")\n}\n\nfunction redactUrl(url: string): string {\n const i = url.indexOf(\"?\")\n return i < 0 ? url : url.slice(0, i + 1) + redactQuery(url.slice(i + 1))\n}\n\n// For values that are either a URL/path or a bare query string (no `?`).\nfunction redactUrlOrQuery(v: string): string {\n return v.includes(\"?\") ? redactUrl(v) : redactQuery(v)\n}\n\ntype Dict = Record<string, unknown>\n\nexport interface SentryEventLike {\n server_name?: string\n user?: Dict\n request?: { url?: string; query_string?: unknown; cookies?: unknown; headers?: Record<string, string>; data?: unknown }\n message?: string\n exception?: { values?: Array<{ type?: string; value?: string }> }\n breadcrumbs?: Array<{ message?: string; data?: Dict }>\n contexts?: { nextjs?: Dict } & Dict\n}\n\nexport function scrubEvent<E extends SentryEventLike>(e: E): E {\n delete e.server_name\n if (e.user) e.user = e.user.id ? { id: e.user.id } : {}\n if (e.request) {\n delete e.request.cookies\n delete e.request.data\n if (e.request.url) e.request.url = redactUrl(e.request.url)\n if (typeof e.request.query_string === \"string\") e.request.query_string = redactQuery(e.request.query_string)\n if (e.request.headers) {\n e.request.headers = Object.fromEntries(\n Object.entries(e.request.headers).filter(([k]) => !SECRET_HEADERS.has(k.toLowerCase())),\n )\n }\n }\n const nextjs = e.contexts?.nextjs\n if (typeof nextjs?.request_path === \"string\") nextjs.request_path = redactUrl(nextjs.request_path)\n if (e.message) e.message = redactText(e.message)\n for (const v of e.exception?.values ?? []) if (v.value) v.value = redactText(v.value)\n for (const b of e.breadcrumbs ?? []) {\n if (b.message) b.message = redactText(b.message)\n const d = b.data\n if (!d) continue\n for (const k of [\"url\", \"from\", \"to\"]) if (typeof d[k] === \"string\") d[k] = redactUrl(d[k])\n if (typeof d[\"http.query\"] === \"string\") d[\"http.query\"] = redactUrlOrQuery(d[\"http.query\"])\n if (Array.isArray(d.arguments)) d.arguments = d.arguments.map((a) => (typeof a === \"string\" ? redactText(a) : a))\n }\n return e\n}\n\nexport interface SentryLogLike {\n message: string\n attributes?: Dict\n}\n\nexport function scrubLog<L extends SentryLogLike>(l: L): L {\n l.message = redactText(l.message)\n if (l.attributes) {\n const out: Dict = {}\n for (const [k, v] of Object.entries(l.attributes)) {\n if (DROP_LOG_ATTRS.has(k)) continue\n out[k] = typeof v === \"string\" ? redactText(v) : v\n }\n l.attributes = out\n }\n return l\n}\n\nexport interface SentrySpanLike {\n description?: string\n data?: Dict\n}\n\nexport function scrubSpan<S extends SentrySpanLike>(s: S): S {\n // Browser page-metric spans are described by the full page URL.\n if (typeof s.description === \"string\") s.description = redactUrl(s.description)\n for (const [k, v] of Object.entries(s.data ?? {})) {\n if (DROP_SPAN_KEYS.has(k) || DROP_SPAN_PREFIXES.some((p) => k.startsWith(p))) delete s.data![k]\n else if (REDACT_KEYS.has(k) || REDACT_PREFIXES.some((p) => k.startsWith(p))) s.data![k] = REDACTED\n else if (typeof v === \"string\" && URL_SPAN_KEYS.has(k)) s.data![k] = redactUrlOrQuery(v)\n }\n return s\n}\n","import { makeDevSinkTransport } from \"./dev-sink\"\nimport type { SentryConfig } from \"./env\"\nimport { createTracesSampler, type SamplingContextLike } from \"./sampler\"\nimport { SECRET_PARAMS, scrubEvent, scrubLog, scrubSpan } from \"./scrub\"\n\nexport type Runtime = \"node\" | \"edge\" | \"browser\"\n\n// Structural copy of @sentry/core's DataCollection (10.75+): stop PII at collection,\n// the scrubbers stay as the backstop. Older 10.x ignores the unknown option.\nexport interface QoreDataCollection {\n userInfo: false\n cookies: false\n httpHeaders: { request: false; response: false }\n httpBodies: []\n databaseQueryData: false\n stackFrameVariables: false\n genAI: { inputs: false; outputs: false }\n urlQueryParams: { deny: string[] }\n}\n\nexport interface QoreSentryOptions {\n dsn: string\n environment: string\n release?: string\n sendDefaultPii: false\n dataCollection: QoreDataCollection\n enableLogs: true\n tracesSampler: (ctx: SamplingContextLike) => number\n tracePropagationTargets: Array<string | RegExp>\n beforeSend: typeof scrubEvent\n beforeSendTransaction: typeof scrubEvent\n beforeSendLog: typeof scrubLog\n beforeSendSpan: typeof scrubSpan\n initialScope: { tags: Record<string, string> }\n transport?: ReturnType<typeof makeDevSinkTransport>\n}\n\nfunction isTty(): boolean {\n try {\n return typeof process !== \"undefined\" && process.stdout?.isTTY === true\n } catch {\n return false\n }\n}\n\nexport function buildOptions(runtime: Runtime, cfg: SentryConfig): QoreSentryOptions {\n return {\n dsn: cfg.dsn,\n environment: cfg.environment,\n release: cfg.release,\n sendDefaultPii: false,\n dataCollection: {\n userInfo: false,\n cookies: false,\n httpHeaders: { request: false, response: false },\n httpBodies: [],\n databaseQueryData: false,\n stackFrameVariables: false,\n genAI: { inputs: false, outputs: false },\n urlQueryParams: { deny: [...SECRET_PARAMS] },\n },\n enableLogs: true,\n tracesSampler: createTracesSampler({\n production: cfg.environment === \"production\",\n readRate: cfg.readSampleRate,\n healthPath: cfg.healthPath,\n }),\n tracePropagationTargets: runtime === \"browser\" ? [/^\\//] : cfg.connOrigins,\n beforeSend: scrubEvent,\n beforeSendTransaction: scrubEvent,\n beforeSendLog: scrubLog,\n beforeSendSpan: scrubSpan,\n initialScope: { tags: cfg.tags },\n ...(cfg.local\n ? { transport: makeDevSinkTransport({ traces: cfg.devTraces ?? \"line\", color: runtime !== \"browser\" && isTty() }) }\n : {}),\n }\n}\n","import { setTelemetryHooks } from \"../telemetry-hooks\"\nimport type { SentryConfig } from \"./env\"\nimport { buildOptions, type Runtime } from \"./options\"\n\nexport interface SentryLike {\n init(options: object): unknown\n logger: Record<\"debug\" | \"info\" | \"warn\" | \"error\", (msg: string, attrs?: Record<string, string | number | boolean>) => void>\n setUser(user: { id: string } | null): void\n}\n\nconst DONE = Symbol.for(\"@quatronic/sdk.sentry-initialised\")\ntype Holder = { [DONE]?: boolean }\nconst holder = globalThis as Holder\n\n// A factory `extra` runs only once the guard passes, inside the try: integrations\n// such as Replay throw when constructed twice.\nexport function initSentry(\n sentry: SentryLike,\n runtime: Runtime,\n cfg: SentryConfig | null,\n extra: object | (() => object) = {},\n): boolean {\n if (!cfg || holder[DONE]) return false\n holder[DONE] = true\n try {\n const extras = typeof extra === \"function\" ? extra() : extra\n sentry.init({ ...extras, ...buildOptions(runtime, cfg) })\n } catch (err) {\n console.error(\"[qore] Sentry init failed\", err)\n return false\n }\n setTelemetryHooks({ log: sentry.logger, setUser: (u) => sentry.setUser(u) })\n return true\n}\n\nexport function resetSentryInitForTests(): void {\n delete holder[DONE]\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sentry-core/noise.ts","../src/sentry-core/sampler.ts","../src/sentry-core/scrub.ts","../src/sentry-core/options.ts","../src/sentry-core/init.ts"],"sourcesContent":["// Pure predicate deciding whether an HTTP request is infrastructural noise that\n// should never become a span: a `_rsc` query param, `/favicon.ico`, the SDK's\n// session poll, source maps, `/_next/*`, plus the caller's prefixes and health path.\n\nexport interface NoiseFilterOptions {\n healthPath?: string\n noisePrefixes?: string[]\n}\n\n// `/api/auth/me` is the SDK's own session poll (PortalProvider fires it on every\n// page mount): a heartbeat, not user activity.\nconst NOISE_EXACT = new Set([\"/favicon.ico\", \"/api/auth/me\"])\n\n// React Server Components navigations/prefetches carry a `?_rsc=` query param;\n// they look like real routes, so a path-prefix filter misses them.\nconst RSC_QUERY_RE = /[?&]_rsc=/\n\nexport function isNoisePath(rawUrl: string, opts: NoiseFilterOptions = {}): boolean {\n if (RSC_QUERY_RE.test(rawUrl)) return true\n const path = (rawUrl.split(\"?\")[0] ?? \"\").trim()\n if (!path) return false\n if (NOISE_EXACT.has(path)) return true\n if (path.endsWith(\".map\")) return true\n if (path.startsWith(\"/_next/\")) return true\n if ((opts.noisePrefixes ?? []).some((p) => path.startsWith(p))) return true\n if (opts.healthPath && path === opts.healthPath) return true\n return false\n}\n","import { isNoisePath } from \"./noise\"\n\nexport interface SamplingContextLike {\n name: string\n attributes?: Record<string, unknown>\n inheritOrSampleWith: (fallback: number) => number\n}\n\nconst MUTATIONS = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"])\n\nconst str = (v: unknown): string | undefined => (typeof v === \"string\" && v ? v : undefined)\n\nfunction pathOf(ctx: SamplingContextLike): string | undefined {\n const a = ctx.attributes ?? {}\n return str(a[\"url.path\"]) ?? str(a[\"http.target\"]) ?? ctx.name.split(\" \")[1]\n}\n\nexport function createTracesSampler(policy: { production: boolean; readRate: number; healthPath: string }) {\n return (ctx: SamplingContextLike): number => {\n const path = pathOf(ctx)\n if (path && isNoisePath(path, { healthPath: policy.healthPath })) return 0\n const a = ctx.attributes ?? {}\n const method = (str(a[\"http.request.method\"]) ?? str(a[\"http.method\"]) ?? \"\").toUpperCase()\n if (MUTATIONS.has(method)) return 1\n return ctx.inheritOrSampleWith(policy.production ? policy.readRate : 1)\n }\n}\n","export const REDACTED = \"●●●\"\n\n// Bounded and anchored (`(?<![...])` + `{1,64}` local part, no `.` in the\n// domain-char class) so it cannot backtrack catastrophically on long runs of\n// email-charset bytes — see the fix-round-1 report for the O(n²) it replaced.\nconst EMAIL = /(?<![A-Z0-9._%+-])[A-Z0-9._%+-]{1,64}@[A-Z0-9-]+(?:\\.[A-Z0-9-]+)*\\.[A-Z]{2,}/gi\nconst JWT = /eyJ[\\w-]+\\.[\\w-]+\\.[\\w-]+/g\nconst AUTH_SCHEME = /\\b(Bearer|Basic)\\s+\\S+/gi\nconst PARAMS_TAIL = /\\bparams:\\s.*$/s\nexport const SECRET_PARAMS: ReadonlySet<string> = new Set([\"token\", \"code\", \"state\", \"password\", \"secret\", \"key\", \"session\", \"access_token\", \"refresh_token\", \"id_token\"])\nconst SECRET_HEADERS = new Set([\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-qdc-connection-token\",\n \"x-forwarded-for\",\n \"x-real-ip\",\n \"forwarded\",\n \"user-agent\",\n])\nconst DROP_LOG_ATTRS = new Set([\"ip\", \"ip_address\", \"user_agent\", \"email\", \"user_email\", \"server.address\"])\nconst REDACT_KEYS = new Set([\"db.connection_string\", \"db.user\"])\nconst REDACT_PREFIXES = [\"net.peer.\", \"net.sock.\", \"net.host.\", \"network.peer.\", \"network.local.\"]\nconst DROP_SPAN_KEYS = new Set([\"user_agent.original\", \"http.user_agent\", \"client.address\", \"http.client_ip\"])\nconst DROP_SPAN_PREFIXES = [\"http.request.header.\", \"http.response.header.\"]\nconst URL_SPAN_KEYS = new Set([\"url.full\", \"http.url\", \"url\", \"http.target\", \"url.query\", \"http.query\"])\n\nexport function redactText(s: string): string {\n return s\n .replace(PARAMS_TAIL, \"params: [redacted]\")\n .replace(AUTH_SCHEME, \"$1 [redacted]\")\n .replace(JWT, \"[jwt]\")\n .replace(EMAIL, \"[email]\")\n}\n\nfunction redactQuery(query: string): string {\n return query\n .split(\"&\")\n .map((pair) => {\n const [k, v] = pair.split(\"=\", 2)\n return v !== undefined && SECRET_PARAMS.has(k!.toLowerCase()) ? `${k}=[redacted]` : pair\n })\n .join(\"&\")\n}\n\nfunction redactUrl(url: string): string {\n const i = url.indexOf(\"?\")\n return i < 0 ? url : url.slice(0, i + 1) + redactQuery(url.slice(i + 1))\n}\n\n// For values that are either a URL/path or a bare query string (no `?`).\nfunction redactUrlOrQuery(v: string): string {\n return v.includes(\"?\") ? redactUrl(v) : redactQuery(v)\n}\n\ntype Dict = Record<string, unknown>\n\nexport interface SentryEventLike {\n server_name?: string\n user?: Dict\n request?: { url?: string; query_string?: unknown; cookies?: unknown; headers?: Record<string, string>; data?: unknown }\n message?: string\n exception?: { values?: Array<{ type?: string; value?: string }> }\n breadcrumbs?: Array<{ message?: string; data?: Dict }>\n contexts?: { nextjs?: Dict } & Dict\n}\n\nexport function scrubEvent<E extends SentryEventLike>(e: E): E {\n delete e.server_name\n if (e.user) e.user = e.user.id ? { id: e.user.id } : {}\n if (e.request) {\n delete e.request.cookies\n delete e.request.data\n if (e.request.url) e.request.url = redactUrl(e.request.url)\n if (typeof e.request.query_string === \"string\") e.request.query_string = redactQuery(e.request.query_string)\n if (e.request.headers) {\n e.request.headers = Object.fromEntries(\n Object.entries(e.request.headers).filter(([k]) => !SECRET_HEADERS.has(k.toLowerCase())),\n )\n }\n }\n const nextjs = e.contexts?.nextjs\n if (typeof nextjs?.request_path === \"string\") nextjs.request_path = redactUrl(nextjs.request_path)\n if (e.message) e.message = redactText(e.message)\n for (const v of e.exception?.values ?? []) if (v.value) v.value = redactText(v.value)\n for (const b of e.breadcrumbs ?? []) {\n if (b.message) b.message = redactText(b.message)\n const d = b.data\n if (!d) continue\n for (const k of [\"url\", \"from\", \"to\"]) if (typeof d[k] === \"string\") d[k] = redactUrl(d[k])\n if (typeof d[\"http.query\"] === \"string\") d[\"http.query\"] = redactUrlOrQuery(d[\"http.query\"])\n if (Array.isArray(d.arguments)) d.arguments = d.arguments.map((a) => (typeof a === \"string\" ? redactText(a) : a))\n }\n return e\n}\n\nexport interface SentryLogLike {\n message: string\n attributes?: Dict\n}\n\nexport function scrubLog<L extends SentryLogLike>(l: L): L {\n l.message = redactText(l.message)\n if (l.attributes) {\n const out: Dict = {}\n for (const [k, v] of Object.entries(l.attributes)) {\n if (DROP_LOG_ATTRS.has(k)) continue\n out[k] = typeof v === \"string\" ? redactText(v) : v\n }\n l.attributes = out\n }\n return l\n}\n\nexport interface SentrySpanLike {\n description?: string\n data?: Dict\n}\n\nexport function scrubSpan<S extends SentrySpanLike>(s: S): S {\n // Browser page-metric spans are described by the full page URL.\n if (typeof s.description === \"string\") s.description = redactUrl(s.description)\n for (const [k, v] of Object.entries(s.data ?? {})) {\n if (DROP_SPAN_KEYS.has(k) || DROP_SPAN_PREFIXES.some((p) => k.startsWith(p))) delete s.data![k]\n else if (REDACT_KEYS.has(k) || REDACT_PREFIXES.some((p) => k.startsWith(p))) s.data![k] = REDACTED\n else if (typeof v === \"string\" && URL_SPAN_KEYS.has(k)) s.data![k] = redactUrlOrQuery(v)\n }\n return s\n}\n","import { makeDevSinkTransport } from \"./dev-sink\"\nimport type { SentryConfig } from \"./env\"\nimport { createTracesSampler, type SamplingContextLike } from \"./sampler\"\nimport { SECRET_PARAMS, scrubEvent, scrubLog, scrubSpan } from \"./scrub\"\n\nexport type Runtime = \"node\" | \"edge\" | \"browser\"\n\n// Structural copy of @sentry/core's DataCollection (10.75+): stop PII at collection,\n// the scrubbers stay as the backstop. Older 10.x ignores the unknown option.\nexport interface QoreDataCollection {\n userInfo: false\n cookies: false\n httpHeaders: { request: false; response: false }\n httpBodies: []\n databaseQueryData: false\n stackFrameVariables: false\n genAI: { inputs: false; outputs: false }\n urlQueryParams: { deny: string[] }\n}\n\nexport interface QoreSentryOptions {\n dsn: string\n environment: string\n release?: string\n sendDefaultPii: false\n dataCollection: QoreDataCollection\n enableLogs: true\n tracesSampler: (ctx: SamplingContextLike) => number\n tracePropagationTargets: Array<string | RegExp>\n beforeSend: typeof scrubEvent\n beforeSendTransaction: typeof scrubEvent\n beforeSendLog: typeof scrubLog\n beforeSendSpan: typeof scrubSpan\n initialScope: { tags: Record<string, string> }\n transport?: ReturnType<typeof makeDevSinkTransport>\n}\n\nfunction isTty(): boolean {\n try {\n return typeof process !== \"undefined\" && process.stdout?.isTTY === true\n } catch {\n return false\n }\n}\n\nexport function buildOptions(runtime: Runtime, cfg: SentryConfig): QoreSentryOptions {\n return {\n dsn: cfg.dsn,\n environment: cfg.environment,\n release: cfg.release,\n sendDefaultPii: false,\n dataCollection: {\n userInfo: false,\n cookies: false,\n httpHeaders: { request: false, response: false },\n httpBodies: [],\n databaseQueryData: false,\n stackFrameVariables: false,\n genAI: { inputs: false, outputs: false },\n urlQueryParams: { deny: [...SECRET_PARAMS] },\n },\n enableLogs: true,\n tracesSampler: createTracesSampler({\n production: cfg.environment === \"production\",\n readRate: cfg.readSampleRate,\n healthPath: cfg.healthPath,\n }),\n tracePropagationTargets: runtime === \"browser\" ? [/^\\//] : cfg.connOrigins,\n beforeSend: scrubEvent,\n beforeSendTransaction: scrubEvent,\n beforeSendLog: scrubLog,\n beforeSendSpan: scrubSpan,\n initialScope: { tags: cfg.tags },\n ...(cfg.local\n ? { transport: makeDevSinkTransport({ traces: cfg.devTraces ?? \"line\", color: runtime !== \"browser\" && isTty() }) }\n : {}),\n }\n}\n","import { setTelemetryHooks } from \"../telemetry-hooks\"\nimport type { SentryConfig } from \"./env\"\nimport { buildOptions, type Runtime } from \"./options\"\n\nexport interface SentryLike {\n init(options: object): unknown\n logger: Record<\"debug\" | \"info\" | \"warn\" | \"error\", (msg: string, attrs?: Record<string, string | number | boolean>) => void>\n setUser(user: { id: string } | null): void\n captureException(err: unknown, hint?: object): unknown\n}\n\nconst DONE = Symbol.for(\"@quatronic/sdk.sentry-initialised\")\ntype Holder = { [DONE]?: boolean }\nconst holder = globalThis as Holder\n\n// A factory `extra` runs only once the guard passes, inside the try: integrations\n// such as Replay throw when constructed twice.\nexport function initSentry(\n sentry: SentryLike,\n runtime: Runtime,\n cfg: SentryConfig | null,\n extra: object | (() => object) = {},\n): boolean {\n if (!cfg || holder[DONE]) return false\n holder[DONE] = true\n try {\n const extras = typeof extra === \"function\" ? extra() : extra\n sentry.init({ ...extras, ...buildOptions(runtime, cfg) })\n } catch (err) {\n console.error(\"[qore] Sentry init failed\", err)\n return false\n }\n setTelemetryHooks({\n log: sentry.logger,\n setUser: (u) => sentry.setUser(u),\n captureException: (err, hint) => {\n sentry.captureException(err, hint)\n },\n })\n return true\n}\n\nexport function resetSentryInitForTests(): void {\n delete holder[DONE]\n}\n"],"mappings":"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","names":[]}
@@ -40,10 +40,16 @@ function getLogger() {
40
40
  function setUserContext(id) {
41
41
  holder[KEY]?.setUser?.(id ? { id } : null);
42
42
  }
43
+ function reportError(err, ctx = {}) {
44
+ const capture = holder[KEY]?.captureException;
45
+ if (capture) return capture(err, { extra: flattenAttrs(ctx) });
46
+ emit("error", { ...ctx, err: err instanceof Error ? err : new Error(String(err)) }, "unhandled error reported");
47
+ }
43
48
 
44
49
  export {
45
50
  setTelemetryHooks,
46
51
  getLogger,
47
- setUserContext
52
+ setUserContext,
53
+ reportError
48
54
  };
49
- //# sourceMappingURL=chunk-MET4SIMB.js.map
55
+ //# sourceMappingURL=chunk-EK6TGWWT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/telemetry-hooks/index.ts"],"sourcesContent":["export type LogAttrs = Record<string, unknown>\ntype Flat = Record<string, string | number | boolean>\ntype Level = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface QoreLogger {\n debug(attrs: LogAttrs, msg: string): void\n info(attrs: LogAttrs, msg: string): void\n warn(attrs: LogAttrs, msg: string): void\n error(attrs: LogAttrs, msg: string): void\n}\n\nexport interface TelemetryHooks {\n log?: Record<Level, (msg: string, attrs?: Flat) => void>\n setUser?: (user: { id: string } | null) => void\n captureException?: (err: unknown, hint: { extra: Flat }) => void\n}\n\nconst KEY = Symbol.for(\"@quatronic/sdk.telemetry-hooks\")\ntype Holder = { [KEY]?: TelemetryHooks }\nconst holder = globalThis as Holder\n\nexport function setTelemetryHooks(hooks: TelemetryHooks): void {\n holder[KEY] = hooks\n}\n\nexport function resetTelemetryHooks(): void {\n delete holder[KEY]\n}\n\nexport function flattenAttrs(attrs: LogAttrs): Flat {\n const out: Flat = {}\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined || value === null) continue\n if (value instanceof Error) {\n out[\"error.type\"] = value.name\n out[\"error.message\"] = value.message\n } else if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n out[key] = value\n } else {\n out[key] = JSON.stringify(value)\n }\n }\n return out\n}\n\nfunction emit(level: Level, attrs: LogAttrs, msg: string): void {\n const flat = flattenAttrs(attrs)\n const sink = holder[KEY]?.log\n if (sink) return sink[level](msg, flat)\n const line = JSON.stringify({ level, msg, ...flat })\n if (level === \"error\") console.error(line)\n else if (level === \"warn\") console.warn(line)\n else console.log(line)\n}\n\nconst logger: QoreLogger = {\n debug: (a, m) => emit(\"debug\", a, m),\n info: (a, m) => emit(\"info\", a, m),\n warn: (a, m) => emit(\"warn\", a, m),\n error: (a, m) => emit(\"error\", a, m),\n}\n\nexport function getLogger(): QoreLogger {\n return logger\n}\n\nexport function setUserContext(id: string | null): void {\n holder[KEY]?.setUser?.(id ? { id } : null)\n}\n\n/**\n * Hands an error the app caught to the error tracker, with flat context.\n *\n * Without a tracker it writes the same JSON line `getLogger().error` would, so\n * a caught error is never silently dropped in an app that has no Sentry.\n */\nexport function reportError(err: unknown, ctx: LogAttrs = {}): void {\n const capture = holder[KEY]?.captureException\n if (capture) return capture(err, { extra: flattenAttrs(ctx) })\n emit(\"error\", { ...ctx, err: err instanceof Error ? err : new Error(String(err)) }, \"unhandled error reported\")\n}\n"],"mappings":";AAiBA,IAAM,MAAM,uBAAO,IAAI,gCAAgC;AAEvD,IAAM,SAAS;AAER,SAAS,kBAAkB,OAA6B;AAC7D,SAAO,GAAG,IAAI;AAChB;AAMO,SAAS,aAAa,OAAuB;AAClD,QAAM,MAAY,CAAC;AACnB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,UAAa,UAAU,KAAM;AAC3C,QAAI,iBAAiB,OAAO;AAC1B,UAAI,YAAY,IAAI,MAAM;AAC1B,UAAI,eAAe,IAAI,MAAM;AAAA,IAC/B,WAAW,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;AAC/F,UAAI,GAAG,IAAI;AAAA,IACb,OAAO;AACL,UAAI,GAAG,IAAI,KAAK,UAAU,KAAK;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,KAAK,OAAc,OAAiB,KAAmB;AAC9D,QAAM,OAAO,aAAa,KAAK;AAC/B,QAAM,OAAO,OAAO,GAAG,GAAG;AAC1B,MAAI,KAAM,QAAO,KAAK,KAAK,EAAE,KAAK,IAAI;AACtC,QAAM,OAAO,KAAK,UAAU,EAAE,OAAO,KAAK,GAAG,KAAK,CAAC;AACnD,MAAI,UAAU,QAAS,SAAQ,MAAM,IAAI;AAAA,WAChC,UAAU,OAAQ,SAAQ,KAAK,IAAI;AAAA,MACvC,SAAQ,IAAI,IAAI;AACvB;AAEA,IAAM,SAAqB;AAAA,EACzB,OAAO,CAAC,GAAG,MAAM,KAAK,SAAS,GAAG,CAAC;AAAA,EACnC,MAAM,CAAC,GAAG,MAAM,KAAK,QAAQ,GAAG,CAAC;AAAA,EACjC,MAAM,CAAC,GAAG,MAAM,KAAK,QAAQ,GAAG,CAAC;AAAA,EACjC,OAAO,CAAC,GAAG,MAAM,KAAK,SAAS,GAAG,CAAC;AACrC;AAEO,SAAS,YAAwB;AACtC,SAAO;AACT;AAEO,SAAS,eAAe,IAAyB;AACtD,SAAO,GAAG,GAAG,UAAU,KAAK,EAAE,GAAG,IAAI,IAAI;AAC3C;AAQO,SAAS,YAAY,KAAc,MAAgB,CAAC,GAAS;AAClE,QAAM,UAAU,OAAO,GAAG,GAAG;AAC7B,MAAI,QAAS,QAAO,QAAQ,KAAK,EAAE,OAAO,aAAa,GAAG,EAAE,CAAC;AAC7D,OAAK,SAAS,EAAE,GAAG,KAAK,KAAK,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,EAAE,GAAG,0BAA0B;AAChH;","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-5XMTUPIY.js";
5
5
  import {
6
6
  getLogger
7
- } from "./chunk-MET4SIMB.js";
7
+ } from "./chunk-EK6TGWWT.js";
8
8
 
9
9
  // src/connections-core/errors.ts
10
10
  var ConnectionError = class extends Error {
@@ -70,4 +70,4 @@ export {
70
70
  CONNECTION_REJECT_REASON_ATTR,
71
71
  requireConnection
72
72
  };
73
- //# sourceMappingURL=chunk-TEBDKNUB.js.map
73
+ //# sourceMappingURL=chunk-ROHIN74F.js.map
@@ -2,7 +2,7 @@ import {
2
2
  CONNECTION_TOKEN_HEADER,
3
3
  ConnectionError,
4
4
  DEFAULT_CONNECTION_TIMEOUT_MS
5
- } from "./chunk-TEBDKNUB.js";
5
+ } from "./chunk-ROHIN74F.js";
6
6
  import {
7
7
  collectConnectionTokens,
8
8
  connKey,
@@ -139,4 +139,4 @@ export {
139
139
  assertConnections,
140
140
  assertInboundConnection
141
141
  };
142
- //# sourceMappingURL=chunk-HQT57ITV.js.map
142
+ //# sourceMappingURL=chunk-ZZTO5Z3N.js.map
@@ -445,6 +445,24 @@ var noSentryInit = {
445
445
  });
446
446
  }
447
447
  };
448
+ var useReportError = {
449
+ meta: meta("QDC-OBS-004 - report caught errors with reportError", "suggestion"),
450
+ create(context) {
451
+ const report = reporter(
452
+ context,
453
+ "QDC-OBS-004 - use reportError(err, ctx) from @quatronic/sdk/observability instead of Sentry.captureException; see migration report-error."
454
+ );
455
+ const sentry = trackSentry();
456
+ return listener({
457
+ ImportDeclaration: (node) => sentry.onImport(node),
458
+ CallExpression: (node) => {
459
+ const c = node.callee;
460
+ const name = c?.type === "Identifier" ? sentry.named.get(c.name ?? "") : c?.type === "MemberExpression" && c.object?.type === "Identifier" && sentry.namespaces.has(c.object.name ?? "") ? c.property?.name : void 0;
461
+ if (name === "captureException") report(node);
462
+ }
463
+ });
464
+ }
465
+ };
448
466
  var observabilityRules = {
449
467
  "no-sentry-init": noSentryInit,
450
468
  "sentry-attribute-namespace": attributeKeyRule(
@@ -456,12 +474,14 @@ var observabilityRules = {
456
474
  "QDC-OBS-003",
457
475
  "no personal data in telemetry keys",
458
476
  (key) => PII.test(key) ? `'${key}' looks like personal data. Send an id instead; see the qore-observability skill.` : void 0
459
- )
477
+ ),
478
+ "use-report-error": useReportError
460
479
  };
461
480
  var OBSERVABILITY_RULE_IDS = {
462
481
  "no-sentry-init": "QDC-OBS-001",
463
482
  "sentry-attribute-namespace": "QDC-OBS-002",
464
- "no-pii-attributes": "QDC-OBS-003"
483
+ "no-pii-attributes": "QDC-OBS-003",
484
+ "use-report-error": "QDC-OBS-004"
465
485
  };
466
486
 
467
487
  // src/eslint/index.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/eslint/ast.ts","../../src/eslint/rules.ts","../../src/eslint/observability.ts","../../src/eslint/index.ts"],"sourcesContent":["// A deliberately narrow view of the AST, for rules that are deliberately\n// syntactic.\n//\n// ESLint's own `Rule.Node` is the ESTree union. It does not describe JSX at\n// all, and it forces a narrowing cascade for every property access — in rules\n// whose whole job is to read three or four fields off a node and report it.\n// So these rules declare exactly the fields they read and cast once, at the\n// visitor boundary, rather than per line.\n//\n// Anything that has to follow a value across functions or files is a Semgrep\n// rule instead; that pack ships in `semgrep/` alongside this plugin.\nexport interface AstNode {\n type: string\n /** `Identifier.name` and `MemberExpression.property.name`. Never a JSX name. */\n name?: string\n value?: unknown\n callee?: AstNode\n object?: AstNode\n property?: AstNode\n source?: AstNode\n arguments?: (AstNode | undefined)[]\n expressions?: (AstNode | undefined)[]\n /** `TemplateLiteral.quasis`: the fixed text between the `${}` holes. */\n quasis?: { value?: { cooked?: string | null; raw?: string } }[]\n /** `BinaryExpression.left`; the rule that reads it only follows the left edge. */\n left?: AstNode\n operator?: string\n /** `ImportDeclaration.specifiers`. */\n specifiers?: AstNode[]\n /** `ImportSpecifier.imported` / `.local`. */\n imported?: AstNode\n local?: AstNode\n /** `ObjectExpression.properties`; a `Property` has `key` and `computed`. */\n properties?: AstNode[]\n key?: AstNode\n computed?: boolean\n /** Set by ESLint on every node it visits. */\n parent?: AstNode\n}\n\n/** Calls whose result is a URL this app decided on, not one a caller supplied. */\nexport const VETTED_CALLS = [\"safeRedirect\", \"safeFetch\", \"vetTarget\", \"clone\"]\n\n/** POSIX form of the file being linted, so the checks read the same on Windows. */\nexport function pathOf(filename: string): string {\n return filename.replaceAll(\"\\\\\", \"/\")\n}\n\n/** Is the linted file at, or inside, one of these project paths? */\nexport function isUnder(filename: string, paths: readonly string[]): boolean {\n const file = pathOf(filename)\n return paths.some((path) => file.endsWith(`/${path}`) || file.includes(`/${path}/`))\n}\n\n/** The called name, whether the callee is `f()` or `x.f()`. */\nexport function calleeName(node: AstNode): string {\n const callee = node.callee\n if (!callee) return \"\"\n if (callee.type === \"Identifier\") return callee.name ?? \"\"\n if (callee.type === \"MemberExpression\" && callee.property?.type === \"Identifier\") {\n return callee.property.name ?? \"\"\n }\n return \"\"\n}\n\n/**\n * A target this app wrote down, rather than one that arrived with the request.\n *\n * `allowIdentifier` is the whole difference between the two advisory rules:\n * judging what a bare variable holds takes dataflow, which is what the Semgrep\n * taint rules do. `qdc/safe-redirect` lets a variable through because\n * `NextResponse.redirect(url)` after `req.nextUrl.clone()` is the canonical\n * Next redirect and appears in every generated app; `qdc/safe-fetch` does not.\n */\nexport function isFixedTarget(node: AstNode | undefined, allowIdentifier: boolean): boolean {\n if (!node) return false\n if (node.type === \"Literal\") return typeof node.value === \"string\"\n if (node.type === \"TemplateLiteral\") return (node.expressions?.length ?? 0) === 0\n if (node.type === \"Identifier\") return allowIdentifier\n if (node.type === \"NewExpression\") {\n // `new URL(\"/login\", req.url)` fixes the path; the base only ever\n // contributes this app's own origin.\n return node.callee?.name === \"URL\" && isFixedTarget(node.arguments?.[0], allowIdentifier)\n }\n if (node.type === \"CallExpression\") return VETTED_CALLS.includes(calleeName(node))\n return false\n}\n","import type { Rule } from \"eslint\"\n\nimport { isPlatformManagedHost } from \"../integrations-core/platform-hosts\"\nimport { type AstNode, calleeName, isFixedTarget, isUnder } from \"./ast\"\n\n/**\n * The fourteen QDC security lint rules.\n *\n * One rule per id, rather than fourteen entries in `no-restricted-syntax`, for\n * two reasons that only show up after the fact:\n *\n * - **Severity is per rule.** `no-restricted-syntax` carries one severity for\n * its whole array, and three of these are advisory on purpose — they fire on\n * shapes that are usually fine, and the Semgrep pack does the dataflow they\n * can only approximate.\n * - **Suppression is per rule.** `// eslint-disable-next-line\n * no-restricted-syntax` switches off every selector on that line, so\n * silencing \"no inline styles\" would silently also silence the\n * `dangerouslySetInnerHTML` ban sitting next to it.\n *\n * Suppress one by its own name, and say why:\n *\n * // eslint-disable-next-line qdc/no-dangerous-html -- sanitised in renderPost()\n */\n\n/** Modules that talk to the database directly, and where that is allowed. */\nconst DB_DRIVERS = [\"pg\", \"postgres\", \"mysql2\", \"mysql2/promise\"]\nconst DB_LAYER = [\"src/db\", \"src/lib/db.ts\"]\n\n/** Node's filesystem API, and where a generated app is expected to use it. */\nconst FS_MODULES = [\"fs\", \"node:fs\", \"fs/promises\", \"node:fs/promises\"]\n// `src/lib/migrate.ts` is the migration runner: it reads the migration files\n// off disk at boot, which is the sanctioned use, and it is not reachable from\n// a request. The backend recipes put it there rather than under src/db.\nconst FS_CALLERS = [\"src/scripts\", \"src/db\", \"src/lib/migrate.ts\"]\n\n/** Where a fetch goes out with the server's own network position. */\nconst SERVER_PATHS = [\"src/app/api\", \"src/lib/server\", \"src/proxy.ts\"]\n\n/** One cast, at the boundary, instead of one per `context.report` call. */\nexport function reporter(context: Rule.RuleContext, message: string): (node: AstNode) => void {\n return (node: AstNode) => {\n context.report({ node: node as unknown as Rule.Node, message })\n }\n}\n\nexport function listener(visitor: Record<string, (node: AstNode) => void>): Rule.RuleListener {\n return visitor as unknown as Rule.RuleListener\n}\n\nexport function meta(description: string, type: \"problem\" | \"suggestion\"): Rule.RuleModule[\"meta\"] {\n return { type, docs: { description }, schema: [] }\n}\n\n/** A rule that reports every node matching one of `selectors`. */\nfunction ban(id: string, message: string, selectors: string[]): Rule.RuleModule {\n const text = `${id} - ${message}`\n return {\n meta: meta(text, \"problem\"),\n create(context) {\n const report = reporter(context, text)\n return listener(Object.fromEntries(selectors.map((selector) => [selector, report])))\n },\n }\n}\n\n/** A rule that bans importing any of `modules`, except from `allowed` paths. */\nfunction banImports(\n id: string,\n message: string,\n modules: string[],\n allowed: string[],\n): Rule.RuleModule {\n const text = `${id} - ${message}`\n return {\n meta: meta(text, \"problem\"),\n create(context) {\n if (isUnder(context.filename, allowed)) return {}\n const report = reporter(context, text)\n const flag = (node: AstNode) => {\n const source = node.source\n if (source?.type === \"Literal\" && typeof source.value === \"string\") {\n if (modules.includes(source.value)) report(node)\n }\n }\n return listener({ ImportDeclaration: flag, ImportExpression: flag })\n },\n }\n}\n\nconst noStringEval: Rule.RuleModule = {\n meta: meta(\"QDC-XSS-005 - no code compiled from a string\", \"problem\"),\n create(context) {\n const report = reporter(\n context,\n \"QDC-XSS-005 - compiling a string into code runs whatever the string turned out to be. Pass a function.\",\n )\n const isStringy = (node: AstNode | undefined) =>\n node?.type === \"TemplateLiteral\" ||\n node?.type === \"BinaryExpression\" ||\n (node?.type === \"Literal\" && typeof node.value === \"string\")\n return listener({\n \"CallExpression[callee.name='eval']\": report,\n \"CallExpression[callee.name='Function']\": report,\n \"NewExpression[callee.name='Function']\": report,\n \"CallExpression[callee.name=/^set(Timeout|Interval)$/]\": (node) => {\n if (isStringy(node.arguments?.[0])) report(node)\n },\n })\n },\n}\n\nconst safeRedirect: Rule.RuleModule = {\n meta: meta(\"QDC-URL-001 - redirect to a target this app decided on\", \"suggestion\"),\n create(context) {\n const report = reporter(\n context,\n \"QDC-URL-001 - redirecting to a value that came with the request is an open redirect. Wrap it in safeRedirect from @quatronic/sdk/next/auth.\",\n )\n return listener({\n \"CallExpression[callee.object.name='NextResponse'][callee.property.name='redirect']\": (\n node,\n ) => {\n if (isFixedTarget(node.arguments?.[0], true)) return\n report(node)\n },\n })\n },\n}\n\nconst safeFetch: Rule.RuleModule = {\n meta: meta(\"QDC-NET-001 - server-side fetch of a caller-supplied URL\", \"suggestion\"),\n create(context) {\n if (!isUnder(context.filename, SERVER_PATHS)) return {}\n const report = reporter(\n context,\n \"QDC-NET-001 - a URL the server did not decide on reaches the private network too. Use safeFetch from @quatronic/sdk/node/net, or connection(alias).fetch for a wired module.\",\n )\n return listener({\n \"CallExpression[callee.name='fetch']\": (node) => {\n if (isFixedTarget(node.arguments?.[0], false)) return\n report(node)\n },\n })\n },\n}\n\n/** Outbound HTTP clients a generated app might reach for. */\nconst HTTP_CLIENTS = [\"fetch\", \"axios\", \"got\", \"ky\"]\nconst HTTP_LIBS = [\"axios\", \"got\", \"ky\"]\nconst HTTP_METHODS = /^(get|post|put|patch|delete|head|request)$/\n\n/** A host that is this cluster or this machine, so not an external API. */\nconst LOCAL_HOST_SUFFIXES = [\".local\", \".internal\", \".svc\", \".cluster.local\"]\nconst ENV_KEY = /^[A-Z][A-Z0-9_]*$/\n\nfunction isLocalHost(host: string): boolean {\n const h = host.toLowerCase()\n if (h === \"localhost\" || h === \"::1\" || h === \"0.0.0.0\" || h.startsWith(\"127.\")) return true\n return LOCAL_HOST_SUFFIXES.some((suffix) => h.endsWith(suffix))\n}\n\n/** The host of an `http(s)://` string, or null when it has none or a `${}` hole cuts it off. */\nfunction hostOf(text: string): string | null {\n const m = /^https?:\\/\\/(?:[^@/?#]*@)?(\\[[^\\]]*\\]|[^/?#:]+)/i.exec(text)\n if (!m?.[1]) return null\n return m[1].replace(/^\\[|\\]$/g, \"\")\n}\n\n/** `process.env.X`, `env.X`, `serverEnv().X` — the env key, when `X` looks like one. */\nfunction envKeyOf(node: AstNode | undefined): string | null {\n if (node?.type !== \"MemberExpression\" || node.property?.type !== \"Identifier\") return null\n const key = node.property.name ?? \"\"\n if (!ENV_KEY.test(key)) return null\n const object = node.object\n if (!object) return null\n if (object.type === \"Identifier\") return object.name === \"env\" ? key : null\n if (object.type === \"MemberExpression\") {\n const isProcessEnv =\n object.object?.type === \"Identifier\" &&\n object.object.name === \"process\" &&\n object.property?.name === \"env\"\n return isProcessEnv ? key : null\n }\n if (object.type === \"CallExpression\") {\n const name = calleeName(object)\n return name === \"env\" || name.endsWith(\"Env\") ? key : null\n }\n return null\n}\n\ntype ExternalTarget = { host: string } | { envKey: string } | null\n\n/**\n * Where a call is going, read off its first argument: a host when the code\n * spells the URL out, an env key when the base comes from the environment,\n * null when the shape says nothing (a bare variable, a relative path).\n */\nfunction externalTargetOf(node: AstNode | undefined): ExternalTarget {\n if (!node) return null\n if (node.type === \"Literal\" && typeof node.value === \"string\") {\n const host = hostOf(node.value)\n return host ? { host } : null\n }\n if (node.type === \"TemplateLiteral\") {\n const head = node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? \"\"\n const host = hostOf(head)\n if (host) return { host }\n // `${process.env.SAP_BASE_URL}/path` — the base is the first hole.\n if (head === \"\") {\n const key = envKeyOf(node.expressions?.[0])\n if (key) return { envKey: key }\n }\n return null\n }\n if (node.type === \"BinaryExpression\" && node.operator === \"+\") return externalTargetOf(node.left)\n if (node.type === \"MemberExpression\") {\n const key = envKeyOf(node)\n return key ? { envKey: key } : null\n }\n if (node.type === \"NewExpression\" && node.callee?.name === \"URL\") {\n // new URL(path, base): the base decides the host; new URL(absolute) has no base.\n const args = node.arguments ?? []\n return externalTargetOf(args[1]) ?? externalTargetOf(args[0])\n }\n return null\n}\n\nfunction isHttpClientCall(node: AstNode): boolean {\n const callee = node.callee\n if (!callee) return false\n if (callee.type === \"Identifier\") return HTTP_CLIENTS.includes(callee.name ?? \"\")\n if (callee.type === \"MemberExpression\") {\n return (\n callee.object?.type === \"Identifier\" &&\n HTTP_LIBS.includes(callee.object.name ?? \"\") &&\n callee.property?.type === \"Identifier\" &&\n HTTP_METHODS.test(callee.property.name ?? \"\")\n )\n }\n return false\n}\n\n/**\n * An outbound call to a host this app has not declared. Advisory: the rule\n * reads one argument, so it sees a spelled-out host or an env-backed base and\n * nothing more. The manifest extractor does the same classification at deploy\n * time and the portal shows the result; this is the editor-time echo of it.\n */\nconst noUndeclaredExternalCall: Rule.RuleModule = {\n meta: meta(\"QDC-NET-002 - outbound call to an external API this app has not declared\", \"suggestion\"),\n create(context) {\n return listener({\n CallExpression: (node) => {\n if (!isHttpClientCall(node)) return\n const target = externalTargetOf(node.arguments?.[0])\n if (!target) return\n let label: string\n if (\"host\" in target) {\n if (isLocalHost(target.host) || isPlatformManagedHost(target.host)) return\n label = target.host\n } else {\n // CONN_* is a wired module connection: the portal already knows the other end.\n if (target.envKey.startsWith(\"CONN_\")) return\n label = `$${target.envKey}`\n }\n context.report({\n node: node as unknown as Rule.Node,\n message: `QDC-NET-002 - ${label} is an external API this app has not declared. Declare it in src/config/integrations.ts and call integration(alias).get/post, so the portal can see and manage it.`,\n })\n },\n })\n },\n}\n\n/** The calls on an integration handle that send a request, and where their path sits. */\nconst INTEGRATION_PATH_ARG: Record<string, number> = { get: 0, post: 0, put: 0, patch: 0, delete: 0, request: 1 }\n/** Reads off a handle that send nothing, so taking them through a variable hides no endpoint. */\nconst INTEGRATION_READS = [\"config\", \"alias\", \"definition\"]\n\nconst isStringLiteral = (node: AstNode | undefined): boolean =>\n node?.type === \"Literal\" && typeof node.value === \"string\"\n\n/**\n * An integration call the portal cannot read. The manifest extractor reads\n * exactly `integration(\"alias\").get(\"/path\")`, alias and path written at the\n * call site, because it reads source, not values. A path passed in through a\n * helper, an alias held in a variable, or a handle stored and called later all\n * reach the API fine and leave the portal's Integrations tab short of that\n * endpoint, with nothing failing to say so. Advisory, like QDC-NET-002.\n */\nconst integrationStaticCall: Rule.RuleModule = {\n meta: meta(\"QDC-NET-003 - an integration call whose alias or path the portal cannot read\", \"suggestion\"),\n create(context) {\n const report = (node: AstNode, message: string) => {\n context.report({ node: node as unknown as Rule.Node, message: `QDC-NET-003 - ${message}` })\n }\n return listener({\n CallExpression: (node) => {\n if (node.callee?.type !== \"Identifier\" || node.callee.name !== \"integration\") return\n if (!isStringLiteral(node.arguments?.[0])) {\n report(\n node,\n 'the alias is not a string literal, so the portal cannot tell which integration this calls. Write integration(\"alias\") here.',\n )\n return\n }\n const member = node.parent\n const method =\n member?.type === \"MemberExpression\" && member.object === node && member.property?.type === \"Identifier\"\n ? (member.property.name ?? \"\")\n : \"\"\n if (INTEGRATION_READS.includes(method)) return\n const call = member?.parent\n const index = INTEGRATION_PATH_ARG[method]\n if (index === undefined || call?.type !== \"CallExpression\" || call.callee !== member) {\n report(\n node,\n 'the integration handle is stored or passed on instead of called here, so the portal cannot see the endpoints called through it. Write integration(\"alias\").get(\"/path\") at each call site.',\n )\n return\n }\n const path = call.arguments?.[index]\n if (path === undefined || isStringLiteral(path) || path.type === \"TemplateLiteral\") return\n report(\n path,\n \"the path is not written at the call site, so the portal cannot list this endpoint. Write it as a string or template literal here, e.g. `/orders/${id}`. A helper may share options, never the path.\",\n )\n },\n })\n },\n}\n\nexport const rules: Record<string, Rule.RuleModule> = {\n \"no-dangerous-html\": ban(\n \"QDC-XSS-001\",\n \"dangerouslySetInnerHTML renders unescaped markup. Render text, or sanitise it first.\",\n [\"JSXAttribute[name.name='dangerouslySetInnerHTML']\"],\n ),\n \"no-inner-html\": ban(\n \"QDC-XSS-002\",\n \"Assigning innerHTML parses the string as markup. Use textContent.\",\n [\n \"AssignmentExpression[left.type='MemberExpression'][left.property.name=/^(innerHTML|outerHTML)$/]\",\n ],\n ),\n \"no-insert-adjacent-html\": ban(\n \"QDC-XSS-003\",\n \"insertAdjacentHTML parses its argument as markup. Build the nodes instead.\",\n [\"CallExpression[callee.property.name='insertAdjacentHTML']\"],\n ),\n \"no-document-write\": ban(\n \"QDC-XSS-004\",\n \"document.write parses its argument as markup and blocks the parser. Render it in React.\",\n [\"CallExpression[callee.object.name='document'][callee.property.name=/^write(ln)?$/]\"],\n ),\n \"no-string-eval\": noStringEval,\n \"no-raw-script-tag\": ban(\n \"QDC-CSP-001\",\n \"A hand-written <script> gets no nonce, and this app's CSP uses 'strict-dynamic', so it will not run. Use next/script.\",\n [\"JSXElement[openingElement.name.name='script']\"],\n ),\n \"no-sql-interpolation\": ban(\n \"QDC-SQL-001\",\n \"An interpolated string reaching .execute() is the injection. Use the sql tag, which parameterises.\",\n [\n \"CallExpression[callee.property.name='execute'] > TemplateLiteral[expressions.length>0]\",\n \"CallExpression[callee.property.name='execute'] > BinaryExpression[operator='+']\",\n ],\n ),\n \"no-sql-raw\": ban(\n \"QDC-SQL-002\",\n \"sql.raw() puts its argument into the query verbatim and sanitises nothing. Interpolate through the sql tag.\",\n [\"MemberExpression[object.name='sql'][property.name='raw']\"],\n ),\n \"no-driver-import\": banImports(\n \"QDC-SQL-003\",\n \"The database driver belongs in the db layer. Everywhere else, go through Drizzle.\",\n DB_DRIVERS,\n DB_LAYER,\n ),\n \"no-fs-import\": banImports(\n \"QDC-FS-001\",\n \"Request handlers should not reach the filesystem. If one must, use safeJoin from @quatronic/sdk/node/fspath.\",\n FS_MODULES,\n FS_CALLERS,\n ),\n \"safe-redirect\": safeRedirect,\n \"safe-fetch\": safeFetch,\n \"no-undeclared-external-call\": noUndeclaredExternalCall,\n \"integration-static-call\": integrationStaticCall,\n}\n\n/**\n * Rule name to QDC id, for docs and for anything that wants to assert the set\n * without re-deriving it from the messages.\n */\nexport const RULE_IDS: Record<string, string> = {\n \"no-dangerous-html\": \"QDC-XSS-001\",\n \"no-inner-html\": \"QDC-XSS-002\",\n \"no-insert-adjacent-html\": \"QDC-XSS-003\",\n \"no-document-write\": \"QDC-XSS-004\",\n \"no-string-eval\": \"QDC-XSS-005\",\n \"no-raw-script-tag\": \"QDC-CSP-001\",\n \"no-sql-interpolation\": \"QDC-SQL-001\",\n \"no-sql-raw\": \"QDC-SQL-002\",\n \"no-driver-import\": \"QDC-SQL-003\",\n \"no-fs-import\": \"QDC-FS-001\",\n \"safe-redirect\": \"QDC-URL-001\",\n \"safe-fetch\": \"QDC-NET-001\",\n \"no-undeclared-external-call\": \"QDC-NET-002\",\n \"integration-static-call\": \"QDC-NET-003\",\n}\n","import type { Rule } from \"eslint\"\n\nimport type { AstNode } from \"./ast\"\nimport { listener, meta, reporter } from \"./rules\"\n\nconst SENTRY_MODULE = /^@sentry\\//\nconst BANNED = new Set([\"init\", \"withSentryConfig\"])\nconst SPAN_STARTERS = new Set([\"startSpan\", \"startSpanManual\", \"startInactiveSpan\"])\nconst CAPTURES = new Set([\"captureException\", \"captureMessage\"])\nconst LOG_LEVELS = new Set([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"])\nconst KEY_SETTERS = new Set([\"setAttribute\", \"setTag\"])\nconst OBJECT_SETTERS = new Set([\"setAttributes\", \"setTags\"])\nconst SPAN_RECEIVER = /^(span|\\w*Span)$/\nconst SCOPE_RECEIVER = /^(scope|\\w*Scope)$/\nconst SPAN_GETTERS = new Set([\"getActiveSpan\", \"getRootSpan\"])\nconst SCOPE_GETTERS = new Set([\"getCurrentScope\", \"getIsolationScope\"])\nconst PII =\n /(^|[._-])(e-?mail|phone|password|passwd|token|secret|iban|bsn|ssn|ip_?address|first_?name|last_?name|full_?name|username)($|[._-])/i\n\n/** Local names bound to `@sentry/*` (namespace, default, or a named export). */\nfunction trackSentry() {\n const namespaces = new Set<string>()\n const named = new Map<string, string>()\n return {\n namespaces,\n named,\n onImport(node: AstNode, onBanned?: (n: AstNode) => void) {\n const src = node.source?.value\n if (typeof src !== \"string\" || !SENTRY_MODULE.test(src)) return\n for (const s of node.specifiers ?? []) {\n const local = s.local?.name ?? \"\"\n if (s.type === \"ImportNamespaceSpecifier\" || s.type === \"ImportDefaultSpecifier\") namespaces.add(local)\n else if (s.type === \"ImportSpecifier\") {\n const imported = s.imported?.name ?? \"\"\n named.set(local, imported)\n if (BANNED.has(imported)) onBanned?.(s)\n }\n }\n },\n }\n}\n\nconst keyOf = (prop: AstNode): string | undefined => {\n if (prop.type !== \"Property\" || prop.computed) return undefined\n if (prop.key?.type === \"Identifier\") return prop.key.name\n return typeof prop.key?.value === \"string\" ? prop.key.value : undefined\n}\n\nconst objectKeys = (node: AstNode | undefined): [string, AstNode][] =>\n node?.type === \"ObjectExpression\"\n ? (node.properties ?? []).flatMap((p) => {\n const k = keyOf(p)\n return k === undefined ? [] : [[k, p] as [string, AstNode]]\n })\n : []\n\nconst propertyNamed = (node: AstNode | undefined, name: string): AstNode | undefined =>\n node?.type === \"ObjectExpression\"\n ? ((node.properties ?? []).find((p) => keyOf(p) === name)?.value as AstNode | undefined)\n : undefined\n\n/** Reports every attribute, tag or log-field key the app hands Sentry. */\nfunction attributeKeyRule(\n id: string,\n description: string,\n check: (key: string) => string | undefined,\n): Rule.RuleModule {\n return {\n meta: meta(`${id} - ${description}`, \"suggestion\"),\n create(context) {\n const sentry = trackSentry()\n const flag = (key: string, node: AstNode) => {\n const problem = check(key)\n if (problem) reporter(context, `${id} - ${problem}`)(node)\n }\n return listener({\n ImportDeclaration: (node) => sentry.onImport(node),\n CallExpression: (node) => {\n const callee = node.callee\n const args = node.arguments ?? []\n let name: string | undefined\n let viaSentry = false\n if (callee?.type === \"Identifier\") {\n name = sentry.named.get(callee.name ?? \"\")\n viaSentry = name !== undefined\n } else if (callee?.type === \"MemberExpression\") {\n name = callee.property?.name\n const obj = callee.object\n viaSentry =\n (obj?.type === \"Identifier\" && sentry.namespaces.has(obj.name ?? \"\")) ||\n (obj?.type === \"MemberExpression\" &&\n obj.property?.name === \"logger\" &&\n obj.object?.type === \"Identifier\" &&\n sentry.namespaces.has(obj.object.name ?? \"\")) ||\n (obj?.type === \"Identifier\" && sentry.named.get(obj.name ?? \"\") === \"logger\")\n }\n if (!name) return\n const obj = callee?.type === \"MemberExpression\" ? callee.object : undefined\n const sentryCall = (names: Set<string>) =>\n obj?.type === \"CallExpression\" &&\n ((obj.callee?.type === \"Identifier\" && names.has(sentry.named.get(obj.callee.name ?? \"\") ?? \"\")) ||\n (obj.callee?.type === \"MemberExpression\" &&\n obj.callee.object?.type === \"Identifier\" &&\n sentry.namespaces.has(obj.callee.object.name ?? \"\") &&\n names.has(obj.callee.property?.name ?? \"\")))\n const recv = obj?.type === \"Identifier\" ? (obj.name ?? \"\") : \"\"\n const spanRecv = SPAN_RECEIVER.test(recv) || sentryCall(SPAN_GETTERS)\n const tagRecv = viaSentry || SCOPE_RECEIVER.test(recv) || sentryCall(SCOPE_GETTERS)\n const keys: [string, AstNode][] = []\n if (\n KEY_SETTERS.has(name) &&\n (name === \"setAttribute\" ? spanRecv : tagRecv) &&\n args[0]?.type === \"Literal\" &&\n typeof args[0].value === \"string\"\n ) {\n keys.push([args[0].value, args[0]])\n } else if (OBJECT_SETTERS.has(name) && (name === \"setAttributes\" ? spanRecv : tagRecv)) {\n keys.push(...objectKeys(args[0]))\n } else if (viaSentry && SPAN_STARTERS.has(name)) {\n keys.push(...objectKeys(propertyNamed(args[0], \"attributes\")))\n } else if (viaSentry && CAPTURES.has(name)) {\n keys.push(...objectKeys(propertyNamed(args[1], \"tags\")))\n } else if (viaSentry && LOG_LEVELS.has(name) && callee?.type === \"MemberExpression\") {\n keys.push(...objectKeys(args[1]))\n }\n for (const [key, at] of keys) flag(key, at)\n },\n })\n },\n }\n}\n\nconst noSentryInit: Rule.RuleModule = {\n meta: meta(\"QDC-OBS-001 - the SDK owns Sentry.init and withSentryConfig\", \"problem\"),\n create(context) {\n const report = reporter(\n context,\n \"QDC-OBS-001 - @quatronic/sdk owns Sentry.init and withSentryConfig. Use Sentry.* directly; see the qore-observability skill.\",\n )\n const sentry = trackSentry()\n return listener({\n ImportDeclaration: (node) => sentry.onImport(node, report),\n CallExpression: (node) => {\n const c = node.callee\n if (\n c?.type === \"MemberExpression\" &&\n c.object?.type === \"Identifier\" &&\n sentry.namespaces.has(c.object.name ?? \"\") &&\n BANNED.has(c.property?.name ?? \"\")\n ) {\n report(node)\n }\n },\n })\n },\n}\n\nexport const observabilityRules: Record<string, Rule.RuleModule> = {\n \"no-sentry-init\": noSentryInit,\n \"sentry-attribute-namespace\": attributeKeyRule(\n \"QDC-OBS-002\",\n \"custom attribute, tag and log keys are qore.<area>.<name>\",\n (key) =>\n key.startsWith(\"qore.\") ? undefined : `'${key}' is not namespaced. Use qore.<area>.<name>, e.g. qore.invoice.id.`,\n ),\n \"no-pii-attributes\": attributeKeyRule(\"QDC-OBS-003\", \"no personal data in telemetry keys\", (key) =>\n PII.test(key)\n ? `'${key}' looks like personal data. Send an id instead; see the qore-observability skill.`\n : undefined,\n ),\n}\n\nexport const OBSERVABILITY_RULE_IDS: Record<string, string> = {\n \"no-sentry-init\": \"QDC-OBS-001\",\n \"sentry-attribute-namespace\": \"QDC-OBS-002\",\n \"no-pii-attributes\": \"QDC-OBS-003\",\n}\n","import type { Linter, Rule } from \"eslint\"\n\nimport { observabilityRules } from \"./observability\"\nimport { RULE_IDS, rules } from \"./rules\"\n\nexport { OBSERVABILITY_RULE_IDS, observabilityRules } from \"./observability\"\nexport { RULE_IDS, rules } from \"./rules\"\n\n/**\n * The QDC security ESLint plugin.\n *\n * It lives here rather than in each generated app because a lint rule is the\n * cheapest security control QDC has, and a rule copied into every app can only\n * be tightened by editing every app. Shipped from the SDK, a corrected rule\n * reaches an app on its next `@quatronic/sdk` bump — the same bump the app\n * pays for everything else in the package.\n *\n * Usage in a generated app's `eslint.config.mjs`:\n *\n * import qdcSecurity from \"@quatronic/sdk/eslint\"\n *\n * export default [\n * ...whatever,\n * ...qdcSecurity.configs.recommended,\n * ]\n *\n * A repository that predates the pack spreads `configs.warn` instead — the\n * same rules and scopes, reporting rather than blocking — and promotes itself\n * to `recommended` once its backlog is clear.\n *\n * The rules are syntactic by design. Anything that has to follow a value\n * across functions and files is a Semgrep rule instead, in this package's\n * `semgrep/` directory, run by the same CI job.\n */\nconst plugin = {\n meta: { name: \"@quatronic/sdk/eslint\" },\n rules: { ...rules, ...observabilityRules },\n}\n\n/** Where a generated app keeps the code these rules are about. */\nconst SRC_FILES = [\"src/**/*.{ts,tsx}\"]\nconst TEST_FILES = [\"**/*.test.{ts,tsx}\", \"**/*.spec.{ts,tsx}\"]\n\nexport interface QdcSecurityConfigs {\n recommended: Linter.Config[]\n /**\n * The same rules, in the same scopes, with every enabled one demoted to\n * \"warn\".\n *\n * For a codebase that predates the pack. Adopting fourteen rules at \"error\"\n * into an existing tree means either a red build on day one or a hand-copied\n * override block in that repo — and a hand-copied block stops covering the\n * pack the moment a fifteenth rule ships, while still reporting green. So\n * the demotion lives here, next to the list it demotes.\n *\n * It is a starting position, not a destination: a pack that warns forever is\n * not a control. Promote the repo to `recommended` once its backlog is clear.\n */\n warn: Linter.Config[]\n /**\n * Telemetry rules for apps whose runtime code logs through Sentry only.\n * Separate from `recommended` so a bump does not turn an older app's lint red.\n */\n observability: Linter.Config[]\n}\n\n/**\n * The plugin object, typed explicitly: an inferred type here reaches into\n * `@eslint/core` and TypeScript refuses to name it in a `.d.ts`.\n */\nexport interface QdcSecurityPlugin {\n meta: { name: string }\n rules: Record<string, Rule.RuleModule>\n configs: QdcSecurityConfigs\n RULE_IDS: Record<string, string>\n}\n\n/**\n * Two blocks, spread into a flat config in this order.\n *\n * The second one is not decoration: a test that proves escaping works has to\n * build the markup it escapes, so the XSS bans are off there. It is a separate\n * block because flat-config rule options **replace** rather than merge — a\n * later block naming a rule wins outright — so the exemption has to come after\n * the block that switches the rules on, and has to name only what it exempts.\n */\nconst recommended: Linter.Config[] = [\n {\n files: SRC_FILES,\n plugins: { qdc: plugin },\n rules: {\n \"qdc/no-dangerous-html\": \"error\",\n \"qdc/no-inner-html\": \"error\",\n \"qdc/no-insert-adjacent-html\": \"error\",\n \"qdc/no-document-write\": \"error\",\n \"qdc/no-string-eval\": \"error\",\n \"qdc/no-raw-script-tag\": \"error\",\n \"qdc/no-sql-interpolation\": \"error\",\n \"qdc/no-sql-raw\": \"error\",\n \"qdc/no-driver-import\": \"error\",\n \"qdc/no-fs-import\": \"error\",\n // Advisory, and `eslint .` does not fail on warnings. All three fire on\n // shapes that are often fine, so they prompt a look rather than block a\n // merge; qdc.redirect.tainted and qdc.ssrf.tainted-fetch in the Semgrep\n // pack follow the value and are what actually decide, and the manifest\n // extractor classifies the same hosts at deploy time for the portal.\n \"qdc/safe-redirect\": \"warn\",\n \"qdc/safe-fetch\": \"warn\",\n \"qdc/no-undeclared-external-call\": \"warn\",\n // Advisory too: the call works either way, the portal just cannot list it.\n \"qdc/integration-static-call\": \"warn\",\n },\n },\n {\n // Tests build markup on purpose, to prove the escaping works.\n files: TEST_FILES,\n plugins: { qdc: plugin },\n rules: {\n \"qdc/no-dangerous-html\": \"off\",\n \"qdc/no-inner-html\": \"off\",\n \"qdc/no-insert-adjacent-html\": \"off\",\n \"qdc/no-document-write\": \"off\",\n \"qdc/no-raw-script-tag\": \"off\",\n // The two location bans are off here for a different reason than the\n // XSS ones above. They do not say \"this is dangerous\"; they say \"this\n // capability belongs in `src/db` or `src/scripts`, not scattered through\n // the app\", and that argument is about the shape of the *application*. A\n // test reads a fixture off disk and an integration test opens a real\n // connection; neither ships, and neither can be reached by a request, so\n // neither is the traversal or injection surface the ban exists for.\n //\n // Found by running the pack over five live tenant repositories before it\n // started blocking merges: `no-fs-import` fired six times in one repo and\n // every hit was a test reading its own fixture.\n \"qdc/no-fs-import\": \"off\",\n \"qdc/no-driver-import\": \"off\",\n // A test that stubs fetch names the host it stubs; nothing leaves the\n // process, and the portal is told about deploys, not test runs.\n \"qdc/no-undeclared-external-call\": \"off\",\n // A test calls a handle it built itself; no deploy reads it.\n \"qdc/integration-static-call\": \"off\",\n },\n },\n]\n\n/**\n * \"off\" is an exemption, not a severity. Demoting it to \"warn\" would switch a\n * rule back on in the scope that deliberately turned it off — the test-file\n * block below being the whole reason that distinction matters.\n */\nconst demote = (config: Linter.Config): Linter.Config => ({\n ...config,\n rules: Object.fromEntries(\n Object.entries(config.rules ?? {}).map(([id, level]) => [\n id,\n level === \"off\" ? \"off\" : \"warn\",\n ]),\n ),\n})\n\nconst warn: Linter.Config[] = recommended.map(demote)\n\nconst observability: Linter.Config[] = [\n {\n files: SRC_FILES,\n plugins: { qdc: plugin },\n rules: {\n \"qdc/no-sentry-init\": \"error\",\n \"no-console\": \"error\",\n \"qdc/sentry-attribute-namespace\": \"warn\",\n \"qdc/no-pii-attributes\": \"warn\",\n },\n },\n {\n // CLI tools and tests print on purpose.\n files: [\"**/seed.ts\", \"src/scripts/**\", ...TEST_FILES],\n rules: { \"no-console\": \"off\" },\n },\n]\n\nexport const configs: QdcSecurityConfigs = { recommended, warn, observability }\n\nconst qdcSecurity: QdcSecurityPlugin = { ...plugin, configs, RULE_IDS }\n\nexport default qdcSecurity\n"],"mappings":"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","names":["obj"]}
1
+ {"version":3,"sources":["../../src/eslint/ast.ts","../../src/eslint/rules.ts","../../src/eslint/observability.ts","../../src/eslint/index.ts"],"sourcesContent":["// A deliberately narrow view of the AST, for rules that are deliberately\n// syntactic.\n//\n// ESLint's own `Rule.Node` is the ESTree union. It does not describe JSX at\n// all, and it forces a narrowing cascade for every property access — in rules\n// whose whole job is to read three or four fields off a node and report it.\n// So these rules declare exactly the fields they read and cast once, at the\n// visitor boundary, rather than per line.\n//\n// Anything that has to follow a value across functions or files is a Semgrep\n// rule instead; that pack ships in `semgrep/` alongside this plugin.\nexport interface AstNode {\n type: string\n /** `Identifier.name` and `MemberExpression.property.name`. Never a JSX name. */\n name?: string\n value?: unknown\n callee?: AstNode\n object?: AstNode\n property?: AstNode\n source?: AstNode\n arguments?: (AstNode | undefined)[]\n expressions?: (AstNode | undefined)[]\n /** `TemplateLiteral.quasis`: the fixed text between the `${}` holes. */\n quasis?: { value?: { cooked?: string | null; raw?: string } }[]\n /** `BinaryExpression.left`; the rule that reads it only follows the left edge. */\n left?: AstNode\n operator?: string\n /** `ImportDeclaration.specifiers`. */\n specifiers?: AstNode[]\n /** `ImportSpecifier.imported` / `.local`. */\n imported?: AstNode\n local?: AstNode\n /** `ObjectExpression.properties`; a `Property` has `key` and `computed`. */\n properties?: AstNode[]\n key?: AstNode\n computed?: boolean\n /** Set by ESLint on every node it visits. */\n parent?: AstNode\n}\n\n/** Calls whose result is a URL this app decided on, not one a caller supplied. */\nexport const VETTED_CALLS = [\"safeRedirect\", \"safeFetch\", \"vetTarget\", \"clone\"]\n\n/** POSIX form of the file being linted, so the checks read the same on Windows. */\nexport function pathOf(filename: string): string {\n return filename.replaceAll(\"\\\\\", \"/\")\n}\n\n/** Is the linted file at, or inside, one of these project paths? */\nexport function isUnder(filename: string, paths: readonly string[]): boolean {\n const file = pathOf(filename)\n return paths.some((path) => file.endsWith(`/${path}`) || file.includes(`/${path}/`))\n}\n\n/** The called name, whether the callee is `f()` or `x.f()`. */\nexport function calleeName(node: AstNode): string {\n const callee = node.callee\n if (!callee) return \"\"\n if (callee.type === \"Identifier\") return callee.name ?? \"\"\n if (callee.type === \"MemberExpression\" && callee.property?.type === \"Identifier\") {\n return callee.property.name ?? \"\"\n }\n return \"\"\n}\n\n/**\n * A target this app wrote down, rather than one that arrived with the request.\n *\n * `allowIdentifier` is the whole difference between the two advisory rules:\n * judging what a bare variable holds takes dataflow, which is what the Semgrep\n * taint rules do. `qdc/safe-redirect` lets a variable through because\n * `NextResponse.redirect(url)` after `req.nextUrl.clone()` is the canonical\n * Next redirect and appears in every generated app; `qdc/safe-fetch` does not.\n */\nexport function isFixedTarget(node: AstNode | undefined, allowIdentifier: boolean): boolean {\n if (!node) return false\n if (node.type === \"Literal\") return typeof node.value === \"string\"\n if (node.type === \"TemplateLiteral\") return (node.expressions?.length ?? 0) === 0\n if (node.type === \"Identifier\") return allowIdentifier\n if (node.type === \"NewExpression\") {\n // `new URL(\"/login\", req.url)` fixes the path; the base only ever\n // contributes this app's own origin.\n return node.callee?.name === \"URL\" && isFixedTarget(node.arguments?.[0], allowIdentifier)\n }\n if (node.type === \"CallExpression\") return VETTED_CALLS.includes(calleeName(node))\n return false\n}\n","import type { Rule } from \"eslint\"\n\nimport { isPlatformManagedHost } from \"../integrations-core/platform-hosts\"\nimport { type AstNode, calleeName, isFixedTarget, isUnder } from \"./ast\"\n\n/**\n * The fourteen QDC security lint rules.\n *\n * One rule per id, rather than fourteen entries in `no-restricted-syntax`, for\n * two reasons that only show up after the fact:\n *\n * - **Severity is per rule.** `no-restricted-syntax` carries one severity for\n * its whole array, and three of these are advisory on purpose — they fire on\n * shapes that are usually fine, and the Semgrep pack does the dataflow they\n * can only approximate.\n * - **Suppression is per rule.** `// eslint-disable-next-line\n * no-restricted-syntax` switches off every selector on that line, so\n * silencing \"no inline styles\" would silently also silence the\n * `dangerouslySetInnerHTML` ban sitting next to it.\n *\n * Suppress one by its own name, and say why:\n *\n * // eslint-disable-next-line qdc/no-dangerous-html -- sanitised in renderPost()\n */\n\n/** Modules that talk to the database directly, and where that is allowed. */\nconst DB_DRIVERS = [\"pg\", \"postgres\", \"mysql2\", \"mysql2/promise\"]\nconst DB_LAYER = [\"src/db\", \"src/lib/db.ts\"]\n\n/** Node's filesystem API, and where a generated app is expected to use it. */\nconst FS_MODULES = [\"fs\", \"node:fs\", \"fs/promises\", \"node:fs/promises\"]\n// `src/lib/migrate.ts` is the migration runner: it reads the migration files\n// off disk at boot, which is the sanctioned use, and it is not reachable from\n// a request. The backend recipes put it there rather than under src/db.\nconst FS_CALLERS = [\"src/scripts\", \"src/db\", \"src/lib/migrate.ts\"]\n\n/** Where a fetch goes out with the server's own network position. */\nconst SERVER_PATHS = [\"src/app/api\", \"src/lib/server\", \"src/proxy.ts\"]\n\n/** One cast, at the boundary, instead of one per `context.report` call. */\nexport function reporter(context: Rule.RuleContext, message: string): (node: AstNode) => void {\n return (node: AstNode) => {\n context.report({ node: node as unknown as Rule.Node, message })\n }\n}\n\nexport function listener(visitor: Record<string, (node: AstNode) => void>): Rule.RuleListener {\n return visitor as unknown as Rule.RuleListener\n}\n\nexport function meta(description: string, type: \"problem\" | \"suggestion\"): Rule.RuleModule[\"meta\"] {\n return { type, docs: { description }, schema: [] }\n}\n\n/** A rule that reports every node matching one of `selectors`. */\nfunction ban(id: string, message: string, selectors: string[]): Rule.RuleModule {\n const text = `${id} - ${message}`\n return {\n meta: meta(text, \"problem\"),\n create(context) {\n const report = reporter(context, text)\n return listener(Object.fromEntries(selectors.map((selector) => [selector, report])))\n },\n }\n}\n\n/** A rule that bans importing any of `modules`, except from `allowed` paths. */\nfunction banImports(\n id: string,\n message: string,\n modules: string[],\n allowed: string[],\n): Rule.RuleModule {\n const text = `${id} - ${message}`\n return {\n meta: meta(text, \"problem\"),\n create(context) {\n if (isUnder(context.filename, allowed)) return {}\n const report = reporter(context, text)\n const flag = (node: AstNode) => {\n const source = node.source\n if (source?.type === \"Literal\" && typeof source.value === \"string\") {\n if (modules.includes(source.value)) report(node)\n }\n }\n return listener({ ImportDeclaration: flag, ImportExpression: flag })\n },\n }\n}\n\nconst noStringEval: Rule.RuleModule = {\n meta: meta(\"QDC-XSS-005 - no code compiled from a string\", \"problem\"),\n create(context) {\n const report = reporter(\n context,\n \"QDC-XSS-005 - compiling a string into code runs whatever the string turned out to be. Pass a function.\",\n )\n const isStringy = (node: AstNode | undefined) =>\n node?.type === \"TemplateLiteral\" ||\n node?.type === \"BinaryExpression\" ||\n (node?.type === \"Literal\" && typeof node.value === \"string\")\n return listener({\n \"CallExpression[callee.name='eval']\": report,\n \"CallExpression[callee.name='Function']\": report,\n \"NewExpression[callee.name='Function']\": report,\n \"CallExpression[callee.name=/^set(Timeout|Interval)$/]\": (node) => {\n if (isStringy(node.arguments?.[0])) report(node)\n },\n })\n },\n}\n\nconst safeRedirect: Rule.RuleModule = {\n meta: meta(\"QDC-URL-001 - redirect to a target this app decided on\", \"suggestion\"),\n create(context) {\n const report = reporter(\n context,\n \"QDC-URL-001 - redirecting to a value that came with the request is an open redirect. Wrap it in safeRedirect from @quatronic/sdk/next/auth.\",\n )\n return listener({\n \"CallExpression[callee.object.name='NextResponse'][callee.property.name='redirect']\": (\n node,\n ) => {\n if (isFixedTarget(node.arguments?.[0], true)) return\n report(node)\n },\n })\n },\n}\n\nconst safeFetch: Rule.RuleModule = {\n meta: meta(\"QDC-NET-001 - server-side fetch of a caller-supplied URL\", \"suggestion\"),\n create(context) {\n if (!isUnder(context.filename, SERVER_PATHS)) return {}\n const report = reporter(\n context,\n \"QDC-NET-001 - a URL the server did not decide on reaches the private network too. Use safeFetch from @quatronic/sdk/node/net, or connection(alias).fetch for a wired module.\",\n )\n return listener({\n \"CallExpression[callee.name='fetch']\": (node) => {\n if (isFixedTarget(node.arguments?.[0], false)) return\n report(node)\n },\n })\n },\n}\n\n/** Outbound HTTP clients a generated app might reach for. */\nconst HTTP_CLIENTS = [\"fetch\", \"axios\", \"got\", \"ky\"]\nconst HTTP_LIBS = [\"axios\", \"got\", \"ky\"]\nconst HTTP_METHODS = /^(get|post|put|patch|delete|head|request)$/\n\n/** A host that is this cluster or this machine, so not an external API. */\nconst LOCAL_HOST_SUFFIXES = [\".local\", \".internal\", \".svc\", \".cluster.local\"]\nconst ENV_KEY = /^[A-Z][A-Z0-9_]*$/\n\nfunction isLocalHost(host: string): boolean {\n const h = host.toLowerCase()\n if (h === \"localhost\" || h === \"::1\" || h === \"0.0.0.0\" || h.startsWith(\"127.\")) return true\n return LOCAL_HOST_SUFFIXES.some((suffix) => h.endsWith(suffix))\n}\n\n/** The host of an `http(s)://` string, or null when it has none or a `${}` hole cuts it off. */\nfunction hostOf(text: string): string | null {\n const m = /^https?:\\/\\/(?:[^@/?#]*@)?(\\[[^\\]]*\\]|[^/?#:]+)/i.exec(text)\n if (!m?.[1]) return null\n return m[1].replace(/^\\[|\\]$/g, \"\")\n}\n\n/** `process.env.X`, `env.X`, `serverEnv().X` — the env key, when `X` looks like one. */\nfunction envKeyOf(node: AstNode | undefined): string | null {\n if (node?.type !== \"MemberExpression\" || node.property?.type !== \"Identifier\") return null\n const key = node.property.name ?? \"\"\n if (!ENV_KEY.test(key)) return null\n const object = node.object\n if (!object) return null\n if (object.type === \"Identifier\") return object.name === \"env\" ? key : null\n if (object.type === \"MemberExpression\") {\n const isProcessEnv =\n object.object?.type === \"Identifier\" &&\n object.object.name === \"process\" &&\n object.property?.name === \"env\"\n return isProcessEnv ? key : null\n }\n if (object.type === \"CallExpression\") {\n const name = calleeName(object)\n return name === \"env\" || name.endsWith(\"Env\") ? key : null\n }\n return null\n}\n\ntype ExternalTarget = { host: string } | { envKey: string } | null\n\n/**\n * Where a call is going, read off its first argument: a host when the code\n * spells the URL out, an env key when the base comes from the environment,\n * null when the shape says nothing (a bare variable, a relative path).\n */\nfunction externalTargetOf(node: AstNode | undefined): ExternalTarget {\n if (!node) return null\n if (node.type === \"Literal\" && typeof node.value === \"string\") {\n const host = hostOf(node.value)\n return host ? { host } : null\n }\n if (node.type === \"TemplateLiteral\") {\n const head = node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? \"\"\n const host = hostOf(head)\n if (host) return { host }\n // `${process.env.SAP_BASE_URL}/path` — the base is the first hole.\n if (head === \"\") {\n const key = envKeyOf(node.expressions?.[0])\n if (key) return { envKey: key }\n }\n return null\n }\n if (node.type === \"BinaryExpression\" && node.operator === \"+\") return externalTargetOf(node.left)\n if (node.type === \"MemberExpression\") {\n const key = envKeyOf(node)\n return key ? { envKey: key } : null\n }\n if (node.type === \"NewExpression\" && node.callee?.name === \"URL\") {\n // new URL(path, base): the base decides the host; new URL(absolute) has no base.\n const args = node.arguments ?? []\n return externalTargetOf(args[1]) ?? externalTargetOf(args[0])\n }\n return null\n}\n\nfunction isHttpClientCall(node: AstNode): boolean {\n const callee = node.callee\n if (!callee) return false\n if (callee.type === \"Identifier\") return HTTP_CLIENTS.includes(callee.name ?? \"\")\n if (callee.type === \"MemberExpression\") {\n return (\n callee.object?.type === \"Identifier\" &&\n HTTP_LIBS.includes(callee.object.name ?? \"\") &&\n callee.property?.type === \"Identifier\" &&\n HTTP_METHODS.test(callee.property.name ?? \"\")\n )\n }\n return false\n}\n\n/**\n * An outbound call to a host this app has not declared. Advisory: the rule\n * reads one argument, so it sees a spelled-out host or an env-backed base and\n * nothing more. The manifest extractor does the same classification at deploy\n * time and the portal shows the result; this is the editor-time echo of it.\n */\nconst noUndeclaredExternalCall: Rule.RuleModule = {\n meta: meta(\"QDC-NET-002 - outbound call to an external API this app has not declared\", \"suggestion\"),\n create(context) {\n return listener({\n CallExpression: (node) => {\n if (!isHttpClientCall(node)) return\n const target = externalTargetOf(node.arguments?.[0])\n if (!target) return\n let label: string\n if (\"host\" in target) {\n if (isLocalHost(target.host) || isPlatformManagedHost(target.host)) return\n label = target.host\n } else {\n // CONN_* is a wired module connection: the portal already knows the other end.\n if (target.envKey.startsWith(\"CONN_\")) return\n label = `$${target.envKey}`\n }\n context.report({\n node: node as unknown as Rule.Node,\n message: `QDC-NET-002 - ${label} is an external API this app has not declared. Declare it in src/config/integrations.ts and call integration(alias).get/post, so the portal can see and manage it.`,\n })\n },\n })\n },\n}\n\n/** The calls on an integration handle that send a request, and where their path sits. */\nconst INTEGRATION_PATH_ARG: Record<string, number> = { get: 0, post: 0, put: 0, patch: 0, delete: 0, request: 1 }\n/** Reads off a handle that send nothing, so taking them through a variable hides no endpoint. */\nconst INTEGRATION_READS = [\"config\", \"alias\", \"definition\"]\n\nconst isStringLiteral = (node: AstNode | undefined): boolean =>\n node?.type === \"Literal\" && typeof node.value === \"string\"\n\n/**\n * An integration call the portal cannot read. The manifest extractor reads\n * exactly `integration(\"alias\").get(\"/path\")`, alias and path written at the\n * call site, because it reads source, not values. A path passed in through a\n * helper, an alias held in a variable, or a handle stored and called later all\n * reach the API fine and leave the portal's Integrations tab short of that\n * endpoint, with nothing failing to say so. Advisory, like QDC-NET-002.\n */\nconst integrationStaticCall: Rule.RuleModule = {\n meta: meta(\"QDC-NET-003 - an integration call whose alias or path the portal cannot read\", \"suggestion\"),\n create(context) {\n const report = (node: AstNode, message: string) => {\n context.report({ node: node as unknown as Rule.Node, message: `QDC-NET-003 - ${message}` })\n }\n return listener({\n CallExpression: (node) => {\n if (node.callee?.type !== \"Identifier\" || node.callee.name !== \"integration\") return\n if (!isStringLiteral(node.arguments?.[0])) {\n report(\n node,\n 'the alias is not a string literal, so the portal cannot tell which integration this calls. Write integration(\"alias\") here.',\n )\n return\n }\n const member = node.parent\n const method =\n member?.type === \"MemberExpression\" && member.object === node && member.property?.type === \"Identifier\"\n ? (member.property.name ?? \"\")\n : \"\"\n if (INTEGRATION_READS.includes(method)) return\n const call = member?.parent\n const index = INTEGRATION_PATH_ARG[method]\n if (index === undefined || call?.type !== \"CallExpression\" || call.callee !== member) {\n report(\n node,\n 'the integration handle is stored or passed on instead of called here, so the portal cannot see the endpoints called through it. Write integration(\"alias\").get(\"/path\") at each call site.',\n )\n return\n }\n const path = call.arguments?.[index]\n if (path === undefined || isStringLiteral(path) || path.type === \"TemplateLiteral\") return\n report(\n path,\n \"the path is not written at the call site, so the portal cannot list this endpoint. Write it as a string or template literal here, e.g. `/orders/${id}`. A helper may share options, never the path.\",\n )\n },\n })\n },\n}\n\nexport const rules: Record<string, Rule.RuleModule> = {\n \"no-dangerous-html\": ban(\n \"QDC-XSS-001\",\n \"dangerouslySetInnerHTML renders unescaped markup. Render text, or sanitise it first.\",\n [\"JSXAttribute[name.name='dangerouslySetInnerHTML']\"],\n ),\n \"no-inner-html\": ban(\n \"QDC-XSS-002\",\n \"Assigning innerHTML parses the string as markup. Use textContent.\",\n [\n \"AssignmentExpression[left.type='MemberExpression'][left.property.name=/^(innerHTML|outerHTML)$/]\",\n ],\n ),\n \"no-insert-adjacent-html\": ban(\n \"QDC-XSS-003\",\n \"insertAdjacentHTML parses its argument as markup. Build the nodes instead.\",\n [\"CallExpression[callee.property.name='insertAdjacentHTML']\"],\n ),\n \"no-document-write\": ban(\n \"QDC-XSS-004\",\n \"document.write parses its argument as markup and blocks the parser. Render it in React.\",\n [\"CallExpression[callee.object.name='document'][callee.property.name=/^write(ln)?$/]\"],\n ),\n \"no-string-eval\": noStringEval,\n \"no-raw-script-tag\": ban(\n \"QDC-CSP-001\",\n \"A hand-written <script> gets no nonce, and this app's CSP uses 'strict-dynamic', so it will not run. Use next/script.\",\n [\"JSXElement[openingElement.name.name='script']\"],\n ),\n \"no-sql-interpolation\": ban(\n \"QDC-SQL-001\",\n \"An interpolated string reaching .execute() is the injection. Use the sql tag, which parameterises.\",\n [\n \"CallExpression[callee.property.name='execute'] > TemplateLiteral[expressions.length>0]\",\n \"CallExpression[callee.property.name='execute'] > BinaryExpression[operator='+']\",\n ],\n ),\n \"no-sql-raw\": ban(\n \"QDC-SQL-002\",\n \"sql.raw() puts its argument into the query verbatim and sanitises nothing. Interpolate through the sql tag.\",\n [\"MemberExpression[object.name='sql'][property.name='raw']\"],\n ),\n \"no-driver-import\": banImports(\n \"QDC-SQL-003\",\n \"The database driver belongs in the db layer. Everywhere else, go through Drizzle.\",\n DB_DRIVERS,\n DB_LAYER,\n ),\n \"no-fs-import\": banImports(\n \"QDC-FS-001\",\n \"Request handlers should not reach the filesystem. If one must, use safeJoin from @quatronic/sdk/node/fspath.\",\n FS_MODULES,\n FS_CALLERS,\n ),\n \"safe-redirect\": safeRedirect,\n \"safe-fetch\": safeFetch,\n \"no-undeclared-external-call\": noUndeclaredExternalCall,\n \"integration-static-call\": integrationStaticCall,\n}\n\n/**\n * Rule name to QDC id, for docs and for anything that wants to assert the set\n * without re-deriving it from the messages.\n */\nexport const RULE_IDS: Record<string, string> = {\n \"no-dangerous-html\": \"QDC-XSS-001\",\n \"no-inner-html\": \"QDC-XSS-002\",\n \"no-insert-adjacent-html\": \"QDC-XSS-003\",\n \"no-document-write\": \"QDC-XSS-004\",\n \"no-string-eval\": \"QDC-XSS-005\",\n \"no-raw-script-tag\": \"QDC-CSP-001\",\n \"no-sql-interpolation\": \"QDC-SQL-001\",\n \"no-sql-raw\": \"QDC-SQL-002\",\n \"no-driver-import\": \"QDC-SQL-003\",\n \"no-fs-import\": \"QDC-FS-001\",\n \"safe-redirect\": \"QDC-URL-001\",\n \"safe-fetch\": \"QDC-NET-001\",\n \"no-undeclared-external-call\": \"QDC-NET-002\",\n \"integration-static-call\": \"QDC-NET-003\",\n}\n","import type { Rule } from \"eslint\"\n\nimport type { AstNode } from \"./ast\"\nimport { listener, meta, reporter } from \"./rules\"\n\nconst SENTRY_MODULE = /^@sentry\\//\nconst BANNED = new Set([\"init\", \"withSentryConfig\"])\nconst SPAN_STARTERS = new Set([\"startSpan\", \"startSpanManual\", \"startInactiveSpan\"])\nconst CAPTURES = new Set([\"captureException\", \"captureMessage\"])\nconst LOG_LEVELS = new Set([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"])\nconst KEY_SETTERS = new Set([\"setAttribute\", \"setTag\"])\nconst OBJECT_SETTERS = new Set([\"setAttributes\", \"setTags\"])\nconst SPAN_RECEIVER = /^(span|\\w*Span)$/\nconst SCOPE_RECEIVER = /^(scope|\\w*Scope)$/\nconst SPAN_GETTERS = new Set([\"getActiveSpan\", \"getRootSpan\"])\nconst SCOPE_GETTERS = new Set([\"getCurrentScope\", \"getIsolationScope\"])\nconst PII =\n /(^|[._-])(e-?mail|phone|password|passwd|token|secret|iban|bsn|ssn|ip_?address|first_?name|last_?name|full_?name|username)($|[._-])/i\n\n/** Local names bound to `@sentry/*` (namespace, default, or a named export). */\nfunction trackSentry() {\n const namespaces = new Set<string>()\n const named = new Map<string, string>()\n return {\n namespaces,\n named,\n onImport(node: AstNode, onBanned?: (n: AstNode) => void) {\n const src = node.source?.value\n if (typeof src !== \"string\" || !SENTRY_MODULE.test(src)) return\n for (const s of node.specifiers ?? []) {\n const local = s.local?.name ?? \"\"\n if (s.type === \"ImportNamespaceSpecifier\" || s.type === \"ImportDefaultSpecifier\") namespaces.add(local)\n else if (s.type === \"ImportSpecifier\") {\n const imported = s.imported?.name ?? \"\"\n named.set(local, imported)\n if (BANNED.has(imported)) onBanned?.(s)\n }\n }\n },\n }\n}\n\nconst keyOf = (prop: AstNode): string | undefined => {\n if (prop.type !== \"Property\" || prop.computed) return undefined\n if (prop.key?.type === \"Identifier\") return prop.key.name\n return typeof prop.key?.value === \"string\" ? prop.key.value : undefined\n}\n\nconst objectKeys = (node: AstNode | undefined): [string, AstNode][] =>\n node?.type === \"ObjectExpression\"\n ? (node.properties ?? []).flatMap((p) => {\n const k = keyOf(p)\n return k === undefined ? [] : [[k, p] as [string, AstNode]]\n })\n : []\n\nconst propertyNamed = (node: AstNode | undefined, name: string): AstNode | undefined =>\n node?.type === \"ObjectExpression\"\n ? ((node.properties ?? []).find((p) => keyOf(p) === name)?.value as AstNode | undefined)\n : undefined\n\n/** Reports every attribute, tag or log-field key the app hands Sentry. */\nfunction attributeKeyRule(\n id: string,\n description: string,\n check: (key: string) => string | undefined,\n): Rule.RuleModule {\n return {\n meta: meta(`${id} - ${description}`, \"suggestion\"),\n create(context) {\n const sentry = trackSentry()\n const flag = (key: string, node: AstNode) => {\n const problem = check(key)\n if (problem) reporter(context, `${id} - ${problem}`)(node)\n }\n return listener({\n ImportDeclaration: (node) => sentry.onImport(node),\n CallExpression: (node) => {\n const callee = node.callee\n const args = node.arguments ?? []\n let name: string | undefined\n let viaSentry = false\n if (callee?.type === \"Identifier\") {\n name = sentry.named.get(callee.name ?? \"\")\n viaSentry = name !== undefined\n } else if (callee?.type === \"MemberExpression\") {\n name = callee.property?.name\n const obj = callee.object\n viaSentry =\n (obj?.type === \"Identifier\" && sentry.namespaces.has(obj.name ?? \"\")) ||\n (obj?.type === \"MemberExpression\" &&\n obj.property?.name === \"logger\" &&\n obj.object?.type === \"Identifier\" &&\n sentry.namespaces.has(obj.object.name ?? \"\")) ||\n (obj?.type === \"Identifier\" && sentry.named.get(obj.name ?? \"\") === \"logger\")\n }\n if (!name) return\n const obj = callee?.type === \"MemberExpression\" ? callee.object : undefined\n const sentryCall = (names: Set<string>) =>\n obj?.type === \"CallExpression\" &&\n ((obj.callee?.type === \"Identifier\" && names.has(sentry.named.get(obj.callee.name ?? \"\") ?? \"\")) ||\n (obj.callee?.type === \"MemberExpression\" &&\n obj.callee.object?.type === \"Identifier\" &&\n sentry.namespaces.has(obj.callee.object.name ?? \"\") &&\n names.has(obj.callee.property?.name ?? \"\")))\n const recv = obj?.type === \"Identifier\" ? (obj.name ?? \"\") : \"\"\n const spanRecv = SPAN_RECEIVER.test(recv) || sentryCall(SPAN_GETTERS)\n const tagRecv = viaSentry || SCOPE_RECEIVER.test(recv) || sentryCall(SCOPE_GETTERS)\n const keys: [string, AstNode][] = []\n if (\n KEY_SETTERS.has(name) &&\n (name === \"setAttribute\" ? spanRecv : tagRecv) &&\n args[0]?.type === \"Literal\" &&\n typeof args[0].value === \"string\"\n ) {\n keys.push([args[0].value, args[0]])\n } else if (OBJECT_SETTERS.has(name) && (name === \"setAttributes\" ? spanRecv : tagRecv)) {\n keys.push(...objectKeys(args[0]))\n } else if (viaSentry && SPAN_STARTERS.has(name)) {\n keys.push(...objectKeys(propertyNamed(args[0], \"attributes\")))\n } else if (viaSentry && CAPTURES.has(name)) {\n keys.push(...objectKeys(propertyNamed(args[1], \"tags\")))\n } else if (viaSentry && LOG_LEVELS.has(name) && callee?.type === \"MemberExpression\") {\n keys.push(...objectKeys(args[1]))\n }\n for (const [key, at] of keys) flag(key, at)\n },\n })\n },\n }\n}\n\nconst noSentryInit: Rule.RuleModule = {\n meta: meta(\"QDC-OBS-001 - the SDK owns Sentry.init and withSentryConfig\", \"problem\"),\n create(context) {\n const report = reporter(\n context,\n \"QDC-OBS-001 - @quatronic/sdk owns Sentry.init and withSentryConfig. Use Sentry.* directly; see the qore-observability skill.\",\n )\n const sentry = trackSentry()\n return listener({\n ImportDeclaration: (node) => sentry.onImport(node, report),\n CallExpression: (node) => {\n const c = node.callee\n if (\n c?.type === \"MemberExpression\" &&\n c.object?.type === \"Identifier\" &&\n sentry.namespaces.has(c.object.name ?? \"\") &&\n BANNED.has(c.property?.name ?? \"\")\n ) {\n report(node)\n }\n },\n })\n },\n}\n\n/**\n * App code reports a caught error through the SDK, not through Sentry, so the\n * tracker behind it can change in one place. Spans, the logger and tags stay\n * `Sentry.*`; only the capture call moves.\n */\nconst useReportError: Rule.RuleModule = {\n meta: meta(\"QDC-OBS-004 - report caught errors with reportError\", \"suggestion\"),\n create(context) {\n const report = reporter(\n context,\n \"QDC-OBS-004 - use reportError(err, ctx) from @quatronic/sdk/observability instead of Sentry.captureException; see migration report-error.\",\n )\n const sentry = trackSentry()\n return listener({\n ImportDeclaration: (node) => sentry.onImport(node),\n CallExpression: (node) => {\n const c = node.callee\n const name =\n c?.type === \"Identifier\"\n ? sentry.named.get(c.name ?? \"\")\n : c?.type === \"MemberExpression\" &&\n c.object?.type === \"Identifier\" &&\n sentry.namespaces.has(c.object.name ?? \"\")\n ? c.property?.name\n : undefined\n if (name === \"captureException\") report(node)\n },\n })\n },\n}\n\nexport const observabilityRules: Record<string, Rule.RuleModule> = {\n \"no-sentry-init\": noSentryInit,\n \"sentry-attribute-namespace\": attributeKeyRule(\n \"QDC-OBS-002\",\n \"custom attribute, tag and log keys are qore.<area>.<name>\",\n (key) =>\n key.startsWith(\"qore.\") ? undefined : `'${key}' is not namespaced. Use qore.<area>.<name>, e.g. qore.invoice.id.`,\n ),\n \"no-pii-attributes\": attributeKeyRule(\"QDC-OBS-003\", \"no personal data in telemetry keys\", (key) =>\n PII.test(key)\n ? `'${key}' looks like personal data. Send an id instead; see the qore-observability skill.`\n : undefined,\n ),\n \"use-report-error\": useReportError,\n}\n\nexport const OBSERVABILITY_RULE_IDS: Record<string, string> = {\n \"no-sentry-init\": \"QDC-OBS-001\",\n \"sentry-attribute-namespace\": \"QDC-OBS-002\",\n \"no-pii-attributes\": \"QDC-OBS-003\",\n \"use-report-error\": \"QDC-OBS-004\",\n}\n","import type { Linter, Rule } from \"eslint\"\n\nimport { observabilityRules } from \"./observability\"\nimport { RULE_IDS, rules } from \"./rules\"\n\nexport { OBSERVABILITY_RULE_IDS, observabilityRules } from \"./observability\"\nexport { RULE_IDS, rules } from \"./rules\"\n\n/**\n * The QDC security ESLint plugin.\n *\n * It lives here rather than in each generated app because a lint rule is the\n * cheapest security control QDC has, and a rule copied into every app can only\n * be tightened by editing every app. Shipped from the SDK, a corrected rule\n * reaches an app on its next `@quatronic/sdk` bump — the same bump the app\n * pays for everything else in the package.\n *\n * Usage in a generated app's `eslint.config.mjs`:\n *\n * import qdcSecurity from \"@quatronic/sdk/eslint\"\n *\n * export default [\n * ...whatever,\n * ...qdcSecurity.configs.recommended,\n * ]\n *\n * A repository that predates the pack spreads `configs.warn` instead — the\n * same rules and scopes, reporting rather than blocking — and promotes itself\n * to `recommended` once its backlog is clear.\n *\n * The rules are syntactic by design. Anything that has to follow a value\n * across functions and files is a Semgrep rule instead, in this package's\n * `semgrep/` directory, run by the same CI job.\n */\nconst plugin = {\n meta: { name: \"@quatronic/sdk/eslint\" },\n rules: { ...rules, ...observabilityRules },\n}\n\n/** Where a generated app keeps the code these rules are about. */\nconst SRC_FILES = [\"src/**/*.{ts,tsx}\"]\nconst TEST_FILES = [\"**/*.test.{ts,tsx}\", \"**/*.spec.{ts,tsx}\"]\n\nexport interface QdcSecurityConfigs {\n recommended: Linter.Config[]\n /**\n * The same rules, in the same scopes, with every enabled one demoted to\n * \"warn\".\n *\n * For a codebase that predates the pack. Adopting fourteen rules at \"error\"\n * into an existing tree means either a red build on day one or a hand-copied\n * override block in that repo — and a hand-copied block stops covering the\n * pack the moment a fifteenth rule ships, while still reporting green. So\n * the demotion lives here, next to the list it demotes.\n *\n * It is a starting position, not a destination: a pack that warns forever is\n * not a control. Promote the repo to `recommended` once its backlog is clear.\n */\n warn: Linter.Config[]\n /**\n * Telemetry rules for apps whose runtime code logs through Sentry only.\n * Separate from `recommended` so a bump does not turn an older app's lint red.\n */\n observability: Linter.Config[]\n}\n\n/**\n * The plugin object, typed explicitly: an inferred type here reaches into\n * `@eslint/core` and TypeScript refuses to name it in a `.d.ts`.\n */\nexport interface QdcSecurityPlugin {\n meta: { name: string }\n rules: Record<string, Rule.RuleModule>\n configs: QdcSecurityConfigs\n RULE_IDS: Record<string, string>\n}\n\n/**\n * Two blocks, spread into a flat config in this order.\n *\n * The second one is not decoration: a test that proves escaping works has to\n * build the markup it escapes, so the XSS bans are off there. It is a separate\n * block because flat-config rule options **replace** rather than merge — a\n * later block naming a rule wins outright — so the exemption has to come after\n * the block that switches the rules on, and has to name only what it exempts.\n */\nconst recommended: Linter.Config[] = [\n {\n files: SRC_FILES,\n plugins: { qdc: plugin },\n rules: {\n \"qdc/no-dangerous-html\": \"error\",\n \"qdc/no-inner-html\": \"error\",\n \"qdc/no-insert-adjacent-html\": \"error\",\n \"qdc/no-document-write\": \"error\",\n \"qdc/no-string-eval\": \"error\",\n \"qdc/no-raw-script-tag\": \"error\",\n \"qdc/no-sql-interpolation\": \"error\",\n \"qdc/no-sql-raw\": \"error\",\n \"qdc/no-driver-import\": \"error\",\n \"qdc/no-fs-import\": \"error\",\n // Advisory, and `eslint .` does not fail on warnings. All three fire on\n // shapes that are often fine, so they prompt a look rather than block a\n // merge; qdc.redirect.tainted and qdc.ssrf.tainted-fetch in the Semgrep\n // pack follow the value and are what actually decide, and the manifest\n // extractor classifies the same hosts at deploy time for the portal.\n \"qdc/safe-redirect\": \"warn\",\n \"qdc/safe-fetch\": \"warn\",\n \"qdc/no-undeclared-external-call\": \"warn\",\n // Advisory too: the call works either way, the portal just cannot list it.\n \"qdc/integration-static-call\": \"warn\",\n },\n },\n {\n // Tests build markup on purpose, to prove the escaping works.\n files: TEST_FILES,\n plugins: { qdc: plugin },\n rules: {\n \"qdc/no-dangerous-html\": \"off\",\n \"qdc/no-inner-html\": \"off\",\n \"qdc/no-insert-adjacent-html\": \"off\",\n \"qdc/no-document-write\": \"off\",\n \"qdc/no-raw-script-tag\": \"off\",\n // The two location bans are off here for a different reason than the\n // XSS ones above. They do not say \"this is dangerous\"; they say \"this\n // capability belongs in `src/db` or `src/scripts`, not scattered through\n // the app\", and that argument is about the shape of the *application*. A\n // test reads a fixture off disk and an integration test opens a real\n // connection; neither ships, and neither can be reached by a request, so\n // neither is the traversal or injection surface the ban exists for.\n //\n // Found by running the pack over five live tenant repositories before it\n // started blocking merges: `no-fs-import` fired six times in one repo and\n // every hit was a test reading its own fixture.\n \"qdc/no-fs-import\": \"off\",\n \"qdc/no-driver-import\": \"off\",\n // A test that stubs fetch names the host it stubs; nothing leaves the\n // process, and the portal is told about deploys, not test runs.\n \"qdc/no-undeclared-external-call\": \"off\",\n // A test calls a handle it built itself; no deploy reads it.\n \"qdc/integration-static-call\": \"off\",\n },\n },\n]\n\n/**\n * \"off\" is an exemption, not a severity. Demoting it to \"warn\" would switch a\n * rule back on in the scope that deliberately turned it off — the test-file\n * block below being the whole reason that distinction matters.\n */\nconst demote = (config: Linter.Config): Linter.Config => ({\n ...config,\n rules: Object.fromEntries(\n Object.entries(config.rules ?? {}).map(([id, level]) => [\n id,\n level === \"off\" ? \"off\" : \"warn\",\n ]),\n ),\n})\n\nconst warn: Linter.Config[] = recommended.map(demote)\n\nconst observability: Linter.Config[] = [\n {\n files: SRC_FILES,\n plugins: { qdc: plugin },\n rules: {\n \"qdc/no-sentry-init\": \"error\",\n \"no-console\": \"error\",\n \"qdc/sentry-attribute-namespace\": \"warn\",\n \"qdc/no-pii-attributes\": \"warn\",\n },\n },\n {\n // CLI tools and tests print on purpose.\n files: [\"**/seed.ts\", \"src/scripts/**\", ...TEST_FILES],\n rules: { \"no-console\": \"off\" },\n },\n]\n\nexport const configs: QdcSecurityConfigs = { recommended, warn, observability }\n\nconst qdcSecurity: QdcSecurityPlugin = { ...plugin, configs, RULE_IDS }\n\nexport default qdcSecurity\n"],"mappings":";;;;;AAyCO,IAAM,eAAe,CAAC,gBAAgB,aAAa,aAAa,OAAO;AAGvE,SAAS,OAAO,UAA0B;AAC/C,SAAO,SAAS,WAAW,MAAM,GAAG;AACtC;AAGO,SAAS,QAAQ,UAAkB,OAAmC;AAC3E,QAAM,OAAO,OAAO,QAAQ;AAC5B,SAAO,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,EAAE,KAAK,KAAK,SAAS,IAAI,IAAI,GAAG,CAAC;AACrF;AAGO,SAAS,WAAW,MAAuB;AAChD,QAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,OAAO,SAAS,aAAc,QAAO,OAAO,QAAQ;AACxD,MAAI,OAAO,SAAS,sBAAsB,OAAO,UAAU,SAAS,cAAc;AAChF,WAAO,OAAO,SAAS,QAAQ;AAAA,EACjC;AACA,SAAO;AACT;AAWO,SAAS,cAAc,MAA2B,iBAAmC;AAC1F,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,UAAW,QAAO,OAAO,KAAK,UAAU;AAC1D,MAAI,KAAK,SAAS,kBAAmB,SAAQ,KAAK,aAAa,UAAU,OAAO;AAChF,MAAI,KAAK,SAAS,aAAc,QAAO;AACvC,MAAI,KAAK,SAAS,iBAAiB;AAGjC,WAAO,KAAK,QAAQ,SAAS,SAAS,cAAc,KAAK,YAAY,CAAC,GAAG,eAAe;AAAA,EAC1F;AACA,MAAI,KAAK,SAAS,iBAAkB,QAAO,aAAa,SAAS,WAAW,IAAI,CAAC;AACjF,SAAO;AACT;;;AC5DA,IAAM,aAAa,CAAC,MAAM,YAAY,UAAU,gBAAgB;AAChE,IAAM,WAAW,CAAC,UAAU,eAAe;AAG3C,IAAM,aAAa,CAAC,MAAM,WAAW,eAAe,kBAAkB;AAItE,IAAM,aAAa,CAAC,eAAe,UAAU,oBAAoB;AAGjE,IAAM,eAAe,CAAC,eAAe,kBAAkB,cAAc;AAG9D,SAAS,SAAS,SAA2B,SAA0C;AAC5F,SAAO,CAAC,SAAkB;AACxB,YAAQ,OAAO,EAAE,MAAoC,QAAQ,CAAC;AAAA,EAChE;AACF;AAEO,SAAS,SAAS,SAAqE;AAC5F,SAAO;AACT;AAEO,SAAS,KAAK,aAAqB,MAAyD;AACjG,SAAO,EAAE,MAAM,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC,EAAE;AACnD;AAGA,SAAS,IAAI,IAAY,SAAiB,WAAsC;AAC9E,QAAM,OAAO,GAAG,EAAE,MAAM,OAAO;AAC/B,SAAO;AAAA,IACL,MAAM,KAAK,MAAM,SAAS;AAAA,IAC1B,OAAO,SAAS;AACd,YAAM,SAAS,SAAS,SAAS,IAAI;AACrC,aAAO,SAAS,OAAO,YAAY,UAAU,IAAI,CAAC,aAAa,CAAC,UAAU,MAAM,CAAC,CAAC,CAAC;AAAA,IACrF;AAAA,EACF;AACF;AAGA,SAAS,WACP,IACA,SACA,SACA,SACiB;AACjB,QAAM,OAAO,GAAG,EAAE,MAAM,OAAO;AAC/B,SAAO;AAAA,IACL,MAAM,KAAK,MAAM,SAAS;AAAA,IAC1B,OAAO,SAAS;AACd,UAAI,QAAQ,QAAQ,UAAU,OAAO,EAAG,QAAO,CAAC;AAChD,YAAM,SAAS,SAAS,SAAS,IAAI;AACrC,YAAM,OAAO,CAAC,SAAkB;AAC9B,cAAM,SAAS,KAAK;AACpB,YAAI,QAAQ,SAAS,aAAa,OAAO,OAAO,UAAU,UAAU;AAClE,cAAI,QAAQ,SAAS,OAAO,KAAK,EAAG,QAAO,IAAI;AAAA,QACjD;AAAA,MACF;AACA,aAAO,SAAS,EAAE,mBAAmB,MAAM,kBAAkB,KAAK,CAAC;AAAA,IACrE;AAAA,EACF;AACF;AAEA,IAAM,eAAgC;AAAA,EACpC,MAAM,KAAK,gDAAgD,SAAS;AAAA,EACpE,OAAO,SAAS;AACd,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,IACF;AACA,UAAM,YAAY,CAAC,SACjB,MAAM,SAAS,qBACf,MAAM,SAAS,sBACd,MAAM,SAAS,aAAa,OAAO,KAAK,UAAU;AACrD,WAAO,SAAS;AAAA,MACd,sCAAsC;AAAA,MACtC,0CAA0C;AAAA,MAC1C,yCAAyC;AAAA,MACzC,yDAAyD,CAAC,SAAS;AACjE,YAAI,UAAU,KAAK,YAAY,CAAC,CAAC,EAAG,QAAO,IAAI;AAAA,MACjD;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,IAAM,eAAgC;AAAA,EACpC,MAAM,KAAK,0DAA0D,YAAY;AAAA,EACjF,OAAO,SAAS;AACd,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,IACF;AACA,WAAO,SAAS;AAAA,MACd,sFAAsF,CACpF,SACG;AACH,YAAI,cAAc,KAAK,YAAY,CAAC,GAAG,IAAI,EAAG;AAC9C,eAAO,IAAI;AAAA,MACb;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,IAAM,YAA6B;AAAA,EACjC,MAAM,KAAK,4DAA4D,YAAY;AAAA,EACnF,OAAO,SAAS;AACd,QAAI,CAAC,QAAQ,QAAQ,UAAU,YAAY,EAAG,QAAO,CAAC;AACtD,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,IACF;AACA,WAAO,SAAS;AAAA,MACd,uCAAuC,CAAC,SAAS;AAC/C,YAAI,cAAc,KAAK,YAAY,CAAC,GAAG,KAAK,EAAG;AAC/C,eAAO,IAAI;AAAA,MACb;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAGA,IAAM,eAAe,CAAC,SAAS,SAAS,OAAO,IAAI;AACnD,IAAM,YAAY,CAAC,SAAS,OAAO,IAAI;AACvC,IAAM,eAAe;AAGrB,IAAM,sBAAsB,CAAC,UAAU,aAAa,QAAQ,gBAAgB;AAC5E,IAAM,UAAU;AAEhB,SAAS,YAAY,MAAuB;AAC1C,QAAM,IAAI,KAAK,YAAY;AAC3B,MAAI,MAAM,eAAe,MAAM,SAAS,MAAM,aAAa,EAAE,WAAW,MAAM,EAAG,QAAO;AACxF,SAAO,oBAAoB,KAAK,CAAC,WAAW,EAAE,SAAS,MAAM,CAAC;AAChE;AAGA,SAAS,OAAO,MAA6B;AAC3C,QAAM,IAAI,mDAAmD,KAAK,IAAI;AACtE,MAAI,CAAC,IAAI,CAAC,EAAG,QAAO;AACpB,SAAO,EAAE,CAAC,EAAE,QAAQ,YAAY,EAAE;AACpC;AAGA,SAAS,SAAS,MAA0C;AAC1D,MAAI,MAAM,SAAS,sBAAsB,KAAK,UAAU,SAAS,aAAc,QAAO;AACtF,QAAM,MAAM,KAAK,SAAS,QAAQ;AAClC,MAAI,CAAC,QAAQ,KAAK,GAAG,EAAG,QAAO;AAC/B,QAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,OAAO,SAAS,aAAc,QAAO,OAAO,SAAS,QAAQ,MAAM;AACvE,MAAI,OAAO,SAAS,oBAAoB;AACtC,UAAM,eACJ,OAAO,QAAQ,SAAS,gBACxB,OAAO,OAAO,SAAS,aACvB,OAAO,UAAU,SAAS;AAC5B,WAAO,eAAe,MAAM;AAAA,EAC9B;AACA,MAAI,OAAO,SAAS,kBAAkB;AACpC,UAAM,OAAO,WAAW,MAAM;AAC9B,WAAO,SAAS,SAAS,KAAK,SAAS,KAAK,IAAI,MAAM;AAAA,EACxD;AACA,SAAO;AACT;AASA,SAAS,iBAAiB,MAA2C;AACnE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,UAAU;AAC7D,UAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,WAAO,OAAO,EAAE,KAAK,IAAI;AAAA,EAC3B;AACA,MAAI,KAAK,SAAS,mBAAmB;AACnC,UAAM,OAAO,KAAK,SAAS,CAAC,GAAG,OAAO,UAAU,KAAK,SAAS,CAAC,GAAG,OAAO,OAAO;AAChF,UAAM,OAAO,OAAO,IAAI;AACxB,QAAI,KAAM,QAAO,EAAE,KAAK;AAExB,QAAI,SAAS,IAAI;AACf,YAAM,MAAM,SAAS,KAAK,cAAc,CAAC,CAAC;AAC1C,UAAI,IAAK,QAAO,EAAE,QAAQ,IAAI;AAAA,IAChC;AACA,WAAO;AAAA,EACT;AACA,MAAI,KAAK,SAAS,sBAAsB,KAAK,aAAa,IAAK,QAAO,iBAAiB,KAAK,IAAI;AAChG,MAAI,KAAK,SAAS,oBAAoB;AACpC,UAAM,MAAM,SAAS,IAAI;AACzB,WAAO,MAAM,EAAE,QAAQ,IAAI,IAAI;AAAA,EACjC;AACA,MAAI,KAAK,SAAS,mBAAmB,KAAK,QAAQ,SAAS,OAAO;AAEhE,UAAM,OAAO,KAAK,aAAa,CAAC;AAChC,WAAO,iBAAiB,KAAK,CAAC,CAAC,KAAK,iBAAiB,KAAK,CAAC,CAAC;AAAA,EAC9D;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAwB;AAChD,QAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,OAAO,SAAS,aAAc,QAAO,aAAa,SAAS,OAAO,QAAQ,EAAE;AAChF,MAAI,OAAO,SAAS,oBAAoB;AACtC,WACE,OAAO,QAAQ,SAAS,gBACxB,UAAU,SAAS,OAAO,OAAO,QAAQ,EAAE,KAC3C,OAAO,UAAU,SAAS,gBAC1B,aAAa,KAAK,OAAO,SAAS,QAAQ,EAAE;AAAA,EAEhD;AACA,SAAO;AACT;AAQA,IAAM,2BAA4C;AAAA,EAChD,MAAM,KAAK,4EAA4E,YAAY;AAAA,EACnG,OAAO,SAAS;AACd,WAAO,SAAS;AAAA,MACd,gBAAgB,CAAC,SAAS;AACxB,YAAI,CAAC,iBAAiB,IAAI,EAAG;AAC7B,cAAM,SAAS,iBAAiB,KAAK,YAAY,CAAC,CAAC;AACnD,YAAI,CAAC,OAAQ;AACb,YAAI;AACJ,YAAI,UAAU,QAAQ;AACpB,cAAI,YAAY,OAAO,IAAI,KAAK,sBAAsB,OAAO,IAAI,EAAG;AACpE,kBAAQ,OAAO;AAAA,QACjB,OAAO;AAEL,cAAI,OAAO,OAAO,WAAW,OAAO,EAAG;AACvC,kBAAQ,IAAI,OAAO,MAAM;AAAA,QAC3B;AACA,gBAAQ,OAAO;AAAA,UACb;AAAA,UACA,SAAS,iBAAiB,KAAK;AAAA,QACjC,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAGA,IAAM,uBAA+C,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,EAAE;AAEhH,IAAM,oBAAoB,CAAC,UAAU,SAAS,YAAY;AAE1D,IAAM,kBAAkB,CAAC,SACvB,MAAM,SAAS,aAAa,OAAO,KAAK,UAAU;AAUpD,IAAM,wBAAyC;AAAA,EAC7C,MAAM,KAAK,gFAAgF,YAAY;AAAA,EACvG,OAAO,SAAS;AACd,UAAM,SAAS,CAAC,MAAe,YAAoB;AACjD,cAAQ,OAAO,EAAE,MAAoC,SAAS,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAC5F;AACA,WAAO,SAAS;AAAA,MACd,gBAAgB,CAAC,SAAS;AACxB,YAAI,KAAK,QAAQ,SAAS,gBAAgB,KAAK,OAAO,SAAS,cAAe;AAC9E,YAAI,CAAC,gBAAgB,KAAK,YAAY,CAAC,CAAC,GAAG;AACzC;AAAA,YACE;AAAA,YACA;AAAA,UACF;AACA;AAAA,QACF;AACA,cAAM,SAAS,KAAK;AACpB,cAAM,SACJ,QAAQ,SAAS,sBAAsB,OAAO,WAAW,QAAQ,OAAO,UAAU,SAAS,eACtF,OAAO,SAAS,QAAQ,KACzB;AACN,YAAI,kBAAkB,SAAS,MAAM,EAAG;AACxC,cAAM,OAAO,QAAQ;AACrB,cAAM,QAAQ,qBAAqB,MAAM;AACzC,YAAI,UAAU,UAAa,MAAM,SAAS,oBAAoB,KAAK,WAAW,QAAQ;AACpF;AAAA,YACE;AAAA,YACA;AAAA,UACF;AACA;AAAA,QACF;AACA,cAAM,OAAO,KAAK,YAAY,KAAK;AACnC,YAAI,SAAS,UAAa,gBAAgB,IAAI,KAAK,KAAK,SAAS,kBAAmB;AACpF;AAAA,UACE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEO,IAAM,QAAyC;AAAA,EACpD,qBAAqB;AAAA,IACnB;AAAA,IACA;AAAA,IACA,CAAC,mDAAmD;AAAA,EACtD;AAAA,EACA,iBAAiB;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,MACE;AAAA,IACF;AAAA,EACF;AAAA,EACA,2BAA2B;AAAA,IACzB;AAAA,IACA;AAAA,IACA,CAAC,2DAA2D;AAAA,EAC9D;AAAA,EACA,qBAAqB;AAAA,IACnB;AAAA,IACA;AAAA,IACA,CAAC,oFAAoF;AAAA,EACvF;AAAA,EACA,kBAAkB;AAAA,EAClB,qBAAqB;AAAA,IACnB;AAAA,IACA;AAAA,IACA,CAAC,+CAA+C;AAAA,EAClD;AAAA,EACA,wBAAwB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA,cAAc;AAAA,IACZ;AAAA,IACA;AAAA,IACA,CAAC,0DAA0D;AAAA,EAC7D;AAAA,EACA,oBAAoB;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,gBAAgB;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,+BAA+B;AAAA,EAC/B,2BAA2B;AAC7B;AAMO,IAAM,WAAmC;AAAA,EAC9C,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,2BAA2B;AAAA,EAC3B,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,qBAAqB;AAAA,EACrB,wBAAwB;AAAA,EACxB,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,+BAA+B;AAAA,EAC/B,2BAA2B;AAC7B;;;ACvZA,IAAM,gBAAgB;AACtB,IAAM,SAAS,oBAAI,IAAI,CAAC,QAAQ,kBAAkB,CAAC;AACnD,IAAM,gBAAgB,oBAAI,IAAI,CAAC,aAAa,mBAAmB,mBAAmB,CAAC;AACnF,IAAM,WAAW,oBAAI,IAAI,CAAC,oBAAoB,gBAAgB,CAAC;AAC/D,IAAM,aAAa,oBAAI,IAAI,CAAC,SAAS,SAAS,QAAQ,QAAQ,SAAS,OAAO,CAAC;AAC/E,IAAM,cAAc,oBAAI,IAAI,CAAC,gBAAgB,QAAQ,CAAC;AACtD,IAAM,iBAAiB,oBAAI,IAAI,CAAC,iBAAiB,SAAS,CAAC;AAC3D,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AACvB,IAAM,eAAe,oBAAI,IAAI,CAAC,iBAAiB,aAAa,CAAC;AAC7D,IAAM,gBAAgB,oBAAI,IAAI,CAAC,mBAAmB,mBAAmB,CAAC;AACtE,IAAM,MACJ;AAGF,SAAS,cAAc;AACrB,QAAM,aAAa,oBAAI,IAAY;AACnC,QAAM,QAAQ,oBAAI,IAAoB;AACtC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,SAAS,MAAe,UAAiC;AACvD,YAAM,MAAM,KAAK,QAAQ;AACzB,UAAI,OAAO,QAAQ,YAAY,CAAC,cAAc,KAAK,GAAG,EAAG;AACzD,iBAAW,KAAK,KAAK,cAAc,CAAC,GAAG;AACrC,cAAM,QAAQ,EAAE,OAAO,QAAQ;AAC/B,YAAI,EAAE,SAAS,8BAA8B,EAAE,SAAS,yBAA0B,YAAW,IAAI,KAAK;AAAA,iBAC7F,EAAE,SAAS,mBAAmB;AACrC,gBAAM,WAAW,EAAE,UAAU,QAAQ;AACrC,gBAAM,IAAI,OAAO,QAAQ;AACzB,cAAI,OAAO,IAAI,QAAQ,EAAG,YAAW,CAAC;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAM,QAAQ,CAAC,SAAsC;AACnD,MAAI,KAAK,SAAS,cAAc,KAAK,SAAU,QAAO;AACtD,MAAI,KAAK,KAAK,SAAS,aAAc,QAAO,KAAK,IAAI;AACrD,SAAO,OAAO,KAAK,KAAK,UAAU,WAAW,KAAK,IAAI,QAAQ;AAChE;AAEA,IAAM,aAAa,CAAC,SAClB,MAAM,SAAS,sBACV,KAAK,cAAc,CAAC,GAAG,QAAQ,CAAC,MAAM;AACrC,QAAM,IAAI,MAAM,CAAC;AACjB,SAAO,MAAM,SAAY,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAsB;AAC5D,CAAC,IACD,CAAC;AAEP,IAAM,gBAAgB,CAAC,MAA2B,SAChD,MAAM,SAAS,sBACT,KAAK,cAAc,CAAC,GAAG,KAAK,CAAC,MAAM,MAAM,CAAC,MAAM,IAAI,GAAG,QACzD;AAGN,SAAS,iBACP,IACA,aACA,OACiB;AACjB,SAAO;AAAA,IACL,MAAM,KAAK,GAAG,EAAE,MAAM,WAAW,IAAI,YAAY;AAAA,IACjD,OAAO,SAAS;AACd,YAAM,SAAS,YAAY;AAC3B,YAAM,OAAO,CAAC,KAAa,SAAkB;AAC3C,cAAM,UAAU,MAAM,GAAG;AACzB,YAAI,QAAS,UAAS,SAAS,GAAG,EAAE,MAAM,OAAO,EAAE,EAAE,IAAI;AAAA,MAC3D;AACA,aAAO,SAAS;AAAA,QACd,mBAAmB,CAAC,SAAS,OAAO,SAAS,IAAI;AAAA,QACjD,gBAAgB,CAAC,SAAS;AACxB,gBAAM,SAAS,KAAK;AACpB,gBAAM,OAAO,KAAK,aAAa,CAAC;AAChC,cAAI;AACJ,cAAI,YAAY;AAChB,cAAI,QAAQ,SAAS,cAAc;AACjC,mBAAO,OAAO,MAAM,IAAI,OAAO,QAAQ,EAAE;AACzC,wBAAY,SAAS;AAAA,UACvB,WAAW,QAAQ,SAAS,oBAAoB;AAC9C,mBAAO,OAAO,UAAU;AACxB,kBAAMA,OAAM,OAAO;AACnB,wBACGA,MAAK,SAAS,gBAAgB,OAAO,WAAW,IAAIA,KAAI,QAAQ,EAAE,KAClEA,MAAK,SAAS,sBACbA,KAAI,UAAU,SAAS,YACvBA,KAAI,QAAQ,SAAS,gBACrB,OAAO,WAAW,IAAIA,KAAI,OAAO,QAAQ,EAAE,KAC5CA,MAAK,SAAS,gBAAgB,OAAO,MAAM,IAAIA,KAAI,QAAQ,EAAE,MAAM;AAAA,UACxE;AACA,cAAI,CAAC,KAAM;AACX,gBAAM,MAAM,QAAQ,SAAS,qBAAqB,OAAO,SAAS;AAClE,gBAAM,aAAa,CAAC,UAClB,KAAK,SAAS,qBACZ,IAAI,QAAQ,SAAS,gBAAgB,MAAM,IAAI,OAAO,MAAM,IAAI,IAAI,OAAO,QAAQ,EAAE,KAAK,EAAE,KAC3F,IAAI,QAAQ,SAAS,sBACpB,IAAI,OAAO,QAAQ,SAAS,gBAC5B,OAAO,WAAW,IAAI,IAAI,OAAO,OAAO,QAAQ,EAAE,KAClD,MAAM,IAAI,IAAI,OAAO,UAAU,QAAQ,EAAE;AAC/C,gBAAM,OAAO,KAAK,SAAS,eAAgB,IAAI,QAAQ,KAAM;AAC7D,gBAAM,WAAW,cAAc,KAAK,IAAI,KAAK,WAAW,YAAY;AACpE,gBAAM,UAAU,aAAa,eAAe,KAAK,IAAI,KAAK,WAAW,aAAa;AAClF,gBAAM,OAA4B,CAAC;AACnC,cACE,YAAY,IAAI,IAAI,MACnB,SAAS,iBAAiB,WAAW,YACtC,KAAK,CAAC,GAAG,SAAS,aAClB,OAAO,KAAK,CAAC,EAAE,UAAU,UACzB;AACA,iBAAK,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC;AAAA,UACpC,WAAW,eAAe,IAAI,IAAI,MAAM,SAAS,kBAAkB,WAAW,UAAU;AACtF,iBAAK,KAAK,GAAG,WAAW,KAAK,CAAC,CAAC,CAAC;AAAA,UAClC,WAAW,aAAa,cAAc,IAAI,IAAI,GAAG;AAC/C,iBAAK,KAAK,GAAG,WAAW,cAAc,KAAK,CAAC,GAAG,YAAY,CAAC,CAAC;AAAA,UAC/D,WAAW,aAAa,SAAS,IAAI,IAAI,GAAG;AAC1C,iBAAK,KAAK,GAAG,WAAW,cAAc,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC;AAAA,UACzD,WAAW,aAAa,WAAW,IAAI,IAAI,KAAK,QAAQ,SAAS,oBAAoB;AACnF,iBAAK,KAAK,GAAG,WAAW,KAAK,CAAC,CAAC,CAAC;AAAA,UAClC;AACA,qBAAW,CAAC,KAAK,EAAE,KAAK,KAAM,MAAK,KAAK,EAAE;AAAA,QAC5C;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,IAAM,eAAgC;AAAA,EACpC,MAAM,KAAK,+DAA+D,SAAS;AAAA,EACnF,OAAO,SAAS;AACd,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,IACF;AACA,UAAM,SAAS,YAAY;AAC3B,WAAO,SAAS;AAAA,MACd,mBAAmB,CAAC,SAAS,OAAO,SAAS,MAAM,MAAM;AAAA,MACzD,gBAAgB,CAAC,SAAS;AACxB,cAAM,IAAI,KAAK;AACf,YACE,GAAG,SAAS,sBACZ,EAAE,QAAQ,SAAS,gBACnB,OAAO,WAAW,IAAI,EAAE,OAAO,QAAQ,EAAE,KACzC,OAAO,IAAI,EAAE,UAAU,QAAQ,EAAE,GACjC;AACA,iBAAO,IAAI;AAAA,QACb;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAOA,IAAM,iBAAkC;AAAA,EACtC,MAAM,KAAK,uDAAuD,YAAY;AAAA,EAC9E,OAAO,SAAS;AACd,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,IACF;AACA,UAAM,SAAS,YAAY;AAC3B,WAAO,SAAS;AAAA,MACd,mBAAmB,CAAC,SAAS,OAAO,SAAS,IAAI;AAAA,MACjD,gBAAgB,CAAC,SAAS;AACxB,cAAM,IAAI,KAAK;AACf,cAAM,OACJ,GAAG,SAAS,eACR,OAAO,MAAM,IAAI,EAAE,QAAQ,EAAE,IAC7B,GAAG,SAAS,sBACV,EAAE,QAAQ,SAAS,gBACnB,OAAO,WAAW,IAAI,EAAE,OAAO,QAAQ,EAAE,IACzC,EAAE,UAAU,OACZ;AACR,YAAI,SAAS,mBAAoB,QAAO,IAAI;AAAA,MAC9C;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEO,IAAM,qBAAsD;AAAA,EACjE,kBAAkB;AAAA,EAClB,8BAA8B;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,CAAC,QACC,IAAI,WAAW,OAAO,IAAI,SAAY,IAAI,GAAG;AAAA,EACjD;AAAA,EACA,qBAAqB;AAAA,IAAiB;AAAA,IAAe;AAAA,IAAsC,CAAC,QAC1F,IAAI,KAAK,GAAG,IACR,IAAI,GAAG,sFACP;AAAA,EACN;AAAA,EACA,oBAAoB;AACtB;AAEO,IAAM,yBAAiD;AAAA,EAC5D,kBAAkB;AAAA,EAClB,8BAA8B;AAAA,EAC9B,qBAAqB;AAAA,EACrB,oBAAoB;AACtB;;;AC/KA,IAAM,SAAS;AAAA,EACb,MAAM,EAAE,MAAM,wBAAwB;AAAA,EACtC,OAAO,EAAE,GAAG,OAAO,GAAG,mBAAmB;AAC3C;AAGA,IAAM,YAAY,CAAC,mBAAmB;AACtC,IAAM,aAAa,CAAC,sBAAsB,oBAAoB;AA6C9D,IAAM,cAA+B;AAAA,EACnC;AAAA,IACE,OAAO;AAAA,IACP,SAAS,EAAE,KAAK,OAAO;AAAA,IACvB,OAAO;AAAA,MACL,yBAAyB;AAAA,MACzB,qBAAqB;AAAA,MACrB,+BAA+B;AAAA,MAC/B,yBAAyB;AAAA,MACzB,sBAAsB;AAAA,MACtB,yBAAyB;AAAA,MACzB,4BAA4B;AAAA,MAC5B,kBAAkB;AAAA,MAClB,wBAAwB;AAAA,MACxB,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMpB,qBAAqB;AAAA,MACrB,kBAAkB;AAAA,MAClB,mCAAmC;AAAA;AAAA,MAEnC,+BAA+B;AAAA,IACjC;AAAA,EACF;AAAA,EACA;AAAA;AAAA,IAEE,OAAO;AAAA,IACP,SAAS,EAAE,KAAK,OAAO;AAAA,IACvB,OAAO;AAAA,MACL,yBAAyB;AAAA,MACzB,qBAAqB;AAAA,MACrB,+BAA+B;AAAA,MAC/B,yBAAyB;AAAA,MACzB,yBAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAYzB,oBAAoB;AAAA,MACpB,wBAAwB;AAAA;AAAA;AAAA,MAGxB,mCAAmC;AAAA;AAAA,MAEnC,+BAA+B;AAAA,IACjC;AAAA,EACF;AACF;AAOA,IAAM,SAAS,CAAC,YAA0C;AAAA,EACxD,GAAG;AAAA,EACH,OAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,OAAO,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;AAAA,MACtD;AAAA,MACA,UAAU,QAAQ,QAAQ;AAAA,IAC5B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,OAAwB,YAAY,IAAI,MAAM;AAEpD,IAAM,gBAAiC;AAAA,EACrC;AAAA,IACE,OAAO;AAAA,IACP,SAAS,EAAE,KAAK,OAAO;AAAA,IACvB,OAAO;AAAA,MACL,sBAAsB;AAAA,MACtB,cAAc;AAAA,MACd,kCAAkC;AAAA,MAClC,yBAAyB;AAAA,IAC3B;AAAA,EACF;AAAA,EACA;AAAA;AAAA,IAEE,OAAO,CAAC,cAAc,kBAAkB,GAAG,UAAU;AAAA,IACrD,OAAO,EAAE,cAAc,MAAM;AAAA,EAC/B;AACF;AAEO,IAAM,UAA8B,EAAE,aAAa,MAAM,cAAc;AAE9E,IAAM,cAAiC,EAAE,GAAG,QAAQ,SAAS,SAAS;AAEtE,IAAO,iBAAQ;","names":["obj"]}
@@ -5,7 +5,7 @@ import "../../chunk-4MBB2UQW.js";
5
5
  import {
6
6
  getLogger,
7
7
  setUserContext
8
- } from "../../chunk-MET4SIMB.js";
8
+ } from "../../chunk-EK6TGWWT.js";
9
9
 
10
10
  // src/next/auth/createAuthRouteHandlers.ts
11
11
  import { NextResponse } from "next/server";