@quatronic/sdk 0.2.1 → 0.2.2-canary-20260916115650

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 (38) hide show
  1. package/README.md +8 -0
  2. package/dist/chunk-2HE2D4SY.js +43 -0
  3. package/dist/chunk-2HE2D4SY.js.map +1 -0
  4. package/dist/{chunk-HKL5OGAF.js → chunk-4YNIBRDA.js} +14 -3
  5. package/dist/chunk-4YNIBRDA.js.map +1 -0
  6. package/dist/{chunk-JIB5TDR6.js → chunk-DAZOIUP6.js} +1 -1
  7. package/dist/chunk-DAZOIUP6.js.map +1 -0
  8. package/dist/{chunk-3VQW3FDC.js → chunk-GWX26DF7.js} +22 -4
  9. package/dist/chunk-GWX26DF7.js.map +1 -0
  10. package/dist/next/auth/index.d.ts +11 -1
  11. package/dist/next/auth/index.js +30 -22
  12. package/dist/next/auth/index.js.map +1 -1
  13. package/dist/next/config/index.js +19 -0
  14. package/dist/next/config/index.js.map +1 -1
  15. package/dist/next/connections/index.d.ts +1 -1
  16. package/dist/next/connections/index.js +2 -2
  17. package/dist/next/observability/register.js +1 -1
  18. package/dist/next/security/index.d.ts +92 -0
  19. package/dist/next/security/index.js +105 -0
  20. package/dist/next/security/index.js.map +1 -0
  21. package/dist/node/connections/index.js +2 -2
  22. package/dist/node/fspath/index.d.ts +69 -0
  23. package/dist/node/fspath/index.js +181 -0
  24. package/dist/node/fspath/index.js.map +1 -0
  25. package/dist/node/identity/index.js +1 -1
  26. package/dist/node/net/index.d.ts +82 -0
  27. package/dist/node/net/index.js +401 -0
  28. package/dist/node/net/index.js.map +1 -0
  29. package/dist/node/observability/register.js +1 -1
  30. package/dist/node/parse/index.d.ts +27 -0
  31. package/dist/node/parse/index.js +66 -0
  32. package/dist/node/parse/index.js.map +1 -0
  33. package/package.json +21 -3
  34. package/semgrep/qdc-python.yaml +169 -0
  35. package/semgrep/qdc-web.yaml +504 -0
  36. package/dist/chunk-3VQW3FDC.js.map +0 -1
  37. package/dist/chunk-HKL5OGAF.js.map +0 -1
  38. package/dist/chunk-JIB5TDR6.js.map +0 -1
package/README.md CHANGED
@@ -7,6 +7,14 @@ Single-package SDK for QDC tenant apps. Exposes:
7
7
 
8
8
  See `docs/superpowers/specs/2026-05-26-per-app-bff-architecture-design.md`.
9
9
 
10
+ ## Security primitives
11
+
12
+ `safeRedirect`, `withSecurityHeaders`, `safeFetch`, `safeJoin`, `safeFilename`,
13
+ `assertUpload` and `parseJsonBounded` cover the operations that produce most web
14
+ vulnerabilities — each one has a short unsafe version that looks correct. What
15
+ each does, and what it deliberately does not do:
16
+ [`docs/security-primitives.md`](docs/security-primitives.md).
17
+
10
18
  ## Permissions
11
19
 
12
20
  The SDK ships a hybrid role/permission layer. Roles come from the Keycloak JWT;
@@ -0,0 +1,43 @@
1
+ // src/security-core/headers.ts
2
+ var STATIC_SECURITY_HEADERS = [
3
+ // Two years, subdomains included, eligible for the preload list. Long enough
4
+ // to be worth preloading; the app is HTTPS-only behind Northflank anyway.
5
+ ["Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload"],
6
+ // Refuse to execute a response whose declared type does not match its use —
7
+ // closes the "upload a .txt that sniffs as HTML" path.
8
+ ["X-Content-Type-Options", "nosniff"],
9
+ // Send the full URL only same-origin; cross-origin gets the bare origin, and
10
+ // HTTPS→HTTP gets nothing.
11
+ ["Referrer-Policy", "strict-origin-when-cross-origin"],
12
+ // Clickjacking. `frame-ancestors` in the CSP is the modern control; this is
13
+ // the fallback for anything that still only reads the legacy header.
14
+ ["X-Frame-Options", "DENY"],
15
+ // Powerful features off by default. An app that needs one opts back in.
16
+ //
17
+ // `payment=()` is deliberately not in this list. It disables the Payment
18
+ // Request API, which is what Apple Pay and Google Pay use in the browser, and
19
+ // an app taking payments is an ordinary case rather than an exotic one. A
20
+ // default that has to be overridden on a common path teaches people to
21
+ // override the header, which costs more than the directive buys.
22
+ ["Permissions-Policy", "camera=(), microphone=(), geolocation=(), usb=()"],
23
+ // Stop another origin getting a scriptable handle on this window.
24
+ //
25
+ // `-allow-popups` rather than plain `same-origin`: both settings block that,
26
+ // but plain `same-origin` also severs the opener reference for popups *we*
27
+ // open, which breaks every "popup authorises, then postMessages the result
28
+ // back" flow — connect-your-Google-Drive is the everyday example. What it
29
+ // buys in exchange is `crossOriginIsolated`, i.e. SharedArrayBuffer and
30
+ // high-resolution timers, which a business app has no use for.
31
+ //
32
+ // Neither setting touches redirects: COOP governs window references, not
33
+ // navigation, so an ordinary OAuth redirect flow is unaffected either way.
34
+ ["Cross-Origin-Opener-Policy", "same-origin-allow-popups"],
35
+ // Refuse to be loaded as a subresource by another origin (Spectre-class
36
+ // side-channel hardening).
37
+ ["Cross-Origin-Resource-Policy", "same-origin"]
38
+ ];
39
+
40
+ export {
41
+ STATIC_SECURITY_HEADERS
42
+ };
43
+ //# sourceMappingURL=chunk-2HE2D4SY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/security-core/headers.ts"],"sourcesContent":["// The response headers a QDC app sends regardless of the request. Defined once\n// here because two places emit them: `withQdcConfig().headers()` at build time\n// (every route Next serves) and `withSecurityHeaders` at request time (also the\n// redirects a proxy issues, which never reach a Next route and so never pick up\n// the build-time set).\n//\n// `X-XSS-Protection` is deliberately absent. It is deprecated, every current\n// browser ignores it, and its legacy filter introduced vulnerabilities of its\n// own — sending it is worse than sending nothing.\nexport const STATIC_SECURITY_HEADERS: ReadonlyArray<readonly [string, string]> = [\n // Two years, subdomains included, eligible for the preload list. Long enough\n // to be worth preloading; the app is HTTPS-only behind Northflank anyway.\n [\"Strict-Transport-Security\", \"max-age=63072000; includeSubDomains; preload\"],\n // Refuse to execute a response whose declared type does not match its use —\n // closes the \"upload a .txt that sniffs as HTML\" path.\n [\"X-Content-Type-Options\", \"nosniff\"],\n // Send the full URL only same-origin; cross-origin gets the bare origin, and\n // HTTPS→HTTP gets nothing.\n [\"Referrer-Policy\", \"strict-origin-when-cross-origin\"],\n // Clickjacking. `frame-ancestors` in the CSP is the modern control; this is\n // the fallback for anything that still only reads the legacy header.\n [\"X-Frame-Options\", \"DENY\"],\n // Powerful features off by default. An app that needs one opts back in.\n //\n // `payment=()` is deliberately not in this list. It disables the Payment\n // Request API, which is what Apple Pay and Google Pay use in the browser, and\n // an app taking payments is an ordinary case rather than an exotic one. A\n // default that has to be overridden on a common path teaches people to\n // override the header, which costs more than the directive buys.\n [\"Permissions-Policy\", \"camera=(), microphone=(), geolocation=(), usb=()\"],\n // Stop another origin getting a scriptable handle on this window.\n //\n // `-allow-popups` rather than plain `same-origin`: both settings block that,\n // but plain `same-origin` also severs the opener reference for popups *we*\n // open, which breaks every \"popup authorises, then postMessages the result\n // back\" flow — connect-your-Google-Drive is the everyday example. What it\n // buys in exchange is `crossOriginIsolated`, i.e. SharedArrayBuffer and\n // high-resolution timers, which a business app has no use for.\n //\n // Neither setting touches redirects: COOP governs window references, not\n // navigation, so an ordinary OAuth redirect flow is unaffected either way.\n [\"Cross-Origin-Opener-Policy\", \"same-origin-allow-popups\"],\n // Refuse to be loaded as a subresource by another origin (Spectre-class\n // side-channel hardening).\n [\"Cross-Origin-Resource-Policy\", \"same-origin\"],\n]\n"],"mappings":";AASO,IAAM,0BAAoE;AAAA;AAAA;AAAA,EAG/E,CAAC,6BAA6B,8CAA8C;AAAA;AAAA;AAAA,EAG5E,CAAC,0BAA0B,SAAS;AAAA;AAAA;AAAA,EAGpC,CAAC,mBAAmB,iCAAiC;AAAA;AAAA;AAAA,EAGrD,CAAC,mBAAmB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ1B,CAAC,sBAAsB,kDAAkD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYzE,CAAC,8BAA8B,0BAA0B;AAAA;AAAA;AAAA,EAGzD,CAAC,gCAAgC,aAAa;AAChD;","names":[]}
@@ -6,7 +6,7 @@ import {
6
6
  connKey,
7
7
  defaultEnv,
8
8
  readConn
9
- } from "./chunk-JIB5TDR6.js";
9
+ } from "./chunk-DAZOIUP6.js";
10
10
 
