@fabrika/builder 0.2.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.
- package/BUILD-CONTRACT.md +41 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/cli/src/bin.ts +66 -0
- package/cli/src/build-status.ts +109 -0
- package/cli/src/catalog.ts +42 -0
- package/cli/src/commands/advance.ts +73 -0
- package/cli/src/commands/build.ts +215 -0
- package/cli/src/commands/ensure-daemon.ts +25 -0
- package/cli/src/commands/gate.ts +165 -0
- package/cli/src/commands/induce.ts +61 -0
- package/cli/src/commands/init.ts +91 -0
- package/cli/src/commands/next.ts +31 -0
- package/cli/src/config.ts +47 -0
- package/cli/src/context.ts +28 -0
- package/cli/src/daemon-client.ts +280 -0
- package/cli/src/daemon-lifecycle.ts +171 -0
- package/cli/src/derive.ts +109 -0
- package/cli/src/index.ts +38 -0
- package/cli/src/induce.ts +151 -0
- package/cli/src/inject-skills.ts +313 -0
- package/cli/src/issues.ts +75 -0
- package/cli/src/layout.ts +124 -0
- package/cli/src/packet.ts +108 -0
- package/cli/src/phase-state.ts +19 -0
- package/cli/src/phases.ts +131 -0
- package/cli/src/skeletons.ts +104 -0
- package/daemon/src/bin.ts +70 -0
- package/daemon/src/build-stream.ts +38 -0
- package/daemon/src/handlers.ts +472 -0
- package/daemon/src/index.ts +123 -0
- package/daemon/src/paths.ts +30 -0
- package/daemon/src/phases.ts +221 -0
- package/daemon/src/static.ts +72 -0
- package/decisions/README.md +133 -0
- package/decisions/patterns/app-deploy.md +55 -0
- package/decisions/patterns/auth-password-kv.md +45 -0
- package/decisions/patterns/count-store.md +44 -0
- package/decisions/patterns/di-container.md +47 -0
- package/decisions/patterns/feature-architecture.md +48 -0
- package/decisions/patterns/lib-publish.md +46 -0
- package/decisions/patterns/monorepo.md +47 -0
- package/decisions/patterns/named-seeds.md +45 -0
- package/decisions/patterns/observability.md +45 -0
- package/decisions/patterns/single-worker-spa.md +74 -0
- package/decisions/patterns/structured-logging.md +49 -0
- package/decisions/patterns/typed-worker-rpc.md +87 -0
- package/decisions/patterns/vozka-app-deploy.md +74 -0
- package/decisions/patterns/write-split.md +47 -0
- package/decisions/technologies/analytics-engine.md +44 -0
- package/decisions/technologies/bindx.md +55 -0
- package/decisions/technologies/bun.md +51 -0
- package/decisions/technologies/buzola.md +80 -0
- package/decisions/technologies/cloudflare-access.md +50 -0
- package/decisions/technologies/cloudflare-vite-plugin.md +43 -0
- package/decisions/technologies/cloudflare.md +50 -0
- package/decisions/technologies/contember.md +65 -0
- package/decisions/technologies/cron-triggers.md +46 -0
- package/decisions/technologies/d1.md +51 -0
- package/decisions/technologies/durable-objects.md +44 -0
- package/decisions/technologies/kv.md +42 -0
- package/decisions/technologies/lopata.md +55 -0
- package/decisions/technologies/nuasite.md +50 -0
- package/decisions/technologies/oblaka.md +48 -0
- package/decisions/technologies/opice.md +40 -0
- package/decisions/technologies/pletivo.md +53 -0
- package/decisions/technologies/postgres.md +47 -0
- package/decisions/technologies/preact.md +44 -0
- package/decisions/technologies/propustka.md +137 -0
- package/decisions/technologies/queue.md +55 -0
- package/decisions/technologies/r2.md +41 -0
- package/decisions/technologies/react-router.md +47 -0
- package/decisions/technologies/react.md +37 -0
- package/decisions/technologies/roj.md +51 -0
- package/decisions/technologies/sentry.md +49 -0
- package/decisions/technologies/tailwind.md +45 -0
- package/decisions/technologies/vite.md +47 -0
- package/decisions/technologies/vozka.md +122 -0
- package/decisions/technologies/workers-ai.md +59 -0
- package/decisions/technologies/wrangler.md +41 -0
- package/decisions/tooling/deptective.md +45 -0
- package/decisions/tooling/edvabe.md +45 -0
- package/index.ts +2 -0
- package/package.json +51 -0
- package/skill/SKILL.md +76 -0
- package/skill/agents/build-orchestrator.md +170 -0
- package/skill/agents/flows-author.md +65 -0
- package/skill/agents/implementer.md +37 -0
- package/skill/agents/openai.yaml +4 -0
- package/skill/agents/opice-skeleton-author.md +68 -0
- package/skill/gates/brief.jsx +20 -0
- package/skill/gates/build.jsx +20 -0
- package/skill/gates/design.jsx +22 -0
- package/skill/gates/flows.jsx +14 -0
- package/skill/gates/review-grid.jsx +32 -0
- package/skill/gates/tech.jsx +17 -0
- package/skill/gates/vision.jsx +27 -0
- package/stacks/README.md +44 -0
- package/stacks/cf-fat.md +64 -0
- package/stacks/cf-internal-app.md +125 -0
- package/stacks/cf-lightweight.md +54 -0
- package/stacks/contember-app.md +53 -0
- package/stacks/content-site.md +45 -0
- package/ui/dist/assets/annotations-runtime.js +1 -0
- package/ui/dist/assets/client.css +2 -0
- package/ui/dist/assets/client.fhjppkvr.js +214 -0
- package/ui/dist/assets/components.js +63 -0
- package/ui/dist/assets/preact-compat.js +1 -0
- package/ui/dist/assets/react-jsx-dev-runtime.js +3 -0
- package/ui/dist/assets/react-jsx-runtime.js +3 -0
- package/ui/dist/assets/runtime.js +1 -0
- package/ui/dist/index.html +36 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: oblaka
|
|
3
|
+
kind: technology
|
|
4
|
+
title: oblaka (Cloudflare IaC / bindings as code)
|
|
5
|
+
slot: iac
|
|
6
|
+
provides: [iac, binding-definitions, wrangler-generation]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: []
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: ["oblaka-iac@^0.0.18"]
|
|
11
|
+
status: wip
|
|
12
|
+
sources:
|
|
13
|
+
- where: "oblaka (repo) + `oblaka-iac` on npm (installed types)"
|
|
14
|
+
read: "The CURRENT binding API, programmatic deploy surface and how `bunx oblaka <file>` generates wrangler.jsonc. Read the live types — do not guess."
|
|
15
|
+
- where: "vozka packages/core + vozka-config"
|
|
16
|
+
read: "How Vozka materializes the same oblaka resource graph for production while an app keeps a thin local-dev shim."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# oblaka
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
A TypeScript DSL for Cloudflare IaC: define the Worker + its bindings in
|
|
24
|
+
`oblaka.ts`, run `bunx oblaka oblaka.ts` to **generate `wrangler.jsonc`**. One typed
|
|
25
|
+
source of truth for Workers, D1, KV, R2, Queues, DO, Analytics Engine and cron.
|
|
26
|
+
Vozka uses oblaka programmatically for production provisioning; standalone
|
|
27
|
+
`oblaka.ts` remains useful as the local lopata config shim.
|
|
28
|
+
|
|
29
|
+
## When & why to use
|
|
30
|
+
|
|
31
|
+
- Any CF stack — declare bindings as code instead of hand-writing `wrangler.jsonc`.
|
|
32
|
+
- A Vozka app — the resource builder in `vozka.config.ts` is still an oblaka
|
|
33
|
+
`Worker`; reuse it from the local shim rather than duplicating the graph.
|
|
34
|
+
|
|
35
|
+
## Impacts & gotchas
|
|
36
|
+
|
|
37
|
+
- **`wrangler.jsonc` is GENERATED — never hand-edit it.** Change `oblaka.ts` and regenerate.
|
|
38
|
+
- Standalone/local shape: `define(({ env }) => buildWorker({ env, domain: undefined }))`,
|
|
39
|
+
where `buildWorker` is exported from `vozka.config.ts`. For a non-Vozka app,
|
|
40
|
+
build the `Worker` inline as before.
|
|
41
|
+
- **Verify every binding constructor against the installed `oblaka-iac` types** — it's WIP and a stack may use bindings (Queue consumer, AE, cron) the constructor surface for which moves. If a binding type is missing, generate the wrangler entry per Cloudflare docs.
|
|
42
|
+
- `vozka-config` and oblaka publish raw TypeScript in some versions; if the app's
|
|
43
|
+
main `tsc` excludes config files, validate them by actually materializing the
|
|
44
|
+
config and running a wrangler dry-run/config check.
|
|
45
|
+
|
|
46
|
+
## Relationships — why
|
|
47
|
+
|
|
48
|
+
- **requires `cloudflare`** — it exists to configure CF. Recommended by every CF binding decision (`d1`/`kv`/`r2`/`queue`/`durable-objects`/`cron-triggers`).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: opice
|
|
3
|
+
kind: technology
|
|
4
|
+
title: opice (AI-driven E2E browser tests)
|
|
5
|
+
slot: e2e-tests
|
|
6
|
+
provides: [e2e-browser-tests, acceptance]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [bun]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: ["@opice/harness@^0.6.1", "@opice/cli@^0.6.1"]
|
|
11
|
+
status: wip
|
|
12
|
+
sources:
|
|
13
|
+
- where: "opice (repo) + `@opice/harness` / `@opice/cli` on npm (installed types)"
|
|
14
|
+
read: "How browserTest/step/invariant work and how the harness runs under `bun test`. Read the installed types — do not guess."
|
|
15
|
+
- where: "The opice-* skills (opice-plan / opice-author / opice-batch / opice-reeval)"
|
|
16
|
+
read: "The authoring procedure: skeletons → authored steps → CI re-eval."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# opice
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
The acceptance layer for fabrika: markdown scenarios → LLM-generated `*.test.ts`
|
|
24
|
+
under `tests/browser/` → deterministic `bun test` runs. `browserTest` + `step` +
|
|
25
|
+
`invariant`, with metadata (`url`, `seeds`, `roles`) that fabrika **induces issues
|
|
26
|
+
from**.
|
|
27
|
+
|
|
28
|
+
## When & why to use
|
|
29
|
+
|
|
30
|
+
- Always, in a fabrika builder build — opice skeletons are the contract every issue satisfies; `url`/`seeds`/`roles` drive foundation-issue induction.
|
|
31
|
+
|
|
32
|
+
## Impacts & gotchas
|
|
33
|
+
|
|
34
|
+
- **Skeleton metadata is a contract**: `url` → a route must exist; `seeds: ['x']` → `seeds/x.sql` must exist; `roles` → an auth foundation. fabrika `induce` unions these.
|
|
35
|
+
- Selector priority: **byRole → byLabel → getByText → el(testid)**. The app must be accessibility-first (every interactive element named, every section a heading) or opice-author has no stable selector.
|
|
36
|
+
- Runs against the **running** dev server (`APP_URL`); `bunx opice test <file>` for one, `bun test tests/browser` for all. WIP — read installed types.
|
|
37
|
+
|
|
38
|
+
## Relationships — why
|
|
39
|
+
|
|
40
|
+
- Standalone (no hard tech deps) — it tests whatever stack the app is built on; runs under **`bun test`** (recommends `bun`) and pairs with `named-seeds` for the seed contract.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: pletivo
|
|
3
|
+
kind: technology
|
|
4
|
+
title: pletivo — Bun-native SSG (Astro-compatible) — fits the bun/non-vite preference
|
|
5
|
+
slot: site-framework
|
|
6
|
+
provides: [ssg, astro-compat, jsx-pages, content-collections, islands, file-routing]
|
|
7
|
+
requires: [bun]
|
|
8
|
+
recommends: [preact, tailwind]
|
|
9
|
+
conflicts: [nuasite]
|
|
10
|
+
npm: ["pletivo@^0.1", "@pletivo/astro-jsx-pages@^0.0.1"]
|
|
11
|
+
status: experimental
|
|
12
|
+
sources:
|
|
13
|
+
- where: "pletivo/README.md + CLAUDE.md"
|
|
14
|
+
read: "Compatibility matrix, project structure, and the critical invariants (island registry reset, JSON-serializable island props)."
|
|
15
|
+
- where: "pletivo/packages/pletivo/src/{index.ts,config.ts,build.ts}"
|
|
16
|
+
read: "Public exports (defineCollection/getCollection, PletivoConfig), the render loop, and page emission."
|
|
17
|
+
- where: "pletivo/packages/pletivo/src/runtime/{island.ts,hydration.ts}"
|
|
18
|
+
read: "Island registration/reset + client hydration strategies (load/idle/visible/media)."
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# pletivo
|
|
22
|
+
|
|
23
|
+
## What it is
|
|
24
|
+
|
|
25
|
+
A **Bun-powered static site generator**, ~2× faster than Astro and a near drop-in for
|
|
26
|
+
it: compiles `.astro` pages + JSX/TSX to static HTML at build, file-based routing
|
|
27
|
+
(`src/pages/`), content collections (Zod), and **Preact islands** with multiple
|
|
28
|
+
hydration strategies. Runs on **Bun** (not Node) — a natural fit for our bun /
|
|
29
|
+
non-vite preference (builds with Bun's pipeline, no separate vite app server).
|
|
30
|
+
|
|
31
|
+
## When & why to use
|
|
32
|
+
|
|
33
|
+
- A **content-focused static site** (blog, docs, marketing) where Astro builds are too slow.
|
|
34
|
+
- You want Astro compatibility (`.astro`, content collections) + lightweight Preact islands, on Bun.
|
|
35
|
+
|
|
36
|
+
## When NOT to use
|
|
37
|
+
|
|
38
|
+
- You need **SSR / per-request dynamic routes** → use a Workers SSR app (`react-router`) instead; pletivo is SSG-only (pre-declare dynamic routes via `getStaticPaths`).
|
|
39
|
+
- A visual-CMS content site → `nuasite`.
|
|
40
|
+
- Very large sites (>5k pages) or Node-only deps.
|
|
41
|
+
|
|
42
|
+
## Impacts & gotchas
|
|
43
|
+
|
|
44
|
+
- **Bun-locked** build/dev; CI needs Bun.
|
|
45
|
+
- **Islands must be JSON-serializable props** (embedded in `data-props`); functions/Dates/circular refs break.
|
|
46
|
+
- **Island registry must reset between pages** (`resetIslandRegistry()`) — the build does it; custom render paths must too, or island metadata bleeds across pages.
|
|
47
|
+
- Client is **Preact**, not React (no React-only island libs).
|
|
48
|
+
|
|
49
|
+
## Relationships — why
|
|
50
|
+
|
|
51
|
+
- **requires `bun`** — it's a Bun CLI/runtime (no Node SSR).
|
|
52
|
+
- **recommends `preact`** (islands) + **`tailwind`** (styling).
|
|
53
|
+
- **conflicts `nuasite`** — same `site-framework` slot; pletivo = fast minimal SSG, nuasite = Astro + visual CMS. Pick one site approach.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: postgres
|
|
3
|
+
kind: technology
|
|
4
|
+
title: PostgreSQL (relational database)
|
|
5
|
+
slot: relational-db
|
|
6
|
+
provides: [relational-db, sql, mutable-state, transactions]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: []
|
|
9
|
+
conflicts: [d1]
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "postgresql.org/docs + the driver you use (e.g. `pg`, `postgres`)"
|
|
14
|
+
read: "Connection/pooling model, transactions, and row-level security if you use it directly."
|
|
15
|
+
- where: "contember-oss/docker-compose.yaml + packages/database"
|
|
16
|
+
read: "A real Postgres-backed setup (Contember Engine) and a fluent query/pool layer over `pg`."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# PostgreSQL
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
The general-purpose relational database for stacks that need a **full SQL server**
|
|
24
|
+
(rich types, transactions, extensions, mature tooling) rather than edge SQLite.
|
|
25
|
+
Required by `contember`; the default store when you run a **Node backend** instead
|
|
26
|
+
of Workers + D1.
|
|
27
|
+
|
|
28
|
+
## When & why to use
|
|
29
|
+
|
|
30
|
+
- You adopt `contember` (it mandates Postgres) or any Node backend needing a real RDBMS.
|
|
31
|
+
- You need Postgres-specific features (JSONB, full-text search, extensions, strong transactional guarantees) D1/SQLite lacks.
|
|
32
|
+
|
|
33
|
+
## When NOT to use
|
|
34
|
+
|
|
35
|
+
- **Cloudflare Workers-first** apps → use `d1` (edge SQLite, local-first, no server). Workers can't open a raw Postgres TCP connection without Hyperdrive/a proxy.
|
|
36
|
+
- A small local-first app where SQLite is plenty.
|
|
37
|
+
|
|
38
|
+
## Impacts & gotchas
|
|
39
|
+
|
|
40
|
+
- You **run and operate a Postgres server** (managed service, container, or self-hosted) — connection strings, pooling, backups.
|
|
41
|
+
- From Workers, direct connections need **Hyperdrive or an HTTP proxy**; the natural home is a long-lived Node process.
|
|
42
|
+
- Migrations are yours (or your framework's, e.g. Contember's diff-generated ones).
|
|
43
|
+
|
|
44
|
+
## Relationships — why
|
|
45
|
+
|
|
46
|
+
- **conflicts `d1`** — same `relational-db` slot; one relational store per app. Postgres = server-side RDBMS, D1 = edge SQLite.
|
|
47
|
+
- **required by `contember`** — its schema/migration/ACL layer is Postgres-specific.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: preact
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Preact (small React-compatible UI framework)
|
|
5
|
+
slot: ui-framework
|
|
6
|
+
provides: [ui-framework, components]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [tailwind]
|
|
9
|
+
conflicts: [react]
|
|
10
|
+
npm: ["preact@^10"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "preactjs.com + installed preact types"
|
|
14
|
+
read: "The Preact 10 API, `preact/hooks`, and the differences from React (events, `jsxImportSource: preact`)."
|
|
15
|
+
- where: "pletivo islands + preactjs.com"
|
|
16
|
+
read: "Where Preact shows up in our stack: pletivo's islands run on Preact."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Preact
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
A ~4 KB React-compatible UI framework. Largely JSX/hooks-compatible with React but
|
|
24
|
+
smaller, with subtle differences. In our stack, `pletivo`'s islands run on Preact.
|
|
25
|
+
|
|
26
|
+
## When & why to use
|
|
27
|
+
|
|
28
|
+
- You want a tiny client runtime (islands, embeddable widgets) — e.g. `pletivo` islands.
|
|
29
|
+
- Bundle size on the client matters more than the React ecosystem's breadth.
|
|
30
|
+
|
|
31
|
+
## When NOT to use
|
|
32
|
+
|
|
33
|
+
- You need the full React ecosystem (`react-router`, `buzola`, `bindx`, Contember admin all assume React) → use `react`.
|
|
34
|
+
|
|
35
|
+
## Impacts & gotchas
|
|
36
|
+
|
|
37
|
+
- `jsxImportSource: preact` in tsconfig; `preact/hooks` for hooks. **Don't mix React and Preact in one bundle.**
|
|
38
|
+
- **DOM-standard event names** (`onchange`, not React's `onChange`) — an easy bug when porting React code.
|
|
39
|
+
- React-only libs (Suspense-dependent, `bindx`, RRv7) won't run on Preact.
|
|
40
|
+
|
|
41
|
+
## Relationships — why
|
|
42
|
+
|
|
43
|
+
- **conflicts `react`** — same `ui-framework` slot; one per app.
|
|
44
|
+
- **recommends `tailwind`** for styling; used by `pletivo`'s islands (no hard requirer — it's the Preact option for island-style UIs).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: propustka
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Propustka — native login, authorization and audit
|
|
5
|
+
slot: authz
|
|
6
|
+
provides: [authentication, sso, authorization, audit, api-tokens]
|
|
7
|
+
requires: [cloudflare, d1]
|
|
8
|
+
recommends: [vozka, buzola]
|
|
9
|
+
conflicts: [cloudflare-access, auth-password-kv]
|
|
10
|
+
npm: ["@propustka/client@^0.0.6", "@propustka/core@^0.0.6"]
|
|
11
|
+
status: wip
|
|
12
|
+
sources:
|
|
13
|
+
- where: "propustka — README.md, architecture.md, packages/core/src/, packages/worker/src/"
|
|
14
|
+
read: "Native session/login architecture, app gates, token/JWKS contract, central human identity and D1 policy/audit model."
|
|
15
|
+
- where: "@propustka/client (installed packages/client/src/{session,client,fake,types}.ts)"
|
|
16
|
+
read: "The current PropustkaAuth, AuthContext, AppGates, setCookie/loginUrl and FakeIamClient surfaces. Version 0.x moves quickly; read the source."
|
|
17
|
+
- where: "propustka examples"
|
|
18
|
+
read: "Runtime gates, local operator context, SSO redirects, response cookies, schemas, and IAM bindings."
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Propustka
|
|
22
|
+
|
|
23
|
+
## What it is
|
|
24
|
+
|
|
25
|
+
The native identity, authorization and audit service for Cloudflare Workers apps.
|
|
26
|
+
There is **no Cloudflare Access edge** in front of a native Propustka app.
|
|
27
|
+
Propustka owns human SSO sessions, per-app permission tokens, `px_` service keys,
|
|
28
|
+
authorization policy and audit logs.
|
|
29
|
+
|
|
30
|
+
An app integrates over an `IAM` Worker-to-Worker service binding and uses
|
|
31
|
+
`PropustkaAuth` as its single request-auth seam. `PropustkaAuth` applies ordered
|
|
32
|
+
per-path gates, verifies cached permission JWTs locally against Propustka's JWKS,
|
|
33
|
+
and only calls the binding when it must exchange a session/key or write audit.
|
|
34
|
+
|
|
35
|
+
## When & why to use
|
|
36
|
+
|
|
37
|
+
- An internal Cloudflare app needs human login plus role/scope-based permissions.
|
|
38
|
+
- The same API supports logged-in people and `px_` service credentials.
|
|
39
|
+
- You want a central people directory, app-defined roles/scopes, revocable keys
|
|
40
|
+
and an audit trail.
|
|
41
|
+
- The target Cloudflare account already runs Propustka (normally alongside Vozka).
|
|
42
|
+
|
|
43
|
+
## When NOT to use
|
|
44
|
+
|
|
45
|
+
- The app is not a Cloudflare Worker; the integration is a service binding.
|
|
46
|
+
- There are no real authz/audit needs and a central IAM dependency is unjustified.
|
|
47
|
+
- The product requires app-owned signup/password recovery; use a product auth
|
|
48
|
+
system instead.
|
|
49
|
+
- Strict hierarchical scopes are fundamental. Propustka scope dimensions are
|
|
50
|
+
flat; containment must be resolved by the app.
|
|
51
|
+
|
|
52
|
+
## Runtime integration
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import type { AppGates, IamRpc } from '@propustka/client'
|
|
56
|
+
import { PropustkaAuth } from '@propustka/client'
|
|
57
|
+
|
|
58
|
+
const gates: AppGates = {
|
|
59
|
+
rules: [
|
|
60
|
+
{ path: '/rpc', kind: 'service' },
|
|
61
|
+
{ path: '/rpc', kind: 'human' },
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await new PropustkaAuth(env.IAM as IamRpc, APP_ID, {
|
|
66
|
+
issuer: env.PROPUSTKA_URL,
|
|
67
|
+
gates,
|
|
68
|
+
}).authenticate(request)
|
|
69
|
+
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
return Response.json(
|
|
72
|
+
{ error: { message: result.reason, loginUrl: result.loginUrl } },
|
|
73
|
+
{ status: result.status },
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!result.context.can('site.write', { type: 'site', value: siteId })) {
|
|
78
|
+
return new Response('Forbidden', { status: 403 })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ctx.waitUntil(result.context.audit({
|
|
82
|
+
action: 'site.write',
|
|
83
|
+
resourceType: 'site',
|
|
84
|
+
resourceId: siteId,
|
|
85
|
+
}))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
When `result.setCookie` is present, append it to the outgoing response. It is the
|
|
89
|
+
short-lived per-app `px_token`; dropping it forces repeated session exchanges and
|
|
90
|
+
can make login appear broken.
|
|
91
|
+
|
|
92
|
+
## Gates and login behavior
|
|
93
|
+
|
|
94
|
+
- `AppGates` are **runtime code**, not deploy-time schema. They replaced
|
|
95
|
+
Cloudflare Access policies and are passed directly to `PropustkaAuth`.
|
|
96
|
+
- Rules are ordered. A common protected path has `service` first and `human`
|
|
97
|
+
second, so a present service key is terminal while a missing key falls through
|
|
98
|
+
to the browser session.
|
|
99
|
+
- A `public` rule creates an anonymous context. A path matching no rule is denied
|
|
100
|
+
(`no_rule`) — native auth is fail-closed.
|
|
101
|
+
- A human miss includes `loginUrl`. The SPA should redirect there and set its
|
|
102
|
+
return URL to the current page, with a short bounce guard so an unprovisioned
|
|
103
|
+
identity does not loop forever.
|
|
104
|
+
|
|
105
|
+
## Authorization model
|
|
106
|
+
|
|
107
|
+
- Keep action ids, scope dimensions and the app id as shared constants. Runtime
|
|
108
|
+
`can()` checks and Vozka's `AppSchema` must use the same vocabulary.
|
|
109
|
+
- `can(action, scope?)` and `scopedTo(action, dimension)` are local pure checks on
|
|
110
|
+
the minted permission set. `scopedTo` returns `null` (all), `[]` (none), or an
|
|
111
|
+
exact list.
|
|
112
|
+
- Audit is the operation that reaches the binding on the hot path; schedule it
|
|
113
|
+
with `ctx.waitUntil`.
|
|
114
|
+
- The app's `schema` (actions/scopes/roles) is reconciled by Vozka during deploy.
|
|
115
|
+
|
|
116
|
+
## Local development and opice
|
|
117
|
+
|
|
118
|
+
Do not require a live IAM Worker locally. An explicit `DEV` var should select a
|
|
119
|
+
deterministic app-local `AuthContext` for a known operator persona. Current
|
|
120
|
+
`FakeIamClient` covers management methods such as principal listing and key
|
|
121
|
+
issue/revoke; it is **not** the replacement for `PropustkaAuth.authenticate()`.
|
|
122
|
+
|
|
123
|
+
Omit the external `IAM` service binding in the local resource graph when the
|
|
124
|
+
local runtime cannot resolve it. Off-local, fail loudly if `IAM` or
|
|
125
|
+
`PROPUSTKA_URL` is missing.
|
|
126
|
+
|
|
127
|
+
## Relationships — why
|
|
128
|
+
|
|
129
|
+
- **requires `cloudflare`** — request auth and audit use a Worker service binding.
|
|
130
|
+
- **requires `d1`** — the separately deployed Propustka service stores identity,
|
|
131
|
+
policy, sessions and audit data in D1.
|
|
132
|
+
- **conflicts `cloudflare-access`** — native Propustka replaces the Access edge;
|
|
133
|
+
combining both creates two competing login/gate systems.
|
|
134
|
+
- **conflicts `auth-password-kv`** — Propustka owns login and sessions; do not add
|
|
135
|
+
a parallel app-owned password session.
|
|
136
|
+
- **recommends `vozka`** for schema reconciliation and **`buzola`** for internal
|
|
137
|
+
app SPAs.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: queue
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare Queues (async buffering)
|
|
5
|
+
slot: null
|
|
6
|
+
provides: [message-queue, async-buffering]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: [r2, oblaka]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/queues (producer send, consumer queue() handler, batching, retries)"
|
|
14
|
+
read: "Producer .send()/.sendBatch(), the queue(batch, env) consumer handler shape, batch size/retry config."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Cloudflare Queues
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
Managed async message queue to **decouple requests/schedules from slow or bursty
|
|
22
|
+
work**. A producer `env.QUEUE.send(msg)` returns fast; a consumer
|
|
23
|
+
`queue(batch, env)` processes messages with retries — e.g. write payloads to R2,
|
|
24
|
+
or run one bounded background audit while D1 tracks its lease/status.
|
|
25
|
+
|
|
26
|
+
## When & why to use
|
|
27
|
+
|
|
28
|
+
- Bursty/spiky ingest (error storms, webhooks) where the request must return immediately (202) and work happens async.
|
|
29
|
+
- Batching expensive writes (R2 puts, D1 upserts) instead of doing them per-request.
|
|
30
|
+
- Long-running background jobs that need retry and bounded concurrency without
|
|
31
|
+
tying their lifetime to an HTTP `waitUntil`.
|
|
32
|
+
|
|
33
|
+
## When NOT to use
|
|
34
|
+
|
|
35
|
+
- Work must complete before the response (synchronous result needed).
|
|
36
|
+
- Trivial, low-volume writes — the queue is overhead.
|
|
37
|
+
|
|
38
|
+
## Impacts & gotchas
|
|
39
|
+
|
|
40
|
+
- Export a **`queue(batch, env)`** handler from the Worker alongside `fetch`. Check the lopata/RRv7 example for the exact export shape.
|
|
41
|
+
- Delivery is **at least once**. Make the consumer idempotent; for long jobs use a
|
|
42
|
+
D1 lease/attempt token and explicit terminal states so a retry cannot run two
|
|
43
|
+
copies or overwrite a newer attempt.
|
|
44
|
+
- Handle every message explicitly with `message.ack()` or `message.retry()` and
|
|
45
|
+
catch failures per message. An uncaught exception can retry the whole batch;
|
|
46
|
+
a message with no disposition is retried according to the consumer policy.
|
|
47
|
+
- Configure batch size, timeout, retry delay, max retries, concurrency and
|
|
48
|
+
visibility timeout from the real job envelope. A single expensive job often
|
|
49
|
+
wants `maxBatchSize: 1`.
|
|
50
|
+
- For ingest, return **202 fast** and let the consumer do R2 + D1. See
|
|
51
|
+
`write-split`. Do not force that batching shape onto one-job-per-message work.
|
|
52
|
+
|
|
53
|
+
## Relationships — why
|
|
54
|
+
|
|
55
|
+
- **requires `cloudflare`** — CF binding. **recommends `r2`** (the usual payload sink) and **`oblaka`** (declare producer + consumer).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: r2
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare R2 (object storage)
|
|
5
|
+
slot: null
|
|
6
|
+
provides: [object-storage, blobs]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: [oblaka, queue]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/r2 (Workers binding: get/put/delete, multipart)"
|
|
14
|
+
read: "The R2 binding API for storing/retrieving objects from a Worker."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Cloudflare R2
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
S3-compatible object storage with no egress fees. Used for **large/append-style
|
|
22
|
+
payloads** — full event bodies, uploads, source maps — keyed by id. Bound as
|
|
23
|
+
`env.R2`.
|
|
24
|
+
|
|
25
|
+
## When & why to use
|
|
26
|
+
|
|
27
|
+
- Storing payloads too big or too numerous for D1 rows (event bodies, files, source maps).
|
|
28
|
+
- Write-heavy blob ingest, especially batched through a `queue`.
|
|
29
|
+
|
|
30
|
+
## When NOT to use
|
|
31
|
+
|
|
32
|
+
- Small structured records you query by field → `d1`. Hot small keys → `kv`.
|
|
33
|
+
|
|
34
|
+
## Impacts & gotchas
|
|
35
|
+
|
|
36
|
+
- Objects are addressed by key; design a stable key scheme (e.g. `events/<projectId>/<id>`).
|
|
37
|
+
- Pair with `queue` for bursty writes: enqueue → consumer batches puts to R2 (keeps the request path fast). See `write-split`.
|
|
38
|
+
|
|
39
|
+
## Relationships — why
|
|
40
|
+
|
|
41
|
+
- **requires `cloudflare`** — CF binding. **recommends `oblaka`** (declare `R2Bucket`) and **`queue`** (buffer bursty payload writes).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: react-router
|
|
3
|
+
kind: technology
|
|
4
|
+
title: React Router v7 (SSR on Workers)
|
|
5
|
+
slot: router
|
|
6
|
+
provides: [router, ssr, loader-action-data]
|
|
7
|
+
requires: [react, vite]
|
|
8
|
+
recommends: [d1]
|
|
9
|
+
conflicts: [buzola]
|
|
10
|
+
npm: ["react-router@^7.13", "@react-router/dev@^7.13", "@react-router/node@^7.13", "isbot@^5"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "reactrouter.com (RRv7 SSR/loader/action) + installed package types"
|
|
14
|
+
read: "Current SSR/loader/action API and the EXACT import paths — they move between minors. Verify against the installed version."
|
|
15
|
+
- where: "lopata/examples/react-router/ — the canonical RRv7-SSR-on-Workers example"
|
|
16
|
+
read: "Worker entry, entry.server, route table wired against CF bindings. The #1 ground truth for this decision."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# React Router v7 (SSR on Workers)
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
Full-stack React Router v7 doing **server-side rendering on the Worker**:
|
|
24
|
+
`app/routes/*.tsx` with a server `loader`/`action` over CF bindings + a default
|
|
25
|
+
component. Self-contained, no RPC/Contember layer — lopata's own example path.
|
|
26
|
+
|
|
27
|
+
## When & why to use
|
|
28
|
+
|
|
29
|
+
- Server-rendered app reading data directly in loaders from `context.env.DB` etc.
|
|
30
|
+
- fabrika's default app architecture when SSR and route loaders fit better than a client-rendered SPA.
|
|
31
|
+
|
|
32
|
+
## When NOT to use
|
|
33
|
+
|
|
34
|
+
- A pure client SPA (no SSR), especially a Contember-style admin → `buzola`.
|
|
35
|
+
|
|
36
|
+
## Impacts & gotchas — the load-bearing ones (anti-staleness)
|
|
37
|
+
|
|
38
|
+
- **Worker entry** (`workers/app.ts`): `createRequestHandler` from **`react-router`** (NOT `@react-router/dev/server`) over `virtual:react-router/server-build`; `fetch` passes `{ env, ctx }` as the load context so loaders read `context.env.DB`. **This is the #1 thing stale memory gets wrong** — check the lopata example.
|
|
39
|
+
- **SSR renderer** (`app/entry.server.tsx`): `renderToReadableStream` from **`react-dom/server.browser`** (NOT `react-dom/server`) + `<ServerRouter>`.
|
|
40
|
+
- **Route table** (`app/routes.ts`): from `@react-router/dev/routes` — `export default [ index('routes/home.tsx'), route('cart','routes/cart.tsx'), … ]`. **Each route URL must match the opice skeleton `url` (contract).**
|
|
41
|
+
- `react-router.config.ts`: `{ ssr: true }`. tsconfig: RRv7 typegen via `rootDirs` (`.react-router/types`).
|
|
42
|
+
|
|
43
|
+
## Relationships — why
|
|
44
|
+
|
|
45
|
+
- **requires `react` + `vite`** (it's a React framework with a vite plugin).
|
|
46
|
+
- **recommends `d1`** — loaders/actions read/write D1 directly.
|
|
47
|
+
- **conflicts `buzola`** — same `router` slot. RRv7 = SSR; buzola = SPA. One router per stack.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: react
|
|
3
|
+
kind: technology
|
|
4
|
+
title: React (UI framework)
|
|
5
|
+
slot: ui-framework
|
|
6
|
+
provides: [ui-framework, components]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [react-router, tailwind]
|
|
9
|
+
conflicts: [preact]
|
|
10
|
+
npm: ["react@^19", "react-dom@^19"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "react.dev + installed react/react-dom types"
|
|
14
|
+
read: "Current React 19 APIs; for SSR-on-Workers note the server.browser render entry (see react-router)."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# React
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
The UI framework for fabrika's generated apps (React 19). Paired with a router:
|
|
22
|
+
`react-router` renders SSR on the Worker, while `buzola` builds a client SPA that
|
|
23
|
+
the Worker can serve as static assets.
|
|
24
|
+
|
|
25
|
+
## When & why to use
|
|
26
|
+
|
|
27
|
+
- Default UI layer — the routers, opice selector conventions, and design-token mockups all assume React.
|
|
28
|
+
|
|
29
|
+
## Impacts & gotchas
|
|
30
|
+
|
|
31
|
+
- SSR on Workers renders via **`renderToReadableStream` from `react-dom/server.browser`** (NOT `react-dom/server`) — the Web Streams path. (Detail lives on `react-router`.)
|
|
32
|
+
- Build accessible markup: every interactive element needs an accessible name (`<label>`/text/`aria-label`) so opice has stable selectors (see `opice`).
|
|
33
|
+
|
|
34
|
+
## Relationships — why
|
|
35
|
+
|
|
36
|
+
- **recommends `react-router`** (default SSR-on-Workers router; `buzola` is the SPA alternative, both require react) **+ `tailwind`** (styling).
|
|
37
|
+
- **conflicts `preact`** — same `ui-framework` slot; `preact` is the smaller alternative (pletivo's islands run on it).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: roj
|
|
3
|
+
kind: technology
|
|
4
|
+
title: roj — agent SDK (@roj-ai/*) for building AI agents
|
|
5
|
+
slot: agent-sdk
|
|
6
|
+
provides: [agent-runtime, llm-orchestration, streaming, sessions, event-sourcing, plugins]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [cloudflare, durable-objects, d1, r2, react]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: ["@roj-ai/sdk@^0.1.18", "@roj-ai/transport@^0.1.18", "@roj-ai/client@^0.1.18", "@roj-ai/client-react@^0.1.18"]
|
|
11
|
+
status: wip
|
|
12
|
+
sources:
|
|
13
|
+
- where: "roj/CLAUDE.md + packages/sdk/src/index.ts"
|
|
14
|
+
read: "The plugin DSL, event sourcing, preset/agent builders, session management, and LLM provider surface (Anthropic/OpenRouter/mock)."
|
|
15
|
+
- where: "roj/packages/demo/"
|
|
16
|
+
read: "A full e2e example: preset + standalone server + React SPA + snapshot tests."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# roj
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
An **agent SDK** (`@roj-ai/*`, the OSS successor to buresh): a TypeScript framework
|
|
24
|
+
for multi-agent systems with persistent sessions, event sourcing, a plugin
|
|
25
|
+
architecture (filesystem/shell/chat/limits/skills/…), and streaming LLM inference.
|
|
26
|
+
Apps define **presets** (agents + plugins + system prompt), wire an LLM provider
|
|
27
|
+
(Anthropic native, OpenRouter, or mock), and expose a REST/WebSocket API. It is
|
|
28
|
+
LLM-provider-agnostic and host-agnostic. It can be composed with Workers, Durable
|
|
29
|
+
Objects, WebSockets, D1, and R2 for a production deployment.
|
|
30
|
+
|
|
31
|
+
## When & why to use
|
|
32
|
+
|
|
33
|
+
- **Building AI agents/assistants** into an app (codegen, research, automation) with multi-turn, stateful tool calls.
|
|
34
|
+
- You want **event-sourced** sessions (auditable, replayable) and pluggable tool/skill libraries without provider lock-in.
|
|
35
|
+
|
|
36
|
+
## When NOT to use
|
|
37
|
+
|
|
38
|
+
- **One-off inference** (single prompt→response) → use the Anthropic SDK / OpenRouter directly.
|
|
39
|
+
- No multi-turn/stateful needs.
|
|
40
|
+
|
|
41
|
+
## Impacts & gotchas
|
|
42
|
+
|
|
43
|
+
- **Client/server split + async sessions** — agents run server-side; the client creates a session and streams updates over WebSocket. Plan UX for async + reconnection.
|
|
44
|
+
- **LLM key handling** — provide and encrypt the provider key; never log it. The default uses Anthropic/Claude.
|
|
45
|
+
- **Tool/sandbox security** — plugins execute shell/filesystem in a sandbox (E2B / `edvabe` locally); sanitize tool inputs.
|
|
46
|
+
- **DO hibernation** — on CF, the in-memory runtime is rebuilt from the event log via an alarm; don't delete session records.
|
|
47
|
+
|
|
48
|
+
## Relationships — why
|
|
49
|
+
|
|
50
|
+
- **slot `agent-sdk`** — the agent layer; composes with any UI/backend.
|
|
51
|
+
- **recommends `cloudflare` + `durable-objects`** (one DO per agent instance), **`d1`/`r2`** (session/artifact storage), **`react`** (`@roj-ai/client-react` chat UI). The SDK itself requires none of these — it runs on Bun/Node too. Pairs with `edvabe` for local sandboxed tool execution.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sentry
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Sentry (error tracking)
|
|
5
|
+
slot: error-tracking
|
|
6
|
+
provides: [error-tracking, exception-capture, source-maps, session-replay]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [observability]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: ["@sentry/cloudflare", "@sentry/react", "@sentry/vite-plugin", "toucan-js"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "Sentry Cloudflare Workers and React SDK documentation"
|
|
14
|
+
read: "SDK initialization, environment and release tags, trace propagation, logger integration, and error boundaries."
|
|
15
|
+
- where: "Sentry docs: Cloudflare Workers (@sentry/cloudflare or toucan-js), source-map upload (@sentry/vite-plugin / wrangler upload_source_maps), beforeSend + fingerprinting"
|
|
16
|
+
read: "Init options, source maps with SENTRY_AUTH_TOKEN, redaction, and fingerprinting for grouping."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Sentry
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
The error-tracking option: capture exceptions + stack traces (with source maps) and
|
|
24
|
+
group them. On Workers via **`@sentry/cloudflare`** (or `toucan-js` for a lighter
|
|
25
|
+
footprint), in the browser via **`@sentry/react`**, with source maps uploaded by
|
|
26
|
+
**`@sentry/vite-plugin`** or wrangler's `upload_source_maps` (gated on
|
|
27
|
+
`SENTRY_AUTH_TOKEN`). Plugs into `structured-logging` as a handler that forwards
|
|
28
|
+
`logger.error` to `captureException` at warn+.
|
|
29
|
+
|
|
30
|
+
## When & why to use
|
|
31
|
+
|
|
32
|
+
- You want managed error aggregation + alerting (and optional browser session replay) rather than building it.
|
|
33
|
+
- Production apps where un-grouped, un-sourcemapped stack traces aren't enough.
|
|
34
|
+
|
|
35
|
+
## When NOT to use
|
|
36
|
+
|
|
37
|
+
- You specifically want **self-hosted** error tracking (the `cf-fat`/error-tracker style — store issues in D1/DO, query via admin) for data residency/cost.
|
|
38
|
+
- Trivial apps where structured logs + AE metrics are enough.
|
|
39
|
+
|
|
40
|
+
## Gotchas
|
|
41
|
+
|
|
42
|
+
- **Redact in `beforeSend`** — strip auth headers / PII before send; errors easily carry secrets.
|
|
43
|
+
- **`enableRpcTracePropagation: true`** — or Durable Object / service-binding errors appear isolated from the originating request.
|
|
44
|
+
- **Source maps must be uploaded in CI** (SENTRY_AUTH_TOKEN) or stacks are minified-useless.
|
|
45
|
+
- **Control cardinality with `fingerprint`** — don't put request ids in the error message or grouping explodes; use intent-based fingerprints. Use `replaysOnErrorSampleRate`, not full session replay, for cost.
|
|
46
|
+
|
|
47
|
+
## Relationships — why
|
|
48
|
+
|
|
49
|
+
- **slot `error-tracking`** — the errors arm of `observability`; **recommends `observability`** (errors enrich traces/metrics). Bridged from `structured-logging` via a Sentry log handler.
|