@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,7 +1,7 @@
1
1
  import {
2
2
  getLogger,
3
3
  setUserContext
4
- } from "../../chunk-MET4SIMB.js";
4
+ } from "../../chunk-EK6TGWWT.js";
5
5
 
6
6
  // src/next/better-auth/plugin.ts
7
7
  import { createAuthMiddleware, getSessionFromCtx } from "better-auth/api";
@@ -3,7 +3,7 @@ import {
3
3
  assertConnections,
4
4
  assertInboundConnection,
5
5
  connection
6
- } from "../../chunk-HQT57ITV.js";
6
+ } from "../../chunk-ZZTO5Z3N.js";
7
7
  import {
8
8
  CONNECTION_REJECTED_ATTR,
9
9
  CONNECTION_REJECT_REASON_ATTR,
@@ -11,9 +11,9 @@ import {
11
11
  ConnectionError,
12
12
  DEFAULT_CONNECTION_TIMEOUT_MS,
13
13
  requireConnection
14
- } from "../../chunk-TEBDKNUB.js";
14
+ } from "../../chunk-ROHIN74F.js";
15
15
  import "../../chunk-5XMTUPIY.js";
16
- import "../../chunk-MET4SIMB.js";
16
+ import "../../chunk-EK6TGWWT.js";
17
17
  export {
18
18
  CONNECTION_REJECTED_ATTR,
19
19
  CONNECTION_REJECT_REASON_ATTR,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  parseJsonBounded
3
3
  } from "../../chunk-FUPTRHT7.js";
4
- import {
5
- proxyMatcher
6
- } from "../../chunk-X6D3TBNS.js";
7
4
  import {
8
5
  STATIC_SECURITY_HEADERS
9
6
  } from "../../chunk-2HE2D4SY.js";
7
+ import {
8
+ proxyMatcher
9
+ } from "../../chunk-X6D3TBNS.js";
10
10
 
11
11
  // src/next/security/index.ts
12
12
  import { NextResponse } from "next/server";
@@ -6,6 +6,7 @@ interface SentryLike {
6
6
  setUser(user: {
7
7
  id: string;
8
8
  } | null): void;
9
+ captureException(err: unknown, hint?: object): unknown;
9
10
  }
10
11
 
11
12
  type BrowserSentry = SentryLike & {
@@ -3,9 +3,9 @@ import {
3
3
  } from "../../chunk-DYOSRJH3.js";
4
4
  import {
5
5
  initSentry
6
- } from "../../chunk-G7VPNC6M.js";
7
- import "../../chunk-MET4SIMB.js";
6
+ } from "../../chunk-5MNPBJYS.js";
8
7
  import "../../chunk-7ASQHY7O.js";
8
+ import "../../chunk-EK6TGWWT.js";
9
9
 
10
10
  // src/next/sentry/client.ts
11
11
  import * as Sentry from "@sentry/nextjs";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  initSentry
3
- } from "../../chunk-G7VPNC6M.js";
4
- import "../../chunk-MET4SIMB.js";
3
+ } from "../../chunk-5MNPBJYS.js";
5
4
  import {
6
5
  readSentryConfig
7
6
  } from "../../chunk-7ASQHY7O.js";
7
+ import "../../chunk-EK6TGWWT.js";
8
8
 
9
9
  // src/next/sentry/register.ts
10
10
  import * as Sentry from "@sentry/nextjs";
@@ -3,7 +3,7 @@ import {
3
3
  assertConnections,
4
4
  assertInboundConnection,
5
5
  connection
6
- } from "../../chunk-HQT57ITV.js";
6
+ } from "../../chunk-ZZTO5Z3N.js";
7
7
  import {
8
8
  CONNECTION_REJECTED_ATTR,
9
9
  CONNECTION_REJECT_REASON_ATTR,
@@ -11,9 +11,9 @@ import {
11
11
  ConnectionError,
12
12
  DEFAULT_CONNECTION_TIMEOUT_MS,
13
13
  requireConnection
14
- } from "../../chunk-TEBDKNUB.js";
14
+ } from "../../chunk-ROHIN74F.js";
15
15
  import "../../chunk-5XMTUPIY.js";
