@datakable/core 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/{codes-B9bCK73-.d.cts → codes-4sYQv_7J.d.cts} +42 -12
- package/dist/{codes-B9bCK73-.d.ts → codes-4sYQv_7J.d.ts} +42 -12
- package/dist/column-filters.d.cts +2 -2
- package/dist/column-filters.d.ts +2 -2
- package/dist/diagnostics/build.cjs +1 -1
- package/dist/diagnostics/build.cjs.map +1 -1
- package/dist/diagnostics/build.js +2 -2
- package/dist/diagnostics/build.js.map +1 -1
- package/dist/diagnostics/console.cjs +15 -4
- package/dist/diagnostics/console.cjs.map +1 -1
- package/dist/diagnostics/console.js +15 -4
- package/dist/diagnostics/console.js.map +1 -1
- package/dist/diagnostics/explain.cjs.map +1 -1
- package/dist/diagnostics/explain.js.map +1 -1
- package/dist/diagnostics/hub.cjs +7 -1
- package/dist/diagnostics/hub.cjs.map +1 -1
- package/dist/diagnostics/hub.js +7 -1
- package/dist/diagnostics/hub.js.map +1 -1
- package/dist/diagnostics/prose.cjs +10 -1
- package/dist/diagnostics/prose.cjs.map +1 -1
- package/dist/diagnostics/prose.js +9 -1
- package/dist/diagnostics/prose.js.map +1 -1
- package/dist/diagnostics/redact.cjs +1 -1
- package/dist/diagnostics/redact.cjs.map +1 -1
- package/dist/diagnostics/redact.js +1 -1
- package/dist/diagnostics/redact.js.map +1 -1
- package/dist/diagnostics/types.cjs.map +1 -1
- package/dist/{explain-i0OATi_W.d.ts → explain-DmwN_RG9.d.cts} +13 -6
- package/dist/{explain-Q8Cg0cZy.d.cts → explain-Dr3SmMNY.d.ts} +13 -6
- package/dist/feature/install.cjs +2 -1
- package/dist/feature/install.cjs.map +1 -1
- package/dist/feature/install.js +2 -1
- package/dist/feature/install.js.map +1 -1
- package/dist/feature/slice-guard.cjs +76 -0
- package/dist/feature/slice-guard.cjs.map +1 -0
- package/dist/feature/slice-guard.js +50 -0
- package/dist/feature/slice-guard.js.map +1 -0
- package/dist/features/column-filters/codes.cjs +2 -19
- package/dist/features/column-filters/codes.cjs.map +1 -1
- package/dist/features/column-filters/codes.js +2 -19
- package/dist/features/column-filters/codes.js.map +1 -1
- package/dist/features/column-filters/column-filters.cjs +37 -22
- package/dist/features/column-filters/column-filters.cjs.map +1 -1
- package/dist/features/column-filters/column-filters.js +37 -22
- package/dist/features/column-filters/column-filters.js.map +1 -1
- package/dist/features/column-filters/match.cjs +6 -2
- package/dist/features/column-filters/match.cjs.map +1 -1
- package/dist/features/column-filters/match.js +6 -2
- package/dist/features/column-filters/match.js.map +1 -1
- package/dist/features/pagination/pagination.cjs +2 -2
- package/dist/features/pagination/pagination.cjs.map +1 -1
- package/dist/features/pagination/pagination.js +2 -2
- package/dist/features/pagination/pagination.js.map +1 -1
- package/dist/features/search/codes.cjs +18 -2
- package/dist/features/search/codes.cjs.map +1 -1
- package/dist/features/search/codes.js +16 -1
- package/dist/features/search/codes.js.map +1 -1
- package/dist/features/search/search-engine.cjs +24 -5
- package/dist/features/search/search-engine.cjs.map +1 -1
- package/dist/features/search/search-engine.js +25 -6
- package/dist/features/search/search-engine.js.map +1 -1
- package/dist/features/sorting/sorting.cjs +55 -24
- package/dist/features/sorting/sorting.cjs.map +1 -1
- package/dist/features/sorting/sorting.js +55 -24
- package/dist/features/sorting/sorting.js.map +1 -1
- package/dist/index.d.cts +26 -17
- package/dist/index.d.ts +26 -17
- package/dist/intercept/runtime.cjs +3 -2
- package/dist/intercept/runtime.cjs.map +1 -1
- package/dist/intercept/runtime.js +3 -2
- package/dist/intercept/runtime.js.map +1 -1
- package/dist/messages/index.cjs +2 -0
- package/dist/messages/index.cjs.map +1 -1
- package/dist/messages/index.js +2 -0
- package/dist/messages/index.js.map +1 -1
- package/dist/pagination.d.cts +2 -2
- package/dist/pagination.d.ts +2 -2
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/{search-async-entry-CuLYPfY_.d.cts → search-async-entry-BiemO3JT.d.cts} +2 -2
- package/dist/{search-async-entry-Cx7xpuVZ.d.ts → search-async-entry-GRj0E06n.d.ts} +2 -2
- package/dist/search-async.d.cts +3 -3
- package/dist/search-async.d.ts +3 -3
- package/dist/search.d.cts +4 -4
- package/dist/search.d.ts +4 -4
- package/dist/server.d.cts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sorting.d.cts +2 -2
- package/dist/sorting.d.ts +2 -2
- package/dist/source/async-control.cjs +2 -1
- package/dist/source/async-control.cjs.map +1 -1
- package/dist/source/async-control.js +2 -1
- package/dist/source/async-control.js.map +1 -1
- package/dist/source/fetcher.cjs.map +1 -1
- package/dist/source/fetcher.js.map +1 -1
- package/dist/source/server.cjs +58 -5
- package/dist/source/server.cjs.map +1 -1
- package/dist/source/server.js +58 -5
- package/dist/source/server.js.map +1 -1
- package/dist/table/create-table.cjs +30 -20
- package/dist/table/create-table.cjs.map +1 -1
- package/dist/table/create-table.js +30 -20
- package/dist/table/create-table.js.map +1 -1
- package/dist/table/index.cjs +3 -0
- package/dist/table/index.cjs.map +1 -1
- package/dist/table/index.js +5 -1
- package/dist/table/index.js.map +1 -1
- package/dist/table/state-codes.cjs +72 -0
- package/dist/table/state-codes.cjs.map +1 -0
- package/dist/table/state-codes.js +45 -0
- package/dist/table/state-codes.js.map +1 -0
- package/dist/table/types.cjs.map +1 -1
- package/dist/table/types.js.map +1 -1
- package/dist/validity.d.cts +2 -2
- package/dist/validity.d.ts +2 -2
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/dist/ingest/test-schemas.cjs +0 -73
- package/dist/ingest/test-schemas.cjs.map +0 -1
- package/dist/ingest/test-schemas.js +0 -46
- package/dist/ingest/test-schemas.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys hashed at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level.\n if (r.rowKeySensitive) return hashRowKey(key)\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys logged as `[sensitive]` at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level. No key-derived value at all:\n // an unsalted 32-bit hash of an SSN or email is reversible by brute force.\n if (r.rowKeySensitive) return SENSITIVE\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|
|
@@ -44,7 +44,7 @@ function custom(r, path, value) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function redactKey(r, path, key) {
|
|
47
|
-
if (r.rowKeySensitive) return
|
|
47
|
+
if (r.rowKeySensitive) return SENSITIVE;
|
|
48
48
|
const c = custom(r, path, key);
|
|
49
49
|
if (c !== void 0) return c;
|
|
50
50
|
return r.level === "safe" ? hashRowKey(key) : key;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys hashed at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level.\n if (r.rowKeySensitive) return hashRowKey(key)\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys logged as `[sensitive]` at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level. No key-derived value at all:\n // an unsalted 32-bit hash of an SSN or email is reversible by brute force.\n if (r.rowKeySensitive) return SENSITIVE\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/types.ts"],"sourcesContent":["import type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type DiagnosticLevel = 'error' | 'warn' | 'info'\n\nexport type Trigger = 'init' | 'data-change' | 'setOptions' | 'hydrate' | 'fetch' | `api:${string}`\n\nexport type Phase = 'ssr' | 'hydrate' | 'client'\n\n/**\n * Where in the developer's code or data (§8.1). `interceptor` and `operation` are additions for\n * plugin-level codes the spec table does not locate (interceptors, op registry).\n */\nexport type Location =\n | { kind: 'column'; index: number; id: string }\n | { kind: 'feature'; name: string }\n | { kind: 'option'; path: string }\n | { kind: 'row'; index: number; key?: string }\n | { kind: 'interceptor'; name: string; op: string }\n | { kind: 'operation'; op: string; slot?: string }\n\n/** A summary of table state — never raw input (§8.1). */\nexport interface DiagnosticStateSummary {\n searchLength: number\n activeFilters: string[]\n sort: string[]\n page: number\n}\n\nexport interface DiagnosticContext {\n /** `options.name`, else `table#n`. */\n table: string\n /** Dev: creation site, e.g. `UsersPage.tsx:42`. */\n createdAt?: string\n /** `@datakable/core@x.y.z` */\n package: string\n trigger: Trigger\n /** Pipeline run id — joins explain() and other diagnostics. */\n run: number\n phase: Phase\n mode: Record<string, 'client' | 'server'>\n state: DiagnosticStateSummary\n occurrences: { count: number; first: number; last: number }\n /** e.g. `DK_SCHEMA_INVALID_ROW#run57` */\n causedBy?: string\n
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/types.ts"],"sourcesContent":["import type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type DiagnosticLevel = 'error' | 'warn' | 'info'\n\nexport type Trigger = 'init' | 'data-change' | 'setOptions' | 'hydrate' | 'fetch' | `api:${string}`\n\nexport type Phase = 'ssr' | 'hydrate' | 'client'\n\n/**\n * Where in the developer's code or data (§8.1). `interceptor` and `operation` are additions for\n * plugin-level codes the spec table does not locate (interceptors, op registry).\n */\nexport type Location =\n | { kind: 'column'; index: number; id: string }\n | { kind: 'feature'; name: string }\n | { kind: 'option'; path: string }\n | { kind: 'row'; index: number; key?: string }\n | { kind: 'interceptor'; name: string; op: string }\n | { kind: 'operation'; op: string; slot?: string }\n\n/** A summary of table state — never raw input (§8.1). */\nexport interface DiagnosticStateSummary {\n searchLength: number\n activeFilters: string[]\n sort: string[]\n page: number\n}\n\nexport interface DiagnosticContext {\n /** `options.name`, else `table#n`. */\n table: string\n /** Dev: creation site, e.g. `UsersPage.tsx:42`. */\n createdAt?: string\n /** `@datakable/core@x.y.z` */\n package: string\n trigger: Trigger\n /** Pipeline run id — joins explain() and other diagnostics. */\n run: number\n phase: Phase\n mode: Record<string, 'client' | 'server'>\n state: DiagnosticStateSummary\n occurrences: { count: number; first: number; last: number }\n /** e.g. `DK_SCHEMA_INVALID_ROW#run57` */\n causedBy?: string\n /**\n * Original error (ES2022 cause). Reaches code you install (`onDiagnostic`, `diagnostic:emit`\n * interceptors, the `redact` seam) and, under `strict`, the thrown `DatakableError`'s `cause`;\n * `getDiagnostics()`, `toJSON` and the console leave it out.\n */\n cause?: unknown\n}\n\nexport interface Diagnostic {\n /** Stable public API; never reused or redefined. */\n code: DiagnosticCode\n level: DiagnosticLevel\n /** What happened, instance-specific. Empty in production unless messages are opted in. */\n problem: string\n where: Location\n /** Consequence + the fallback the system applied. */\n impact: string\n /** Concrete fixes, best first. */\n fix: string[]\n /** `docs/errors.md#<code>` (+ query params in production). */\n docs: string\n /** The catalog details. Redaction (T26) rewrites them before any sink sees them. */\n details: Record<string, unknown>\n context: DiagnosticContext\n /** `code@where` — grouping key (dedupe, Sentry). */\n fingerprint: string\n /** Safe serializer: bigint, cycles and Errors never throw; `context.cause` is left out. */\n toJSON(): object\n}\n\n/** Per-call additions to the live context. */\nexport interface EmitOptions {\n cause?: unknown\n causedBy?: string\n run?: number\n trigger?: Trigger\n phase?: Phase\n /**\n * Not printed by the default console sink (another table printed it in this process, #38). Still\n * stored and delivered to `onDiagnostic`.\n */\n quiet?: boolean\n}\n\n/** Emits a catalog diagnostic; `details` is typed by `code` (see `DiagnosticCatalog`). */\nexport type Emit = <C extends DiagnosticCode>(\n code: C,\n details: DiagnosticDetails<C>,\n options?: EmitOptions,\n) => void\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as RowIssue, E as Emit, a as EmitOptions, D as Diagnostic, T as Trigger, P as Phase, I as InvalidRowsMode, F as FlaggedRow, b as Row, c as DiagnosticCode, d as DiagnosticDetails, e as TableSchema, f as RedactLevel, g as RedactFn, h as InvalidRow, i as RowKey } from './codes-
|
|
1
|
+
import { R as RowIssue, E as Emit, a as EmitOptions, D as Diagnostic, T as Trigger, P as Phase, I as InvalidRowsMode, F as FlaggedRow, b as Row, c as DiagnosticCode, d as DiagnosticDetails, e as TableSchema, f as RedactLevel, g as RedactFn, h as InvalidRow, i as RowKey } from './codes-4sYQv_7J.cjs';
|
|
2
2
|
|
|
3
3
|
/** Runtime types an option can accept (`'array'` is checked before `'object'`). */
|
|
4
4
|
type OptionType = 'string' | 'number' | 'boolean' | 'function' | 'object' | 'array';
|
|
@@ -463,7 +463,8 @@ type ColumnValues = Float64Array | readonly unknown[];
|
|
|
463
463
|
|
|
464
464
|
/**
|
|
465
465
|
* What `options.fetcher` receives (§7.1): only the state of the server-mode steps, plus the
|
|
466
|
-
* request's `signal` (aborted when superseded, timed out or the table is destroyed).
|
|
466
|
+
* request's `signal` (aborted when superseded, timed out or the table is destroyed). The state may
|
|
467
|
+
* be restored (URL, storage): treat these params as untrusted input on the backend (#100).
|
|
467
468
|
*/
|
|
468
469
|
interface FetchParams {
|
|
469
470
|
/** The applied search query (`search({ mode: 'server' })`). */
|
|
@@ -492,6 +493,8 @@ interface FetchResult<T> {
|
|
|
492
493
|
* What `table.refetch()` resolves to; it never rejects. `'applied'`: the rows were loaded;
|
|
493
494
|
* `'superseded'`: a newer request (or `destroy()`) replaced it; `'error'`: it failed
|
|
494
495
|
* (`DK_FETCH_FAILED`, `DK_FETCH_BAD_RESPONSE`, `DK_ASYNC_TIMEOUT`) and the previous rows are kept.
|
|
496
|
+
* On a `createDeferred` table it is the outcome of the request that runs after `startTable`
|
|
497
|
+
* (`'superseded'` when the table is destroyed before it starts, #90).
|
|
495
498
|
*/
|
|
496
499
|
interface FetchOutcome {
|
|
497
500
|
status: 'applied' | 'superseded' | 'error';
|
|
@@ -598,7 +601,9 @@ declare function emitTableDiagnostic<C extends DiagnosticCode>(table: object, co
|
|
|
598
601
|
interface ServerSource extends TableExtension {
|
|
599
602
|
/**
|
|
600
603
|
* Starts a fetcher request when the server-mode state differs from the last request's (or
|
|
601
|
-
* `force`: `refetch()`). Resolves with the outcome; never rejects.
|
|
604
|
+
* `force`: `refetch()`). Resolves with the outcome; never rejects. Deferred (`createDeferred`),
|
|
605
|
+
* `force` resolves with the request that runs after `startTable` (`'superseded'` when the table
|
|
606
|
+
* is destroyed first, #90).
|
|
602
607
|
*/
|
|
603
608
|
sync(force?: boolean): Promise<FetchOutcome> | undefined;
|
|
604
609
|
/** `rowCount` of the last applied fetcher result (§6.5). */
|
|
@@ -639,7 +644,7 @@ interface TableDiagnosticsOptions {
|
|
|
639
644
|
/**
|
|
640
645
|
* Redaction (§8.2): a level, or a custom `redact(path, value)` on top of the default level
|
|
641
646
|
* (`'keys'` in development, `'safe'` in production). Columns with `sensitive: true` are never
|
|
642
|
-
* logged; a `rowKey` path into a sensitive column is
|
|
647
|
+
* logged; a `rowKey` path into a sensitive column is logged as `[sensitive]` (no hash) at every level.
|
|
643
648
|
*/
|
|
644
649
|
redact?: RedactLevel | RedactFn;
|
|
645
650
|
}
|
|
@@ -757,7 +762,9 @@ In = T> {
|
|
|
757
762
|
explainRow(key: RowKey): RowExplanation;
|
|
758
763
|
/**
|
|
759
764
|
* Runs `options.fetcher` again with the current params (§7.1), e.g. a Retry button after
|
|
760
|
-
* `DK_FETCH_FAILED`. Never rejects. Without a fetcher: `{ status: 'applied' }` at once.
|
|
765
|
+
* `DK_FETCH_FAILED`. Never rejects. Without a fetcher: `{ status: 'applied' }` at once. On a
|
|
766
|
+
* `createDeferred` table it settles with the request that runs after `startTable`
|
|
767
|
+
* (`'superseded'` when the table is destroyed first, #90).
|
|
761
768
|
*/
|
|
762
769
|
refetch(): Promise<FetchOutcome>;
|
|
763
770
|
/** Releases listeners. Later mutating calls are no-ops that report `DK_TABLE_DESTROYED`. */
|
|
@@ -1010,7 +1017,7 @@ interface RowStep {
|
|
|
1010
1017
|
}
|
|
1011
1018
|
/** What `table.explainRow(key)` returns (§8.4); `String(e)` is the console report. */
|
|
1012
1019
|
interface RowExplanation {
|
|
1013
|
-
/** The key asked for, redacted (a hash under `'safe'`). */
|
|
1020
|
+
/** The key asked for, redacted (a hash under `'safe'`, `[sensitive]` for a sensitive rowKey). */
|
|
1014
1021
|
readonly key: unknown;
|
|
1015
1022
|
readonly found: boolean;
|
|
1016
1023
|
/** Position in `getData()`; absent when not found or dropped by the schema. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as RowIssue, E as Emit, a as EmitOptions, D as Diagnostic, T as Trigger, P as Phase, I as InvalidRowsMode, F as FlaggedRow, b as Row, c as DiagnosticCode, d as DiagnosticDetails, e as TableSchema, f as RedactLevel, g as RedactFn, h as InvalidRow, i as RowKey } from './codes-
|
|
1
|
+
import { R as RowIssue, E as Emit, a as EmitOptions, D as Diagnostic, T as Trigger, P as Phase, I as InvalidRowsMode, F as FlaggedRow, b as Row, c as DiagnosticCode, d as DiagnosticDetails, e as TableSchema, f as RedactLevel, g as RedactFn, h as InvalidRow, i as RowKey } from './codes-4sYQv_7J.js';
|
|
2
2
|
|
|
3
3
|
/** Runtime types an option can accept (`'array'` is checked before `'object'`). */
|
|
4
4
|
type OptionType = 'string' | 'number' | 'boolean' | 'function' | 'object' | 'array';
|
|
@@ -463,7 +463,8 @@ type ColumnValues = Float64Array | readonly unknown[];
|
|
|
463
463
|
|
|
464
464
|
/**
|
|
465
465
|
* What `options.fetcher` receives (§7.1): only the state of the server-mode steps, plus the
|
|
466
|
-
* request's `signal` (aborted when superseded, timed out or the table is destroyed).
|
|
466
|
+
* request's `signal` (aborted when superseded, timed out or the table is destroyed). The state may
|
|
467
|
+
* be restored (URL, storage): treat these params as untrusted input on the backend (#100).
|
|
467
468
|
*/
|
|
468
469
|
interface FetchParams {
|
|
469
470
|
/** The applied search query (`search({ mode: 'server' })`). */
|
|
@@ -492,6 +493,8 @@ interface FetchResult<T> {
|
|
|
492
493
|
* What `table.refetch()` resolves to; it never rejects. `'applied'`: the rows were loaded;
|
|
493
494
|
* `'superseded'`: a newer request (or `destroy()`) replaced it; `'error'`: it failed
|
|
494
495
|
* (`DK_FETCH_FAILED`, `DK_FETCH_BAD_RESPONSE`, `DK_ASYNC_TIMEOUT`) and the previous rows are kept.
|
|
496
|
+
* On a `createDeferred` table it is the outcome of the request that runs after `startTable`
|
|
497
|
+
* (`'superseded'` when the table is destroyed before it starts, #90).
|
|
495
498
|
*/
|
|
496
499
|
interface FetchOutcome {
|
|
497
500
|
status: 'applied' | 'superseded' | 'error';
|
|
@@ -598,7 +601,9 @@ declare function emitTableDiagnostic<C extends DiagnosticCode>(table: object, co
|
|
|
598
601
|
interface ServerSource extends TableExtension {
|
|
599
602
|
/**
|
|
600
603
|
* Starts a fetcher request when the server-mode state differs from the last request's (or
|
|
601
|
-
* `force`: `refetch()`). Resolves with the outcome; never rejects.
|
|
604
|
+
* `force`: `refetch()`). Resolves with the outcome; never rejects. Deferred (`createDeferred`),
|
|
605
|
+
* `force` resolves with the request that runs after `startTable` (`'superseded'` when the table
|
|
606
|
+
* is destroyed first, #90).
|
|
602
607
|
*/
|
|
603
608
|
sync(force?: boolean): Promise<FetchOutcome> | undefined;
|
|
604
609
|
/** `rowCount` of the last applied fetcher result (§6.5). */
|
|
@@ -639,7 +644,7 @@ interface TableDiagnosticsOptions {
|
|
|
639
644
|
/**
|
|
640
645
|
* Redaction (§8.2): a level, or a custom `redact(path, value)` on top of the default level
|
|
641
646
|
* (`'keys'` in development, `'safe'` in production). Columns with `sensitive: true` are never
|
|
642
|
-
* logged; a `rowKey` path into a sensitive column is
|
|
647
|
+
* logged; a `rowKey` path into a sensitive column is logged as `[sensitive]` (no hash) at every level.
|
|
643
648
|
*/
|
|
644
649
|
redact?: RedactLevel | RedactFn;
|
|
645
650
|
}
|
|
@@ -757,7 +762,9 @@ In = T> {
|
|
|
757
762
|
explainRow(key: RowKey): RowExplanation;
|
|
758
763
|
/**
|
|
759
764
|
* Runs `options.fetcher` again with the current params (§7.1), e.g. a Retry button after
|
|
760
|
-
* `DK_FETCH_FAILED`. Never rejects. Without a fetcher: `{ status: 'applied' }` at once.
|
|
765
|
+
* `DK_FETCH_FAILED`. Never rejects. Without a fetcher: `{ status: 'applied' }` at once. On a
|
|
766
|
+
* `createDeferred` table it settles with the request that runs after `startTable`
|
|
767
|
+
* (`'superseded'` when the table is destroyed first, #90).
|
|
761
768
|
*/
|
|
762
769
|
refetch(): Promise<FetchOutcome>;
|
|
763
770
|
/** Releases listeners. Later mutating calls are no-ops that report `DK_TABLE_DESTROYED`. */
|
|
@@ -1010,7 +1017,7 @@ interface RowStep {
|
|
|
1010
1017
|
}
|
|
1011
1018
|
/** What `table.explainRow(key)` returns (§8.4); `String(e)` is the console report. */
|
|
1012
1019
|
interface RowExplanation {
|
|
1013
|
-
/** The key asked for, redacted (a hash under `'safe'`). */
|
|
1020
|
+
/** The key asked for, redacted (a hash under `'safe'`, `[sensitive]` for a sensitive rowKey). */
|
|
1014
1021
|
readonly key: unknown;
|
|
1015
1022
|
readonly found: boolean;
|
|
1016
1023
|
/** Position in `getData()`; absent when not found or dropped by the schema. */
|
package/dist/feature/install.cjs
CHANGED
|
@@ -27,6 +27,7 @@ module.exports = __toCommonJS(install_exports);
|
|
|
27
27
|
var import_build = require("../diagnostics/build.cjs");
|
|
28
28
|
var import_catalog = require("../diagnostics/catalog.cjs");
|
|
29
29
|
var import_process_guard = require("../diagnostics/process-guard.cjs");
|
|
30
|
+
var import_prose = require("../diagnostics/prose.cjs");
|
|
30
31
|
var import_classify = require("../intercept/classify.cjs");
|
|
31
32
|
var import_codes = require("./codes.cjs");
|
|
32
33
|
(0, import_catalog.keepDiagnostics)(import_codes.featureDiagnostics);
|
|
@@ -121,7 +122,7 @@ function missingApi(failed, host) {
|
|
|
121
122
|
feature,
|
|
122
123
|
hook: "createApi",
|
|
123
124
|
method: key,
|
|
124
|
-
fallback: `${failed.length === 1 ? "api" : "apis"}-missing|${key}`
|
|
125
|
+
fallback: `${failed.length === 1 ? "api" : "apis"}-missing|${(0, import_prose.proseArg)(key)}`
|
|
125
126
|
})
|
|
126
127
|
);
|
|
127
128
|
return void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feature/install.ts"],"sourcesContent":["import { throwConfigError } from '../diagnostics/build'\nimport { keepDiagnostics } from '../diagnostics/catalog'\nimport '../diagnostics/process-guard'\nimport type { Emit } from '../diagnostics/types'\nimport { errorName } from '../intercept/classify'\nimport type { Table, TableState } from '../table/types'\nimport { featureDiagnostics } from './codes'\nimport type { AnyFeature, FeatureContext, SliceUpdater } from './types'\n\nkeepDiagnostics(featureDiagnostics)\n\n/** What `createTable` gives the installer: the store operations a feature context needs. */\nexport interface FeatureHost {\n readonly table: Table<unknown, TableState>\n /** The table name, for config errors thrown before the first emit. */\n readonly tableName: string\n readonly emit: Emit\n getSlice(name: string): unknown\n /** `state:dispatch` of one slice (where = the feature name); notifies unless inside an API call. */\n setSlice(name: string, updater: SliceUpdater<unknown>): void\n /**\n * Runs an API method: no-op + `DK_TABLE_DESTROYED` after destroy (returning `destroyed?.()`),\n * trigger `api:<method>`, listeners notified once when the outermost call returns.\n */\n call(method: string, fn: () => unknown, destroyed?: () => unknown): unknown\n}\n\nfunction reportFailure(\n emit: Emit,\n feature: string,\n hook: 'initialState' | 'createApi',\n fallback: string,\n e: unknown,\n) {\n const name = errorName(e)\n emit(\n 'DK_FEATURE_FAILED',\n { feature, hook, fallback, ...(name === undefined ? {} : { errorName: name }) },\n { cause: e },\n )\n}\n\n/**\n * The starting snapshot: each feature's `initialState()` under its name, then `given`\n * (`options.initialState`) per slice on top. `initialState()` is not called for a given slice.\n */\nexport function initialSlices(\n features: readonly AnyFeature[],\n given: object | undefined,\n emit: Emit,\n): TableState {\n const out: TableState = {}\n for (const f of features) {\n if (given && Object.hasOwn(given, f.name)) continue\n try {\n out[f.name] = f.initialState()\n } catch (e) {\n out[f.name] = undefined\n reportFailure(emit, f.name, 'initialState', 'slice-undefined', e)\n }\n }\n return given ? Object.assign(out, given) : out\n}\n\n/**\n * Creates every feature API (in the order the features were listed) and merges it onto the table.\n * Throws `DatakableConfigError` `DK_FEATURE_API_CONFLICT` when a member name is already taken by a\n * core table method or by an earlier feature. Methods are wrapped once here (cold path).\n */\nexport function installApis(features: readonly AnyFeature[], host: FeatureHost): void {\n const target = host.table as unknown as Record<string, unknown>\n const owners = new Map<string, string | null>()\n for (const k of Object.keys(target)) owners.set(k, null)\n /** Features whose `createApi()` threw: their methods are missing from the table. */\n const failed: string[] = []\n\n for (const f of features) {\n const name = f.name\n const ctx: FeatureContext<unknown> = {\n name,\n getState: () => host.getSlice(name),\n setState: (updater) => host.setSlice(name, updater),\n table: host.table,\n emit: host.emit,\n }\n let api: unknown\n try {\n api = f.createApi(ctx)\n } catch (e) {\n reportFailure(host.emit, name, 'createApi', 'api-not-installed', e)\n failed.push(name)\n continue\n }\n if (api === null || typeof api !== 'object') continue\n const members = api as Record<string, unknown>\n const keys = Object.keys(members)\n for (const method of keys) {\n if (owners.has(method)) {\n throwConfigError(\n 'DK_FEATURE_API_CONFLICT',\n { method, feature: name, owner: owners.get(method) ?? null },\n { table: host.tableName },\n )\n }\n }\n for (const method of keys) {\n owners.set(method, name)\n const member = members[method]\n const destroyed = f.destroyedResult?.[method]\n target[method] =\n typeof member === 'function'\n ? (...args: unknown[]) => host.call(method, () => member.apply(members, args), destroyed)\n : member\n }\n }\n if ((process.env.NODE_ENV !== \"production\") && failed.length > 0) Object.setPrototypeOf(target, missingApi(failed, host))\n}\n\n/**\n * Names generic code probes on any object (await, JSON, React, test runners, inspectors): never a\n * feature method, so never stubbed.\n */\nconst PROBES = new Set([\n 'then',\n 'catch',\n 'finally',\n 'toJSON',\n 'asymmetricMatch',\n 'nodeType',\n 'tagName',\n])\nconst isProbe = (k: string) => PROBES.has(k) || /^[_$@]/.test(k)\n\n/**\n * Dev (R4): the prototype of a table where a feature's `createApi()` threw. The types still promise\n * that feature's methods, so a call to a name the table does not have is a no-op that reports\n * `DK_FEATURE_FAILED` (with the method) instead of a raw TypeError at the caller. Only reads of\n * missing names reach it: the table keeps its identity and own members, `in` / `hasOwn` stay\n * false, and `Object.prototype` members, symbols and generic probes (`then`, `toJSON`, …) resolve\n * as usual.\n */\nfunction missingApi(failed: readonly string[], host: FeatureHost): object {\n const feature = failed.join(', ')\n const stubs = new Map<string, () => undefined>()\n return new Proxy(Object.prototype, {\n get(proto, key, receiver) {\n if (typeof key !== 'string' || key in proto || isProbe(key)) {\n return Reflect.get(proto, key, receiver)\n }\n let stub = stubs.get(key)\n if (!stub) {\n stub = () => {\n host.call(key, () =>\n host.emit('DK_FEATURE_FAILED', {\n feature,\n hook: 'createApi',\n method: key,\n fallback: `${failed.length === 1 ? 'api' : 'apis'}-missing|${key}`,\n }),\n )\n return undefined\n }\n stubs.set(key, stub)\n }\n return stub\n },\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiC;AACjC,qBAAgC;AAChC,2BAAO;
|
|
1
|
+
{"version":3,"sources":["../../src/feature/install.ts"],"sourcesContent":["import { throwConfigError } from '../diagnostics/build'\nimport { keepDiagnostics } from '../diagnostics/catalog'\nimport '../diagnostics/process-guard'\nimport { proseArg } from '../diagnostics/prose'\nimport type { Emit } from '../diagnostics/types'\nimport { errorName } from '../intercept/classify'\nimport type { Table, TableState } from '../table/types'\nimport { featureDiagnostics } from './codes'\nimport type { AnyFeature, FeatureContext, SliceUpdater } from './types'\n\nkeepDiagnostics(featureDiagnostics)\n\n/** What `createTable` gives the installer: the store operations a feature context needs. */\nexport interface FeatureHost {\n readonly table: Table<unknown, TableState>\n /** The table name, for config errors thrown before the first emit. */\n readonly tableName: string\n readonly emit: Emit\n getSlice(name: string): unknown\n /** `state:dispatch` of one slice (where = the feature name); notifies unless inside an API call. */\n setSlice(name: string, updater: SliceUpdater<unknown>): void\n /**\n * Runs an API method: no-op + `DK_TABLE_DESTROYED` after destroy (returning `destroyed?.()`),\n * trigger `api:<method>`, listeners notified once when the outermost call returns.\n */\n call(method: string, fn: () => unknown, destroyed?: () => unknown): unknown\n}\n\nfunction reportFailure(\n emit: Emit,\n feature: string,\n hook: 'initialState' | 'createApi',\n fallback: string,\n e: unknown,\n) {\n const name = errorName(e)\n emit(\n 'DK_FEATURE_FAILED',\n { feature, hook, fallback, ...(name === undefined ? {} : { errorName: name }) },\n { cause: e },\n )\n}\n\n/**\n * The starting snapshot: each feature's `initialState()` under its name, then `given`\n * (`options.initialState`) per slice on top. `initialState()` is not called for a given slice.\n */\nexport function initialSlices(\n features: readonly AnyFeature[],\n given: object | undefined,\n emit: Emit,\n): TableState {\n const out: TableState = {}\n for (const f of features) {\n if (given && Object.hasOwn(given, f.name)) continue\n try {\n out[f.name] = f.initialState()\n } catch (e) {\n out[f.name] = undefined\n reportFailure(emit, f.name, 'initialState', 'slice-undefined', e)\n }\n }\n return given ? Object.assign(out, given) : out\n}\n\n/**\n * Creates every feature API (in the order the features were listed) and merges it onto the table.\n * Throws `DatakableConfigError` `DK_FEATURE_API_CONFLICT` when a member name is already taken by a\n * core table method or by an earlier feature. Methods are wrapped once here (cold path).\n */\nexport function installApis(features: readonly AnyFeature[], host: FeatureHost): void {\n const target = host.table as unknown as Record<string, unknown>\n const owners = new Map<string, string | null>()\n for (const k of Object.keys(target)) owners.set(k, null)\n /** Features whose `createApi()` threw: their methods are missing from the table. */\n const failed: string[] = []\n\n for (const f of features) {\n const name = f.name\n const ctx: FeatureContext<unknown> = {\n name,\n getState: () => host.getSlice(name),\n setState: (updater) => host.setSlice(name, updater),\n table: host.table,\n emit: host.emit,\n }\n let api: unknown\n try {\n api = f.createApi(ctx)\n } catch (e) {\n reportFailure(host.emit, name, 'createApi', 'api-not-installed', e)\n failed.push(name)\n continue\n }\n if (api === null || typeof api !== 'object') continue\n const members = api as Record<string, unknown>\n const keys = Object.keys(members)\n for (const method of keys) {\n if (owners.has(method)) {\n throwConfigError(\n 'DK_FEATURE_API_CONFLICT',\n { method, feature: name, owner: owners.get(method) ?? null },\n { table: host.tableName },\n )\n }\n }\n for (const method of keys) {\n owners.set(method, name)\n const member = members[method]\n const destroyed = f.destroyedResult?.[method]\n target[method] =\n typeof member === 'function'\n ? (...args: unknown[]) => host.call(method, () => member.apply(members, args), destroyed)\n : member\n }\n }\n if ((process.env.NODE_ENV !== \"production\") && failed.length > 0) Object.setPrototypeOf(target, missingApi(failed, host))\n}\n\n/**\n * Names generic code probes on any object (await, JSON, React, test runners, inspectors): never a\n * feature method, so never stubbed.\n */\nconst PROBES = new Set([\n 'then',\n 'catch',\n 'finally',\n 'toJSON',\n 'asymmetricMatch',\n 'nodeType',\n 'tagName',\n])\nconst isProbe = (k: string) => PROBES.has(k) || /^[_$@]/.test(k)\n\n/**\n * Dev (R4): the prototype of a table where a feature's `createApi()` threw. The types still promise\n * that feature's methods, so a call to a name the table does not have is a no-op that reports\n * `DK_FEATURE_FAILED` (with the method) instead of a raw TypeError at the caller. Only reads of\n * missing names reach it: the table keeps its identity and own members, `in` / `hasOwn` stay\n * false, and `Object.prototype` members, symbols and generic probes (`then`, `toJSON`, …) resolve\n * as usual.\n */\nfunction missingApi(failed: readonly string[], host: FeatureHost): object {\n const feature = failed.join(', ')\n const stubs = new Map<string, () => undefined>()\n return new Proxy(Object.prototype, {\n get(proto, key, receiver) {\n if (typeof key !== 'string' || key in proto || isProbe(key)) {\n return Reflect.get(proto, key, receiver)\n }\n let stub = stubs.get(key)\n if (!stub) {\n stub = () => {\n host.call(key, () =>\n host.emit('DK_FEATURE_FAILED', {\n feature,\n hook: 'createApi',\n method: key,\n fallback: `${failed.length === 1 ? 'api' : 'apis'}-missing|${proseArg(key)}`,\n }),\n )\n return undefined\n }\n stubs.set(key, stub)\n }\n return stub\n },\n })\n}\n"],"mappings":"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","names":[]}
|
package/dist/feature/install.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { throwConfigError } from "../diagnostics/build.js";
|
|
3
3
|
import { keepDiagnostics } from "../diagnostics/catalog.js";
|
|
4
4
|
import "../diagnostics/process-guard.js";
|
|
5
|
+
import { proseArg } from "../diagnostics/prose.js";
|
|
5
6
|
import { errorName } from "../intercept/classify.js";
|
|
6
7
|
import { featureDiagnostics } from "./codes.js";
|
|
7
8
|
keepDiagnostics(featureDiagnostics);
|
|
@@ -96,7 +97,7 @@ function missingApi(failed, host) {
|
|
|
96
97
|
feature,
|
|
97
98
|
hook: "createApi",
|
|
98
99
|
method: key,
|
|
99
|
-
fallback: `${failed.length === 1 ? "api" : "apis"}-missing|${key}`
|
|
100
|
+
fallback: `${failed.length === 1 ? "api" : "apis"}-missing|${proseArg(key)}`
|
|
100
101
|
})
|
|
101
102
|
);
|
|
102
103
|
return void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feature/install.ts"],"sourcesContent":["import { throwConfigError } from '../diagnostics/build'\nimport { keepDiagnostics } from '../diagnostics/catalog'\nimport '../diagnostics/process-guard'\nimport type { Emit } from '../diagnostics/types'\nimport { errorName } from '../intercept/classify'\nimport type { Table, TableState } from '../table/types'\nimport { featureDiagnostics } from './codes'\nimport type { AnyFeature, FeatureContext, SliceUpdater } from './types'\n\nkeepDiagnostics(featureDiagnostics)\n\n/** What `createTable` gives the installer: the store operations a feature context needs. */\nexport interface FeatureHost {\n readonly table: Table<unknown, TableState>\n /** The table name, for config errors thrown before the first emit. */\n readonly tableName: string\n readonly emit: Emit\n getSlice(name: string): unknown\n /** `state:dispatch` of one slice (where = the feature name); notifies unless inside an API call. */\n setSlice(name: string, updater: SliceUpdater<unknown>): void\n /**\n * Runs an API method: no-op + `DK_TABLE_DESTROYED` after destroy (returning `destroyed?.()`),\n * trigger `api:<method>`, listeners notified once when the outermost call returns.\n */\n call(method: string, fn: () => unknown, destroyed?: () => unknown): unknown\n}\n\nfunction reportFailure(\n emit: Emit,\n feature: string,\n hook: 'initialState' | 'createApi',\n fallback: string,\n e: unknown,\n) {\n const name = errorName(e)\n emit(\n 'DK_FEATURE_FAILED',\n { feature, hook, fallback, ...(name === undefined ? {} : { errorName: name }) },\n { cause: e },\n )\n}\n\n/**\n * The starting snapshot: each feature's `initialState()` under its name, then `given`\n * (`options.initialState`) per slice on top. `initialState()` is not called for a given slice.\n */\nexport function initialSlices(\n features: readonly AnyFeature[],\n given: object | undefined,\n emit: Emit,\n): TableState {\n const out: TableState = {}\n for (const f of features) {\n if (given && Object.hasOwn(given, f.name)) continue\n try {\n out[f.name] = f.initialState()\n } catch (e) {\n out[f.name] = undefined\n reportFailure(emit, f.name, 'initialState', 'slice-undefined', e)\n }\n }\n return given ? Object.assign(out, given) : out\n}\n\n/**\n * Creates every feature API (in the order the features were listed) and merges it onto the table.\n * Throws `DatakableConfigError` `DK_FEATURE_API_CONFLICT` when a member name is already taken by a\n * core table method or by an earlier feature. Methods are wrapped once here (cold path).\n */\nexport function installApis(features: readonly AnyFeature[], host: FeatureHost): void {\n const target = host.table as unknown as Record<string, unknown>\n const owners = new Map<string, string | null>()\n for (const k of Object.keys(target)) owners.set(k, null)\n /** Features whose `createApi()` threw: their methods are missing from the table. */\n const failed: string[] = []\n\n for (const f of features) {\n const name = f.name\n const ctx: FeatureContext<unknown> = {\n name,\n getState: () => host.getSlice(name),\n setState: (updater) => host.setSlice(name, updater),\n table: host.table,\n emit: host.emit,\n }\n let api: unknown\n try {\n api = f.createApi(ctx)\n } catch (e) {\n reportFailure(host.emit, name, 'createApi', 'api-not-installed', e)\n failed.push(name)\n continue\n }\n if (api === null || typeof api !== 'object') continue\n const members = api as Record<string, unknown>\n const keys = Object.keys(members)\n for (const method of keys) {\n if (owners.has(method)) {\n throwConfigError(\n 'DK_FEATURE_API_CONFLICT',\n { method, feature: name, owner: owners.get(method) ?? null },\n { table: host.tableName },\n )\n }\n }\n for (const method of keys) {\n owners.set(method, name)\n const member = members[method]\n const destroyed = f.destroyedResult?.[method]\n target[method] =\n typeof member === 'function'\n ? (...args: unknown[]) => host.call(method, () => member.apply(members, args), destroyed)\n : member\n }\n }\n if ((process.env.NODE_ENV !== \"production\") && failed.length > 0) Object.setPrototypeOf(target, missingApi(failed, host))\n}\n\n/**\n * Names generic code probes on any object (await, JSON, React, test runners, inspectors): never a\n * feature method, so never stubbed.\n */\nconst PROBES = new Set([\n 'then',\n 'catch',\n 'finally',\n 'toJSON',\n 'asymmetricMatch',\n 'nodeType',\n 'tagName',\n])\nconst isProbe = (k: string) => PROBES.has(k) || /^[_$@]/.test(k)\n\n/**\n * Dev (R4): the prototype of a table where a feature's `createApi()` threw. The types still promise\n * that feature's methods, so a call to a name the table does not have is a no-op that reports\n * `DK_FEATURE_FAILED` (with the method) instead of a raw TypeError at the caller. Only reads of\n * missing names reach it: the table keeps its identity and own members, `in` / `hasOwn` stay\n * false, and `Object.prototype` members, symbols and generic probes (`then`, `toJSON`, …) resolve\n * as usual.\n */\nfunction missingApi(failed: readonly string[], host: FeatureHost): object {\n const feature = failed.join(', ')\n const stubs = new Map<string, () => undefined>()\n return new Proxy(Object.prototype, {\n get(proto, key, receiver) {\n if (typeof key !== 'string' || key in proto || isProbe(key)) {\n return Reflect.get(proto, key, receiver)\n }\n let stub = stubs.get(key)\n if (!stub) {\n stub = () => {\n host.call(key, () =>\n host.emit('DK_FEATURE_FAILED', {\n feature,\n hook: 'createApi',\n method: key,\n fallback: `${failed.length === 1 ? 'api' : 'apis'}-missing|${key}`,\n }),\n )\n return undefined\n }\n stubs.set(key, stub)\n }\n return stub\n },\n })\n}\n"],"mappings":";AAAA,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC,OAAO;
|
|
1
|
+
{"version":3,"sources":["../../src/feature/install.ts"],"sourcesContent":["import { throwConfigError } from '../diagnostics/build'\nimport { keepDiagnostics } from '../diagnostics/catalog'\nimport '../diagnostics/process-guard'\nimport { proseArg } from '../diagnostics/prose'\nimport type { Emit } from '../diagnostics/types'\nimport { errorName } from '../intercept/classify'\nimport type { Table, TableState } from '../table/types'\nimport { featureDiagnostics } from './codes'\nimport type { AnyFeature, FeatureContext, SliceUpdater } from './types'\n\nkeepDiagnostics(featureDiagnostics)\n\n/** What `createTable` gives the installer: the store operations a feature context needs. */\nexport interface FeatureHost {\n readonly table: Table<unknown, TableState>\n /** The table name, for config errors thrown before the first emit. */\n readonly tableName: string\n readonly emit: Emit\n getSlice(name: string): unknown\n /** `state:dispatch` of one slice (where = the feature name); notifies unless inside an API call. */\n setSlice(name: string, updater: SliceUpdater<unknown>): void\n /**\n * Runs an API method: no-op + `DK_TABLE_DESTROYED` after destroy (returning `destroyed?.()`),\n * trigger `api:<method>`, listeners notified once when the outermost call returns.\n */\n call(method: string, fn: () => unknown, destroyed?: () => unknown): unknown\n}\n\nfunction reportFailure(\n emit: Emit,\n feature: string,\n hook: 'initialState' | 'createApi',\n fallback: string,\n e: unknown,\n) {\n const name = errorName(e)\n emit(\n 'DK_FEATURE_FAILED',\n { feature, hook, fallback, ...(name === undefined ? {} : { errorName: name }) },\n { cause: e },\n )\n}\n\n/**\n * The starting snapshot: each feature's `initialState()` under its name, then `given`\n * (`options.initialState`) per slice on top. `initialState()` is not called for a given slice.\n */\nexport function initialSlices(\n features: readonly AnyFeature[],\n given: object | undefined,\n emit: Emit,\n): TableState {\n const out: TableState = {}\n for (const f of features) {\n if (given && Object.hasOwn(given, f.name)) continue\n try {\n out[f.name] = f.initialState()\n } catch (e) {\n out[f.name] = undefined\n reportFailure(emit, f.name, 'initialState', 'slice-undefined', e)\n }\n }\n return given ? Object.assign(out, given) : out\n}\n\n/**\n * Creates every feature API (in the order the features were listed) and merges it onto the table.\n * Throws `DatakableConfigError` `DK_FEATURE_API_CONFLICT` when a member name is already taken by a\n * core table method or by an earlier feature. Methods are wrapped once here (cold path).\n */\nexport function installApis(features: readonly AnyFeature[], host: FeatureHost): void {\n const target = host.table as unknown as Record<string, unknown>\n const owners = new Map<string, string | null>()\n for (const k of Object.keys(target)) owners.set(k, null)\n /** Features whose `createApi()` threw: their methods are missing from the table. */\n const failed: string[] = []\n\n for (const f of features) {\n const name = f.name\n const ctx: FeatureContext<unknown> = {\n name,\n getState: () => host.getSlice(name),\n setState: (updater) => host.setSlice(name, updater),\n table: host.table,\n emit: host.emit,\n }\n let api: unknown\n try {\n api = f.createApi(ctx)\n } catch (e) {\n reportFailure(host.emit, name, 'createApi', 'api-not-installed', e)\n failed.push(name)\n continue\n }\n if (api === null || typeof api !== 'object') continue\n const members = api as Record<string, unknown>\n const keys = Object.keys(members)\n for (const method of keys) {\n if (owners.has(method)) {\n throwConfigError(\n 'DK_FEATURE_API_CONFLICT',\n { method, feature: name, owner: owners.get(method) ?? null },\n { table: host.tableName },\n )\n }\n }\n for (const method of keys) {\n owners.set(method, name)\n const member = members[method]\n const destroyed = f.destroyedResult?.[method]\n target[method] =\n typeof member === 'function'\n ? (...args: unknown[]) => host.call(method, () => member.apply(members, args), destroyed)\n : member\n }\n }\n if ((process.env.NODE_ENV !== \"production\") && failed.length > 0) Object.setPrototypeOf(target, missingApi(failed, host))\n}\n\n/**\n * Names generic code probes on any object (await, JSON, React, test runners, inspectors): never a\n * feature method, so never stubbed.\n */\nconst PROBES = new Set([\n 'then',\n 'catch',\n 'finally',\n 'toJSON',\n 'asymmetricMatch',\n 'nodeType',\n 'tagName',\n])\nconst isProbe = (k: string) => PROBES.has(k) || /^[_$@]/.test(k)\n\n/**\n * Dev (R4): the prototype of a table where a feature's `createApi()` threw. The types still promise\n * that feature's methods, so a call to a name the table does not have is a no-op that reports\n * `DK_FEATURE_FAILED` (with the method) instead of a raw TypeError at the caller. Only reads of\n * missing names reach it: the table keeps its identity and own members, `in` / `hasOwn` stay\n * false, and `Object.prototype` members, symbols and generic probes (`then`, `toJSON`, …) resolve\n * as usual.\n */\nfunction missingApi(failed: readonly string[], host: FeatureHost): object {\n const feature = failed.join(', ')\n const stubs = new Map<string, () => undefined>()\n return new Proxy(Object.prototype, {\n get(proto, key, receiver) {\n if (typeof key !== 'string' || key in proto || isProbe(key)) {\n return Reflect.get(proto, key, receiver)\n }\n let stub = stubs.get(key)\n if (!stub) {\n stub = () => {\n host.call(key, () =>\n host.emit('DK_FEATURE_FAILED', {\n feature,\n hook: 'createApi',\n method: key,\n fallback: `${failed.length === 1 ? 'api' : 'apis'}-missing|${proseArg(key)}`,\n }),\n )\n return undefined\n }\n stubs.set(key, stub)\n }\n return stub\n },\n })\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/feature/slice-guard.ts
|
|
21
|
+
var slice_guard_exports = {};
|
|
22
|
+
__export(slice_guard_exports, {
|
|
23
|
+
sliceGuard: () => sliceGuard,
|
|
24
|
+
sliceGuardDiagnostics: () => sliceGuardDiagnostics
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(slice_guard_exports);
|
|
27
|
+
var import_catalog = require("../diagnostics/catalog.cjs");
|
|
28
|
+
var import_process_guard = require("../diagnostics/process-guard.cjs");
|
|
29
|
+
var import_define = require("../intercept/define.cjs");
|
|
30
|
+
var import_state_codes = require("../table/state-codes.cjs");
|
|
31
|
+
var sliceGuardDiagnostics = [
|
|
32
|
+
(0, import_catalog.defineDiagnostic)(
|
|
33
|
+
{
|
|
34
|
+
code: "DK_INVALID_STATE",
|
|
35
|
+
level: "warn",
|
|
36
|
+
where: (d) => ({ kind: "option", path: `state.${d.slice}` })
|
|
37
|
+
},
|
|
38
|
+
process.env.NODE_ENV !== "production" && import_state_codes.stateMessages.DK_INVALID_STATE
|
|
39
|
+
)
|
|
40
|
+
];
|
|
41
|
+
(0, import_catalog.keepDiagnostics)(sliceGuardDiagnostics);
|
|
42
|
+
function sliceGuard(name, fix) {
|
|
43
|
+
const hosts = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
const check = (ctx, s) => {
|
|
45
|
+
const out = fix(s);
|
|
46
|
+
if (!out) return;
|
|
47
|
+
ctx.emit("DK_INVALID_STATE", { slice: name, received: out[1], fallback: out[2] });
|
|
48
|
+
ctx.setState(out[0]);
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
interceptor: (0, import_define.defineInterceptor)({
|
|
52
|
+
name: `${name}.state`,
|
|
53
|
+
on: ["state:dispatch", "table:setOptions"],
|
|
54
|
+
intercept(ictx, next) {
|
|
55
|
+
const ctx = hosts.get(ictx.table);
|
|
56
|
+
if (!ctx) return next();
|
|
57
|
+
const before = ictx.table.getState()[name];
|
|
58
|
+
const out = next();
|
|
59
|
+
const now = ctx.getState();
|
|
60
|
+
if (!Object.is(before, now)) check(ctx, now);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
/** Registers the table (call from `createApi`) and checks the slice it starts with. */
|
|
65
|
+
install(ctx) {
|
|
66
|
+
hosts.set(ctx.table, ctx);
|
|
67
|
+
check(ctx, ctx.getState());
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
72
|
+
0 && (module.exports = {
|
|
73
|
+
sliceGuard,
|
|
74
|
+
sliceGuardDiagnostics
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=slice-guard.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/feature/slice-guard.ts"],"sourcesContent":["import { defineDiagnostic, keepDiagnostics } from '../diagnostics/catalog'\nimport '../diagnostics/process-guard'\nimport { defineInterceptor } from '../intercept/define'\nimport { stateMessages } from '../table/state-codes'\nimport type { Table, TableState } from '../table/types'\nimport type { FeatureContext } from './types'\n\n/** Defined here: only the development guard reports it, so production bundles drop it. */\nexport const sliceGuardDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_INVALID_STATE',\n level: 'warn',\n where: (d) => ({ kind: 'option', path: `state.${d.slice}` }),\n },\n (process.env.NODE_ENV !== \"production\") && stateMessages.DK_INVALID_STATE,\n ),\n]\n\nkeepDiagnostics(sliceGuardDiagnostics)\n\n/** A replacement for a malformed slice: the valid slice, what was wrong, what was stored. */\nexport type SliceFix<S> = readonly [slice: S, received: string, fallback: string]\n\n/**\n * Replaces a malformed slice written to the store (`initialState`, `setState`, the controlled\n * `options.state`), like pagination's sanitize, with `DK_INVALID_STATE` (#89): restored state (URL,\n * storage) is untrusted. Development only: production reads a malformed slice as empty in the\n * feature's API and row model instead. `fix(s)` returns `undefined` for a valid slice. One guard per\n * feature instance; the instance may serve several tables.\n */\nexport function sliceGuard<S>(name: string, fix: (s: unknown) => SliceFix<S> | undefined) {\n const hosts = new WeakMap<object, FeatureContext<S>>()\n const check = (ctx: FeatureContext<S>, s: unknown) => {\n const out = fix(s)\n if (!out) return\n ctx.emit('DK_INVALID_STATE', { slice: name, received: out[1], fallback: out[2] })\n ctx.setState(out[0])\n }\n return {\n interceptor: defineInterceptor({\n name: `${name}.state`,\n on: ['state:dispatch', 'table:setOptions'],\n intercept(ictx, next) {\n const ctx = hosts.get(ictx.table as object)\n if (!ctx) return next()\n const before = (ictx.table as Table<unknown, TableState>).getState()[name]\n const out = next()\n const now = ctx.getState()\n if (!Object.is(before, now)) check(ctx, now)\n return out\n },\n }),\n /** Registers the table (call from `createApi`) and checks the slice it starts with. */\n install(ctx: FeatureContext<S>) {\n hosts.set(ctx.table, ctx)\n check(ctx, ctx.getState())\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAkD;AAClD,2BAAO;AACP,oBAAkC;AAClC,yBAA8B;AAKvB,IAAM,wBAAwB;AAAA,MACnC;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,CAAC,OAAO,EAAE,MAAM,UAAU,MAAM,SAAS,EAAE,KAAK,GAAG;AAAA,IAC5D;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,iCAAc;AAAA,EAC3D;AACF;AAAA,IAEA,gCAAgB,qBAAqB;AAY9B,SAAS,WAAc,MAAc,KAA8C;AACxF,QAAM,QAAQ,oBAAI,QAAmC;AACrD,QAAM,QAAQ,CAAC,KAAwB,MAAe;AACpD,UAAM,MAAM,IAAI,CAAC;AACjB,QAAI,CAAC,IAAK;AACV,QAAI,KAAK,oBAAoB,EAAE,OAAO,MAAM,UAAU,IAAI,CAAC,GAAG,UAAU,IAAI,CAAC,EAAE,CAAC;AAChF,QAAI,SAAS,IAAI,CAAC,CAAC;AAAA,EACrB;AACA,SAAO;AAAA,IACL,iBAAa,iCAAkB;AAAA,MAC7B,MAAM,GAAG,IAAI;AAAA,MACb,IAAI,CAAC,kBAAkB,kBAAkB;AAAA,MACzC,UAAU,MAAM,MAAM;AACpB,cAAM,MAAM,MAAM,IAAI,KAAK,KAAe;AAC1C,YAAI,CAAC,IAAK,QAAO,KAAK;AACtB,cAAM,SAAU,KAAK,MAAqC,SAAS,EAAE,IAAI;AACzE,cAAM,MAAM,KAAK;AACjB,cAAM,MAAM,IAAI,SAAS;AACzB,YAAI,CAAC,OAAO,GAAG,QAAQ,GAAG,EAAG,OAAM,KAAK,GAAG;AAC3C,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA;AAAA,IAED,QAAQ,KAAwB;AAC9B,YAAM,IAAI,IAAI,OAAO,GAAG;AACxB,YAAM,KAAK,IAAI,SAAS,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/feature/slice-guard.ts
|
|
2
|
+
import { defineDiagnostic, keepDiagnostics } from "../diagnostics/catalog.js";
|
|
3
|
+
import "../diagnostics/process-guard.js";
|
|
4
|
+
import { defineInterceptor } from "../intercept/define.js";
|
|
5
|
+
import { stateMessages } from "../table/state-codes.js";
|
|
6
|
+
var sliceGuardDiagnostics = [
|
|
7
|
+
defineDiagnostic(
|
|
8
|
+
{
|
|
9
|
+
code: "DK_INVALID_STATE",
|
|
10
|
+
level: "warn",
|
|
11
|
+
where: (d) => ({ kind: "option", path: `state.${d.slice}` })
|
|
12
|
+
},
|
|
13
|
+
process.env.NODE_ENV !== "production" && stateMessages.DK_INVALID_STATE
|
|
14
|
+
)
|
|
15
|
+
];
|
|
16
|
+
keepDiagnostics(sliceGuardDiagnostics);
|
|
17
|
+
function sliceGuard(name, fix) {
|
|
18
|
+
const hosts = /* @__PURE__ */ new WeakMap();
|
|
19
|
+
const check = (ctx, s) => {
|
|
20
|
+
const out = fix(s);
|
|
21
|
+
if (!out) return;
|
|
22
|
+
ctx.emit("DK_INVALID_STATE", { slice: name, received: out[1], fallback: out[2] });
|
|
23
|
+
ctx.setState(out[0]);
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
interceptor: defineInterceptor({
|
|
27
|
+
name: `${name}.state`,
|
|
28
|
+
on: ["state:dispatch", "table:setOptions"],
|
|
29
|
+
intercept(ictx, next) {
|
|
30
|
+
const ctx = hosts.get(ictx.table);
|
|
31
|
+
if (!ctx) return next();
|
|
32
|
+
const before = ictx.table.getState()[name];
|
|
33
|
+
const out = next();
|
|
34
|
+
const now = ctx.getState();
|
|
35
|
+
if (!Object.is(before, now)) check(ctx, now);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
/** Registers the table (call from `createApi`) and checks the slice it starts with. */
|
|
40
|
+
install(ctx) {
|
|
41
|
+
hosts.set(ctx.table, ctx);
|
|
42
|
+
check(ctx, ctx.getState());
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
sliceGuard,
|
|
48
|
+
sliceGuardDiagnostics
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=slice-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/feature/slice-guard.ts"],"sourcesContent":["import { defineDiagnostic, keepDiagnostics } from '../diagnostics/catalog'\nimport '../diagnostics/process-guard'\nimport { defineInterceptor } from '../intercept/define'\nimport { stateMessages } from '../table/state-codes'\nimport type { Table, TableState } from '../table/types'\nimport type { FeatureContext } from './types'\n\n/** Defined here: only the development guard reports it, so production bundles drop it. */\nexport const sliceGuardDiagnostics = [\n defineDiagnostic(\n {\n code: 'DK_INVALID_STATE',\n level: 'warn',\n where: (d) => ({ kind: 'option', path: `state.${d.slice}` }),\n },\n (process.env.NODE_ENV !== \"production\") && stateMessages.DK_INVALID_STATE,\n ),\n]\n\nkeepDiagnostics(sliceGuardDiagnostics)\n\n/** A replacement for a malformed slice: the valid slice, what was wrong, what was stored. */\nexport type SliceFix<S> = readonly [slice: S, received: string, fallback: string]\n\n/**\n * Replaces a malformed slice written to the store (`initialState`, `setState`, the controlled\n * `options.state`), like pagination's sanitize, with `DK_INVALID_STATE` (#89): restored state (URL,\n * storage) is untrusted. Development only: production reads a malformed slice as empty in the\n * feature's API and row model instead. `fix(s)` returns `undefined` for a valid slice. One guard per\n * feature instance; the instance may serve several tables.\n */\nexport function sliceGuard<S>(name: string, fix: (s: unknown) => SliceFix<S> | undefined) {\n const hosts = new WeakMap<object, FeatureContext<S>>()\n const check = (ctx: FeatureContext<S>, s: unknown) => {\n const out = fix(s)\n if (!out) return\n ctx.emit('DK_INVALID_STATE', { slice: name, received: out[1], fallback: out[2] })\n ctx.setState(out[0])\n }\n return {\n interceptor: defineInterceptor({\n name: `${name}.state`,\n on: ['state:dispatch', 'table:setOptions'],\n intercept(ictx, next) {\n const ctx = hosts.get(ictx.table as object)\n if (!ctx) return next()\n const before = (ictx.table as Table<unknown, TableState>).getState()[name]\n const out = next()\n const now = ctx.getState()\n if (!Object.is(before, now)) check(ctx, now)\n return out\n },\n }),\n /** Registers the table (call from `createApi`) and checks the slice it starts with. */\n install(ctx: FeatureContext<S>) {\n hosts.set(ctx.table, ctx)\n check(ctx, ctx.getState())\n },\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB,uBAAuB;AAClD,OAAO;AACP,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAKvB,IAAM,wBAAwB;AAAA,EACnC;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,OAAO,CAAC,OAAO,EAAE,MAAM,UAAU,MAAM,SAAS,EAAE,KAAK,GAAG;AAAA,IAC5D;AAAA,IACC,QAAQ,IAAI,aAAa,gBAAiB,cAAc;AAAA,EAC3D;AACF;AAEA,gBAAgB,qBAAqB;AAY9B,SAAS,WAAc,MAAc,KAA8C;AACxF,QAAM,QAAQ,oBAAI,QAAmC;AACrD,QAAM,QAAQ,CAAC,KAAwB,MAAe;AACpD,UAAM,MAAM,IAAI,CAAC;AACjB,QAAI,CAAC,IAAK;AACV,QAAI,KAAK,oBAAoB,EAAE,OAAO,MAAM,UAAU,IAAI,CAAC,GAAG,UAAU,IAAI,CAAC,EAAE,CAAC;AAChF,QAAI,SAAS,IAAI,CAAC,CAAC;AAAA,EACrB;AACA,SAAO;AAAA,IACL,aAAa,kBAAkB;AAAA,MAC7B,MAAM,GAAG,IAAI;AAAA,MACb,IAAI,CAAC,kBAAkB,kBAAkB;AAAA,MACzC,UAAU,MAAM,MAAM;AACpB,cAAM,MAAM,MAAM,IAAI,KAAK,KAAe;AAC1C,YAAI,CAAC,IAAK,QAAO,KAAK;AACtB,cAAM,SAAU,KAAK,MAAqC,SAAS,EAAE,IAAI;AACzE,cAAM,MAAM,KAAK;AACjB,cAAM,MAAM,IAAI,SAAS;AACzB,YAAI,CAAC,OAAO,GAAG,QAAQ,GAAG,EAAG,OAAM,KAAK,GAAG;AAC3C,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA;AAAA,IAED,QAAQ,KAAwB;AAC9B,YAAM,IAAI,IAAI,OAAO,GAAG;AACxB,YAAM,KAAK,IAAI,SAAS,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;","names":[]}
|