@fabrika/app 0.4.1 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/app",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -93,13 +93,13 @@
93
93
  "test": "bun test --path-ignore-patterns='src/react/**' && bun test --isolate src/react"
94
94
  },
95
95
  "dependencies": {
96
- "@fabrika/auth": "^0.4.1",
97
- "@fabrika/auth-core": "^0.4.1",
98
- "@fabrika/db": "^0.4.1",
99
- "@fabrika/jwks": "^0.4.1",
100
- "@fabrika/platform": "^0.4.1",
101
- "@fabrika/platform-node": "^0.4.1",
102
- "@fabrika/proxy-core": "^0.4.1",
96
+ "@fabrika/auth": "^0.4.3",
97
+ "@fabrika/auth-core": "^0.4.3",
98
+ "@fabrika/db": "^0.4.3",
99
+ "@fabrika/jwks": "^0.4.3",
100
+ "@fabrika/platform": "^0.4.3",
101
+ "@fabrika/platform-node": "^0.4.3",
102
+ "@fabrika/proxy-core": "^0.4.3",
103
103
  "jose": "^5.9.0"
104
104
  },
105
105
  "peerDependencies": {
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fabrika-app
3
- version: 0.4.1
3
+ version: 0.4.3
4
4
  description: >
5
5
  How to write and change a Fabrika application: src/vocabulary.ts, db/migrations/, src/db/tables.ts, the
6
6
  slices' endpoints.ts with their handlers, fabrika.contract.ts, jobs, files, webhooks, share links, connectors
@@ -19,21 +19,20 @@ the SPA is added with it.
19
19
  "test": "bun test --isolate"
20
20
  },
21
21
  "dependencies": {
22
- "@fabrika/app": "^0.4.1",
23
- "@fabrika/db": "^0.4.1",
24
- "@fabrika/provider-zerops": "^0.4.1",
25
- "@fabrika/proxy-core": "^0.4.1",
26
- "@fabrika/ui": "^0.4.1",
27
- "@sentry/browser": "10.69.0",
22
+ "@fabrika/app": "^0.4.3",
23
+ "@fabrika/db": "^0.4.3",
24
+ "@fabrika/provider-zerops": "^0.4.3",
25
+ "@fabrika/proxy-core": "^0.4.3",
26
+ "@fabrika/ui": "^0.4.3",
28
27
  "kysely": "0.29.6",
29
28
  "react": "^19.0.0",
30
29
  "react-dom": "^19.0.0",
31
30
  "zod": "^3.25.76"
32
31
  },