16
- import "../../chunk-MET4SIMB.js";
16
+ import "../../chunk-EK6TGWWT.js";
17
17
  export {
18
18
  CONNECTION_REJECTED_ATTR,
19
19
  CONNECTION_REJECT_REASON_ATTR,
@@ -4,12 +4,12 @@ import {
4
4
  import {
5
5
  ConnectionError,
6
6
  requireConnection
7
- } from "../../chunk-TEBDKNUB.js";
7
+ } from "../../chunk-ROHIN74F.js";
8
8
  import {
9
9
  defaultEnv,
10
10
  readConn
11
11
  } from "../../chunk-5XMTUPIY.js";
12
- import "../../chunk-MET4SIMB.js";
12
+ import "../../chunk-EK6TGWWT.js";
13
13
 
14
14
  // src/identity-core/jwksCache.ts
15
15
  import { createRemoteJWKSet } from "jose";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  initSentry
3
- } from "../../chunk-G7VPNC6M.js";
4
- import "../../chunk-MET4SIMB.js";
3
+ } from "../../chunk-5MNPBJYS.js";
5
4
  import {
6
5
  readSentryConfig
7
6
  } from "../../chunk-7ASQHY7O.js";
7
+ import "../../chunk-EK6TGWWT.js";
8
8
 
9
9
  // src/node/sentry/register.ts
10
10
  import * as Sentry from "@sentry/node";
