@refraction-ui/astro 0.5.1 → 0.7.0

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.
@@ -0,0 +1,240 @@
1
+ /**
2
+ * library-origin-error — builds the **library-origin error envelope** for
3
+ * errors that originate inside a `@refraction-ui/*` package.
4
+ *
5
+ * The envelope is expressed THROUGH the existing telemetry record contract
6
+ * (`@refraction-ui/logger`'s `LogRecord`) — the contract is reused, NOT
7
+ * redefined. To keep `@refraction-ui/shared` zero-dependency, the record is
8
+ * mirrored structurally (see {@link DevFeedbackRecord}) rather than imported;
9
+ * a consumer wiring the real logger sink satisfies it structurally.
10
+ *
11
+ * Aggressive redaction (epic #247 guardrails): the payload is package,
12
+ * componentName, version, a normalized stack **fingerprint hash**, and
13
+ * framework only. Never app state, props values, user data, PII, or full app
14
+ * stack frames.
15
+ */
16
+
17
+ import type { DevFeedbackRecord, DevFeedbackSink } from './dev-feedback.js'
18
+
19
+ /** Matches any stack frame that references a `@refraction-ui/*` package. */
20
+ const REFRACTION_FRAME = /@refraction-ui\//
21
+
22
+ /** Frameworks a refraction-ui adapter can run under. */
23
+ export type LibraryFramework =
24
+ | 'react'
25
+ | 'angular'
26
+ | 'astro'
27
+ | 'vue'
28
+ | 'svelte'
29
+ | 'vanilla'
30
+ | 'unknown'
31
+
32
+ /** Inputs describing where a library-origin error came from. */
33
+ export interface LibraryOriginErrorInput {
34
+ /** Originating package, e.g. `@refraction-ui/react`. */
35
+ package: string
36
+ /** refraction-ui component the error originated in, e.g. `Dialog`. */
37
+ componentName: string
38
+ /** Originating package version, e.g. `0.1.5`. */
39
+ version: string
40
+ /** Host framework the component was running under. */
41
+ framework: LibraryFramework
42
+ /** The thrown error (or its stack string). Used ONLY to derive a hash. */
43
+ error?: unknown
44
+ }
45
+
46
+ /**
47
+ * The redacted, library-origin payload carried in `LogRecord.context`. Contains
48
+ * NO app data — only the five identifying fields plus a stack fingerprint hash.
49
+ */
50
+ export interface LibraryOriginEnvelope {
51
+ /** Marks this record as a library-origin error (for downstream routing). */
52
+ origin: 'refraction-ui'
53
+ package: string
54
+ componentName: string
55
+ version: string
56
+ framework: LibraryFramework
57
+ /** Stable, app-data-free hash of the normalized stack. */
58
+ fingerprint: string
59
+ }
60
+
61
+ /**
62
+ * Normalize a stack trace into a deterministic, app-data-free string before
63
+ * hashing:
64
+ * - keep only frames that reference a `@refraction-ui/*` package (drop the
65
+ * app's own frames — library-origin only),
66
+ * - strip absolute paths, line/column numbers, query strings, and hashes,
67
+ * - sort-free (preserve call order) but whitespace-collapsed.
68
+ *
69
+ * If no refraction-ui frames are present the function returns an empty string,
70
+ * which yields a stable "no-frames" fingerprint rather than leaking app frames.
71
+ */
72
+ function normalizeStack(stack: string): string {
73
+ const frames = stack
74
+ .split('\n')
75
+ .map((line) => line.trim())
76
+ .filter((line) => line.startsWith('at ') || line.includes('@'))
77
+ .filter((line) => REFRACTION_FRAME.test(line))
78
+ .map((line) =>
79
+ line
80
+ // drop everything from the path/url onward, keep the symbol
81
+ .replace(/\((?:.*?)(@refraction-ui\/[^):]+)[^)]*\)/, '($1)')
82
+ .replace(/(@refraction-ui\/[^\s:?#]+)[^\s)]*/g, '$1')
83
+ // strip :line:col
84
+ .replace(/:\d+:\d+/g, '')
85
+ // strip line:col without colon prefix
86
+ .replace(/:\d+\b/g, '')
87
+ // strip query/hash
88
+ .replace(/[?#][^\s)]*/g, '')
89
+ .replace(/\s+/g, ' ')
90
+ .trim(),
91
+ )
92
+
93
+ return frames.join('\n')
94
+ }
95
+
96
+ /**
97
+ * Deterministic, dependency-free 53-bit string hash (cyrb53). Stable across
98
+ * runs and processes for the same input — required so the intake can dedupe by
99
+ * fingerprint. NOT cryptographic; it only needs to be collision-resistant
100
+ * enough to group identical normalized stacks.
101
+ */
102
+ function cyrb53(str: string, seed = 0): string {
103
+ let h1 = 0xdeadbeef ^ seed
104
+ let h2 = 0x41c6ce57 ^ seed
105
+ for (let i = 0; i < str.length; i++) {
106
+ const ch = str.charCodeAt(i)
107
+ h1 = Math.imul(h1 ^ ch, 2654435761)
108
+ h2 = Math.imul(h2 ^ ch, 1597334677)
109
+ }
110
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
111
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
112
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
113
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
114
+ const out = 4294967296 * (2097151 & h2) + (h1 >>> 0)
115
+ return out.toString(16).padStart(14, '0')
116
+ }
117
+
118
+ function stackOf(error: unknown): string {
119
+ if (typeof error === 'string') return error
120
+ if (
121
+ error &&
122
+ typeof error === 'object' &&
123
+ 'stack' in error &&
124
+ typeof (error as { stack?: unknown }).stack === 'string'
125
+ ) {
126
+ return (error as { stack: string }).stack
127
+ }
128
+ return ''
129
+ }
130
+
131
+ /**
132
+ * Compute the stable stack fingerprint for a library-origin error. Exposed
133
+ * separately so capture seams / tests can assert fingerprint stability without
134
+ * building the whole record.
135
+ */
136
+ export function stackFingerprint(error: unknown): string {
137
+ const normalized = normalizeStack(stackOf(error))
138
+ return cyrb53(normalized)
139
+ }
140
+
141
+ /**
142
+ * Build the redacted library-origin envelope for an error.
143
+ */
144
+ export function libraryOriginEnvelope(
145
+ input: LibraryOriginErrorInput,
146
+ ): LibraryOriginEnvelope {
147
+ return {
148
+ origin: 'refraction-ui',
149
+ package: input.package,
150
+ componentName: input.componentName,
151
+ version: input.version,
152
+ framework: input.framework,
153
+ fingerprint: stackFingerprint(input.error),
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Build the full library-origin error record, expressed THROUGH the existing
159
+ * telemetry record contract (`LogRecord`, mirrored as {@link DevFeedbackRecord}
160
+ * to avoid a hard logger dependency). The redacted envelope rides in
161
+ * `context`; no app data, props, PII, or app stack frames are included.
162
+ *
163
+ * This record is exactly what a consumer-wired telemetry sink (or the
164
+ * `devWarn`/`devError` injected sink) consumes.
165
+ */
166
+ export function libraryOriginError(
167
+ input: LibraryOriginErrorInput,
168
+ ): DevFeedbackRecord {
169
+ const envelope = libraryOriginEnvelope(input)
170
+ return {
171
+ level: 'error',
172
+ message: `${input.package}/${input.componentName}: library-origin error`,
173
+ timestamp: Date.now(),
174
+ context: { ...envelope },
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Library-origin predicate — `true` iff the error's stack contains at least
180
+ * one frame that references a `@refraction-ui/*` package. This is the gate
181
+ * every per-framework capture seam uses: only library-origin errors are
182
+ * eligible; the app's own errors return `false` and pass through untouched.
183
+ *
184
+ * Mirrors exactly the frame-detection rule {@link stackFingerprint} normalizes
185
+ * by — no new contract, just the companion predicate the seams need. Never
186
+ * throws for a missing/odd error (returns `false`).
187
+ */
188
+ export function isLibraryOriginError(error: unknown): boolean {
189
+ const stack = stackOf(error)
190
+ if (!stack) return false
191
+ return REFRACTION_FRAME.test(stack)
192
+ }
193
+
194
+ /**
195
+ * The fixed identity of the refraction-ui package/component a seam tags an
196
+ * error with. The seam supplies this; the stack itself supplies the
197
+ * fingerprint. Never carries app data.
198
+ */
199
+ export type LibraryOriginIdentity = Pick<
200
+ LibraryOriginErrorInput,
201
+ 'package' | 'componentName' | 'version' | 'framework'
202
+ >
203
+
204
+ /**
205
+ * The single capture primitive shared by every per-framework seam (React error
206
+ * boundary, Angular `ErrorHandler`, Astro middleware). It performs the entire
207
+ * guarded flow in one place so the seams stay thin and identical in behavior:
208
+ *
209
+ * 1. **Library-origin filter** — if the error's stack has no
210
+ * `@refraction-ui/*` frame it is the app's own error: return `null` and do
211
+ * NOT touch, capture, or forward it.
212
+ * 2. **Tag** — build the redacted {@link libraryOriginError} record
213
+ * (package/componentName/version/framework + app-data-free fingerprint).
214
+ * 3. **Route to the optional sink** — forward the record to the
215
+ * consumer-injected sink ONLY if one was wired; a broken sink can never
216
+ * break the consumer app. With no sink this is a no-op beyond returning
217
+ * the record (so a seam can still surface it locally).
218
+ *
219
+ * @returns the tagged record for a library-origin error, or `null` when the
220
+ * error is app-origin (the "pass through untouched" signal).
221
+ */
222
+ export function captureLibraryOriginError(
223
+ error: unknown,
224
+ identity: LibraryOriginIdentity,
225
+ sink: DevFeedbackSink | null | undefined,
226
+ ): DevFeedbackRecord | null {
227
+ if (!isLibraryOriginError(error)) return null
228
+
229
+ const record = libraryOriginError({ ...identity, error })
230
+
231
+ if (sink) {
232
+ try {
233
+ sink.log(record)
234
+ } catch {
235
+ // A broken sink must never break the consumer app.
236
+ }
237
+ }
238
+
239
+ return record
240
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {
@@ -93,7 +93,9 @@
93
93
  "@refraction-ui/astro-link-card": "workspace:*",
94
94
  "@refraction-ui/astro-card-grid": "workspace:*",
95
95
  "@refraction-ui/astro-payment": "workspace:*",
96
- "@refraction-ui/astro-command-input": "workspace:*"
96
+ "@refraction-ui/astro-command-input": "workspace:*",
97
+ "@refraction-ui/astro-logger": "workspace:*",
98
+ "@refraction-ui/astro-analytics": "workspace:*"
97
99
  },
98
100
  "publishConfig": {
99
101
  "access": "public"