11
11
  // src/connections-core/connection.ts
12
12
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -28,7 +28,18 @@ function connection(alias, opts = {}) {
28
28
  const base = /^https?:\/\//i.test(rawBase) ? rawBase : `http://${rawBase}`;
29
29
  const token = readConn(env, alias, "TOKEN");
30
30
  const { timeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS, signal: caller, headers, ...rest } = init;
31
- const url = /^https?:\/\//i.test(path) ? path : base.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
31
+ const absolute = /^https?:\/\//i.test(path);
32
+ if (absolute) {
33
+ console.warn(
34
+ JSON.stringify({
35
+ level: "warn",
36
+ event: "qdc.connection.absolute_url",
37
+ alias,
38
+ message: "absolute URL passed to connection().fetch; this will throw in the next minor release"
39
+ })
40
+ );
41
+ }
42
+ const url = absolute ? path : base.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
32
43
  const controller = new AbortController();
33
44
  const timer = setTimeout(() => controller.abort(), timeoutMs);
34
45
  if (caller) {
@@ -126,4 +137,4 @@ export {
126
137
  assertConnections,
127
138
  assertInboundConnection
128
139
  };
129
- //# sourceMappingURL=chunk-HKL5OGAF.js.map
140
+ //# sourceMappingURL=chunk-4YNIBRDA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connections-core/connection.ts","../src/connections-core/assertConnection.ts"],"sourcesContent":["import { SpanStatusCode, trace } from \"@opentelemetry/api\"\nimport { ConnectionError } from \"./errors\"\nimport { CONNECTION_TOKEN_HEADER, DEFAULT_CONNECTION_TIMEOUT_MS } from \"./constants\"\nimport { connKey, readConn, defaultEnv, type Env } from \"./env\"\n\nexport interface PostgresMaterial {\n uri: string\n host?: string\n port?: string\n user?: string\n password?: string\n db?: string\n}\nexport interface RedisMaterial {\n url: string\n}\nexport interface S3Material {\n endpoint: string\n bucket: string\n accessKey: string\n secretKey: string\n}\n\nexport interface ConnectionHandle {\n fetch(path: string, init?: RequestInit & { timeoutMs?: number }): Promise<Response>\n postgres(): PostgresMaterial\n redis(): RedisMaterial\n s3(): S3Material\n}\n\nexport function connection(alias: string, opts: { env?: Env } = {}): ConnectionHandle {\n const env = opts.env ?? defaultEnv()\n\n function required(suffix: string, code: \"not_wired\" | \"missing_material\"): string {\n const value = readConn(env, alias, suffix)\n if (value === undefined || value === \"\") {\n throw new ConnectionError(code, {\n message: `Connection \"${alias}\" is missing ${connKey(alias, suffix)}`,\n status: 500,\n })\n }\n return value\n }\n\n return {\n async fetch(path, init = {}) {\n const rawBase = required(\"URL\", \"not_wired\")\n // Module-service connections carry a bare host:port (NF internal DNS,\n // always plain HTTP) — portal-api never prefixes a scheme.\n const base = /^https?:\\/\\//i.test(rawBase) ? rawBase : `http://${rawBase}`\n const token = readConn(env, alias, \"TOKEN\")\n const { timeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS, signal: caller, headers, ...rest } = init\n\n // An absolute `path` ignores the wired base entirely, so a caller-supplied\n // one turns connection().fetch into an arbitrary-request primitive that\n // carries the connection token to whatever host it names. A\n // protocol-relative \"//evil.example\" is not affected: it does not match\n // here, and the join below strips its leading slashes.\n //\n // Warned in this release, refused in the next minor — see \"absolute_url\"\n // in errors.ts. Passing a whole URL is a thing working code does today,\n // so throwing without a release of warning would break apps at an upgrade\n // they had no reason to read the notes for.\n const absolute = /^https?:\\/\\//i.test(path)\n if (absolute) {\n // Edge-safe structured log, same shape as requireConnection's rejection.\n console.warn(\n JSON.stringify({\n level: \"warn\",\n event: \"qdc.connection.absolute_url\",\n alias,\n message:\n \"absolute URL passed to connection().fetch; this will throw in the next minor release\",\n }),\n )\n }\n\n const url = absolute ? path : base.replace(/\\/+$/, \"\") + \"/\" + path.replace(/^\\/+/, \"\")\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n if (caller) {\n if (caller.aborted) controller.abort()\n else caller.addEventListener(\"abort\", () => controller.abort(), { once: true })\n }\n\n const mergedHeaders = new Headers(headers)\n if (token) mergedHeaders.set(CONNECTION_TOKEN_HEADER, token)\n\n const tracer = trace.getTracer(\"@quatronic/sdk\")\n // Deterministic portal naming: the portal maps qdc.connection.alias to the\n // provider module/resource instead of reverse-guessing NF hostnames. Auto\n // http/undici child spans nest under this one and fold into it at render.\n return tracer.startActiveSpan(\n `→ ${alias}`,\n { attributes: { \"qdc.connection.alias\": alias, \"qdc.span.type\": \"outbound_call\" } },\n async (span) => {\n try {\n const res = await fetch(url, { ...rest, headers: mergedHeaders, signal: controller.signal })\n if (res.status >= 500) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${res.status}` })\n }\n return res\n } catch (err) {\n span.recordException(err as Error)\n span.setStatus({ code: SpanStatusCode.ERROR })\n throw err\n } finally {\n span.end()\n clearTimeout(timer)\n }\n },\n )\n },\n\n postgres() {\n const material: PostgresMaterial = { uri: required(\"URI\", \"missing_material\") }\n const host = readConn(env, alias, \"HOST\")\n if (host) material.host = host\n const port = readConn(env, alias, \"PORT\")\n if (port) material.port = port\n const user = readConn(env, alias, \"USER\")\n if (user) material.user = user\n const password = readConn(env, alias, \"PASSWORD\")\n if (password) material.password = password\n const db = readConn(env, alias, \"DB\")\n if (db) material.db = db\n return material\n },\n\n redis() {\n return { url: required(\"URL\", \"missing_material\") }\n },\n\n s3() {\n return {\n endpoint: required(\"ENDPOINT\", \"missing_material\"),\n bucket: required(\"BUCKET\", \"missing_material\"),\n accessKey: required(\"ACCESS_KEY\", \"missing_material\"),\n secretKey: required(\"SECRET_KEY\", \"missing_material\"),\n }\n },\n }\n}\n","import { ConnectionError } from \"./errors\"\nimport { collectConnectionTokens, connKey, readConn, defaultEnv, type Env } from \"./env\"\n\nexport type ConnectionKind = \"service\" | \"postgres\" | \"redis\" | \"s3\"\n\nconst REQUIRED_SUFFIXES: Record<ConnectionKind, string[]> = {\n service: [\"URL\"],\n postgres: [\"URI\"],\n redis: [\"URL\"],\n s3: [\"ENDPOINT\", \"BUCKET\", \"ACCESS_KEY\", \"SECRET_KEY\"],\n}\n\n/**\n * Fail-fast at app startup: a module that declares a connection but is missing\n * its CONN_<ALIAS>_* env crashes with a clear message instead of a mystery 500\n * on first use.\n */\nexport function assertConnection(\n alias: string,\n kind: ConnectionKind,\n opts: { env?: Env } = {},\n): void {\n const env = opts.env ?? defaultEnv()\n const missing = REQUIRED_SUFFIXES[kind]\n .filter((suffix) => {\n const value = readConn(env, alias, suffix)\n return value === undefined || value === \"\"\n })\n .map((suffix) => connKey(alias, suffix))\n\n if (missing.length > 0) {\n throw new ConnectionError(\"missing_material\", {\n message: `Connection \"${alias}\" (${kind}) is not wired: missing ${missing.join(\", \")}`,\n status: 500,\n })\n }\n}\n\n/** Batch form — validate several declared connections at startup. */\nexport function assertConnections(\n specs: Array<{ alias: string; kind: ConnectionKind }>,\n opts: { env?: Env } = {},\n): void {\n for (const { alias, kind } of specs) assertConnection(alias, kind, opts)\n}\n\n/**\n * Provider-side fail-fast at startup: a module that guards incoming requests\n * with `requireConnection` needs at least one `CONN_*_TOKEN` injected. Unlike a\n * consumer it does not know its callers' aliases, so `assertConnection` (which\n * is alias-specific) does not apply. This checks — alias-agnostically — that\n * *some* inbound token is wired.\n *\n * Use this instead of probing `requireConnection` with a tokenless `Request`:\n * that probe emits a misleading `qdc.connection.rejected` / `missing_token`\n * warning on every healthy boot (the guard always logs before it throws),\n * whereas this reads the env directly and stays silent unless nothing is wired.\n */\nexport function assertInboundConnection(opts: { env?: Env } = {}): void {\n const env = opts.env ?? defaultEnv()\n if (collectConnectionTokens(env).length === 0) {\n throw new ConnectionError(\"not_wired\", {\n message: \"No CONN_*_TOKEN configured — no connection is wired to this module.\",\n status: 500,\n })\n }\n}\n"],"mappings":"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","names":[]}
@@ -92,4 +92,4 @@ export {
92
92
  defaultEnv,
93
93
  requireConnection
94
94
  };
95
- //# sourceMappingURL=chunk-JIB5TDR6.js.map
95
+ //# sourceMappingURL=chunk-DAZOIUP6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connections-core/errors.ts","../src/connections-core/constants.ts","../src/connections-core/requireConnection.ts","../src/connections-core/env.ts"],"sourcesContent":["/** Why a connection guard / client / assertion rejected. */\nexport type ConnectionErrorCode =\n | \"not_wired\" // no CONN_*_TOKEN in env (guard) or missing CONN_<ALIAS>_URL (client)\n | \"missing_token\" // X-QDC-Connection-Token header absent (guard)\n | \"invalid_token\" // incoming token matched no candidate (guard)\n | \"missing_material\" // a required CONN_<ALIAS>_* resource key is absent\n | \"absolute_url\" // connection().fetch was given an absolute URL, bypassing the wired base\n\n/**\n * Single error shape for the connections subpath, mirroring AuthError in\n * next/auth. `status` defaults to 401 (guard rejections, which a route maps to\n * a 401); config-shaped errors pass `status: 500`.\n */\nexport class ConnectionError extends Error {\n readonly code: ConnectionErrorCode\n readonly status: number\n\n constructor(code: ConnectionErrorCode, opts?: { message?: string; status?: number }) {\n super(opts?.message ?? code)\n this.name = \"ConnectionError\"\n this.code = code\n this.status = opts?.status ?? 401\n }\n}\n","/** Header carrying the per-connection shared-secret token. Fixed in v1. */\nexport const CONNECTION_TOKEN_HEADER = \"X-QDC-Connection-Token\"\n\n/** Default request timeout for connection().fetch, overridable per call. */\nexport const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000\n\n/** OTel span attribute set to `true` when the guard rejects a request. */\nexport const CONNECTION_REJECTED_ATTR = \"qdc.connection.rejected\"\n\n/** OTel span attribute carrying the rejection reason code. */\nexport const CONNECTION_REJECT_REASON_ATTR = \"qdc.connection.reject_reason\"\n","import { trace } from \"@opentelemetry/api\"\nimport { ConnectionError, type ConnectionErrorCode } from \"./errors\"\nimport {\n CONNECTION_TOKEN_HEADER,\n CONNECTION_REJECTED_ATTR,\n CONNECTION_REJECT_REASON_ATTR,\n} from \"./constants\"\nimport { collectConnectionTokens, defaultEnv, type Env } from \"./env\"\n\n/**\n * Provider-side guard. Reads CONN_*_TOKEN from its own env and validates the\n * incoming X-QDC-Connection-Token against all of them in constant time.\n * Returns the matched <ALIAS>; throws ConnectionError otherwise.\n *\n * Edge-capable: pure Web Crypto + header/env reads. No Node-only crypto, no\n * Node logger. Async because crypto.subtle is async.\n */\nexport async function requireConnection(\n req: Request,\n opts: { env?: Env } = {},\n): Promise<string> {\n const env = opts.env ?? defaultEnv()\n const candidates = collectConnectionTokens(env)\n const incoming = req.headers.get(CONNECTION_TOKEN_HEADER) ?? undefined\n\n if (candidates.length === 0) reject(\"not_wired\")\n if (incoming === undefined || incoming === \"\") reject(\"missing_token\")\n\n const alias = await matchAny(incoming as string, candidates)\n if (alias === null) reject(\"invalid_token\")\n return alias as string\n}\n\nfunction reject(code: ConnectionErrorCode): never {\n const span = trace.getActiveSpan()\n if (span) {\n span.setAttribute(CONNECTION_REJECTED_ATTR, true)\n span.setAttribute(CONNECTION_REJECT_REASON_ATTR, code)\n }\n // Edge-safe structured log — node logger is Node-only and must not be imported here.\n console.warn(JSON.stringify({ level: \"warn\", event: \"qdc.connection.rejected\", code }))\n throw new ConnectionError(code)\n}\n\n/**\n * Constant-time match of `incoming` against every candidate token, no early\n * exit. Uses a per-call random HMAC key (double-HMAC pattern) so the byte\n * comparison runs over fixed-length (32-byte) digests regardless of input\n * length. Empty set → null (caller fails closed before reaching here).\n */\nasync function matchAny(\n incoming: string,\n candidates: Array<{ alias: string; token: string }>,\n): Promise<string | null> {\n if (candidates.length === 0) return null\n const enc = new TextEncoder()\n const key = await crypto.subtle.generateKey({ name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"])\n const incomingDigest = new Uint8Array(await crypto.subtle.sign(\"HMAC\", key, enc.encode(incoming)))\n\n let matched: string | null = null\n for (const { alias, token } of candidates) {\n const candDigest = new Uint8Array(await crypto.subtle.sign(\"HMAC\", key, enc.encode(token)))\n let diff = incomingDigest.length ^ candDigest.length\n for (let i = 0; i < incomingDigest.length; i++) {\n diff |= (incomingDigest[i] ?? 0) ^ (candDigest[i] ?? 0)\n }\n if (diff === 0) matched = alias // no break — constant pass over the whole set\n }\n return matched\n}\n","export type Env = Record<string, string | undefined>\n\nconst TOKEN_KEY_RE = /^CONN_(.+)_TOKEN$/\n\n/**\n * Mirror of portal-api connection_material.alias_for_provider:\n * uppercase, every run of non-[A-Z0-9] → single \"_\", strip leading/trailing \"_\".\n */\nexport function normalizeAlias(alias: string): string {\n return alias\n .toUpperCase()\n .replace(/[^A-Z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n}\n\nexport function connKey(alias: string, suffix: string): string {\n return `CONN_${normalizeAlias(alias)}_${suffix}`\n}\n\nexport function readConn(env: Env, alias: string, suffix: string): string | undefined {\n return env[connKey(alias, suffix)]\n}\n\n/**\n * Every CONN_*_TOKEN in env, with its <ALIAS> (already normalized — it comes\n * straight out of the env key). Empty values are ignored.\n */\nexport function collectConnectionTokens(env: Env): Array<{ alias: string; token: string }> {\n const out: Array<{ alias: string; token: string }> = []\n for (const [key, value] of Object.entries(env)) {\n if (value == null || value === \"\") continue\n const m = TOKEN_KEY_RE.exec(key)\n if (m && m[1]) out.push({ alias: m[1], token: value })\n }\n return out\n}\n\n/** process.env, narrowed. Used when the caller does not inject opts.env. */\nexport function defaultEnv(): Env {\n return process.env as Env\n}\n"],"mappings":";AAaO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC;AAAA,EACA;AAAA,EAET,YAAY,MAA2B,MAA8C;AACnF,UAAM,MAAM,WAAW,IAAI;AAC3B,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS,MAAM,UAAU;AAAA,EAChC;AACF;;;ACtBO,IAAM,0BAA0B;AAGhC,IAAM,gCAAgC;AAGtC,IAAM,2BAA2B;AAGjC,IAAM,gCAAgC;;;ACV7C,SAAS,aAAa;;;ACEtB,IAAM,eAAe;AAMd,SAAS,eAAe,OAAuB;AACpD,SAAO,MACJ,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE;AAC3B;AAEO,SAAS,QAAQ,OAAe,QAAwB;AAC7D,SAAO,QAAQ,eAAe,KAAK,CAAC,IAAI,MAAM;AAChD;AAEO,SAAS,SAAS,KAAU,OAAe,QAAoC;AACpF,SAAO,IAAI,QAAQ,OAAO,MAAM,CAAC;AACnC;AAMO,SAAS,wBAAwB,KAAmD;AACzF,QAAM,MAA+C,CAAC;AACtD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,QAAI,SAAS,QAAQ,UAAU,GAAI;AACnC,UAAM,IAAI,aAAa,KAAK,GAAG;AAC/B,QAAI,KAAK,EAAE,CAAC,EAAG,KAAI,KAAK,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,MAAM,CAAC;AAAA,EACvD;AACA,SAAO;AACT;AAGO,SAAS,aAAkB;AAChC,SAAO,QAAQ;AACjB;;;ADvBA,eAAsB,kBACpB,KACA,OAAsB,CAAC,GACN;AACjB,QAAM,MAAM,KAAK,OAAO,WAAW;AACnC,QAAM,aAAa,wBAAwB,GAAG;AAC9C,QAAM,WAAW,IAAI,QAAQ,IAAI,uBAAuB,KAAK;AAE7D,MAAI,WAAW,WAAW,EAAG,QAAO,WAAW;AAC/C,MAAI,aAAa,UAAa,aAAa,GAAI,QAAO,eAAe;AAErE,QAAM,QAAQ,MAAM,SAAS,UAAoB,UAAU;AAC3D,MAAI,UAAU,KAAM,QAAO,eAAe;AAC1C,SAAO;AACT;AAEA,SAAS,OAAO,MAAkC;AAChD,QAAM,OAAO,MAAM,cAAc;AACjC,MAAI,MAAM;AACR,SAAK,aAAa,0BAA0B,IAAI;AAChD,SAAK,aAAa,+BAA+B,IAAI;AAAA,EACvD;AAEA,UAAQ,KAAK,KAAK,UAAU,EAAE,OAAO,QAAQ,OAAO,2BAA2B,KAAK,CAAC,CAAC;AACtF,QAAM,IAAI,gBAAgB,IAAI;AAChC;AAQA,eAAe,SACb,UACA,YACwB;AACxB,MAAI,WAAW,WAAW,EAAG,QAAO;AACpC,QAAM,MAAM,IAAI,YAAY;AAC5B,QAAM,MAAM,MAAM,OAAO,OAAO,YAAY,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;AAC9F,QAAM,iBAAiB,IAAI,WAAW,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,QAAQ,CAAC,CAAC;AAEjG,MAAI,UAAyB;AAC7B,aAAW,EAAE,OAAO,MAAM,KAAK,YAAY;AACzC,UAAM,aAAa,IAAI,WAAW,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,KAAK,CAAC,CAAC;AAC1F,QAAI,OAAO,eAAe,SAAS,WAAW;AAC9C,aAAS,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK;AAC9C,eAAS,eAAe,CAAC,KAAK,MAAM,WAAW,CAAC,KAAK;AAAA,IACvD;AACA,QAAI,SAAS,EAAG,WAAU;AAAA,EAC5B;AACA,SAAO;AACT;","names":[]}
@@ -29,7 +29,7 @@ import { hostname } from "os";
29
29
  // package.json
30
30
  var package_default = {
31
31
  name: "@quatronic/sdk",
32
- version: "0.2.1",
32
+ version: "0.2.2-canary-20260916115650",
33
33
  type: "module",
34
34
  private: false,
35
35
  publishConfig: {
@@ -37,7 +37,8 @@ var package_default = {
37
37
  access: "public"
38
38
  },
39
39
  files: [
40
- "dist"
40
+ "dist",
41
+ "semgrep"
41
42
  ],
42
43
  bin: {
43
44
  "qdc-setup-env": "./dist/cli/setup-env.js"
@@ -79,6 +80,10 @@ var package_default = {
79
80
  types: "./dist/next/config/index.d.ts",
80
81
  import: "./dist/next/config/index.js"
81
82
  },
83
+ "./next/security": {
84
+ types: "./dist/next/security/index.d.ts",
85
+ import: "./dist/next/security/index.js"
86
+ },
82
87
  "./next/connections": {
83
88
  types: "./dist/next/connections/index.d.ts",
84
89
  import: "./dist/next/connections/index.js"
@@ -87,6 +92,18 @@ var package_default = {
87
92
  types: "./dist/node/connections/index.d.ts",
88
93
  import: "./dist/node/connections/index.js"
89
94
  },
95
+ "./node/net": {
96
+ types: "./dist/node/net/index.d.ts",
97
+ import: "./dist/node/net/index.js"
98
+ },
99
+ "./node/fspath": {
100
+ types: "./dist/node/fspath/index.d.ts",
101
+ import: "./dist/node/fspath/index.js"
102
+ },
103
+ "./node/parse": {
104
+ types: "./dist/node/parse/index.d.ts",
105
+ import: "./dist/node/parse/index.js"
106
+ },
90
107
  "./node/identity": {
91
108
  types: "./dist/node/identity/index.d.ts",
92
109
  import: "./dist/node/identity/index.js"
@@ -108,7 +125,8 @@ var package_default = {
108
125
  test: "vitest run",
109
126
  "test:watch": "vitest",
110
127
  typecheck: "tsc --noEmit && tsc -p tsconfig.test.json",
111
- lint: "eslint src tests"
128
+ lint: "eslint src tests",
129
+ calibrate: "node scripts/calibrate.mjs"
112
130
  },
113
131
  dependencies: {
114
132
  "@opentelemetry/api": "^1.9.1",
@@ -409,4 +427,4 @@ function startNodeSdk(opts) {
409
427
  export {
410
428
  startNodeSdk
411
429
  };
412
- //# sourceMappingURL=chunk-3VQW3FDC.js.map
430
+ //# sourceMappingURL=chunk-GWX26DF7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/observability-core/start-node-sdk.ts","../src/observability-core/resource.ts","../package.json","../src/observability-core/noise-filter.ts","../src/observability-core/http-sanitize.ts","../src/observability-core/span-scrub.ts","../src/observability-core/scrub-span-processor.ts","../src/observability-core/format-fatal.ts"],"sourcesContent":["// Shared NodeSDK bootstrap for every server runtime (Next and plain Node). The\n// only per-runtime difference is the noise-prefix list. Importing this module's\n// caller (a register entry) must happen before pino/node:http are first imported\n// so the instrumentations can patch them.\nimport { NodeSDK } from \"@opentelemetry/sdk-node\"\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\"\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\"\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\"\nimport { PeriodicExportingMetricReader } from \"@opentelemetry/sdk-metrics\"\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\"\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-base\"\nimport { HttpInstrumentation } from \"@opentelemetry/instrumentation-http\"\nimport { UndiciInstrumentation } from \"@opentelemetry/instrumentation-undici\"\nimport { PinoInstrumentation } from \"@opentelemetry/instrumentation-pino\"\nimport { PgInstrumentation } from \"@opentelemetry/instrumentation-pg\"\nimport { buildResource } from \"./resource\"\nimport { qdcSanitizeHttp, qdcTracesSampleRatio, qdcEnhancedDbReporting, qdcHealthPath } from \"./env\"\nimport { shouldIgnoreIncoming, shouldIgnoreOutgoing } from \"./noise-filter\"\nimport { sanitizeHttpSpanAttributes } from \"./http-sanitize\"\nimport { ScrubSpanProcessor } from \"./scrub-span-processor\"\nimport { formatFatal } from \"./format-fatal\"\n\nexport interface StartNodeSdkOptions {\n noisePrefixes: string[]\n}\n\nexport function startNodeSdk(opts: StartNodeSdkOptions): NodeSDK {\n const { noisePrefixes } = opts\n\n // Sampling defaults before NodeSDK reads env. Default 100% in all envs; an\n // explicit OTEL_TRACES_SAMPLER always wins.\n if (!process.env.OTEL_TRACES_SAMPLER) process.env.OTEL_TRACES_SAMPLER = \"parentbased_traceidratio\"\n if (!process.env.OTEL_TRACES_SAMPLER_ARG) process.env.OTEL_TRACES_SAMPLER_ARG = qdcTracesSampleRatio()\n\n const healthPath = qdcHealthPath()\n\n const httpInstrumentation = new HttpInstrumentation({\n ignoreIncomingRequestHook: (req) => shouldIgnoreIncoming(req, { healthPath, noisePrefixes }),\n ignoreOutgoingRequestHook: (req) => shouldIgnoreOutgoing({ path: req.path ?? undefined }, { healthPath, noisePrefixes }),\n ...(qdcSanitizeHttp()\n ? { requestHook: (span, request) => sanitizeHttpSpanAttributes(span, request as { url?: string; path?: string }) }\n : {}),\n })\n\n // Cross-module / cross-service propagation: global fetch (undici) is the natural\n // way a Next server and a Node backend call CONN_<ALIAS>_URL. instrumentation-http\n // does NOT see undici, so without this the traceparent is never injected and the\n // hop starts a new trace. Injection is unconditional on origin (server-to-server,\n // no CORS) — that is what continues the trace across modules.\n const undiciInstrumentation = new UndiciInstrumentation({\n ignoreRequestHook: (request) =>\n shouldIgnoreOutgoing({ path: (request as { path?: string }).path }, { healthPath, noisePrefixes }),\n ...(qdcSanitizeHttp()\n ? { requestHook: (span, request) =>\n sanitizeHttpSpanAttributes(span, { path: (request as { path?: string }).path }, \"stable\") }\n : {}),\n })\n\n // DB spans. Full SQL + values only off-prod (synthetic data); parametrized only\n // in prod-like envs. `requireParentSpan` drops queries that run with no active\n // parent span — connection-pool warmup, keepalive pings, startup/migration — so\n // each no longer surfaces as its own root trace. Queries inside an HTTP request\n // (or an `observe()` wrapper) still nest under that span and are kept.\n const pgInstrumentation = new PgInstrumentation({\n enhancedDatabaseReporting: qdcEnhancedDbReporting(),\n requireParentSpan: true,\n })\n\n let sdk: NodeSDK\n try {\n sdk = new NodeSDK({\n resource: buildResource(),\n spanProcessors: [new ScrubSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter()))],\n metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),\n logRecordProcessor: new BatchLogRecordProcessor(new OTLPLogExporter()),\n instrumentations: [httpInstrumentation, undiciInstrumentation, pgInstrumentation, new PinoInstrumentation()],\n })\n } catch (err: unknown) {\n process.stderr.write(`[qdc-sdk] initialization failed: ${err}\\n`)\n process.exit(1)\n }\n\n try {\n sdk.start()\n } catch (err: unknown) {\n process.stderr.write(`[qdc-sdk] SDK start failed: ${err}\\n`)\n process.exit(1)\n }\n\n const gracefulShutdown = async (signal: string): Promise<void> => {\n process.stderr.write(`[qdc-sdk] ${signal} received, shutting down\\n`)\n try { await sdk.shutdown() } catch { /* ignore */ }\n process.exit(0)\n }\n process.on(\"SIGTERM\", () => void gracefulShutdown(\"SIGTERM\"))\n process.on(\"SIGINT\", () => void gracefulShutdown(\"SIGINT\"))\n process.on(\"uncaughtException\", async (err) => {\n process.stderr.write(`[qdc-sdk] fatal: ${formatFatal(err)}\\n`)\n try { await sdk.shutdown() } finally { process.exit(1) }\n })\n process.on(\"unhandledRejection\", async (reason) => {\n process.stderr.write(`[qdc-sdk] fatal: ${formatFatal(reason)}\\n`)\n try { await sdk.shutdown() } finally { process.exit(1) }\n })\n\n return sdk\n}\n","import { type Resource, resourceFromAttributes } from \"@opentelemetry/resources\"\nimport { hostname } from \"node:os\"\nimport { qdcAppId, qdcCustomerId, qdcEnv, qdcModule } from \"./env\"\nimport pkg from \"../../package.json\" with { type: \"json\" }\n\nconst SDK_VERSION = (pkg as { version: string }).version\n\n/**\n * Maps the QDC identity env vars to the OpenTelemetry resource attributes used\n * by the SDK's server-side observability. The NodeSDK carries this resource on\n * every span and log record (the latter via the in-process PinoInstrumentation),\n * so it is the single source of truth for tenant labels.\n *\n * Identity is read from NEXT_PUBLIC_QDC_* with a fallback to legacy QDC_* (see\n * ./env). Throws if the customer or app id are missing — required to land logs\n * and traces under the correct tenant namespace.\n */\nexport function buildResourceAttributes(): Record<string, string> {\n const customerId = qdcCustomerId()\n if (!customerId)\n throw new Error(\"NEXT_PUBLIC_QDC_CUSTOMER_ID (or legacy QDC_CUSTOMER_ID) env var is required\")\n\n const appId = qdcAppId()\n if (!appId)\n throw new Error(\"NEXT_PUBLIC_QDC_APP_ID (or legacy QDC_APP_ID) env var is required\")\n\n const env = qdcEnv()\n\n const attrs: Record<string, string> = {\n \"service.name\": appId,\n \"service.namespace\": customerId,\n \"deployment.environment\": env,\n \"host.name\": hostname(),\n \"qdc.customer_id\": customerId,\n \"qdc.app_id\": appId,\n \"qdc.sdk.version\": SDK_VERSION,\n }\n const mod = qdcModule()\n if (mod) attrs[\"qdc.module\"] = mod\n return attrs\n}\n\nexport function buildResource(): Resource {\n return resourceFromAttributes(buildResourceAttributes())\n}\n","{\n \"name\": \"@quatronic/sdk\",\n \"version\": \"0.2.2-canary-20260916115650\",\n \"type\": \"module\",\n \"private\": false,\n \"publishConfig\": {\n \"registry\": \"https://registry.npmjs.org\",\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\",\n \"semgrep\"\n ],\n \"bin\": {\n \"qdc-setup-env\": \"./dist/cli/setup-env.js\"\n },\n \"exports\": {\n \"./auth\": {\n \"types\": \"./dist/auth/index.d.ts\",\n \"import\": \"./dist/auth/index.js\"\n },\n \"./next/auth\": {\n \"types\": \"./dist/next/auth/index.d.ts\",\n \"import\": \"./dist/next/auth/index.js\"\n },\n \"./next/better-auth\": {\n \"types\": \"./dist/next/better-auth/index.d.ts\",\n \"import\": \"./dist/next/better-auth/index.js\"\n },\n \"./observability\": {\n \"types\": \"./dist/observability/index.d.ts\",\n \"import\": \"./dist/observability/index.js\"\n },\n \"./next/observability\": {\n \"types\": \"./dist/next/observability/index.d.ts\",\n \"import\": \"./dist/next/observability/index.js\"\n },\n \"./next/observability/register\": {\n \"types\": \"./dist/next/observability/register.d.ts\",\n \"import\": \"./dist/next/observability/register.js\"\n },\n \"./node/observability\": {\n \"types\": \"./dist/node/observability/index.d.ts\",\n \"import\": \"./dist/node/observability/index.js\"\n },\n \"./node/observability/register\": {\n \"types\": \"./dist/node/observability/register.d.ts\",\n \"import\": \"./dist/node/observability/register.js\"\n },\n \"./next/config\": {\n \"types\": \"./dist/next/config/index.d.ts\",\n \"import\": \"./dist/next/config/index.js\"\n },\n \"./next/security\": {\n \"types\": \"./dist/next/security/index.d.ts\",\n \"import\": \"./dist/next/security/index.js\"\n },\n \"./next/connections\": {\n \"types\": \"./dist/next/connections/index.d.ts\",\n \"import\": \"./dist/next/connections/index.js\"\n },\n \"./node/connections\": {\n \"types\": \"./dist/node/connections/index.d.ts\",\n \"import\": \"./dist/node/connections/index.js\"\n },\n \"./node/net\": {\n \"types\": \"./dist/node/net/index.d.ts\",\n \"import\": \"./dist/node/net/index.js\"\n },\n \"./node/fspath\": {\n \"types\": \"./dist/node/fspath/index.d.ts\",\n \"import\": \"./dist/node/fspath/index.js\"\n },\n \"./node/parse\": {\n \"types\": \"./dist/node/parse/index.d.ts\",\n \"import\": \"./dist/node/parse/index.js\"\n },\n \"./node/identity\": {\n \"types\": \"./dist/node/identity/index.d.ts\",\n \"import\": \"./dist/node/identity/index.js\"\n },\n \"./node/db\": {\n \"types\": \"./dist/node/db/index.d.ts\",\n \"import\": \"./dist/node/db/index.js\"\n },\n \"./next/db\": {\n \"types\": \"./dist/next/db/index.d.ts\",\n \"import\": \"./dist/next/db/index.js\"\n }\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"surface\": \"node scripts/api-surface.mjs\",\n \"surface:check\": \"node scripts/api-surface.mjs --check\",\n \"prepublishOnly\": \"pnpm build\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.test.json\",\n \"lint\": \"eslint src tests\",\n \"calibrate\": \"node scripts/calibrate.mjs\"\n },\n \"dependencies\": {\n \"@opentelemetry/api\": \"^1.9.1\",\n \"@opentelemetry/context-zone\": \"2.7.1\",\n \"@opentelemetry/exporter-logs-otlp-http\": \"^0.218.0\",\n \"@opentelemetry/exporter-metrics-otlp-http\": \"^0.218.0\",\n \"@opentelemetry/exporter-trace-otlp-http\": \"^0.218.0\",\n \"@opentelemetry/instrumentation\": \"^0.218.0\",\n \"@opentelemetry/instrumentation-document-load\": \"^0.63.0\",\n \"@opentelemetry/instrumentation-fetch\": \"^0.218.0\",\n \"@opentelemetry/instrumentation-http\": \"^0.218.0\",\n \"@opentelemetry/instrumentation-pg\": \"^0.71.0\",\n \"@opentelemetry/instrumentation-pino\": \"^0.64.0\",\n \"@opentelemetry/instrumentation-undici\": \"^0.28.0\",\n \"@opentelemetry/resources\": \"^2.7.1\",\n \"@opentelemetry/sdk-logs\": \"^0.218.0\",\n \"@opentelemetry/sdk-metrics\": \"^2.7.1\",\n \"@opentelemetry/sdk-node\": \"^0.218.0\",\n \"@opentelemetry/sdk-trace-base\": \"2.7.1\",\n \"@opentelemetry/sdk-trace-web\": \"2.7.1\",\n \"jose\": \"^5.9.6\",\n \"pino\": \"^10.3.1\"\n },\n \"peerDependencies\": {\n \"@better-auth/core\": \">=1.6\",\n \"better-auth\": \">=1.6\",\n \"drizzle-orm\": \">=0.36\",\n \"next\": \">=15\",\n \"pg\": \">=8.13\",\n \"react\": \">=18\",\n \"react-dom\": \">=18\"\n },\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"next\": {\n \"optional\": true\n },\n \"drizzle-orm\": {\n \"optional\": true\n },\n \"pg\": {\n \"optional\": true\n },\n \"better-auth\": {\n \"optional\": true\n },\n \"@better-auth/core\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@better-auth/core\": \"^1.6.24\",\n \"@eslint/js\": \"^10.0.1\",\n \"@opentelemetry/instrumentation\": \"^0.218.0\",\n \"@opentelemetry/sdk-trace-base\": \"^2.7.0\",\n \"@testcontainers/postgresql\": \"^12.0.0\",\n \"@testing-library/react\": \"^16.1.0\",\n \"@types/node\": \"^22.10.2\",\n \"@types/pg\": \"^8.11.10\",\n \"@types/react\": \"^18.3.17\",\n \"@types/react-dom\": \"^18.3.5\",\n \"@vitest/coverage-v8\": \"^2.1.9\",\n \"better-auth\": \"^1.6.24\",\n \"drizzle-kit\": \"^0.30.1\",\n \"drizzle-orm\": \"^0.38.2\",\n \"eslint\": \"^10.5.0\",\n \"jsdom\": \"^25.0.1\",\n \"msw\": \"^2.7.0\",\n \"next\": \"^15.1.0\",\n \"pg\": \"^8.13.1\",\n \"react\": \"^18.3.1\",\n \"react-dom\": \"^18.3.1\",\n \"testcontainers\": \"^10.16.0\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"typescript-eslint\": \"^8.61.1\",\n \"vitest\": \"^2.1.8\"\n }\n}\n","// Pure predicate deciding whether an HTTP request is infrastructural noise that\n// should never become a span. Framework-agnostic rules are built in: favicon,\n// source maps, OPTIONS, RSC navigations/prefetches (`?_rsc=` query + RSC headers),\n// and Next's `/_next/*` internals. Callers pass any additional environment-specific\n// prefixes (and an optional health path) via NoiseFilterOptions.\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. The meaningful auth flows keep\n// their labeled spans (Log in, Complete login, Log out, Refresh session);\n// dropping the inbound span here also drops its pg children (requireParentSpan).\nconst NOISE_EXACT = new Set([\"/favicon.ico\", \"/api/auth/me\"])\n\n// React Server Components navigations/prefetches/data-fetches. Next signals them\n// with a `?_rsc=` query param and/or an `RSC` / `Next-Router-Prefetch` request\n// header. They are real-looking routes, so a path-prefix filter misses them.\n// NOTE: `Next-Router-State-Tree` is intentionally NOT in this list — Next.js\n// sends it on server actions too (POST + Next-Action), not only on RSC navigations,\n// so it is not a safe RSC discriminator.\nconst RSC_QUERY_RE = /[?&]_rsc=/\nconst RSC_HEADERS = [\"rsc\", \"next-router-prefetch\"]\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\nexport function shouldIgnoreIncoming(\n req: { url?: string; method?: string; headers?: Record<string, unknown> },\n opts: NoiseFilterOptions = {},\n): boolean {\n if ((req.method ?? \"\").toUpperCase() === \"OPTIONS\") return true\n const headers = req.headers ?? {}\n // A Next.js server action is real work — never noise — even though it also\n // carries router headers. Exempt it explicitly so its inbound span (and the\n // DB child gated by pg requireParentSpan) is preserved.\n if (headers[\"next-action\"] != null) return false\n if (RSC_HEADERS.some((h) => headers[h] != null)) return true\n return isNoisePath(req.url ?? \"\", opts)\n}\n\nexport function shouldIgnoreOutgoing(\n req: { path?: string },\n opts: NoiseFilterOptions = {},\n): boolean {\n return isNoisePath(req.path ?? \"\", opts)\n}\n","// Pure HTTP-span PII sanitizer. No OTel SDK import so it is unit-testable in\n// isolation. register.ts wires sanitizeHttpSpanAttributes into the\n// HttpInstrumentation requestHook when QDC_OTEL_SANITIZE_HTTP is on.\n\nconst REDACTED = \"[redacted]\"\n\nexport type SemconvMode = \"legacy\" | \"stable\" | \"dup\"\n\n// instrumentation-http emits legacy attribute names by default and the stable\n// names (url.full/url.query/url.path) only when OTEL_SEMCONV_STABILITY_OPT_IN\n// selects \"http\" (stable only) or \"http/dup\" (both).\nexport function httpSemconvMode(\n optIn: string | undefined = process.env.OTEL_SEMCONV_STABILITY_OPT_IN,\n): SemconvMode {\n const tokens = (optIn ?? \"\").split(\",\").map((t) => t.trim().toLowerCase())\n if (tokens.includes(\"http/dup\")) return \"dup\"\n if (tokens.includes(\"http\")) return \"stable\"\n return \"legacy\"\n}\n\ninterface SpanLike {\n setAttribute(key: string, value: string): unknown\n}\n\ninterface RequestLike {\n url?: string // IncomingMessage (server spans): path + query\n path?: string // ClientRequest (outgoing spans): path + query\n}\n\nexport function sanitizeHttpSpanAttributes(\n span: SpanLike,\n request: RequestLike,\n mode: SemconvMode = httpSemconvMode(),\n): void {\n const raw = request.url ?? request.path ?? \"\"\n const queryStart = raw.indexOf(\"?\")\n const hasQuery = queryStart !== -1\n const path = hasQuery ? raw.slice(0, queryStart) : raw\n\n // Legacy semconv: redact http.url (once the query is gone it only duplicates\n // http.host + http.target); http.target keeps the path. Skip http.target when\n // we have no path info, so we don't clobber the instrumentation's \"/\" default.\n if (mode === \"legacy\" || mode === \"dup\") {\n if (path) span.setAttribute(\"http.target\", path)\n span.setAttribute(\"http.url\", REDACTED)\n }\n\n // Stable semconv: the query lives in url.full / url.query; url.path is already\n // query-less, so leave it intact. Only redact url.query when a query was\n // actually present — the instrumentation omits it otherwise, and writing it\n // unconditionally would falsely signal a redacted query on clean paths.\n if (mode === \"stable\" || mode === \"dup\") {\n span.setAttribute(\"url.full\", REDACTED)\n if (hasQuery) span.setAttribute(\"url.query\", REDACTED)\n }\n}\n","// Leaf-noise predicate + attribute redaction, pure so they unit-test without the\n// OTel SDK. Wired into export by scrub-span-processor.ts. Contract:\n// docs (QDC spec 2026-07-02): the SDK drops ONLY leaf spans — spans that never\n// have children — so parent/child chains can never break. Structural framework\n// spans (BaseServer.handleRequest, AppRender.fetch, undici) stay; the portal\n// folds those at render time.\n\nexport const REDACTED = \"●●●\"\n\n// next.span_type values that are pure leaves and never carry information a\n// developer needs: sub-millisecond router/renderer bookkeeping.\nconst NEXT_LEAF_NOISE = new Set([\n \"Middleware.execute\",\n \"NextNodeServer.findPageComponents\",\n \"NextNodeServer.startResponse\",\n])\n\n// pg connection-pool bookkeeping; the actual query span (pg.query:*) stays.\nconst NOISE_SPAN_NAMES = new Set([\"pg-pool.connect\", \"pg.connect\"])\n\nexport function isNoiseLeafSpan(name: string, attributes: Record<string, unknown>): boolean {\n const nextType = attributes[\"next.span_type\"]\n if (typeof nextType === \"string\" && NEXT_LEAF_NOISE.has(nextType)) return true\n return NOISE_SPAN_NAMES.has(name)\n}\n\nconst REDACT_KEYS = new Set([\"db.connection_string\", \"db.user\"])\nconst REDACT_PREFIXES = [\"net.peer.\", \"net.sock.\", \"net.host.\", \"network.peer.\", \"network.local.\"]\n\nexport function redactSpanAttributes(attributes: Record<string, unknown>): void {\n for (const key of Object.keys(attributes)) {\n if (REDACT_KEYS.has(key) || REDACT_PREFIXES.some((p) => key.startsWith(p))) {\n attributes[key] = REDACTED\n }\n }\n}\n","import type { Context } from \"@opentelemetry/api\"\nimport type { ReadableSpan, Span, SpanProcessor } from \"@opentelemetry/sdk-trace-base\"\nimport { isNoiseLeafSpan, redactSpanAttributes } from \"./span-scrub\"\n\n/** Wraps the export processor: drops leaf-noise spans and redacts sensitive\n * attributes on everything else. Runs in ALL envs — connection strings and\n * internal IPs never belong in the telemetry backend. */\nexport class ScrubSpanProcessor implements SpanProcessor {\n constructor(private readonly inner: SpanProcessor) {}\n\n onStart(span: Span, parentContext: Context): void {\n this.inner.onStart(span, parentContext)\n }\n\n onEnd(span: ReadableSpan): void {\n if (isNoiseLeafSpan(span.name, span.attributes as Record<string, unknown>)) return\n redactSpanAttributes(span.attributes as Record<string, unknown>)\n this.inner.onEnd(span)\n }\n\n forceFlush(): Promise<void> {\n return this.inner.forceFlush()\n }\n\n shutdown(): Promise<void> {\n return this.inner.shutdown()\n }\n}\n","// A crash that reaches the SDK's last-resort handlers is often the only thing an\n// operator ever sees about it: the process dies before any log exporter flushes,\n// so all that survives is this one stderr line in the platform's container log.\n// `${err}` would reduce it to \"Error: self-signed certificate\" — no stack, no\n// cause, no error code, i.e. no way to tell which of the app's boot steps threw.\nexport function formatFatal(err: unknown): string {\n if (!(err instanceof Error)) return String(err)\n\n let out = err.stack ?? `${err.name}: ${err.message}`\n\n const code = (err as { code?: unknown }).code\n if (typeof code === \"string\" || typeof code === \"number\") out += `\\n code: ${code}`\n\n // Fetch/undici and pg wrap the real failure in `cause`; without it a boot\n // crash reads as a generic \"TypeError: fetch failed\".\n let cause: unknown = (err as { cause?: unknown }).cause\n let depth = 0\n while (cause !== undefined && depth < 3) {\n const rendered = cause instanceof Error ? (cause.stack ?? `${cause.name}: ${cause.message}`) : String(cause)\n out += `\\n caused by: ${rendered}`\n cause = cause instanceof Error ? (cause as { cause?: unknown }).cause : undefined\n depth += 1\n }\n\n return out\n}\n"],"mappings":"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","names":["REDACTED"]}
@@ -239,4 +239,14 @@ declare function extractRoles(payload: JwtPayload, clientId: string | undefined)
239
239
  type Handler = (req: Request) => Promise<Response> | Response;
240
240
  declare function withConnectionToken(handler: Handler): Handler;
241
241
 
242
- export { AuthError, type AuthRouteHandlers, type AuthUser, type JwtPayload, type NextAuthOptions, type PermissionGuards, type PermissionMap, type PermissionOf, type Session, createAuthRouteHandlers, createPermissionGuards, decodeJwtPayload, definePermissions, extractRoles, getAccessToken, getAccessTokenFor, makeGuards, permissionsForRoles, requireAccessToken, requireAccessTokenFor, requireAuth, requireAuthFor, runAuthMigrations, withConnectionToken };
242
+ /**
243
+ * Reduce a caller-supplied redirect target to a same-origin relative path.
244
+ *
245
+ * Anything absolute, protocol-relative, or unparseable resolves to `fallback`.
246
+ * The fallback is validated by the same rule, so passing a tainted value there
247
+ * cannot be used to smuggle an open redirect through the back door; a fallback
248
+ * that fails too yields `"/"`.
249
+ */
250
+ declare function safeRedirect(candidate: string | null | undefined, fallback?: string): string;
251
+
252
+ export { AuthError, type AuthRouteHandlers, type AuthUser, type JwtPayload, type NextAuthOptions, type PermissionGuards, type PermissionMap, type PermissionOf, type Session, createAuthRouteHandlers, createPermissionGuards, decodeJwtPayload, definePermissions, extractRoles, getAccessToken, getAccessTokenFor, makeGuards, permissionsForRoles, requireAccessToken, requireAccessTokenFor, requireAuth, requireAuthFor, runAuthMigrations, safeRedirect, withConnectionToken };
@@ -585,45 +585,52 @@ function organizationClaimHasAlias(raw, alias) {
585
585
 
586
586
  // src/next/auth/loginState.ts
587
587
  import { webcrypto as webcrypto4 } from "crypto";
588
- function randomB64Url(bytes) {
589
- const buf = new Uint8Array(bytes);
590
- webcrypto4.getRandomValues(buf);
591
- return Buffer.from(buf).toString("base64url");
592
- }
593
- async function sha256B64Url(input) {
594
- const data = new TextEncoder().encode(input);
595
- const digest = await webcrypto4.subtle.digest("SHA-256", data);
596
- return Buffer.from(new Uint8Array(digest)).toString("base64url");
597
- }
588
+
589
+ // src/auth-core/sanitize.ts
598
590
  var CONTROL_CHARS = /[\x00-\x1f\x7f]/g;
599
- function sanitizeRedirect(raw) {
600
- if (!raw) return "/";
591
+ function normalizePath(raw) {
592
+ if (!raw) return null;
601
593
  const cleaned = raw.replace(CONTROL_CHARS, "");
602
- if (!cleaned.startsWith("/") || cleaned.startsWith("//")) return "/";
594
+ if (!cleaned.startsWith("/") || cleaned.startsWith("//")) return null;
603
595
  try {
604
596
  const base = new URL("https://sanitize.invalid");
605
597
  const resolved = new URL(cleaned, base);
606
- if (resolved.origin !== base.origin) return "/";
598
+ if (resolved.origin !== base.origin) return null;
607
599
  return resolved.pathname + resolved.search + resolved.hash;
608
600
  } catch {
609
- return "/";
601
+ return null;
610
602
  }
611
603
  }
604
+ function safeRedirect(candidate, fallback = "/") {
605
+ return normalizePath(candidate) ?? normalizePath(fallback) ?? "/";
606
+ }
612
607
  var LOGIN_HINT_MAX = 320;
613
608
  var IDP_HINT_MAX = 255;
614
609
  var IDP_ALIAS = /^[A-Za-z0-9._-]+$/;
615
- function sanitizeLoginHint(raw) {
610
+ function safeLoginHint(raw) {
616
611
  if (!raw) return void 0;
617
612
  const cleaned = raw.replace(CONTROL_CHARS, "").trim().slice(0, LOGIN_HINT_MAX);
618
613
  return cleaned.length > 0 ? cleaned : void 0;
619
614
  }
620
- function sanitizeIdpHint(raw) {
615
+ function safeIdpHint(raw) {
621
616
  if (!raw) return void 0;
622
617
  const cleaned = raw.trim();
623
618
  if (cleaned.length === 0 || cleaned.length > IDP_HINT_MAX) return void 0;
624
619
  return IDP_ALIAS.test(cleaned) ? cleaned : void 0;
625
620
  }
626
621
 
622
+ // src/next/auth/loginState.ts
623
+ function randomB64Url(bytes) {
624
+ const buf = new Uint8Array(bytes);
625
+ webcrypto4.getRandomValues(buf);
626
+ return Buffer.from(buf).toString("base64url");
627
+ }
628
+ async function sha256B64Url(input) {
629
+ const data = new TextEncoder().encode(input);
630
+ const digest = await webcrypto4.subtle.digest("SHA-256", data);
631
+ return Buffer.from(new Uint8Array(digest)).toString("base64url");
632
+ }
633
+
627
634
  // src/next/auth/loginBinding.ts
628
635
  var LOGIN_COOKIE_NAME = "qdc_login";
629
636
  var MAX_BOUND_STATES = 3;
@@ -900,9 +907,9 @@ async function handleLogin(req, ctx) {
900
907
  },
901
908
  async () => {
902
909
  const log = useLogger(req);
903
- const redirect = sanitizeRedirect(req.nextUrl.searchParams.get("redirect"));
904
- const loginHint = sanitizeLoginHint(req.nextUrl.searchParams.get("login_hint"));
905
- const kcIdpHint = sanitizeIdpHint(req.nextUrl.searchParams.get("kc_idp_hint"));
910
+ const redirect = safeRedirect(req.nextUrl.searchParams.get("redirect"));
911
+ const loginHint = safeLoginHint(req.nextUrl.searchParams.get("login_hint"));
912
+ const kcIdpHint = safeIdpHint(req.nextUrl.searchParams.get("kc_idp_hint"));
906
913
  const stale = await ctx.handoff.read(req.headers.get("cookie"));
907
914
  if (stale) await ctx.refreshStore.revokeSession(stale.sessionId);
908
915
  const state = randomB64Url(32);
@@ -1135,7 +1142,7 @@ async function handleLogoutRedirect(req, ctx) {
1135
1142
  idTokenHint ??= await ctx.refreshStore.getIdTokenHint(id) ?? void 0;
1136
1143
  }
1137
1144
  const rawRedirect = req.nextUrl.searchParams.get("redirect");
1138
- const target = rawRedirect ? sanitizeRedirect(rawRedirect) : "/login";
1145
+ const target = rawRedirect ? safeRedirect(rawRedirect) : "/login";
1139
1146
  const landing = new URL(LOGGED_OUT_PATH, ctx.publicOrigin);
1140
1147
  landing.searchParams.set("redirect", target);
1141
1148
  const endSessionUrl = ctx.kcClient.buildEndSessionUrl({
@@ -1148,7 +1155,7 @@ async function handleLogoutRedirect(req, ctx) {
1148
1155
  });
1149
1156
  }
1150
1157
  async function handleLoggedOut(req, ctx) {
1151
- const target = sanitizeRedirect(req.nextUrl.searchParams.get("redirect"));
1158
+ const target = safeRedirect(req.nextUrl.searchParams.get("redirect"));
1152
1159
  const attr = target.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
1153
1160
  const literal = JSON.stringify(target).replace(/</g, "\\u003c");
1154
1161
  const res = new NextResponse(
@@ -1825,6 +1832,7 @@ export {
1825
1832
  requireAuth,
1826
1833
  requireAuthFor,
1827
1834
  runAuthMigrations,
1835
+ safeRedirect,
1828
1836
  withConnectionToken
1829
1837
  };
1830
1838
  //# sourceMappingURL=index.js.map