@@ -0,0 +1,10 @@
1
+ type LogAttrs = Record<string, unknown>;
2
+ /**
3
+ * Hands an error the app caught to the error tracker, with flat context.
4
+ *
5
+ * Without a tracker it writes the same JSON line `getLogger().error` would, so
6
+ * a caught error is never silently dropped in an app that has no Sentry.
7
+ */
8
+ declare function reportError(err: unknown, ctx?: LogAttrs): void;
9
+
10
+ export { reportError };
@@ -0,0 +1,7 @@
1
+ import {
2
+ reportError
3
+ } from "../chunk-EK6TGWWT.js";
4
+ export {
5
+ reportError
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,97 @@
1
+ // jscodeshift transform for migration no-raw-fetch. Moves the server-side
2
+ // fetch calls that QDC-NET-001 (safe-fetch) flags onto safeFetch, where the
3
+ // move cannot change behaviour beyond what safeFetch exists to change.
4
+ //
5
+ // npx jscodeshift@17.3.0 --parser=tsx \
6
+ // -t node_modules/@quatronic/sdk/migrations/no-raw-fetch/codemod.mjs src
7
+ //
8
+ // It rewrites a call only when it is the global `fetch`, in a server path, with
9
+ // a URL the server did not fix, and an init that SafeFetchInit can hold. Every
10
+ // other call is left exactly as it was; instructions.md covers those.
11
+
12
+ const SERVER_PATHS = ["src/app/api", "src/lib/server", "src/proxy.ts"]
13
+ const MODULE = "@quatronic/sdk/node/net"
14
+ // SafeFetchInit's keys. `cache`, `next`, `redirect`, `credentials` and the
15
+ // rest have no safeFetch equivalent, so a call carrying one needs a person.
16
+ const INIT_KEYS = new Set(["method", "headers", "body", "signal"])
17
+
18
+ function isServerFile(path) {
19
+ const file = path.replace(/\\/g, "/")
20
+ return SERVER_PATHS.some((p) => file.endsWith(`/${p}`) || file.includes(`/${p}/`) || file.startsWith(`${p}/`) || file === p)
21
+ }
22
+
23
+ /** Mirrors isFixedTarget in src/eslint/ast.ts: a URL the rule does not flag. */
24
+ function isFixed(node) {
25
+ if (!node) return false
26
+ if (node.type === "StringLiteral" || node.type === "Literal") return typeof node.value === "string"
27
+ if (node.type === "TemplateLiteral") return node.expressions.length === 0
28
+ if (node.type === "NewExpression") return node.callee.type === "Identifier" && node.callee.name === "URL" && isFixed(node.arguments[0])
29
+ return false
30
+ }
31
+
32
+ function keyName(prop) {
33
+ if (prop.computed) return null
34
+ if (prop.key.type === "Identifier") return prop.key.name
35
+ return typeof prop.key.value === "string" ? prop.key.value : null
36
+ }
37
+
38
+ /** An init safeFetch accepts as is: absent, or a literal of SafeFetchInit keys. */
39
+ function isPortableInit(node) {
40
+ if (!node) return true
41
+ if (node.type !== "ObjectExpression") return false
42
+ return node.properties.every((p) => {
43
+ if (p.type !== "ObjectProperty" && p.type !== "Property") return false
44
+ const key = keyName(p)
45
+ if (!key || !INIT_KEYS.has(key)) return false
46
+ // `headers: new Headers(...)` or a variable may not be a plain record.
47
+ return key !== "headers" || p.value.type === "ObjectExpression"
48
+ })
49
+ }
50
+
51
+ export default function transformer(file, api) {
52
+ if (!isServerFile(file.path)) return undefined
53
+ const j = api.jscodeshift
54
+ const root = j(file.source)
55
+
56
+ // A local or imported `fetch` is not the global one.
57
+ const shadowed =
58
+ root.find(j.Identifier, { name: "fetch" }).filter((p) => {
59
+ const parent = p.parent.node
60
+ return (
61
+ (parent.type === "VariableDeclarator" && parent.id === p.node) ||
62
+ (parent.type.startsWith("Function") && parent.id === p.node) ||
63
+ parent.type === "ImportSpecifier" ||
64
+ parent.type === "ImportDefaultSpecifier" ||
65
+ (parent.type.startsWith("Function") && parent.params.includes(p.node))
66
+ )
67
+ }).length > 0
68
+ if (shadowed) return undefined
69
+
70
+ const calls = root
71
+ .find(j.CallExpression, { callee: { type: "Identifier", name: "fetch" } })
72
+ .filter((p) => p.node.arguments.length >= 1 && p.node.arguments.length <= 2)
73
+ .filter((p) => !isFixed(p.node.arguments[0]) && p.node.arguments[0].type !== "SpreadElement")
74
+ .filter((p) => isPortableInit(p.node.arguments[1]))
75
+ if (calls.length === 0) return undefined
76
+
77
+ calls.forEach((p) => {
78
+ p.node.callee = j.identifier("safeFetch")
79
+ })
80
+
81
+ const existing = root.find(j.ImportDeclaration, { source: { value: MODULE } })
82
+ const has = existing.find(j.ImportSpecifier, { imported: { name: "safeFetch" } }).length > 0
83
+ if (!has) {
84
+ const spec = j.importSpecifier(j.identifier("safeFetch"))
85
+ const into = existing.filter((p) => p.node.importKind !== "type")
86
+ if (into.length > 0) into.get().node.specifiers.push(spec)
87
+ else {
88
+ const decl = j.importDeclaration([spec], j.literal(MODULE))
89
+ const imports = root.find(j.ImportDeclaration)
90
+ if (imports.length > 0) imports.at(imports.length - 1).insertAfter(decl)
91
+ else root.get().node.program.body.unshift(decl)
92
+ }
93
+ }
94
+ return root.toSource({ quote: "double" })
95
+ }
96
+
97
+ export const parser = "tsx"
@@ -0,0 +1,68 @@
1
+ # no-raw-fetch
2
+
3
+ Server code that fetches a URL it did not fix itself uses `safeFetch` from
4
+ `@quatronic/sdk/node/net`, not the global `fetch`.
5
+
6
+ **Rule:** `qdc/safe-fetch` (QDC-NET-001), in `configs.recommended`. It covers
7
+ `src/app/api`, `src/lib/server` and `src/proxy.ts`.
8
+
9
+ ## Why
10
+
11
+ A URL built from input can point at the private network: the metadata service,
12
+ another tenant's internal service, the database. `safeFetch` resolves the host,
13
+ refuses private, loopback and link-local addresses, and re-checks every
14
+ redirect. The global `fetch` does none of that.
15
+
16
+ ## First, pick the right client
17
+
18
+ | The call goes to | Use |
19
+ |---|---|
20
+ | another module of this app | `connection(alias).fetch` from `@quatronic/sdk/node/connections` |
21
+ | a third-party API the app declares | `integration(alias)` from `@quatronic/sdk/node/integrations` |
22
+ | a URL that comes from input or data | `safeFetch` (this migration) |
23
+
24
+ The codemod only knows the last case. Move the other two by hand first.
25
+
26
+ ## Run the codemod
27
+
28
+ ```bash
29
+ npx jscodeshift@17.3.0 --parser=tsx \
30
+ -t node_modules/@quatronic/sdk/migrations/no-raw-fetch/codemod.mjs src
31
+ ```
32
+
33
+ It rewrites `fetch(url)` and `fetch(url, init)` to `safeFetch(...)` and adds the
34
+ import, when all of these hold:
35
+
36
+ - the file is in one of the paths above;
37
+ - `fetch` is the global one (not imported or declared in the file);
38
+ - the URL is not a fixed string, which the rule does not flag;
39
+ - `init` is absent or an object literal with only `method`, `headers` (an
40
+ object literal), `body` and `signal`.
41
+
42
+ ```ts
43
+ const res = await fetch(target, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) })
44
+ // →
45
+ const res = await safeFetch(target, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) })
46
+ ```
47
+
48
+ The printer may add semicolons to the lines it touched; run your formatter. Then
49
+ run `pnpm lint` to see the calls it left.
50
+
51
+ ## Behaviour that changes
52
+
53
+ Check each rewritten call against these. The codemod cannot.
54
+
55
+ - **https only.** An `http:` URL is refused. Pass `{ protocols: ["https:", "http:"] }` as the third argument only if the target cannot do TLS.
56
+ - **No redirects.** Pass `{ maxRedirects: 3 }` if the call relied on following them. Each hop is checked again.
57
+ - **5 s timeout and 5 MiB response cap** for the whole request. Raise them with `timeoutMs` and `maxBytes`.
58
+ - **A refused target throws `SafeFetchError`** (`reason` says why). Catch it where the old code handled a network error, and answer the caller with a 4xx, not a 500.
59
+
60
+ ## By hand
61
+
62
+ | The call has | Do this |
63
+ |---|---|
64
+ | `headers: new Headers(...)` or a variable | Pass a plain object: `Object.fromEntries(headers)`. |
65
+ | a `ReadableStream` or `FormData` body | Read it into a string or `Uint8Array` first. `safeFetch` replays the body on a redirect, so it cannot stream. |
66
+ | `cache`, `next: { revalidate }` | `safeFetch` does not use Next's data cache. Cache the result yourself, or keep `fetch` for a fixed URL (the rule allows that). |
67
+ | `redirect: "manual"` | That is the default. Drop the key. |
68
+ | `credentials`, `mode`, `keepalive` | Browser options. Server code does not need them. Drop the key. |
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "no-raw-fetch",
3
+ "rule": "safe-fetch",
4
+ "introducedIn": "0.7.0",
5
+ "testOnly": true
6
+ }
@@ -0,0 +1,94 @@
1
+ // jscodeshift transform for migration report-error. Moves the mechanical
2
+ // Sentry.captureException calls that QDC-OBS-004 flags onto reportError.
3
+ //
4
+ // npx jscodeshift@17.3.0 --parser=tsx \
5
+ // -t node_modules/@quatronic/sdk/migrations/report-error/codemod.mjs src
6
+ //
7
+ // Rewritten: `captureException(err)` and `captureException(err, { extra })`,
8
+ // through a namespace or a named import of any @sentry/* package. A call that
9
+ // passes tags, a level, a scope callback or anything else is left for a person;
10
+ // instructions.md says what to do with each.
11
+
12
+ const SENTRY = /^@sentry\//
13
+ const MODULE = "@quatronic/sdk/observability"
14
+
15
+ function keyName(prop) {
16
+ if (prop.computed) return null
17
+ if (prop.key.type === "Identifier") return prop.key.name
18
+ return typeof prop.key.value === "string" ? prop.key.value : null
19
+ }
20
+
21
+ /** The context to hand reportError, or false when the hint does not map. */
22
+ function contextOf(hint) {
23
+ if (!hint) return null
24
+ if (hint.type !== "ObjectExpression" || hint.properties.length !== 1) return false
25
+ const only = hint.properties[0]
26
+ if ((only.type !== "ObjectProperty" && only.type !== "Property") || keyName(only) !== "extra") return false
27
+ return only.value
28
+ }
29
+
30
+ export default function transformer(file, api) {
31
+ const j = api.jscodeshift
32
+ const root = j(file.source)
33
+
34
+ const namespaces = new Set()
35
+ const named = new Set()
36
+ const sentryImports = root.find(j.ImportDeclaration).filter((p) => SENTRY.test(String(p.node.source.value)))
37
+ sentryImports.forEach((p) => {
38
+ for (const s of p.node.specifiers ?? []) {
39
+ if (s.type === "ImportNamespaceSpecifier" || s.type === "ImportDefaultSpecifier") namespaces.add(s.local.name)
40
+ else if (s.type === "ImportSpecifier" && s.imported.name === "captureException") named.add(s.local.name)
41
+ }
42
+ })
43
+ if (namespaces.size === 0 && named.size === 0) return undefined
44
+
45
+ let changed = 0
46
+ root.find(j.CallExpression).forEach((p) => {
47
+ const c = p.node.callee
48
+ const isCapture =
49
+ (c.type === "Identifier" && named.has(c.name)) ||
50
+ (c.type === "MemberExpression" &&
51
+ !c.computed &&
52
+ c.object.type === "Identifier" &&
53
+ namespaces.has(c.object.name) &&
54
+ c.property.type === "Identifier" &&
55
+ c.property.name === "captureException")
56
+ if (!isCapture) return
57
+ const [err, hint, ...rest] = p.node.arguments
58
+ if (!err || rest.length > 0 || err.type === "SpreadElement") return
59
+ const ctx = contextOf(hint)
60
+ if (ctx === false) return
61
+ p.node.callee = j.identifier("reportError")
62
+ p.node.arguments = ctx ? [err, ctx] : [err]
63
+ changed++
64
+ })
65
+ if (changed === 0) return undefined
66
+
67
+ // Drop a named captureException import nothing uses any more.
68
+ const stillUsed = (name) =>
69
+ root
70
+ .find(j.Identifier, { name })
71
+ .filter((p) => j(p).closest(j.ImportDeclaration).length === 0).length > 0
72
+ sentryImports.forEach((p) => {
73
+ const before = p.node.specifiers.length
74
+ p.node.specifiers = p.node.specifiers.filter(
75
+ (s) => s.type !== "ImportSpecifier" || s.imported.name !== "captureException" || stillUsed(s.local.name),
76
+ )
77
+ if (before > 0 && p.node.specifiers.length === 0) j(p).remove()
78
+ })
79
+
80
+ const existing = root.find(j.ImportDeclaration, { source: { value: MODULE } })
81
+ if (existing.find(j.ImportSpecifier, { imported: { name: "reportError" } }).length === 0) {
82
+ const spec = j.importSpecifier(j.identifier("reportError"))
83
+ if (existing.length > 0) existing.get().node.specifiers.push(spec)
84
+ else {
85
+ const decl = j.importDeclaration([spec], j.literal(MODULE))
86
+ const imports = root.find(j.ImportDeclaration)
87
+ if (imports.length > 0) imports.at(imports.length - 1).insertAfter(decl)
88
+ else root.get().node.program.body.unshift(decl)
89
+ }
90
+ }
91
+ return root.toSource({ quote: "double" })
92
+ }
93
+
94
+ export const parser = "tsx"
@@ -0,0 +1,46 @@
1
+ # report-error
2
+
3
+ Report a caught error with `reportError` from `@quatronic/sdk/observability`,
4
+ not with `Sentry.captureException`.
5
+
6
+ **Rule:** `qdc/use-report-error` (QDC-OBS-004). This is a test-only migration, so no shipped config switches the rule on; run it explicitly with `eslint --rule "qdc/use-report-error: warn"`.
7
+
8
+ ## Why
9
+
10
+ The SDK owns the error tracker: it initialises Sentry and wires it into its
11
+ telemetry hooks. An app that calls `reportError` keeps reporting when that
12
+ wiring changes (another tracker, a different scrubbing policy, a test fake).
13
+ An app that calls Sentry directly has to be edited for each of those changes.
14
+ Without a tracker, `reportError` writes an error line to the console instead of
15
+ dropping the error.
16
+
17
+ Spans, `Sentry.logger` and tags stay `Sentry.*`. Only the capture call moves.
18
+
19
+ ## Run the codemod
20
+
21
+ ```bash
22
+ npx jscodeshift@17.3.0 --parser=tsx \
23
+ -t node_modules/@quatronic/sdk/migrations/report-error/codemod.mjs src
24
+ ```
25
+
26
+ It rewrites these calls:
27
+
28
+ ```ts
29
+ Sentry.captureException(err) // → reportError(err)
30
+ Sentry.captureException(err, { extra: { "qore.invoice.id": id } })
31
+ // → reportError(err, { "qore.invoice.id": id })
32
+ ```
33
+
34
+ It adds the import and removes a named `captureException` import that nothing
35
+ uses any more. Run your formatter (the printer may add semicolons), then
36
+ `pnpm lint` to see what is left.
37
+
38
+ ## By hand
39
+
40
+ | You pass | Do this |
41
+ |---|---|
42
+ | `{ tags: {...} }` | Put the tags in the context: `reportError(err, { "qore.area.name": v })`. Keys follow QDC-OBS-002 (`qore.<area>.<name>`) and QDC-OBS-003 (no personal data). |
43
+ | `{ level: "warning" }` or `captureMessage` | It is not an error. Use `Sentry.logger.warn(...)`. |
44
+ | `{ user: ... }` | Leave it out. The SDK sets the user from the session; the context carries ids only. |
45
+ | a scope callback `(scope) => ...` | Move what the callback sets into the context object. |
46
+ | `{ fingerprint: [...] }` | Keep that call as `Sentry.captureException` and suppress the line with a reason: `// eslint-disable-next-line qdc/use-report-error -- custom grouping for <why>`. |
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "report-error",
3
+ "rule": "use-report-error",
4
+ "introducedIn": "0.7.0",
5
+ "testOnly": true
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatronic/sdk",
3
- "version": "0.7.0-rc.6",
3
+ "version": "0.7.0-rc.7",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "migrations",
12
13
  "semgrep",