33
32
  "devDependencies": {
34
- "@fabrika/check": "^0.4.1",
35
- "@fabrika/cli": "^0.4.1",
36
- "@fabrika/platform": "^0.4.1",
33
+ "@fabrika/check": "^0.4.3",
34
+ "@fabrika/cli": "^0.4.3",
35
+ "@fabrika/platform": "^0.4.3",
37
36
  "@happy-dom/global-registrator": "^20.11.2",
38
37
  "@types/bun": "latest",
39
38
  "@types/react": "^19.0.0",
@@ -48,8 +47,9 @@ the SPA is added with it.
48
47
  Bun still links the workspace copies.
49
48
  - `build:web` builds the SPA: add it, `build-web.ts`, `web/`, the contract's `site` mount and the `bun run build:web`
50
49
  line of `zerops.yaml` when the application gets an SPA, and not before.
51
- - `kysely` is there for the generated schema's types. `@sentry/browser` reports the SPA's errors to Operations, from
52
- the DSN and release the framework serves at `<prefix>/__fabrika-config.json`. The same file carries
50
+ - `kysely` is there for the generated schema's types. The SPA reports its errors to Operations by itself:
51
+ `FabrikaProvider` reads the DSN, release and environment the framework serves at `<prefix>/__fabrika-config.json`
52
+ and sends every uncaught error and unhandled rejection there; add no error-reporting SDK. The same file carries
53
53
  `public: { <VARIABLE>: <value> }`: each form challenge's `siteKey`, and the variables the SPA mount lists in
54
54
  `c.spa(prefix, { …, public: ['SIGNUP_URL'] })`. A page reads a per-environment public value (a site key, a link
55
55
  target) there, never as a build-time constant; it holds no other variable, and the contract refuses a secret in it.
@@ -338,6 +338,7 @@ const built = await Bun.build({
338
338
  outdir,
339
339
  target: 'browser',
340
340
  define: { 'process.env.NODE_ENV': JSON.stringify('production') },
341
+ sourcemap: 'external',
341
342
  plugins: [shellSlicePlugin({ root: import.meta.dir })],
342
343
  })
343
344
  if (!built.success) {
@@ -357,6 +358,12 @@ await Bun.write(
357
358
  the rows are passed in.
358
359
  - The last step points the page's assets at the SPA's prefix (`/`): the mount answers `index.html` for every path
359
360
  under it, so a deep link such as `/tasks/<id>` renders. `define` gives React's production build.
361
+ - `sourcemap: 'external'` writes each script's map beside it, and the page names none. The platform build uploads the
362
+ maps to Operations for the release the page reports, so an error's frames show the original source; the mount
363
+ never serves a `.map` file (404). Keep the maps external: `inline` would ship the source to every visitor.
364
+ - **Errors.** `FabrikaProvider` reports the page's uncaught errors and unhandled rejections to Operations, with the
365
+ release and the environment, through the DSN the mount serves; without a DSN (local development, a test) it reports
366
+ nothing. An application that must not report passes `reportErrors={false}`.
360
367
  - Set text through JSX, never `dangerouslySetInnerHTML` or `innerHTML`. Same-origin calls carry the session cookie.
361
368
  - **Who is signed in.** Every member SPA serves `<prefix>/__fabrika-me.json` to the person it admitted:
362
369
  `{ label, email, actions: { app, scopes } }`. `useCaller()` (`@fabrika/app/react`) answers `{ label, email,
@@ -96,7 +96,8 @@ if (root !== null) {
96
96
  - `FabrikaProvider` holds the clients of the mounts the page calls. A hook takes a procedure of a client
97
97
  (`api.tasks.get`) as the typed name of the call and runs it on the provider's client of that mount; a test hands the
98
98
  provider a client over the test kit instead (below). An SPA mounted under a prefix passes it: `spa="/app"`. Its bare
99
- prefix (`/app`) redirects to `/app/`, so a link may name either.
99
+ prefix (`/app`) redirects to `/app/`, so a link may name either. It also reports the page's uncaught errors to
100
+ Operations (`reportErrors={false}` turns that off); never add an error-reporting SDK.
100
101
  - `createRouter` answers typed paths: `href('task', { id })`, `navigate('project', { orgId, projectId })`, `<Link
101
102
  to="task" params={{ id }}>`, and `useRoute()`, the match or `null`. `<Router>` makes every plain `<a href>` under the
102
103
  SPA navigate without a reload: the kit's components render links that way. Pass `{ base: '/app' }` for a prefix.
@@ -0,0 +1,209 @@
1
+ // Browser error reporting on the paved road. `FabrikaProvider` reads the SPA mount's browser config and, when it names
2
+ // an Operations DSN, sends every uncaught error and unhandled rejection of the page to that ingest as one Sentry error
3
+ // event. Operations ingests error events only, so nothing else of the page leaves it.
4
+
5
+ import { z } from 'zod/v4'
6
+ import type { RpcFetch } from './transport'
7
+
8
+ const BrowserConfigSchema = z.object({
9
+ dsn: z.string().nullable(),
10
+ release: z.string().nullable(),
11
+ environment: z.string().nullable(),
12
+ })
13
+
14
+ /** One stack frame in Sentry's shape, the file a script URL. */
15
+ export interface BrowserFrame {
16
+ readonly filename: string
17
+ readonly function?: string
18
+ readonly lineno: number
19
+ readonly colno: number
20
+ /** A frame of a script of the page's own origin. */
21
+ readonly in_app: boolean
22
+ }
23
+
24
+ /** Where the page reports, read from its browser config. */
25
+ export interface ReportingTarget {
26
+ /** The ingest's envelope endpoint, authenticated by the DSN's public key in its query, as the Sentry SDK sends it. */
27
+ readonly envelopeUrl: string
28
+ readonly release: string | null
29
+ readonly environment: string | null
30
+ }
31
+
32
+ /** What the reporter listens on and reads the page's address from: `window` in a page. */
33
+ export interface ReportingPage {
34
+ addEventListener(type: 'error' | 'unhandledrejection', listener: (event: Event) => void): void
35
+ removeEventListener(type: 'error' | 'unhandledrejection', listener: (event: Event) => void): void
36
+ readonly location: { readonly origin: string; readonly pathname: string }
37
+ }
38
+
39
+ const PUBLIC_KEY = /^[0-9a-f]{32}$/
40
+ const PROJECT_ID = /^[1-9][0-9]{0,18}$/
41
+ /** A page that keeps failing stops reporting here; Operations rate-limits the whole source, not one page. */
42
+ export const MAX_REPORTS_PER_PAGE = 20
43
+ const MAX_FRAMES = 50
44
+ const MAX_TEXT = 2_048
45
+ const TYPE_NAME = /^[A-Za-z_$][\w$.]{0,127}$/
46
+
47
+ /**
48
+ * The envelope endpoint of an ingest DSN `https://<key>@<host>/<project>`, or `null` for any other value. The server's
49
+ * reporter parses the same DSN (`jobs/report.ts`); it is server code, so a page does not import it.
50
+ */
51
+ export function envelopeUrlOf(dsn: string): string | null {
52
+ let url: URL
53
+ try {
54
+ url = new URL(dsn.trim())
55
+ } catch {
56
+ return null
57
+ }
58
+ const project = url.pathname.slice(1)
59
+ if ((url.protocol !== 'https:' && url.protocol !== 'http:') || !PUBLIC_KEY.test(url.username) || url.password !== '' || !PROJECT_ID.test(project)) {
60
+ return null
61
+ }
62
+ return `${url.origin}/api/${project}/envelope/?sentry_key=${url.username}&sentry_version=7`
63
+ }
64
+
65
+ /** The reporting target the browser config names, or `null` when it names no usable DSN. */
66
+ export function reportingTargetOf(config: unknown): ReportingTarget | null {
67
+ const parsed = BrowserConfigSchema.safeParse(config)
68
+ if (!parsed.success || parsed.data.dsn === null) return null
69
+ const envelopeUrl = envelopeUrlOf(parsed.data.dsn)
70
+ return envelopeUrl === null ? null : { envelopeUrl, release: parsed.data.release, environment: parsed.data.environment }
71
+ }
72
+
73
+ const cut = (text: string): string => text.length <= MAX_TEXT ? text : `${text.slice(0, MAX_TEXT)}…`
74
+
75
+ const V8_FRAME = /^\s*at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?$/
76
+ const GECKO_FRAME = /^(?:(.*?)@)?(.+?):(\d+):(\d+)$/
77
+
78
+ function parseFrame(line: string, origin: string): BrowserFrame | null {
79
+ const match = /^\s*at /.test(line) ? V8_FRAME.exec(line) : GECKO_FRAME.exec(line)
80
+ const filename = match?.[2]
81
+ if (match === null || filename === undefined || !URL.canParse(filename)) return null
82
+ const name = match[1]?.replace(/^(?:async|new) /, '').trim()
83
+ return {
84
+ filename: cut(filename),
85
+ ...(name === undefined || name === '' ? {} : { function: cut(name) }),
86
+ lineno: Number(match[3]),
87
+ colno: Number(match[4]),
88
+ in_app: filename.startsWith(`${origin}/`),
89
+ }
90
+ }
91
+
92
+ /**
93
+ * The frames of a V8, SpiderMonkey or JavaScriptCore `stack`, oldest call first as Sentry orders them, the
94
+ * `MAX_FRAMES` nearest the throw kept. A frame whose file is no URL (an `eval`, a native function) is left out.
95
+ */
96
+ export function parseBrowserStack(stack: string, origin: string): BrowserFrame[] {
97
+ const frames: BrowserFrame[] = []
98
+ for (const line of stack.split('\n')) {
99
+ const frame = parseFrame(line, origin)
100
+ if (frame !== null) frames.push(frame)
101
+ if (frames.length === MAX_FRAMES) break
102
+ }
103
+ return frames.reverse()
104
+ }
105
+
106
+ type Mechanism = 'onerror' | 'onunhandledrejection'
107
+
108
+ interface ReportedException {
109
+ readonly type: string
110
+ readonly value: string
111
+ readonly mechanism: { readonly type: Mechanism; readonly handled: false }
112
+ readonly stacktrace?: { readonly frames: readonly BrowserFrame[] }
113
+ }
114
+
115
+ function exceptionOf(thrown: unknown, message: string, mechanism: Mechanism, origin: string): ReportedException {
116
+ if (thrown instanceof Error) {
117
+ const frames = typeof thrown.stack === 'string' ? parseBrowserStack(thrown.stack, origin) : []
118
+ return {
119
+ type: TYPE_NAME.test(thrown.name) ? thrown.name : 'Error',
120
+ value: cut(thrown.message),
121
+ mechanism: { type: mechanism, handled: false },
122
+ ...(frames.length === 0 ? {} : { stacktrace: { frames } }),
123
+ }
124
+ }
125
+ const value = typeof thrown === 'string' ? thrown : message !== '' ? message : `a ${typeof thrown} was thrown`
126
+ return { type: mechanism === 'onerror' ? 'Error' : 'UnhandledRejection', value: cut(value), mechanism: { type: mechanism, handled: false } }
127
+ }
128
+
129
+ const hexId = (): string => crypto.randomUUID().replaceAll('-', '')
130
+
131
+ /** The envelope of one error event. The page's address goes without its query and fragment, which may carry a key. */
132
+ export function browserEnvelope(
133
+ exception: ReportedException,
134
+ target: ReportingTarget,
135
+ page: ReportingPage['location'],
136
+ now: Date,
137
+ ): string {
138
+ const eventId = hexId()
139
+ const event = {
140
+ event_id: eventId,
141
+ timestamp: now.toISOString(),
142
+ level: 'error',
143
+ platform: 'javascript',
144
+ logger: 'fabrika.browser',
145
+ request: { url: `${page.origin}${page.pathname}` },
146
+ exception: { values: [exception] },
147
+ ...(target.release === null ? {} : { release: target.release }),
148
+ ...(target.environment === null ? {} : { environment: target.environment }),
149
+ }
150
+ return [JSON.stringify({ event_id: eventId, sent_at: now.toISOString() }), JSON.stringify({ type: 'event' }), JSON.stringify(event), ''].join('\n')
151
+ }
152
+
153
+ const signatureOf = (exception: ReportedException): string =>
154
+ JSON.stringify([exception.type, exception.value, exception.stacktrace?.frames.at(-1) ?? null])
155
+
156
+ /**
157
+ * Listen for the page's uncaught errors and unhandled rejections and send each to `target`; answers the uninstall.
158
+ * An error equal to the one just sent is not sent again, and the page sends at most `MAX_REPORTS_PER_PAGE`. A failed
159
+ * delivery is dropped: reporting never throws into the page.
160
+ */
161
+ export function installErrorReporting(page: ReportingPage, target: ReportingTarget, send: RpcFetch): () => void {
162
+ let sent = 0
163
+ let previous = ''
164
+ const report = (exception: ReportedException): void => {
165
+ const signature = signatureOf(exception)
166
+ if (sent >= MAX_REPORTS_PER_PAGE || signature === previous) return
167
+ sent++
168
+ previous = signature
169
+ // A simple request (no custom header, a text body) needs no CORS preflight; `keepalive` outlives a navigation.
170
+ void send(target.envelopeUrl, {
171
+ method: 'POST',
172
+ headers: { 'content-type': 'text/plain;charset=UTF-8' },
173
+ body: browserEnvelope(exception, target, page.location, new Date()),
174
+ credentials: 'omit',
175
+ keepalive: true,
176
+ }).then((response) => response.body?.cancel(), () => {})
177
+ }
178
+ // Read by name: an `ErrorEvent`'s `error` and `message`, a `PromiseRejectionEvent`'s `reason`, whichever class made them.
179
+ const onError = (event: Event): void => {
180
+ const error: unknown = Reflect.get(event, 'error')
181
+ const message: unknown = Reflect.get(event, 'message')
182
+ report(exceptionOf(error, typeof message === 'string' ? message : '', 'onerror', page.location.origin))
183
+ }
184
+ const onRejection = (event: Event): void => {
185
+ const reason: unknown = Reflect.get(event, 'reason')
186
+ report(exceptionOf(reason, '', 'onunhandledrejection', page.location.origin))
187
+ }
188
+ page.addEventListener('error', onError)
189
+ page.addEventListener('unhandledrejection', onRejection)
190
+ return () => {
191
+ page.removeEventListener('error', onError)
192
+ page.removeEventListener('unhandledrejection', onRejection)
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Read the browser config at `configUrl` and install reporting when it names a DSN; answers the uninstall, or `null`
198
+ * when nothing was installed. A missing, refused or unreadable config (local development, a test) installs nothing.
199
+ */
200
+ export async function startErrorReporting(configUrl: string, fetch: RpcFetch, page: ReportingPage): Promise<(() => void) | null> {
201
+ let target: ReportingTarget | null
202
+ try {
203
+ const response = await fetch(configUrl, { credentials: 'same-origin' })
204
+ target = response.ok ? reportingTargetOf(await response.json()) : null
205
+ } catch {
206
+ return null
207
+ }
208
+ return target === null ? null : installErrorReporting(page, target, fetch)
209
+ }
@@ -1,5 +1,5 @@
1
- // What an SPA mount answers at `<prefix>/__fabrika-config.json`: the browser's Operations settings and the public
2
- // values the contract names, and nothing else of the environment.
1
+ // What an SPA mount answers at `<prefix>/__fabrika-config.json`: the browser's Operations settings (DSN, release and
2
+ // environment) and the public values the contract names, and nothing else of the environment.
3
3
 
4
4
  import type { ContractArtifact } from '@fabrika/auth-core'
5
5
  import type { CapturedEnvironment } from './environment'
@@ -8,9 +8,11 @@ export interface BrowserConfig {
8
8
  /** The Operations ingest DSN (a public key), or `null` without one. */
9
9
  readonly dsn: string | null
10
10
  readonly release: string | null
11
+ /** `FABRIKA_ENVIRONMENT`, which the page's error reports carry, or `null` when unset. */
12
+ readonly environment: string | null
11
13
  /**
12
14
  * Every form challenge's site key, and the variables the mount lists in `public`, by variable name. Absent when
13
- * there is none, so the file of an application without them holds the two Operations values only.
15
+ * there is none, so the file of an application without them holds the three Operations values only.
14
16
  */
15
17
  readonly public?: Readonly<Record<string, string>>
16
18
  }
@@ -48,7 +50,8 @@ export function browserConfigs(
48
50
  else own[variable] = value
49
51
  }
50
52
  const served = { ...siteKeys, ...own }
51
- configs.set(name, Object.freeze(Object.keys(served).length === 0 ? { ...operations } : { ...operations, public: Object.freeze(served) }))
53
+ const reporting = { ...operations, environment: environment.framework['FABRIKA_ENVIRONMENT']?.trim() || null }
54
+ configs.set(name, Object.freeze(Object.keys(served).length === 0 ? reporting : { ...reporting, public: Object.freeze(served) }))
52
55
  }
53
56
  return configs
54
57
  }
@@ -300,7 +300,7 @@ export class ContractRuntime {
300
300
  if (rawRequestTarget(request.url).pathname === bootstrapPath(served.prefix)) {
301
301
  return bootstrapResponse()
302
302
  }
303
- return await withBootstrap(await this.#assets().serve({ prefix: served.prefix, dir: served.dir }, request), served.prefix)
303
+ return await withBootstrap(await this.#asset(served, request), served.prefix)
304
304
  case 'procedure':
305
305
  return await this.#procedure(served, request)
306
306
  case 'file':
@@ -345,6 +345,14 @@ export class ContractRuntime {
345
345
  }
346
346
  return jsonResponse(pageCallerOf(caller.principal, caller.perms, this.#parts.actions), 200)
347
347
  }
348
+ return await this.#asset(served, request)
349
+ }
350
+
351
+ /** A file of the mount's directory. A source map goes to Operations at build and is never served: it holds the source. */
352
+ async #asset(served: { readonly prefix: string; readonly dir: string }, request: Request): Promise<Response> {
353
+ if (rawRequestTarget(request.url).pathname.endsWith('.map')) {
354
+ throw new NotFoundError('A source map is not served')
355
+ }
348
356
  return await this.#assets().serve({ prefix: served.prefix, dir: served.dir }, request)
349
357
  }
350
358
 
@@ -1,13 +1,15 @@
1
- // `FabrikaProvider`: the page's clients, the cache of their answers, and the caller document of the SPA mount. The
2
- // hooks take a procedure of a client as the typed name of a call; the provider runs it on the client it holds for
3
- // that mount, so a test hands in a client over the test kit and the page's own module-level client stays as it is.
1
+ // `FabrikaProvider`: the page's clients, the cache of their answers, the caller document of the SPA mount, and the
2
+ // page's error reporting. The hooks take a procedure of a client as the typed name of a call; the provider runs it on
3
+ // the client it holds for that mount, so a test hands in a client over the test kit and the page's own module-level
4
+ // client stays as it is.
4
5
 
5
- import { createContext, type ReactNode, useContext, useState } from 'react'
6
+ import { createContext, type ReactNode, useContext, useEffect, useState } from 'react'
6
7
  import { z } from 'zod/v4'
7
8
  import { type PageCaller, parsePageCaller } from '../client/caller'
8
9
  import { attachmentAt, type ClientNode, clientNodeOf, downloadFile } from '../client/contract'
10
+ import { startErrorReporting } from '../client/error-reporting'
9
11
  import { RpcError, type RpcFetch } from '../client/transport'
10
- import { joinPath, PAGE_CALLER_FILE } from '../contract/defaults'
12
+ import { BROWSER_CONFIG_FILE, joinPath, PAGE_CALLER_FILE } from '../contract/defaults'
11
13
  import { type FabrikaError, toFabrikaError } from './errors'
12
14
  import { QueryStore, type StoreTarget } from './store'
13
15
 
@@ -149,13 +151,35 @@ export interface FabrikaProviderProps {
149
151
  readonly clients: readonly object[]
150
152
  /** The prefix of the SPA mount the page is served from: the caller document is `<spa>/__fabrika-me.json`. */
151
153
  readonly spa?: string
152
- /** What the caller document is fetched with; the global `fetch` by default (a test passes the kit's). */
154
+ /**
155
+ * What the caller document, the browser config and the error reports are fetched with; the global `fetch` by default
156
+ * (a test passes the kit's).
157
+ */
153
158
  readonly fetch?: RpcFetch
159
+ /**
160
+ * Report the page's uncaught errors and unhandled rejections to Operations, through the DSN and release the mount
161
+ * serves at `<spa>/__fabrika-config.json`; `true` by default. Without a DSN nothing is reported.
162
+ */
163
+ readonly reportErrors?: boolean
154
164
  readonly children?: ReactNode
155
165
  }
156
166
 
157
- export function FabrikaProvider({ clients, spa = '/', fetch: fetchCaller, children }: FabrikaProviderProps): ReactNode {
158
- const [value] = useState(() => fabrika(clients, spa, fetchCaller ?? ((input, init) => fetch(input, init))))
167
+ export function FabrikaProvider({ clients, spa = '/', fetch: fetchGiven, reportErrors = true, children }: FabrikaProviderProps): ReactNode {
168
+ const [send] = useState<RpcFetch>(() => fetchGiven ?? ((input, init) => fetch(input, init)))
169
+ const [value] = useState(() => fabrika(clients, spa, send))
170
+ useEffect(() => {
171
+ if (!reportErrors || typeof window === 'undefined') return
172
+ let stopped = false
173
+ let uninstall: (() => void) | null = null
174
+ void startErrorReporting(joinPath(spa, BROWSER_CONFIG_FILE), send, window).then((installed) => {
175
+ if (stopped) installed?.()
176
+ else uninstall = installed
177
+ })
178
+ return () => {
179
+ stopped = true
180
+ uninstall?.()
181
+ }
182
+ }, [reportErrors, spa, send])
159
183
  return <FabrikaContext.Provider value={value}>{children}</FabrikaContext.Provider>
160
184
  }
161
185