13
14
  "skills",
14
15
  "postinstall.mjs"
@@ -50,6 +51,10 @@
50
51
  "types": "./dist/node/sentry/register.d.ts",
51
52
  "import": "./dist/node/sentry/register.js"
52
53
  },
54
+ "./observability": {
55
+ "types": "./dist/observability/index.d.ts",
56
+ "import": "./dist/observability/index.js"
57
+ },
53
58
  "./next/config": {
54
59
  "types": "./dist/next/config/index.d.ts",
55
60
  "import": "./dist/next/config/index.js"
@@ -127,6 +132,8 @@
127
132
  "build": "tsup",
128
133
  "surface": "node scripts/api-surface.mjs",
129
134
  "surface:check": "node scripts/api-surface.mjs --check",
135
+ "migration:lint": "node scripts/migration-lint.mjs",
136
+ "release:manifest": "node scripts/release-manifest.mjs",
130
137
  "prepublishOnly": "pnpm build",
131
138
  "postinstall": "node postinstall.mjs",
132
139
  "test": "vitest run",
@@ -185,12 +192,13 @@
185
192
  },
186
193
  "devDependencies": {
187
194
  "@better-auth/core": "^1.6.24",
188
- "@sentry/nextjs": "10.75.2",
189
- "@sentry/node": "10.75.2",
190
195
  "@eslint/js": "^10.0.1",
191
196
  "@opentelemetry/sdk-trace-base": "^2.7.0",
197
+ "@sentry/nextjs": "10.75.2",
198
+ "@sentry/node": "10.75.2",
192
199
  "@testcontainers/postgresql": "^12.0.0",
193
200
  "@testing-library/react": "^16.1.0",
201
+ "@types/jscodeshift": "17.3.0",
194
202
  "@types/node": "^22.10.2",
195
203
  "@types/pg": "^8.11.10",
196
204
  "@types/react": "^18.3.17",
@@ -200,6 +208,7 @@
200
208
  "drizzle-kit": "^0.30.1",
201
209
  "drizzle-orm": "^0.38.2",
202
210
  "eslint": "^10.5.0",
211
+ "jscodeshift": "17.3.0",
203
212
  "jsdom": "^25.0.1",
204
213
  "msw": "^2.7.0",
205
214
  "next": "^15.1.0",
@@ -1 +0,0 @@
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}\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"],"mappings":";AAgBA,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;","names":[]}
@@ -1,31 +0,0 @@
1
- CREATE SCHEMA IF NOT EXISTS auth;
2
-
3
- CREATE TABLE IF NOT EXISTS auth.sessions (
4
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
5
- user_sub varchar(64) NOT NULL,
6
- user_email varchar(320),
7
- organization_id varchar(64),
8
- organization_alias varchar(255),
9
- access_token text NOT NULL,
10
- access_expires_at timestamptz NOT NULL,
11
- refresh_ct bytea NOT NULL,
12
- refresh_iv bytea NOT NULL,
13
- refresh_expires_at timestamptz NOT NULL,
14
- created_at timestamptz NOT NULL DEFAULT now(),
15
- updated_at timestamptz NOT NULL DEFAULT now(),
16
- revoked_at timestamptz
17
- );
18
-
19
- CREATE INDEX IF NOT EXISTS ix_auth_sessions_user_sub
20
- ON auth.sessions(user_sub) WHERE revoked_at IS NULL;
21
-
22
- CREATE TABLE IF NOT EXISTS auth.login_state (
23
- state varchar(128) PRIMARY KEY,
24
- code_verifier varchar(255) NOT NULL,
25
- redirect_path text NOT NULL DEFAULT '/',
26
- created_at timestamptz NOT NULL DEFAULT now(),
27
- expires_at timestamptz NOT NULL
28
- );
29
-
30
- CREATE INDEX IF NOT EXISTS ix_auth_login_state_expires
31
- ON auth.login_state(expires_at);
@@ -1 +0,0 @@
1
- ALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS id_token text;