ConusAI vendor v0.1.0-dev.20261027+9b1131fb

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:18 +00:00
commit f23f4e535d
159 changed files with 4293 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules/
.next/
.env*
!.env.example
next-env.d.ts
tsconfig.tsbuildinfo
playwright-report/
test-results/
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps=true
+89
View File
@@ -0,0 +1,89 @@
# ConusAI Showcase
Notes with attachments on Postgres and Blob Storage, audited, observed and
metered: every ConusAI SDK in one Next.js app built with Feature-Sliced Design
(showcase plan D11). The template is proven in this repository by
`scripts/vendor/test-template.sh` at the same commit as the SDKs it uses (D18).
## Pinned versions
Pinned on 2026-10-06 with `npm view <package> version` (showcase §2.7):
| Package | Version |
|---|---|
| next | 16.3.8 |
| react, react-dom | 19.3.0 |
| typescript | 5.9.3 (5.x: the `@conusai-sdk/plugin` peer requires ^5) |
| drizzle-orm, drizzle-kit | 0.45.3, 0.31.11 |
| postgres | 3.4.9 |
| zod | 4.6.5 |
| server-only | 0.0.1 |
| vitest | 5.0.3 |
| eslint, eslint-config-next, @eslint/eslintrc | 9.39.5 (ESLint 10 breaks the React plugin bundled by eslint-config-next), 16.3.8, 3.3.7 |
| steiger, @feature-sliced/steiger-plugin | 0.7.0, 0.8.0 |
| @playwright/test | 1.63.0 |
| playwright-core | 1.63.0 (`@axe-core/playwright`'s peer; must equal `@playwright/test`'s copy, or a lockfile-less `npm install` hoists the newest and the two `Page` types diverge under `tsc`) |
| @types/node, @types/react, @types/react-dom | 26.6.4, 19.3.0, 19.3.0 |
The `@conusai-sdk/*` packages the template imports are pinned in
`package.json` at the version the SDK source tree carries at the same commit;
`test/sdk-deps.test.ts` fails when an imported package is missing or its pin
differs. The template ships **no `package-lock.json`** (decision of
2026-10-09, showcase plan D19): a lockfile without the scoped entries makes
`npm ci` fail, and one carrying them records `resolved` URLs of one install's
registry host, which is wrong on every other. The platform build runs
`npm install` against the `.npmrc` the deployer mounts as a BuildKit secret
(D6/D7), whose scoped registry serves exactly the pinned versions;
`test-template.sh` proves the same locally from the bundle's tarballs.
## Layout
- `app/` routes only; each file re-exports from `src/views`, `src/app` or a
feature's route handler.
- `src/app`, `src/views`, `src/widgets`, `src/features`, `src/entities`,
`src/shared`: the FSD layers, imports point downward only (checked by `steiger`).
- `src/shared/config/env.ts`: the environment, parsed once with zod (D12).
- `src/shared/api/*/server.ts`: server-only SDK clients (`server-only`).
- `src/shared/lib/visitor.ts`: the signed `sc_vid` visitor cookie (D16).
## What each slice demonstrates
| Slice | Demonstrates |
|---|---|
| `features/create-note` | A note in Postgres, audited as `note.created`; NATS announces it and Qdrant indexes it when those are linked. |
| `features/edit-note`, `features/delete-note` | Writes scoped to the author, each audited (`note.updated`, `note.deleted`). |
| `features/upload-file`, `features/delete-file` | Attachments in Blob Storage, one usage event per upload with an idempotency key; a per-minute quota when KV is linked. |
| `features/download-file` | An attachment's bytes at `/files/<id>`, downloaded server-side through the Blob API and streamed to the visitor who attached it: the platform's blob URL needs the deployment token, so the browser cannot open it. |
| `features/publish-note-event` | An author re-announces a note on NATS (`note.announced` in the audit log); another visitor is refused. |
| `features/search-notes` | Similar-note search in Qdrant, when Qdrant is linked. |
| `features/export-notes` | The visitor's notes written as one JSON object to the dedicated S3 bucket, when S3 is linked. |
| `features/rate-limit` | The uploads left this minute, read from KV, when KV is linked. |
| `features/simulate-traffic` | A scripted visitor journey that produces observed page views, a started journey and an error. |
| `widgets/note-list`, `widgets/attachments` | The visitor's notes and their attachments, with the controls each feature provides. |
| `widgets/audit-timeline`, `widgets/usage-meter` | The platform's audit events for this project, and the visitor's metered uploads and bytes. |
| `widgets/enable-card`, `widgets/module-panel` | One slot per optional module: its feature when linked, its enable card (text from the platform's catalog) when not (D13). |
| `views/dashboard`, `views/audit`, `views/platform` | The composed pages; the platform page lists each optional service as linked or not. |
## Optional modules
The showcase reads the variables the platform injects for each linked service
(D8, D13). A module without its variables shows its enable card instead of its
feature. Its variables are `REDIS_URL` (KV), `NATS_URL` and `NATS_TOKEN`,
`QDRANT_URL` and `QDRANT_API_KEY`, `GIT_URL`, and `AWS_ENDPOINT_URL`, `S3_BUCKET`,
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. The S3 names are the ones the platform
injects for an S3 service.
## Commands
- `npm run typecheck`, `npm run steiger`, `npm run lint`, `npm test`, `npm run build`
- `npm run e2e` runs the Playwright suite against a production build. It starts
`e2e/fake-platform.mjs` in place of the platform and withholds the optional
module variables, so every enable card shows. The suite covers creating a note
and its audit entry, an attachment moving the usage meter, the enable cards,
axe (no serious or critical issues on each page) and the Core Web Vitals
budgets (LCP at most 2.5 s, INP at most 200 ms) measured in Chromium.
- `npm test` also runs `src/shared/lib/optional.live.test.ts` against live NATS,
Qdrant and S3 when `NATS_URL`, `QDRANT_URL` and `AWS_ENDPOINT_URL` are set; without
them it is skipped.
- `bash scripts/vendor/test-template.sh` from the repository root runs all of them against the bundle;
`bash scripts/vendor/test-template.sh --e2e` adds the Playwright suite.
+17
View File
@@ -0,0 +1,17 @@
import { sql } from 'drizzle-orm'
import { db, serverErrors, withRequest } from '@/shared/api'
// 200 when Postgres answers; the platform probes this path (health_check_path).
export const dynamic = 'force-dynamic'
export async function GET(request: Request) {
return withRequest(request, async () => {
try {
await db().execute(sql`select 1`)
return Response.json({ status: 'ok' })
} catch (error) {
serverErrors().captureException(error)
return Response.json({ status: 'unavailable' }, { status: 503 })
}
})
}
+3
View File
@@ -0,0 +1,3 @@
export const dynamic = 'force-dynamic'
export { AuditPage as default } from '@/views/audit'
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
export { GET } from '@/features/download-file'
+1
View File
@@ -0,0 +1 @@
export { RootLayout as default, metadata } from '@/app/layouts/root'
+1
View File
@@ -0,0 +1 @@
export { DashboardPage as default } from '@/views/dashboard'
+3
View File
@@ -0,0 +1,3 @@
export const dynamic = 'force-dynamic'
export { PlatformPage as default } from '@/views/platform'
+6
View File
@@ -0,0 +1,6 @@
{
"schema_version": 2,
"health_check": {
"path": "/api/health"
}
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'postgresql',
schema: ['./src/entities/note/model/schema.ts', './src/entities/file/model/schema.ts'],
out: './drizzle',
})
+8
View File
@@ -0,0 +1,8 @@
CREATE TABLE "notes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"body" text DEFAULT '' NOT NULL,
"author_id" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
+13
View File
@@ -0,0 +1,13 @@
CREATE TABLE "files" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"note_id" uuid NOT NULL,
"author_id" text NOT NULL,
"name" text NOT NULL,
"content_type" text NOT NULL,
"size" integer NOT NULL,
"url" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "files" ADD CONSTRAINT "files_note_id_notes_id_fk" FOREIGN KEY ("note_id") REFERENCES "notes"("id") ON DELETE cascade ON UPDATE no action;
+72
View File
@@ -0,0 +1,72 @@
{
"id": "cbd1580b-e469-4710-a530-13305ad7a627",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.notes": {
"name": "notes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"author_id": {
"name": "author_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+135
View File
@@ -0,0 +1,135 @@
{
"id": "6c019c3b-9836-4d91-99a6-a1f20b9fe445",
"prevId": "cbd1580b-e469-4710-a530-13305ad7a627",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.notes": {
"name": "notes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"author_id": {
"name": "author_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.files": {
"name": "files",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"note_id": {
"name": "note_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"author_id": {
"name": "author_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content_type": {
"name": "content_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1791259122531,
"tag": "0000_init",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1791259684816,
"tag": "0001_files",
"breakpoints": true
}
]
}
+61
View File
@@ -0,0 +1,61 @@
import { expect, test, type Page } from '@playwright/test'
// Showcase D23: register() in instrumentation.ts starts the OTLP exporters when the platform
// injects OTEL_EXPORTER_OTLP_ENDPOINT, and every server action logs through shared/api/observe's
// logger(). This proves the whole chain against a real `next start` production server (not dev,
// not a mock of register()): a log a server action emits reaches the fake platform's
// /otel/v1/logs route within the exporter's 5 s batch interval.
const platform = 'http://127.0.0.1:4010'
interface LogRecordJson {
body?: { stringValue?: string }
attributes?: Array<{ key: string; value: { stringValue?: string } }>
}
type OtelState = { otel: { logs: Array<{ authorization: string | null; body: { resourceLogs: Array<{ scopeLogs: Array<{ logRecords: LogRecordJson[] }> }> } }> } }
async function otelLogRecords(): Promise<LogRecordJson[]> {
const response = await fetch(`${platform}/__e2e/state`)
const state = (await response.json()) as OtelState
return state.otel.logs.flatMap((post) => post.body.resourceLogs.flatMap((r) => r.scopeLogs.flatMap((s) => s.logRecords)))
}
function attr(record: LogRecordJson, key: string): string | undefined {
return record.attributes?.find((kv) => kv.key === key)?.value.stringValue
}
async function createNote(page: Page, title: string): Promise<void> {
await page.goto('/')
await page.getByLabel('Title').fill(title)
await page.getByLabel('Body').fill('observability e2e')
await page.getByRole('button', { name: 'Create note' }).click()
await expect(page.getByRole('heading', { name: title })).toBeVisible()
}
test('a note.created server action log reaches the OTLP receiver within the batch interval', async ({ page }) => {
const title = `otlp ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
await createNote(page, title)
await expect
.poll(
async () => (await otelLogRecords()).some((r) => r.body?.stringValue === 'note.created' && attr(r, 'outcome') === 'ok'),
// BatchExporter's default flush interval is 5 s (exporter.ts); this allows one full
// interval plus slack for the request round trip and CI scheduling jitter.
{ timeout: 9_000 },
)
.toBe(true)
})
test('the OTLP request carries the deployment token, not the public client', async ({ page }) => {
const title = `otlp-auth ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
await createNote(page, title)
await expect
.poll(async () => (await otelLogRecords()).some((r) => r.body?.stringValue === 'note.created'), { timeout: 9_000 })
.toBe(true)
const response = await fetch(`${platform}/__e2e/state`)
const state = (await response.json()) as OtelState
expect(state.otel.logs.every((post) => post.authorization === 'Bearer e2e-deployment-token')).toBe(true)
})
+40
View File
@@ -0,0 +1,40 @@
import { expect, test } from '@playwright/test'
// Proves, against a real production build (next build + next start, plan D18), that a
// visit actually uploads a session replay segment — not just that the SDK is configured
// to try. instrumentation-client.ts's /api/_conusai/* calls are same-origin and answered
// by the platform's edge on a real deployment (docs/how-to/use-platform-analytics-in-an-
// app.md §"Why this works"); this app has no edge in front of it here, so the test stands
// in for it with the minimum the SDK needs: a session id from /ids, and a place to post
// the envelope. This caught the stage regression (2026-10-11, showcase plan D24) where
// mode: 'always' alone produced zero /api/_conusai/envelope requests because replay is a
// 'consented' item and the default consent mode never sends those.
const SESSION_ID = '11111111-1111-1111-1111-111111111111'
test('a visit uploads a session replay segment within 15 s on a production build', async ({ page }) => {
let envelopePosts = 0
// Analytics, vitals and errors also call the same-origin /api/_conusai/* paths; answer
// them all generically so the run is quiet and deterministic, except the two the
// recorder actually needs.
await page.route('**/api/_conusai/**', (route) => {
const url = route.request().url()
if (url.includes('/api/_conusai/ids')) {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ v: 1, session_id: SESSION_ID }) })
}
if (url.includes('/api/_conusai/envelope')) {
envelopePosts += 1
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
}
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
})
await page.goto('/')
await page.getByLabel('Title').click()
await page.mouse.wheel(0, 200)
await page.goto('/audit')
await page.goto('/')
await expect.poll(() => envelopePosts, { timeout: 15_000, message: 'no POST to /api/_conusai/envelope (replay segment)' }).toBeGreaterThan(0)
})
+122
View File
@@ -0,0 +1,122 @@
import AxeBuilder from '@axe-core/playwright'
import { expect, test, type Page } from '@playwright/test'
// The end-to-end run of the showcase (plan S4.8): a note, an attachment, the audit
// entry and usage on the platform, the enable cards, accessibility and the Core Web
// Vitals budgets. The platform is the fake in e2e/fake-platform.mjs.
const platform = 'http://127.0.0.1:4010'
type PlatformState = {
audit: Array<{ action: string; resource_id: string | null }>
usage: Array<{ idempotency_key: string; meter: string }>
}
async function platformState(): Promise<PlatformState> {
const response = await fetch(`${platform}/__e2e/state`)
return (await response.json()) as PlatformState
}
function uniqueTitle(label: string): string {
return `${label} ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
}
async function createNote(page: Page, title: string): Promise<void> {
await page.goto('/')
await page.getByLabel('Title').fill(title)
await page.getByLabel('Body').fill('written by the end-to-end run')
await page.getByRole('button', { name: 'Create note' }).click()
await expect(page.getByRole('heading', { name: title })).toBeVisible()
}
test('a note is audited on the platform and listed on the audit page', async ({ page }) => {
const title = uniqueTitle('audited')
await createNote(page, title)
await expect
.poll(async () => (await platformState()).audit.some((event) => event.action === 'note.created'), { timeout: 15_000 })
.toBe(true)
await page.goto('/audit')
await expect(page.getByText('note.created').first()).toBeVisible()
})
test('an attachment moves the usage meter and is metered on the platform', async ({ page }) => {
const title = uniqueTitle('attached')
await createNote(page, title)
const meter = page.getByText(/uploads, \d+ bytes stored/)
await expect(meter).toHaveText(/^0 uploads, 0 bytes stored$/)
const usageBefore = (await platformState()).usage.length
const note = page.getByRole('listitem').filter({ hasText: title })
await note.getByLabel('Attachment').setInputFiles({
name: 'notes.txt',
mimeType: 'text/plain',
buffer: Buffer.from('sixteen bytes!!!'),
})
await note.getByRole('button', { name: 'Attach', exact: true }).click()
await expect(meter).toHaveText(/^1 uploads, 16 bytes stored$/)
await expect
.poll(async () => (await platformState()).usage.length, { timeout: 15_000 })
.toBeGreaterThan(usageBefore)
// The attachment downloads through the app: the platform's blob URL needs the
// deployment token, which the browser does not have.
const href = await note.getByRole('link', { name: 'notes.txt' }).getAttribute('href')
expect(href).toMatch(/^\/files\/[0-9a-f-]+$/)
const download = await page.request.get(href ?? '')
expect(download.status()).toBe(200)
expect(await download.text()).toBe('sixteen bytes!!!')
})
test('each optional module without its variables shows its enable card', async ({ page }) => {
await page.goto('/')
await expect(page.getByText(/to enable this\.$/)).toHaveCount(5)
await expect(page.getByText('Link KV Store to enable this.')).toBeVisible()
await expect(page.getByText(/uploads left this minute/)).toHaveCount(0)
})
for (const path of ['/', '/audit', '/platform']) {
test(`axe finds no serious or critical issues on ${path}`, async ({ page }) => {
await page.goto(path)
const results = await new AxeBuilder({ page }).analyze()
const blocking = results.violations
.filter((violation) => violation.impact === 'serious' || violation.impact === 'critical')
.map((violation) => `${violation.id} (${violation.nodes.length} node(s))`)
expect(blocking).toEqual([])
})
}
test('the dashboard meets the Core Web Vitals budgets: LCP at most 2.5 s, INP at most 200 ms', async ({ page }) => {
await page.addInitScript(() => {
const vitals = window as unknown as { lcp: number; inp: number }
vitals.lcp = 0
vitals.inp = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) vitals.lcp = entry.startTime
}).observe({ type: 'largest-contentful-paint', buffered: true })
// Event Timing entries carry the interaction id and the duration INP is built from.
new PerformanceObserver((list) => {
for (const entry of list.getEntries() as Array<PerformanceEntry & { interactionId?: number }>) {
if ((entry.interactionId ?? 0) > 0) vitals.inp = Math.max(vitals.inp, entry.duration)
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 } as PerformanceObserverInit)
})
await page.goto('/', { waitUntil: 'load' })
await page.getByLabel('Title').fill(uniqueTitle('vitals'))
await page.getByLabel('Body').fill('an interaction for the input delay budget')
await page.getByRole('button', { name: 'Create note' }).click()
await expect(page.getByText('an interaction for the input delay budget')).toBeVisible()
const vitals = await page.evaluate(() => {
const measured = window as unknown as { lcp: number; inp: number }
return { lcp: measured.lcp, inp: measured.inp }
})
expect(vitals.lcp).toBeGreaterThan(0)
expect(vitals.lcp).toBeLessThanOrEqual(2500)
expect(vitals.inp).toBeLessThanOrEqual(200)
})
+18
View File
@@ -0,0 +1,18 @@
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTypescript from 'eslint-config-next/typescript'
const config = [
{ ignores: ['.next/**', 'node_modules/**', 'drizzle/**'] },
...nextVitals,
...nextTypescript,
{
rules: {
// `useActionState`'s action takes (state, formData); an action that only
// needs the latter still has to name the former. `_`-prefixed is the
// call site's way of saying "unused on purpose".
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
},
]
export default config
+34
View File
@@ -0,0 +1,34 @@
// Runs in the browser before the app. On the platform the page's own origin is
// the collector, so no configuration is needed here.
//
// Replay mode 'always' (showcase plan D22): the SDK's default ('onError') only uploads a
// replay when an error is captured or replay.flush() is called, so a plain showcase visit
// with no error produces nothing to show on Analytics > Session Replays. 'always' streams
// from page start regardless of errors, so every visit is recorded.
//
// consent: 'opt-out' (showcase plan D24): replay (like identify) is a 'consented' item
// (@conusai-sdk/observe's core/consent.ts verdict()), and the default consent mode,
// 'anonymous', never sends consented items — mode: 'always' alone does not start the
// recorder at all, because canRecord() gates on core.permits('consented'). The showcase
// has no consent banner (plan D16: no user auth), so 'opt-out' starts the session granted;
// GPC/DNT still revokes it (respectGPC defaults to true).
//
// Every module explicit, on purpose (showcase plan D25): @conusai-sdk/observe
// enables only `errors` by default — analytics, vitals, traces, replay and logs
// are each off unless named under `modules`. A showcase that forgot one would
// show nothing on that console page after a real visit (first seen 2026-10-11,
// stage: Analytics read 0 visitors/0 sessions and Observe read "Traces off"/
// "Audit off" because only replay was enabled). test/instrumentation-client.test.ts
// fails if any of the five is left off or set to a bare `false`.
import { instrumentationClient } from '@conusai-sdk/observe-next'
instrumentationClient({
consent: 'opt-out',
modules: {
analytics: true,
vitals: true,
traces: true,
replay: { mode: 'always' },
logs: true,
},
})
+4
View File
@@ -0,0 +1,4 @@
// Next calls this once per server process. The Node error client starts here (plan §4.4), and so
// do the OTLP log/trace exporters, when the platform injected OTEL_EXPORTER_OTLP_ENDPOINT (D23);
// shared/api/observe's `logger()` is how the rest of the app reaches the logger this starts.
export { register } from '@conusai-sdk/observe-next'
+16
View File
@@ -0,0 +1,16 @@
// Applies the SQL migrations in drizzle/ before the app starts (plan D15).
// `npm start` runs it first, so a deploy never serves against an old schema.
import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres'
const url = process.env.POSTGRES_URL
if (!url) {
console.error('POSTGRES_URL is required to apply migrations')
process.exit(1)
}
const client = postgres(url, { max: 1 })
await migrate(drizzle(client), { migrationsFolder: 'drizzle' })
await client.end()
console.log('migrations applied')
+13
View File
@@ -0,0 +1,13 @@
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactStrictMode: true,
poweredByHeader: false,
experimental: {
// Uploads post to a server action, whose body Next caps at 1 MB by default;
// the app accepts attachments up to 5 MiB (entities/file MAX_UPLOAD_BYTES).
serverActions: { bodySizeLimit: '6mb' },
},
}
export default nextConfig
+56
View File
@@ -0,0 +1,56 @@
{
"name": "conusai-showcase",
"version": "0.1.0",
"private": true,
"description": "ConusAI Showcase: notes with attachments on Postgres and Blob Storage, every ConusAI SDK in one Next.js app.",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "node migrate.mjs && next start",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"steiger": "steiger src",
"test": "vitest run",
"e2e": "playwright test",
"migrate": "node migrate.mjs",
"db:generate": "drizzle-kit generate"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1146.0",
"@conusai-sdk/api": "0.1.4-dev.20261027.g9b1131fb",
"@conusai-sdk/blob": "0.0.7-dev.20261027.g9b1131fb",
"@conusai-sdk/kv": "0.0.5-dev.20261027.g9b1131fb",
"@conusai-sdk/node-sdk": "0.2.6-dev.20261027.g9b1131fb",
"@conusai-sdk/observe": "0.1.0-dev.20261027.g9b1131fb",
"@conusai-sdk/observe-next": "0.1.0-dev.20261027.g9b1131fb",
"@conusai-sdk/observe-node": "0.1.1-dev.20261027.g9b1131fb",
"@conusai-sdk/observe-react": "0.1.0-dev.20261027.g9b1131fb",
"@conusai-sdk/usage": "0.1.1-dev.20261027.g9b1131fb",
"drizzle-orm": "0.45.3",
"nats": "2.29.3",
"next": "16.4.2",
"postgres": "3.4.9",
"react": "19.3.0",
"react-dom": "19.3.0",
"server-only": "0.0.1",
"zod": "4.6.5"
},
"devDependencies": {
"@axe-core/playwright": "4.13.0",
"@conusai-sdk/observe-testing": "0.1.0-dev.20261027.g9b1131fb",
"@eslint/eslintrc": "3.3.7",
"@feature-sliced/steiger-plugin": "0.8.0",
"@playwright/test": "1.63.0",
"@types/node": "26.6.4",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"drizzle-kit": "0.31.11",
"eslint": "9.39.5",
"eslint-config-next": "16.3.8",
"playwright-core": "1.63.0",
"steiger": "0.7.0",
"typescript": "5.9.3",
"vite": "8.3.4",
"vitest": "5.0.3"
}
}
+60
View File
@@ -0,0 +1,60 @@
import { defineConfig } from '@playwright/test'
// The app runs against @conusai-kit/testing's fake platform. The optional
// module variables are withheld, so the dashboard shows every enable card (plan D13).
const platform = 'http://127.0.0.1:4010'
const fakePlatformCommand = process.env.CONUSAI_FAKE_PLATFORM_COMMAND
if (!fakePlatformCommand) {
throw new Error('CONUSAI_FAKE_PLATFORM_COMMAND must run @conusai-kit/testing/fake-platform')
}
// The variables of the optional modules, withheld from the app under test.
const optional = new Set([
'NATS_URL',
'NATS_TOKEN',
'QDRANT_URL',
'QDRANT_API_KEY',
'REDIS_URL',
'GIT_URL',
'AWS_ENDPOINT_URL',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
])
const base = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined && !optional.has(entry[0])),
)
export default defineConfig({
testDir: 'e2e',
workers: 1,
reporter: [['list']],
use: { baseURL: 'http://localhost:3000' },
webServer: [
{
command: fakePlatformCommand,
url: `${platform}/__e2e/health`,
reuseExistingServer: false,
},
{
command: 'npm run start',
url: 'http://localhost:3000/api/health',
reuseExistingServer: false,
timeout: 120_000,
env: {
...base,
CONUSAI_API_URL: `${platform}/api`,
CONUSAI_API_TOKEN: 'e2e-deployment-token',
CONUSAI_PROJECT_ID: 'e2e',
CONUSAI_DSN: "http://e2e@127.0.0.1:4010/1",
CONUSAI_RELEASE: 'e2e',
USAGE_ORG_SLUG: 'e2e',
// Mirrors env_resolver.rs's OTEL_* derivation from CONUSAI_API_URL/CONUSAI_API_TOKEN
// (showcase D23): without these, register() never calls initOtlp() and the otlp e2e
// spec would pass for the wrong reason (nothing ever attempted, rather than delivered).
OTEL_EXPORTER_OTLP_ENDPOINT: `${platform}/api/otel`,
OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf',
OTEL_EXPORTER_OTLP_HEADERS: 'Authorization=Bearer e2e-deployment-token',
OTEL_SERVICE_NAME: 'showcase-e2e',
},
},
],
})
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<rect width="32" height="32" rx="6" fill="#2563eb" />
<text x="16" y="22" font-family="system-ui, sans-serif" font-size="16" font-weight="700" fill="#ffffff" text-anchor="middle">C</text>
</svg>

After

Width:  |  Height:  |  Size: 283 B

+232
View File
@@ -0,0 +1,232 @@
/* Global stylesheet for the Showcase.
*
* The template deliberately ships no Tailwind or CSS framework (D11 keeps
* every layer dependency-light), so this file carries the whole design with
* element selectors only — no class names exist in the markup. Style new
* markup with semantic elements (main, section, aside, form, ol/ul, pre)
* rather than adding utility classes.
*/
:root {
--bg: #f6f7f9;
--surface: #ffffff;
--border: #e2e5ea;
--text: #1a202c;
--muted: #5b6472;
--accent: #4f46e5;
--accent-hover: #4338ca;
--danger: #b91c1c;
--radius: 10px;
--shadow: 0 1px 2px rgb(16 24 40 / 0.06);
}
* {
box-sizing: border-box;
}
html {
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
Helvetica,
Arial,
sans-serif;
line-height: 1.55;
color: var(--text);
background: var(--bg);
}
body {
margin: 0;
}
main {
max-width: 56rem;
margin: 0 auto;
padding: 2rem 1.25rem 4rem;
}
h1 {
font-size: 1.75rem;
line-height: 1.2;
margin: 0 0 0.5rem;
}
h2 {
font-size: 1.15rem;
margin: 1.75rem 0 0.5rem;
}
p {
margin: 0.5rem 0;
color: var(--muted);
}
main > p:first-of-type {
margin-top: 0;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Forms: the create/edit note forms and every action button. */
form {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin: 1.25rem 0;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.9rem;
font-weight: 500;
color: var(--text);
}
input,
textarea {
font: inherit;
padding: 0.5rem 0.65rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
color: var(--text);
}
input:focus-visible,
textarea:focus-visible,
button:focus-visible,
a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
textarea {
resize: vertical;
}
button {
align-self: flex-start;
font: inherit;
font-weight: 500;
padding: 0.5rem 1rem;
border: 1px solid transparent;
border-radius: var(--radius);
background: var(--accent);
color: #fff;
cursor: pointer;
}
button:hover {
background: var(--accent-hover);
}
form[action=''] + button,
main > button {
margin-bottom: 1.25rem;
}
/* Lists: notes (ul) and the audit timeline (ol). */
ul,
ol {
list-style: none;
margin: 1rem 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
ul li,
ol li {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 0.9rem 1rem;
}
ul li h2 {
margin-top: 0;
}
time {
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* The module panel: one card per optional service, enable cards included. */
section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: 0.9rem;
margin: 1.5rem 0;
}
section > div {
display: flex;
}
section > div > aside,
section > div > form {
flex: 1;
margin: 0;
}
section aside {
background: var(--surface);
border: 1px dashed var(--border);
border-radius: var(--radius);
padding: 0.9rem 1rem;
}
section aside p {
margin: 0;
}
section aside p + p {
margin-top: 0.35rem;
font-size: 0.85rem;
}
section form {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.9rem 1rem;
}
section button {
margin: 0;
}
/* The platform page's raw description payload. */
pre {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.9rem 1rem;
overflow-x: auto;
font-size: 0.85rem;
}
@media (max-width: 40rem) {
main {
padding: 1.25rem 1rem 3rem;
}
}
+31
View File
@@ -0,0 +1,31 @@
import type { Metadata } from 'next'
import type { ReactNode } from 'react'
import { Observe } from '@/app/observability/observe'
import '../globals.css'
/** The document title every page inherits (axe: document-title). */
export const metadata: Metadata = {
title: 'ConusAI Showcase',
description: 'Notes, attachments, audit and usage on the ConusAI platform',
// `app/favicon.ico` and `app/icon.svg` already serve `/favicon.ico` and
// `/icon.svg` through Next's own file convention; `icons.icon` here is what
// makes the page's own <link> tag name `/favicon.svg` (served from
// `public/favicon.svg`) instead of `/icon.svg`, since both paths 404'd on
// every visit before this (stage sweep, 2026-10-11).
icons: {
icon: [
{ url: '/favicon.svg', type: 'image/svg+xml' },
{ url: '/favicon.ico', sizes: 'any' },
],
},
}
export function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Observe>{children}</Observe>
</body>
</html>
)
}
+13
View File
@@ -0,0 +1,13 @@
'use client'
import type { ReactNode } from 'react'
import { ErrorBoundary, ObserveProvider } from '@conusai-sdk/observe-react'
/** The browser observability root: the SDK instance, and a boundary that reports what React catches. */
export function Observe({ children }: { children: ReactNode }) {
return (
<ObserveProvider>
<ErrorBoundary fallback={<p>Something went wrong. The error has been reported.</p>}>{children}</ErrorBoundary>
</ObserveProvider>
)
}
+50
View File
@@ -0,0 +1,50 @@
import { and, eq } from 'drizzle-orm'
import type { drizzle } from 'drizzle-orm/postgres-js'
import { files, type FileRow } from '@/entities/file/model/schema'
/** The handle the shared `db()` returns (`@/shared/api`). */
type Db = ReturnType<typeof drizzle>
export type NewFile = Omit<FileRow, 'id' | 'createdAt'>
/**
* Attachment persistence. Writes name their author, as the notes repository does:
* a visitor removes only the files they attached.
*/
export function fileRepository(db: Db) {
return {
async create(input: NewFile): Promise<FileRow> {
const [row] = await db.insert(files).values(input).returning()
if (!row) throw new Error('the insert returned no row')
return row
},
async listByNote(noteId: string): Promise<FileRow[]> {
return db.select().from(files).where(eq(files.noteId, noteId))
},
/** The attachment, or `undefined` when `authorId` did not attach it. */
async get(id: string, authorId: string): Promise<FileRow | undefined> {
const [row] = await db
.select()
.from(files)
.where(and(eq(files.id, id), eq(files.authorId, authorId)))
.limit(1)
return row
},
/** Every attachment the visitor made, across their notes. */
async listByAuthor(authorId: string): Promise<FileRow[]> {
return db.select().from(files).where(eq(files.authorId, authorId))
},
/** The removed file, or `undefined` when `authorId` did not attach it. */
async remove(id: string, authorId: string): Promise<FileRow | undefined> {
const [row] = await db
.delete(files)
.where(and(eq(files.id, id), eq(files.authorId, authorId)))
.returning()
return row
},
}
}
+4
View File
@@ -0,0 +1,4 @@
export { files, type FileRow } from './model/schema'
export { fileId } from './model/id'
export { MAX_UPLOAD_BYTES, ALLOWED_CONTENT_TYPES, uploadLimits, type UploadCandidate } from './model/limits'
export { fileRepository, type NewFile } from './api/repository'
+4
View File
@@ -0,0 +1,4 @@
import { z } from 'zod'
/** A file id as it arrives in a URL or a form: a UUID, nothing else. */
export const fileId = z.uuid()
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { MAX_UPLOAD_BYTES, uploadLimits } from '@/entities/file'
describe('uploadLimits', () => {
const ok = { name: 'plan.pdf', size: 1024, contentType: 'application/pdf' }
it('accepts an allowed type within the size limit', () => {
expect(uploadLimits.parse(ok)).toEqual(ok)
})
it('refuses a file over the size limit', () => {
expect(() => uploadLimits.parse({ ...ok, size: MAX_UPLOAD_BYTES + 1 })).toThrow(/5 MiB/)
})
it('refuses an empty file', () => {
expect(() => uploadLimits.parse({ ...ok, size: 0 })).toThrow(/empty/)
})
it('refuses a content type that is not on the list', () => {
expect(() => uploadLimits.parse({ ...ok, contentType: 'application/x-msdownload' })).toThrow(/not accepted/)
})
})
+22
View File
@@ -0,0 +1,22 @@
import { z } from 'zod'
/** The largest attachment accepted, in bytes (5 MiB). */
export const MAX_UPLOAD_BYTES = 5 * 1024 * 1024
/** The content types an attachment may have. Anything else is refused before upload. */
export const ALLOWED_CONTENT_TYPES = [
'image/png',
'image/jpeg',
'image/gif',
'application/pdf',
'text/plain',
'text/markdown',
] as const
export const uploadLimits = z.object({
name: z.string().trim().min(1, 'A file needs a name').max(255),
size: z.number().int().min(1, 'The file is empty').max(MAX_UPLOAD_BYTES, 'The file is larger than 5 MiB'),
contentType: z.enum(ALLOWED_CONTENT_TYPES, { error: 'That file type is not accepted' }),
})
export type UploadCandidate = z.infer<typeof uploadLimits>
+19
View File
@@ -0,0 +1,19 @@
import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
/**
* An attachment on a note, stored in Blob Storage. `note_id` is a foreign key to
* notes(id) with ON DELETE CASCADE, added by the migration: an entity cannot
* import another entity's table (FSD), so the schema here carries the column only.
*/
export const files = pgTable('files', {
id: uuid('id').primaryKey().defaultRandom(),
noteId: uuid('note_id').notNull(),
authorId: text('author_id').notNull(),
name: text('name').notNull(),
contentType: text('content_type').notNull(),
size: integer('size').notNull(),
url: text('url').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export type FileRow = typeof files.$inferSelect
+68
View File
@@ -0,0 +1,68 @@
import { randomUUID } from 'node:crypto'
import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { noteRepository, type NoteDb } from '@/entities/note'
// Runs against a real Postgres: POSTGRES_URL must name one. Each run migrates a
// schema of its own and drops it afterwards, so runs never share rows.
const url = process.env.POSTGRES_URL
const schema = `note_test_${randomUUID().replaceAll('-', '')}`
let admin: postgres.Sql
let client: postgres.Sql
let repo: ReturnType<typeof noteRepository>
const ALICE = 'visitor-alice'
const BOB = 'visitor-bob'
beforeAll(async () => {
expect(url, 'POSTGRES_URL must name a test Postgres').toBeTruthy()
admin = postgres(url as string, { max: 1 })
await admin.unsafe(`create schema "${schema}"`)
client = postgres(url as string, { max: 1, connection: { search_path: schema } })
const db = drizzle(client) as NoteDb
await migrate(db, { migrationsFolder: 'drizzle', migrationsSchema: schema })
repo = noteRepository(db)
})
afterAll(async () => {
await client?.end()
await admin?.unsafe(`drop schema if exists "${schema}" cascade`)
await admin?.end()
})
describe('noteRepository', () => {
it('creates a note and reads it back with its author', async () => {
const created = await repo.create({ title: 'Plan', body: 'draft', authorId: ALICE })
const read = await repo.get(created.id)
expect(read).toMatchObject({ title: 'Plan', body: 'draft', authorId: ALICE })
})
it('lists only the notes of the asked-for author', async () => {
await repo.create({ title: 'Bob note', body: '', authorId: BOB })
const aliceNotes = await repo.listByAuthor(ALICE)
expect(aliceNotes.length).toBeGreaterThan(0)
expect(aliceNotes.every((note) => note.authorId === ALICE)).toBe(true)
})
it('lets the author update their own note', async () => {
const created = await repo.create({ title: 'Old', body: '', authorId: ALICE })
const updated = await repo.update(created.id, ALICE, { title: 'New', body: 'edited' })
expect(updated).toMatchObject({ id: created.id, title: 'New', body: 'edited' })
})
it('does not let another visitor update the note', async () => {
const created = await repo.create({ title: 'Mine', body: '', authorId: ALICE })
const attempt = await repo.update(created.id, BOB, { title: 'Hijacked', body: '' })
expect(attempt).toBeUndefined()
expect((await repo.get(created.id))?.title).toBe('Mine')
})
it('removes only for the author', async () => {
const created = await repo.create({ title: 'Doomed', body: '', authorId: ALICE })
expect(await repo.remove(created.id, BOB)).toBe(false)
expect(await repo.remove(created.id, ALICE)).toBe(true)
expect(await repo.get(created.id)).toBeUndefined()
})
})
+59
View File
@@ -0,0 +1,59 @@
import { and, desc, eq } from 'drizzle-orm'
import type { drizzle } from 'drizzle-orm/postgres-js'
import { notes, type Note } from '@/entities/note/model/schema'
import type { NoteInput } from '@/entities/note/model/input'
/** The handle the shared `db()` returns (`@/shared/api/db/server`). */
export type NoteDb = ReturnType<typeof drizzle>
/**
* Note persistence over one database handle. The handle is a parameter so a test
* can run the same code against its own Postgres.
*
* Every write names its author in the `where`: a visitor can change only the
* notes they wrote (tenancy, D16).
*/
export function noteRepository(db: NoteDb) {
return {
async create(input: NoteInput & { authorId: string }): Promise<Note> {
const [row] = await db.insert(notes).values(input).returning()
if (!row) throw new Error('the insert returned no row')
return row
},
async get(id: string): Promise<Note | undefined> {
const [row] = await db.select().from(notes).where(eq(notes.id, id)).limit(1)
return row
},
async listByAuthor(authorId: string, limit = 50): Promise<Note[]> {
return db
.select()
.from(notes)
.where(eq(notes.authorId, authorId))
.orderBy(desc(notes.createdAt))
.limit(limit)
},
/** The updated note, or `undefined` when `authorId` did not write it. */
async update(id: string, authorId: string, input: NoteInput): Promise<Note | undefined> {
const [row] = await db
.update(notes)
.set({ ...input, updatedAt: new Date() })
.where(and(eq(notes.id, id), eq(notes.authorId, authorId)))
.returning()
return row
},
/** `true` when the note existed and `authorId` wrote it. */
async remove(id: string, authorId: string): Promise<boolean> {
const rows = await db
.delete(notes)
.where(and(eq(notes.id, id), eq(notes.authorId, authorId)))
.returning({ id: notes.id })
return rows.length > 0
},
}
}
export type NoteRepository = ReturnType<typeof noteRepository>
+3
View File
@@ -0,0 +1,3 @@
export { notes, type Note } from './model/schema'
export { noteInput, noteId, type NoteInput } from './model/input'
export { noteRepository, type NoteDb, type NoteRepository } from './api/repository'
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { noteId, noteInput } from '@/entities/note'
describe('noteInput', () => {
it('trims the title and defaults the body', () => {
expect(noteInput.parse({ title: ' Plan ' })).toEqual({ title: 'Plan', body: '' })
})
it('refuses a blank title', () => {
expect(() => noteInput.parse({ title: ' ' })).toThrow(/title/)
})
it('refuses a title past 200 characters', () => {
expect(() => noteInput.parse({ title: 'x'.repeat(201) })).toThrow()
})
})
describe('noteId', () => {
it('accepts a UUID and refuses anything else', () => {
expect(noteId.parse('00000000-0000-4000-8000-000000000001')).toBe('00000000-0000-4000-8000-000000000001')
expect(() => noteId.parse('not-a-uuid')).toThrow()
})
})
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod'
/** The fields a visitor writes. Parsed at the boundary, before any write (plan §4.4). */
export const noteInput = z.object({
title: z.string().trim().min(1, 'A note needs a title').max(200),
body: z.string().max(20000).default(''),
})
export type NoteInput = z.infer<typeof noteInput>
/** A note id as it arrives in a form or a URL: a UUID, nothing else. */
export const noteId = z.uuid()
+13
View File
@@ -0,0 +1,13 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
/** A note. `author_id` is the visitor's id from the signed `sc_vid` cookie (D16). */
export const notes = pgTable('notes', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
body: text('body').notNull().default(''),
authorId: text('author_id').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
export type Note = typeof notes.$inferSelect
@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// The action is the unit under test: its collaborators are replaced, so each
// test asserts what the action wrote and audited, and nothing else.
const mocks = vi.hoisted(() => {
const row = { id: '00000000-0000-4000-8000-000000000001', title: 'Plan', body: '', authorId: 'visitor-1' }
const values = vi.fn(() => ({ returning: async () => [row] }))
const insert = vi.fn(() => ({ values }))
return {
row,
values,
insert,
log: vi.fn(async () => {}),
publishNoteCreated: vi.fn(async () => {}),
indexNote: vi.fn(async () => {}),
revalidatePath: vi.fn(),
loggerInfo: vi.fn(),
loggerRecordException: vi.fn(),
}
})
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => ({ insert: mocks.insert }),
audit: () => ({ log: mocks.log }),
serverErrors: () => ({ captureException: vi.fn() }),
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
}))
vi.mock('@/shared/lib/optional', () => ({
publishNoteCreated: mocks.publishNoteCreated,
indexNote: mocks.indexNote,
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { createNote } from '@/features/create-note/api/actions'
function form(fields: Record<string, string>): FormData {
const data = new FormData()
for (const [key, value] of Object.entries(fields)) data.set(key, value)
return data
}
describe('createNote', () => {
beforeEach(() => {
mocks.values.mockClear()
mocks.insert.mockClear()
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.loggerInfo.mockClear()
mocks.loggerRecordException.mockClear()
})
it('writes the note under the visitor and audits exactly one note.created', async () => {
await createNote(form({ title: 'Plan', body: '' }))
expect(mocks.values).toHaveBeenCalledWith({ title: 'Plan', body: '', authorId: 'visitor-1' })
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('note.created', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: mocks.row.id },
})
expect(mocks.revalidatePath).toHaveBeenCalledWith('/')
expect(mocks.loggerInfo).toHaveBeenCalledTimes(1)
expect(mocks.loggerInfo).toHaveBeenCalledWith('note.created', { 'note.id': mocks.row.id, outcome: 'ok' })
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('refuses an invalid title before any write or audit, and logs the failure', async () => {
await expect(createNote(form({ title: ' ' }))).rejects.toThrow()
expect(mocks.insert).not.toHaveBeenCalled()
expect(mocks.log).not.toHaveBeenCalled()
expect(mocks.loggerInfo).not.toHaveBeenCalled()
expect(mocks.loggerRecordException).toHaveBeenCalledTimes(1)
expect(mocks.loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'note.created' })
})
it('announces and indexes the new note when the optional modules are linked', async () => {
await createNote(form({ title: 'Plan', body: 'draft' }))
expect(mocks.publishNoteCreated).toHaveBeenCalledWith({ id: mocks.row.id, title: 'Plan' })
expect(mocks.indexNote).toHaveBeenCalledWith({ id: mocks.row.id, title: 'Plan', body: '' })
})
})
+28
View File
@@ -0,0 +1,28 @@
'use server'
import { revalidatePath } from 'next/cache'
import { noteInput, noteRepository } from '@/entities/note'
import { audit, db, logger, serverErrors } from '@/shared/api'
import { indexNote, publishNoteCreated } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Parse, write, audit, revalidate (plan §4.4): the shape every mutation follows. */
export async function createNote(form: FormData): Promise<void> {
try {
const input = noteInput.parse({ title: form.get('title'), body: form.get('body') ?? '' })
const actor = await ensureVisitor()
const note = await noteRepository(db()).create({ ...input, authorId: actor })
await audit().log('note.created', { actor: { kind: 'human', id: actor }, target: { id: note.id } })
logger()?.info('note.created', { 'note.id': note.id, outcome: 'ok' })
// Optional modules: NATS hears of the note and Qdrant indexes it when linked. A failure is
// reported, not raised: the note is saved.
await Promise.all([
publishNoteCreated({ id: note.id, title: note.title }),
indexNote({ id: note.id, title: note.title, body: note.body }),
]).catch((error: unknown) => serverErrors().captureException(error))
revalidatePath('/')
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'note.created' })
throw error
}
}
+2
View File
@@ -0,0 +1,2 @@
export { createNote } from './api/actions'
export { CreateNoteForm } from './ui/CreateNoteForm'
@@ -0,0 +1,17 @@
import { createNote } from '@/features/create-note/api/actions'
export function CreateNoteForm() {
return (
<form action={createNote}>
<label>
Title
<input name="title" required maxLength={200} />
</label>
<label>
Body
<textarea name="body" rows={4} />
</label>
<button type="submit">Create note</button>
</form>
)
}
@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const FILE = '00000000-0000-4000-8000-0000000000ff'
const mocks = vi.hoisted(() => ({
del: vi.fn(async () => {}),
log: vi.fn(async () => {}),
revalidatePath: vi.fn(),
loggerInfo: vi.fn(),
loggerRecordException: vi.fn(),
removed: [] as Array<{ id: string; url: string }>,
}))
const fakeDb = {
delete: () => ({
where: () => ({
returning: async () => mocks.removed,
}),
}),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => fakeDb,
audit: () => ({ log: mocks.log }),
blob: () => ({ del: mocks.del }),
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { deleteFile } from '@/features/delete-file/api/actions'
describe('deleteFile', () => {
beforeEach(() => {
mocks.del.mockClear()
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.loggerInfo.mockClear()
mocks.loggerRecordException.mockClear()
mocks.removed.length = 0
})
it('removes the stored bytes and audits file.deleted once, and logs it', async () => {
mocks.removed.push({ id: FILE, url: 'https://blob.example.com/notes/n/a.png' })
await deleteFile(FILE)
expect(mocks.del).toHaveBeenCalledWith('https://blob.example.com/notes/n/a.png')
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('file.deleted', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: FILE },
})
expect(mocks.loggerInfo).toHaveBeenCalledTimes(1)
expect(mocks.loggerInfo).toHaveBeenCalledWith('file.deleted', { 'file.id': FILE, outcome: 'ok' })
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('touches nothing when the visitor did not attach the file, and logs it denied', async () => {
await deleteFile(FILE)
expect(mocks.del).not.toHaveBeenCalled()
expect(mocks.log).not.toHaveBeenCalled()
expect(mocks.loggerInfo).toHaveBeenCalledWith('file.deleted', { 'file.id': FILE, outcome: 'denied' })
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
})
+27
View File
@@ -0,0 +1,27 @@
'use server'
import { revalidatePath } from 'next/cache'
import { fileRepository } from '@/entities/file'
import { noteId } from '@/entities/note'
import { audit, blob, db, logger } from '@/shared/api'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Remove an attachment the visitor attached: its row first, then its stored bytes. */
export async function deleteFile(id: string): Promise<void> {
try {
const key = noteId.parse(id)
const actor = await ensureVisitor()
const removed = await fileRepository(db()).remove(key, actor)
if (!removed) {
logger()?.info('file.deleted', { 'file.id': key, outcome: 'denied' })
return
}
await blob().del(removed.url)
await audit().log('file.deleted', { actor: { kind: 'human', id: actor }, target: { id: key } })
logger()?.info('file.deleted', { 'file.id': key, outcome: 'ok' })
revalidatePath('/')
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'file.deleted', 'file.id': id })
throw error
}
}
+2
View File
@@ -0,0 +1,2 @@
export { deleteFile } from './api/actions'
export { DeleteFileButton } from './ui/DeleteFileButton'
@@ -0,0 +1,9 @@
import { deleteFile } from '@/features/delete-file/api/actions'
export function DeleteFileButton({ id }: { id: string }) {
return (
<form action={deleteFile.bind(null, id)}>
<button type="submit">Remove attachment</button>
</form>
)
}
@@ -0,0 +1,55 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const NOTE = '00000000-0000-4000-8000-0000000000aa'
const mocks = vi.hoisted(() => ({
log: vi.fn(async () => {}),
revalidatePath: vi.fn(),
loggerInfo: vi.fn(),
loggerRecordException: vi.fn(),
removed: [] as Array<{ id: string }>,
}))
const fakeDb = {
delete: () => ({ where: () => ({ returning: async () => mocks.removed }) }),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => fakeDb,
audit: () => ({ log: mocks.log }),
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { deleteNote } from '@/features/delete-note/api/actions'
describe('deleteNote', () => {
beforeEach(() => {
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.loggerInfo.mockClear()
mocks.loggerRecordException.mockClear()
mocks.removed.length = 0
})
it('audits note.deleted exactly once with the actor and the note, and logs it', async () => {
mocks.removed.push({ id: NOTE })
await deleteNote(NOTE)
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('note.deleted', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: NOTE },
})
expect(mocks.loggerInfo).toHaveBeenCalledTimes(1)
expect(mocks.loggerInfo).toHaveBeenCalledWith('note.deleted', { 'note.id': NOTE, outcome: 'ok' })
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('audits nothing when the visitor did not write the note, and logs it denied', async () => {
await deleteNote(NOTE)
expect(mocks.log).not.toHaveBeenCalled()
expect(mocks.loggerInfo).toHaveBeenCalledWith('note.deleted', { 'note.id': NOTE, outcome: 'denied' })
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
})
+25
View File
@@ -0,0 +1,25 @@
'use server'
import { revalidatePath } from 'next/cache'
import { noteId, noteRepository } from '@/entities/note'
import { audit, db, logger } from '@/shared/api'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Delete a note the visitor wrote. A note they did not write is left untouched. */
export async function deleteNote(id: string): Promise<void> {
try {
const key = noteId.parse(id)
const actor = await ensureVisitor()
const removed = await noteRepository(db()).remove(key, actor)
if (!removed) {
logger()?.info('note.deleted', { 'note.id': key, outcome: 'denied' })
return
}
await audit().log('note.deleted', { actor: { kind: 'human', id: actor }, target: { id: key } })
logger()?.info('note.deleted', { 'note.id': key, outcome: 'ok' })
revalidatePath('/')
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'note.deleted', 'note.id': id })
throw error
}
}
+2
View File
@@ -0,0 +1,2 @@
export { deleteNote } from './api/actions'
export { DeleteNoteButton } from './ui/DeleteNoteButton'
@@ -0,0 +1,9 @@
import { deleteNote } from '@/features/delete-note/api/actions'
export function DeleteNoteButton({ id }: { id: string }) {
return (
<form action={deleteNote.bind(null, id)}>
<button type="submit">Delete</button>
</form>
)
}
@@ -0,0 +1,81 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// The download route runs against fakes: a Blob Storage client whose download
// answers with known bytes, and a database whose attachment row is set per test.
const FILE = '00000000-0000-4000-8000-0000000000ff'
const mocks = vi.hoisted(() => ({
download: vi.fn(async () => new Response('sixteen bytes!!!')),
row: undefined as { id: string; name: string; contentType: string; url: string } | undefined,
visitor: 'visitor-1' as string | null,
}))
const fakeDb = {
select: () => ({ from: () => ({ where: () => ({ limit: async () => (mocks.row ? [mocks.row] : []) }) }) }),
}
vi.mock('@/shared/api', () => ({
db: () => fakeDb,
blob: () => ({ download: mocks.download }),
}))
vi.mock('@/shared/lib/visitor-session', () => ({ currentVisitor: async () => mocks.visitor }))
import { downloadAttachment, GET } from '@/features/download-file/api/route'
describe('downloadAttachment', () => {
beforeEach(() => {
mocks.download.mockClear()
mocks.visitor = 'visitor-1'
mocks.row = {
id: FILE,
name: 'notes.txt',
contentType: 'text/plain',
url: 'https://blob.example.com/api/blob/7/notes/n/notes.txt',
}
})
it('streams the bytes with the stored content type and an attachment disposition', async () => {
const response = await downloadAttachment(FILE)
expect(response.status).toBe(200)
expect(await response.text()).toBe('sixteen bytes!!!')
expect(response.headers.get('content-type')).toBe('text/plain')
expect(response.headers.get('content-disposition')).toBe("attachment; filename*=UTF-8''notes.txt")
expect(mocks.download).toHaveBeenCalledWith(mocks.row?.url)
})
it('answers 404 for a file row the visitor does not own', async () => {
mocks.row = undefined
const response = await downloadAttachment(FILE)
expect(response.status).toBe(404)
expect(mocks.download).not.toHaveBeenCalled()
})
it('answers 404 when there is no visitor cookie', async () => {
mocks.visitor = null
expect((await downloadAttachment(FILE)).status).toBe(404)
expect(mocks.download).not.toHaveBeenCalled()
})
it('answers 404 for an id that is not a UUID', async () => {
expect((await downloadAttachment('../../etc/passwd')).status).toBe(404)
expect(mocks.download).not.toHaveBeenCalled()
})
it('encodes a hostile file name into the disposition', async () => {
if (mocks.row) mocks.row.name = 'a"\r\nb.txt'
const response = await downloadAttachment(FILE)
expect(response.headers.get('content-disposition')).toBe("attachment; filename*=UTF-8''a%22%0D%0Ab.txt")
})
})
describe('GET', () => {
it('takes the id from the route params', async () => {
mocks.row = {
id: FILE,
name: 'notes.txt',
contentType: 'text/plain',
url: 'https://blob.example.com/api/blob/7/notes/n/notes.txt',
}
const response = await GET(new Request('http://localhost/files/x'), { params: Promise.resolve({ id: FILE }) })
expect(response.status).toBe(200)
})
})
+37
View File
@@ -0,0 +1,37 @@
import 'server-only'
import { fileId, fileRepository } from '@/entities/file'
import { blob, db } from '@/shared/api'
import { currentVisitor } from '@/shared/lib/visitor-session'
function notFound(): Response {
return new Response('not found', { status: 404 })
}
/**
* The bytes of one attachment, for the visitor who attached it. The platform's
* blob URL needs the deployment token, which the browser does not have, so the
* app downloads server-side and streams the bytes back. Anything that is not
* the visitor's own attachment answers 404, never 403.
*/
export async function downloadAttachment(id: string): Promise<Response> {
const parsed = fileId.safeParse(id)
if (!parsed.success) return notFound()
const visitor = await currentVisitor()
if (!visitor) return notFound()
const file = await fileRepository(db()).get(parsed.data, visitor)
if (!file) return notFound()
const upstream = await blob().download(file.url)
if (!upstream.body) return notFound()
return new Response(upstream.body, {
headers: {
'content-type': file.contentType,
// RFC 5987: the name is percent-encoded, so quotes and CR/LF cannot inject.
'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent(file.name)}`,
},
})
}
/** `GET /files/<id>`: the route handler the app router re-exports. */
export async function GET(_request: Request, context: { params: Promise<{ id: string }> }): Promise<Response> {
return downloadAttachment((await context.params).id)
}
+1
View File
@@ -0,0 +1 @@
export { GET } from './api/route'
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const NOTE = '00000000-0000-4000-8000-0000000000aa'
const mocks = vi.hoisted(() => ({
log: vi.fn(async () => {}),
revalidatePath: vi.fn(),
loggerInfo: vi.fn(),
loggerRecordException: vi.fn(),
updated: undefined as { id: string } | undefined,
}))
const fakeDb = {
update: () => ({
set: () => ({
where: () => ({ returning: async () => (mocks.updated ? [mocks.updated] : []) }),
}),
}),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => fakeDb,
audit: () => ({ log: mocks.log }),
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { updateNote } from '@/features/edit-note/api/actions'
function edit(title: string): FormData {
const data = new FormData()
data.set('title', title)
data.set('body', '')
return data
}
describe('updateNote', () => {
beforeEach(() => {
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.loggerInfo.mockClear()
mocks.loggerRecordException.mockClear()
mocks.updated = undefined
})
it('audits note.updated exactly once with the actor and the note, and logs the outcome', async () => {
mocks.updated = { id: NOTE }
await updateNote(NOTE, edit('Renamed'))
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('note.updated', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: NOTE },
})
expect(mocks.loggerInfo).toHaveBeenCalledTimes(1)
expect(mocks.loggerInfo).toHaveBeenCalledWith('note.updated', { 'note.id': NOTE, outcome: 'ok' })
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('audits nothing when the visitor did not write the note, and logs it denied', async () => {
await updateNote(NOTE, edit('Renamed'))
expect(mocks.log).not.toHaveBeenCalled()
expect(mocks.loggerInfo).toHaveBeenCalledWith('note.updated', { 'note.id': NOTE, outcome: 'denied' })
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('logs the failure when the input is invalid', async () => {
await expect(updateNote(NOTE, edit(' '))).rejects.toThrow()
expect(mocks.log).not.toHaveBeenCalled()
expect(mocks.loggerInfo).not.toHaveBeenCalled()
expect(mocks.loggerRecordException).toHaveBeenCalledTimes(1)
expect(mocks.loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'note.updated', 'note.id': NOTE })
})
})
+26
View File
@@ -0,0 +1,26 @@
'use server'
import { revalidatePath } from 'next/cache'
import { noteId, noteInput, noteRepository } from '@/entities/note'
import { audit, db, logger } from '@/shared/api'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Edit a note the visitor wrote. A note they did not write is left untouched. */
export async function updateNote(id: string, form: FormData): Promise<void> {
try {
const key = noteId.parse(id)
const input = noteInput.parse({ title: form.get('title'), body: form.get('body') ?? '' })
const actor = await ensureVisitor()
const note = await noteRepository(db()).update(key, actor, input)
if (!note) {
logger()?.info('note.updated', { 'note.id': key, outcome: 'denied' })
return
}
await audit().log('note.updated', { actor: { kind: 'human', id: actor }, target: { id: note.id } })
logger()?.info('note.updated', { 'note.id': note.id, outcome: 'ok' })
revalidatePath('/')
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'note.updated', 'note.id': id })
throw error
}
}
+2
View File
@@ -0,0 +1,2 @@
export { updateNote } from './api/actions'
export { EditNoteForm } from './ui/EditNoteForm'
@@ -0,0 +1,18 @@
import type { Note } from '@/entities/note'
import { updateNote } from '@/features/edit-note/api/actions'
export function EditNoteForm({ note }: { note: Note }) {
return (
<form action={updateNote.bind(null, note.id)}>
<label>
Title
<input name="title" defaultValue={note.title} required maxLength={200} />
</label>
<label>
Body
<textarea name="body" rows={3} defaultValue={note.body} />
</label>
<button type="submit">Save</button>
</form>
)
}
@@ -0,0 +1,63 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
exportToS3: vi.fn(async () => 'exports/visitor-1/now.json' as string | null),
notes: [{ id: 'n-1', title: 'Plan' }],
loggerInfo: vi.fn(),
loggerRecordException: vi.fn(),
log: vi.fn(async () => {}),
captureException: vi.fn(),
}))
vi.mock('@/shared/api', () => ({
db: () => ({
select: () => ({ from: () => ({ where: () => ({ orderBy: () => ({ limit: async () => mocks.notes }) }) }) }),
}),
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
audit: () => ({ log: mocks.log }),
serverErrors: () => ({ captureException: mocks.captureException }),
}))
vi.mock('@/shared/lib/optional', () => ({ exportToS3: mocks.exportToS3 }))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { exportNotes } from '@/features/export-notes/api/actions'
describe('exportNotes', () => {
beforeEach(() => {
mocks.exportToS3.mockClear()
mocks.loggerInfo.mockClear()
mocks.loggerRecordException.mockClear()
mocks.log.mockClear()
mocks.captureException.mockClear()
})
it('writes the visitor’s own notes to S3 once, logs the outcome, and audits notes.exported with the key', async () => {
const result = await exportNotes(null, new FormData())
expect(mocks.exportToS3).toHaveBeenCalledTimes(1)
expect(mocks.exportToS3).toHaveBeenCalledWith('visitor-1', mocks.notes)
expect(result).toEqual({ ok: true, key: 'exports/visitor-1/now.json', size: expect.any(Number) })
expect(mocks.loggerInfo).toHaveBeenCalledWith('notes.exported', { 'actor.id': 'visitor-1', 'export.count': 1, outcome: 'ok' })
expect(mocks.log).toHaveBeenCalledWith('notes.exported', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: 'exports/visitor-1/now.json' },
})
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('reports S3 as not linked, logs it as skipped, and audits nothing, when exportToS3 returns null', async () => {
mocks.exportToS3.mockResolvedValueOnce(null)
const result = await exportNotes(null, new FormData())
expect(result).toEqual({ ok: false, message: 'S3 is not linked; nothing was exported.' })
expect(mocks.loggerInfo).toHaveBeenCalledWith('notes.exported', { 'actor.id': 'visitor-1', 'export.count': 1, outcome: 'skipped' })
expect(mocks.log).not.toHaveBeenCalled()
})
it('reports the error, logs and captures it, when the S3 write throws', async () => {
mocks.exportToS3.mockRejectedValueOnce(new Error('bucket unreachable'))
const result = await exportNotes(null, new FormData())
expect(result).toEqual({ ok: false, message: 'bucket unreachable' })
expect(mocks.loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'notes.exported', 'actor.id': 'visitor-1' })
expect(mocks.captureException).toHaveBeenCalledTimes(1)
expect(mocks.log).not.toHaveBeenCalled()
})
})
+33
View File
@@ -0,0 +1,33 @@
'use server'
import { noteRepository } from '@/entities/note'
import { audit, db, logger, serverErrors } from '@/shared/api'
import { exportToS3 } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
export type ExportResult = { ok: true; key: string; size: number } | { ok: false; message: string }
/**
* Writes the visitor's notes to the S3 bucket as one JSON object (S3 module).
*
* The caller (a `useActionState` form) needs the object key/size or the error to show
* the visitor, and the write needs an audit event like every other action here — before
* this, the button gave no feedback at all and nothing landed in the audit timeline
* (stage sweep, 2026-10-11). The `ExportResult | null` / `FormData` signature is
* `useActionState`'s action shape, not a plain server action.
*/
export async function exportNotes(_state: ExportResult | null, _form: FormData): Promise<ExportResult> {
const actor = await ensureVisitor()
const notes = await noteRepository(db()).listByAuthor(actor)
try {
const key = await exportToS3(actor, notes)
logger()?.info('notes.exported', { 'actor.id': actor, 'export.count': notes.length, outcome: key ? 'ok' : 'skipped' })
if (!key) return { ok: false, message: 'S3 is not linked; nothing was exported.' }
await audit().log('notes.exported', { actor: { kind: 'human', id: actor }, target: { id: key } })
return { ok: true, key, size: Buffer.byteLength(JSON.stringify(notes)) }
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'notes.exported', 'actor.id': actor })
serverErrors().captureException(error)
return { ok: false, message: error instanceof Error ? error.message : 'the export failed' }
}
}
+2
View File
@@ -0,0 +1,2 @@
export { exportNotes } from './api/actions'
export { ExportNotesButton } from './ui/ExportNotesButton'
@@ -0,0 +1,18 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { ExportNotesButton } from '@/features/export-notes/ui/ExportNotesButton'
// The button gave no feedback at all after submit: no message, no object key, and
// nothing in the audit timeline (stage sweep, 2026-10-11; the audit half is covered by
// exportNotes's own tests). This only checks the button's initial render — before any
// submission `useActionState`'s state is `null` — so neither the success nor the error
// branch has leaked into the markup by mistake.
describe('ExportNotesButton', () => {
it('renders the form with no result before any submission', () => {
const markup = renderToStaticMarkup(<ExportNotesButton />)
expect(markup).toContain('<form')
expect(markup).toContain('Export my notes to S3')
expect(markup).not.toContain('Exported')
expect(markup).not.toMatch(/role="alert"/)
})
})
@@ -0,0 +1,21 @@
'use client'
import { useActionState } from 'react'
import { exportNotes, type ExportResult } from '@/features/export-notes/api/actions'
export function ExportNotesButton() {
const [result, action, pending] = useActionState<ExportResult | null, FormData>(exportNotes, null)
return (
<form action={action}>
<button type="submit" disabled={pending}>
Export my notes to S3
</button>
{result?.ok && (
<p>
Exported {result.size} bytes to <code>{result.key}</code>.
</p>
)}
{result && !result.ok && <p role="alert">{result.message}</p>}
</form>
)
}
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const publishNoteCreated = vi.fn<(note: { id: string; title: string }) => Promise<void>>(async () => undefined)
const log = vi.fn<(...args: unknown[]) => Promise<void>>(async () => undefined)
const get = vi.fn<(id: string) => Promise<{ id: string; title: string; authorId: string } | undefined>>()
const loggerInfo = vi.fn()
const loggerRecordException = vi.fn()
let visitor = 'visitor-a'
vi.mock('@/shared/lib/optional', () => ({ publishNoteCreated: (note: { id: string; title: string }) => publishNoteCreated(note) }))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => visitor }))
vi.mock('@/entities/note', () => ({ noteRepository: () => ({ get }) }))
vi.mock('@/shared/api', () => ({
db: () => ({}),
audit: () => ({ log }),
logger: () => ({ info: loggerInfo, recordException: loggerRecordException }),
}))
const { announceNote } = await import('@/features/publish-note-event/api/actions')
describe('announceNote', () => {
beforeEach(() => {
publishNoteCreated.mockClear()
log.mockClear()
get.mockReset()
loggerInfo.mockClear()
loggerRecordException.mockClear()
visitor = 'visitor-a'
})
it('publishes the author\'s note once and audits the announcement', async () => {
get.mockResolvedValue({ id: 'note-1', title: 'Budget', authorId: 'visitor-a' })
await announceNote('note-1')
expect(publishNoteCreated).toHaveBeenCalledTimes(1)
expect(publishNoteCreated).toHaveBeenCalledWith({ id: 'note-1', title: 'Budget' })
expect(log).toHaveBeenCalledWith('note.announced', expect.objectContaining({ target: { id: 'note-1' } }))
expect(loggerInfo).toHaveBeenCalledTimes(1)
expect(loggerInfo).toHaveBeenCalledWith('note.announced', { 'note.id': 'note-1', outcome: 'ok' })
expect(loggerRecordException).not.toHaveBeenCalled()
})
it('refuses another visitor\'s note and publishes nothing, and logs the refusal', async () => {
get.mockResolvedValue({ id: 'note-1', title: 'Budget', authorId: 'visitor-b' })
await expect(announceNote('note-1')).rejects.toThrow('only the author')
expect(publishNoteCreated).not.toHaveBeenCalled()
expect(log).not.toHaveBeenCalled()
expect(loggerInfo).not.toHaveBeenCalled()
expect(loggerRecordException).toHaveBeenCalledTimes(1)
expect(loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'note.announced', 'note.id': 'note-1' })
})
it('refuses a note that does not exist', async () => {
get.mockResolvedValue(undefined)
await expect(announceNote('missing')).rejects.toThrow('only the author')
expect(publishNoteCreated).not.toHaveBeenCalled()
expect(loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'note.announced', 'note.id': 'missing' })
})
})
@@ -0,0 +1,30 @@
'use server'
import { noteRepository } from '@/entities/note'
import { audit, db, logger } from '@/shared/api'
import { publishNoteCreated } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/**
* Announces one of the visitor's own notes on NATS again, as a `note.created`
* event (plan §4.4). Only the author may announce a note; anyone else gets a refusal
* and no event. A no-op when NATS is not linked.
*/
export async function announceNote(noteId: string): Promise<void> {
try {
const actor = await ensureVisitor()
const note = await noteRepository(db()).get(noteId)
if (!note || note.authorId !== actor) throw new Error('only the author can announce this note')
await publishNoteCreated({ id: note.id, title: note.title })
await audit().log('note.announced', { actor: { kind: 'human', id: actor }, target: { id: note.id } })
logger()?.info('note.announced', { 'note.id': note.id, outcome: 'ok' })
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'note.announced', 'note.id': noteId })
throw error
}
}
/** The form action behind the button: the note id travels as a hidden field. */
export async function announceFromForm(form: FormData): Promise<void> {
await announceNote(String(form.get('id') ?? ''))
}
+2
View File
@@ -0,0 +1,2 @@
export { AnnounceNoteButton } from './ui/AnnounceNoteButton'
export { announceNote } from './api/actions'
@@ -0,0 +1,25 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { AnnounceNoteButton } from '@/features/publish-note-event/ui/AnnounceNoteButton'
// The note list rendered this button unconditionally while a nearby card read
// "Link NATS to enable this" (stage sweep, 2026-10-11): clicking it with NATS
// unlinked submitted a form whose action is a no-op (`publishNoteCreated` returns
// early without a connection). The button must agree with the module panel's own
// `nats` slot about whether the module is linked, consistent with the enable-card
// pattern used everywhere else in the template.
describe('AnnounceNoteButton', () => {
it('renders the live form when NATS is linked', () => {
const markup = renderToStaticMarkup(AnnounceNoteButton({ id: 'note-1', linked: true }))
expect(markup).toContain('<form')
expect(markup).toContain('Announce')
expect(markup).not.toContain('disabled')
})
it('renders a disabled button with the enable-card explanation when NATS is not linked', () => {
const markup = renderToStaticMarkup(AnnounceNoteButton({ id: 'note-1', linked: false }))
expect(markup).not.toContain('<form')
expect(markup).toContain('disabled')
expect(markup).toContain('Link NATS to enable this.')
})
})
@@ -0,0 +1,27 @@
import { announceFromForm } from '@/features/publish-note-event/api/actions'
/**
* "Announce" on one of the visitor's notes: publishes its `note.created` event again.
* `linked` is the NATS module's own linked state (plan D13): the action is a no-op
* without NATS, so the button must not invite a click that does nothing — it renders
* disabled with the same "Link ... to enable this" explanation the enable-card gives
* the NATS slot in the module panel, rather than quietly succeeding at nothing.
*/
export function AnnounceNoteButton({ id, linked }: { id: string; linked: boolean }) {
if (!linked) {
return (
<p>
<button type="button" disabled>
Announce
</button>{' '}
Link NATS to enable this.
</p>
)
}
return (
<form action={announceFromForm}>
<input type="hidden" name="id" value={id} />
<button type="submit">Announce</button>
</form>
)
}
+1
View File
@@ -0,0 +1 @@
export { UploadsLeft } from './ui/UploadsLeft'
@@ -0,0 +1,12 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/shared/lib/optional', () => ({ uploadsLeft: vi.fn(async () => 3) }))
vi.mock('@/shared/lib/visitor-session', () => ({ currentVisitor: vi.fn(async () => 'visitor-a') }))
describe('UploadsLeft', () => {
it('renders the uploads the visitor has left this minute', async () => {
const { UploadsLeft } = await import('@/features/rate-limit/ui/UploadsLeft')
expect(renderToStaticMarkup(await UploadsLeft())).toContain('3 uploads left this minute')
})
})
@@ -0,0 +1,9 @@
import { uploadsLeft } from '@/shared/lib/optional'
import { currentVisitor } from '@/shared/lib/visitor-session'
/** How many uploads the visitor has left this minute (KV module). */
export async function UploadsLeft() {
const visitor = await currentVisitor()
const left = visitor ? await uploadsLeft(visitor) : null
return <p>{left ?? 0} uploads left this minute</p>
}
+1
View File
@@ -0,0 +1 @@
export { SearchNotes } from './ui/SearchNotes'
@@ -0,0 +1,26 @@
import { similarNotes } from '@/shared/lib/optional'
/** Similar-notes search (Qdrant module): a GET form, so a search is a plain link. */
export async function SearchNotes({ query }: { query: string | undefined }) {
const hits: Awaited<ReturnType<typeof similarNotes>> = query ? await similarNotes(query) : []
return (
<section>
<form method="get">
<label>
Find similar notes
<input name="q" defaultValue={query ?? ''} />
</label>
<button type="submit">Search</button>
</form>
{hits && hits.length > 0 ? (
<ol>
{hits.map((hit) => (
<li key={hit.id}>
{hit.payload.title} ({hit.score.toFixed(2)})
</li>
))}
</ol>
) : null}
</section>
)
}
+2
View File
@@ -0,0 +1,2 @@
export { SimulateTrafficButton } from './ui/SimulateTrafficButton'
export { runJourney } from './model/journey'
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { createTestObserve } from '@conusai-sdk/observe-testing'
import { runJourney } from '@/features/simulate-traffic/model/journey'
describe('runJourney', () => {
it('sends the page view, the custom event and the captured error', async () => {
const observe = createTestObserve({ modules: { analytics: true } })
runJourney(observe)
await observe.flush()
const kinds = observe.mock.sent.map(({ item }) => item.kind)
// Custom events travel under their app. name; a captured error is kind `error`.
expect(kinds).toContain('app.page_view')
expect(kinds).toContain('app.journey_started')
expect(kinds).toContain('error')
const error = observe.mock.sent.find(({ item }) => item.kind === 'error')?.item
expect(JSON.stringify(error)).toContain('simulated journey failure')
})
})
@@ -0,0 +1,8 @@
import type { Observe } from '@conusai-sdk/observe'
/** What one simulated journey reports: a page view, a custom event and a captured error. */
export function runJourney(observe: Pick<Observe, 'track' | 'captureException'>): void {
observe.track('page_view', { path: '/' })
observe.track('journey_started', { steps: 3 })
observe.captureException(new Error('simulated journey failure'), { tags: { journey: 'simulate-traffic' } })
}
@@ -0,0 +1,14 @@
'use client'
import { useObserve } from '@conusai-sdk/observe-react'
import { runJourney } from '@/features/simulate-traffic/model/journey'
/** Runs one simulated journey, so a visitor sees the signals land in the console. */
export function SimulateTrafficButton() {
const observe = useObserve()
return (
<button type="button" onClick={() => runJourney(observe)}>
Simulate a user journey
</button>
)
}
@@ -0,0 +1,140 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// The action runs against fakes: a Blob Storage client that records what it was
// given, and a database whose note and attachment rows are set per test.
const NOTE = '00000000-0000-4000-8000-0000000000aa'
const mocks = vi.hoisted(() => ({
put: vi.fn(async (pathname: string) => ({ url: `https://blob.example.com/${pathname}` })),
log: vi.fn(async () => {}),
recordUsage: vi.fn(async () => ({ accepted: 1, duplicates: 0, rejected: [] })),
consumeUploadQuota: vi.fn(async () => null as { allowed: boolean; remaining: number } | null),
revalidatePath: vi.fn(),
loggerInfo: vi.fn(),
loggerRecordException: vi.fn(),
note: undefined as { id: string; authorId: string } | undefined,
inserted: [] as Record<string, unknown>[],
}))
const fakeDb = {
select: () => ({ from: () => ({ where: () => ({ limit: async () => (mocks.note ? [mocks.note] : []) }) }) }),
insert: () => ({
values: (row: Record<string, unknown>) => ({
returning: async () => {
mocks.inserted.push(row)
return [{ id: '00000000-0000-4000-8000-0000000000ff', ...row, createdAt: new Date() }]
},
}),
}),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => fakeDb,
audit: () => ({ log: mocks.log }),
blob: () => ({ put: mocks.put }),
recordUsage: mocks.recordUsage,
serverErrors: () => ({ captureException: vi.fn() }),
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
}))
vi.mock('@/shared/config', () => ({
getEnv: () => ({ CONUSAI_API_URL: 'https://console.example.com', CONUSAI_API_TOKEN: 'dt_fixture' }),
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
vi.mock('@/shared/lib/optional', () => ({ consumeUploadQuota: mocks.consumeUploadQuota }))
import { uploadFile } from '@/features/upload-file/api/actions'
function attach(file: File): FormData {
const data = new FormData()
data.set('file', file)
return data
}
describe('uploadFile', () => {
beforeEach(() => {
mocks.put.mockClear()
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.consumeUploadQuota.mockReset()
mocks.consumeUploadQuota.mockResolvedValue(null)
mocks.inserted.length = 0
mocks.note = { id: NOTE, authorId: 'visitor-1' }
mocks.loggerInfo.mockClear()
mocks.loggerRecordException.mockClear()
})
it('refuses a file over the size limit before any upload, and logs the failure', async () => {
const big = new File([new Uint8Array(5 * 1024 * 1024 + 1)], 'big.png', { type: 'image/png' })
await expect(uploadFile(NOTE, attach(big))).rejects.toThrow(/5 MiB/)
expect(mocks.put).not.toHaveBeenCalled()
expect(mocks.log).not.toHaveBeenCalled()
expect(mocks.loggerInfo).not.toHaveBeenCalled()
expect(mocks.loggerRecordException).toHaveBeenCalledTimes(1)
expect(mocks.loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'file.uploaded', 'note.id': NOTE })
})
it('refuses a disallowed type before any upload', async () => {
const exe = new File(['MZ'], 'run.exe', { type: 'application/x-msdownload' })
await expect(uploadFile(NOTE, attach(exe))).rejects.toThrow(/not accepted/)
expect(mocks.put).not.toHaveBeenCalled()
})
it('refuses a note the visitor did not write, without uploading or spending quota', async () => {
mocks.note = { id: NOTE, authorId: 'visitor-2' }
const png = new File(['png'], 'a.png', { type: 'image/png' })
await expect(uploadFile(NOTE, attach(png))).rejects.toThrow(/not yours/)
expect(mocks.put).not.toHaveBeenCalled()
expect(mocks.inserted).toHaveLength(0)
expect(mocks.consumeUploadQuota).not.toHaveBeenCalled()
})
it('stores the bytes, records the attachment, and audits file.uploaded once', async () => {
const png = new File(['png'], 'a.png', { type: 'image/png' })
await uploadFile(NOTE, attach(png))
expect(mocks.put).toHaveBeenCalledTimes(1)
expect(mocks.put.mock.calls[0]?.[0]).toBe(`notes/${NOTE}/a.png`)
expect(mocks.inserted[0]).toMatchObject({
noteId: NOTE,
authorId: 'visitor-1',
name: 'a.png',
contentType: 'image/png',
size: 3,
url: `https://blob.example.com/notes/${NOTE}/a.png`,
})
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('file.uploaded', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: '00000000-0000-4000-8000-0000000000ff' },
})
expect(mocks.loggerInfo).toHaveBeenCalledTimes(1)
expect(mocks.loggerInfo).toHaveBeenCalledWith('file.uploaded', {
'file.id': '00000000-0000-4000-8000-0000000000ff',
'note.id': NOTE,
outcome: 'ok',
})
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('sends exactly one usage event, keyed by the attachment, for one upload', async () => {
const png = new File(['png'], 'a.png', { type: 'image/png' })
await uploadFile(NOTE, attach(png))
expect(mocks.recordUsage).toHaveBeenCalledTimes(1)
const [config, events] = mocks.recordUsage.mock.calls[0] as unknown as [
{ baseUrl: string; token: string },
Array<{ idempotency_key: string; meter: string; quantity: string }>,
]
expect(config).toEqual({ baseUrl: 'https://console.example.com', token: 'dt_fixture' })
expect(events).toHaveLength(1)
expect(events[0]).toMatchObject({ idempotency_key: 'upload:00000000-0000-4000-8000-0000000000ff', meter: 'uploads', quantity: '1' })
})
it('refuses an upload over the minute allowance before any write', async () => {
mocks.consumeUploadQuota.mockResolvedValue({ allowed: false, remaining: 0 })
const png = new File(['png'], 'a.png', { type: 'image/png' })
await expect(uploadFile(NOTE, attach(png))).rejects.toThrow(/limit/)
expect(mocks.put).not.toHaveBeenCalled()
expect(mocks.inserted).toHaveLength(0)
})
})
+68
View File
@@ -0,0 +1,68 @@
'use server'
import { revalidatePath } from 'next/cache'
import { fileRepository, uploadLimits } from '@/entities/file'
import { noteId, noteRepository } from '@/entities/note'
import { getEnv } from '@/shared/config'
import { audit, blob, db, logger, recordUsage, serverErrors } from '@/shared/api'
import { consumeUploadQuota } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/**
* Attach a file to a note the visitor wrote. The limits are checked before any
* upload, so a refused file never reaches Blob Storage.
*/
export async function uploadFile(note: string, form: FormData): Promise<void> {
try {
const key = noteId.parse(note)
const upload = form.get('file')
if (!(upload instanceof File)) throw new Error('choose a file to upload')
const limits = uploadLimits.parse({
name: upload.name,
size: upload.size,
contentType: upload.type,
})
const actor = await ensureVisitor()
const owner = await noteRepository(db()).get(key)
if (!owner || owner.authorId !== actor) throw new Error('that note is not yours to attach to')
// With KV linked, each visitor has a per-minute upload allowance; without it, none.
// Checked after ownership: an attempt on someone else's note spends no quota.
const quota = await consumeUploadQuota(actor)
if (quota && !quota.allowed) throw new Error('the upload limit for this minute is reached')
const stored = await blob().put(`notes/${key}/${limits.name}`, Buffer.from(await upload.arrayBuffer()), {
contentType: limits.contentType,
})
const row = await fileRepository(db()).create({
noteId: key,
authorId: actor,
name: limits.name,
contentType: limits.contentType,
size: limits.size,
url: stored.url,
})
await audit().log('file.uploaded', { actor: { kind: 'human', id: actor }, target: { id: row.id } })
logger()?.info('file.uploaded', { 'file.id': row.id, 'note.id': key, outcome: 'ok' })
// One upload, one usage event. The key is the attachment's id, so a resend stores nothing twice.
// A failed send is reported, not raised: the upload itself succeeded.
const env = getEnv()
if (env.USAGE_ORG_SLUG) {
await recordUsage({ baseUrl: env.CONUSAI_API_URL, token: env.CONUSAI_API_TOKEN, orgSlug: env.USAGE_ORG_SLUG }, [
{
idempotencyKey: `upload:${row.id}`,
customer: { externalId: 'showcase' },
meter: 'uploads',
quantity: 1,
properties: { bytes: limits.size, content_type: limits.contentType },
},
]).catch((error: unknown) => serverErrors().captureException(error))
} else {
serverErrors().captureException(new Error('USAGE_ORG_SLUG is required to report usage'))
}
revalidatePath('/')
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'file.uploaded', 'note.id': note })
throw error
}
}
+2
View File
@@ -0,0 +1,2 @@
export { uploadFile } from './api/actions'
export { UploadFileForm } from './ui/UploadFileForm'
@@ -0,0 +1,13 @@
import { uploadFile } from '@/features/upload-file/api/actions'
export function UploadFileForm({ noteId }: { noteId: string }) {
return (
<form action={uploadFile.bind(null, noteId)}>
<label>
Attachment
<input name="file" type="file" required />
</label>
<button type="submit">Attach</button>
</form>
)
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+37
View File
@@ -0,0 +1,37 @@
import 'server-only'
import { ConusAIClient } from '@conusai-sdk/node-sdk'
import { createAuditClient, defineAuditEvents, type AuditClient } from '@conusai-sdk/node-sdk/audit'
import { getEnv } from '@/shared/config/env'
/** The actions this app writes to the audit log (plan §4.4): one definition, typed `log()`. */
export const events = defineAuditEvents({
'note.created': { target: 'note' },
'note.updated': { target: 'note' },
'note.deleted': { target: 'note' },
'note.announced': { target: 'note' },
'file.uploaded': { target: 'file' },
'file.deleted': { target: 'file' },
'notes.exported': { target: 'export' },
})
export type AppAuditClient = AuditClient<typeof events>
let client: AppAuditClient | undefined
/** The audit client. It reads `CONUSAI_API_URL` and `CONUSAI_API_TOKEN` itself (D8). */
export function audit(): AppAuditClient {
client ??= createAuditClient({ events })
return client
}
/**
* The project's own audit events, newest first, one page. The deployment token
* carries `audit:read` (showcase D9), so the platform answers for this project only.
*/
export async function readAuditEvents(options: { cursor?: string; limit?: number } = {}) {
const env = getEnv()
const platform = new ConusAIClient({ baseUrl: env.CONUSAI_API_URL, apiKey: env.CONUSAI_API_TOKEN })
const { data } = await platform.readEvents({ query: options, throwOnError: true })
if (!data) throw new Error('the platform returned no audit page')
return data
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+10
View File
@@ -0,0 +1,10 @@
import 'server-only'
import { createClient, type BlobClient } from '@conusai-sdk/blob'
let client: BlobClient | undefined
/** The Blob Storage client. It reads `CONUSAI_API_URL` and `CONUSAI_API_TOKEN` itself (D8). */
export function blob(): BlobClient {
client ??= createClient()
return client
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+14
View File
@@ -0,0 +1,14 @@
import 'server-only'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { getEnv } from '@/shared/config/env'
export type Db = ReturnType<typeof drizzle>
let connection: Db | undefined
/** The Postgres connection every server module shares, opened on first use. */
export function db(): Db {
connection ??= drizzle(postgres(getEnv().POSTGRES_URL))
return connection
}
+12
View File
@@ -0,0 +1,12 @@
// The public API of the shared server clients. Each one is `server-only`; a
// client component that imports this file fails the build, which is the guard.
export * from './audit'
export * from './blob'
export * from './db'
export * from './kv'
export * from './nats'
export * from './observe'
export * from './platform'
export * from './qdrant'
export * from './s3'
export * from './usage'
+1
View File
@@ -0,0 +1 @@
export * from './server'
+15
View File
@@ -0,0 +1,15 @@
import 'server-only'
import { createClient, type KV } from '@conusai-sdk/kv'
import { getEnv, modules } from '@/shared/config/env'
let client: KV | undefined
/**
* The KV client when the KV module is linked (D13: `REDIS_URL` present), else
* `null`. Callers render the enable card instead of calling a dead service.
*/
export function kv(): KV | null {
if (!modules(getEnv()).kv) return null
client ??= createClient()
return client
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+40
View File
@@ -0,0 +1,40 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// The NATS client module is loaded fresh per test: its memoized connection is
// module state. `connect` is scripted per test.
const mocks = vi.hoisted(() => ({ connect: vi.fn() }))
vi.mock('nats', () => ({ connect: mocks.connect }))
vi.mock('@/shared/config/env', async () => {
const actual = await vi.importActual<typeof import('@/shared/config/env')>('@/shared/config/env')
return { ...actual, getEnv: () => ({ NATS_URL: 'nats://nats.example.test:4222', NATS_TOKEN: undefined }) }
})
async function freshNats() {
vi.resetModules()
const client = await import('@/shared/api/nats/server')
return client.nats
}
describe('nats', () => {
beforeEach(() => mocks.connect.mockReset())
it('retries the connection after a failed connect instead of serving the rejection forever', async () => {
mocks.connect.mockRejectedValueOnce(new Error('nats down')).mockResolvedValueOnce({ closed: false })
const nats = await freshNats()
await expect(nats()).rejects.toThrow('nats down')
// The rejection must not be memoized: the next call connects again.
await expect(nats()).resolves.toEqual({ closed: false })
expect(mocks.connect).toHaveBeenCalledTimes(2)
})
it('memoizes a successful connection', async () => {
mocks.connect.mockResolvedValue({ closed: false })
const nats = await freshNats()
await expect(nats()).resolves.toEqual({ closed: false })
expect(nats()).toBe(nats())
expect(mocks.connect).toHaveBeenCalledTimes(1)
})
})
+22
View File
@@ -0,0 +1,22 @@
import 'server-only'
import { connect, type NatsConnection } from 'nats'
import { getEnv, modules } from '@/shared/config/env'
let connection: Promise<NatsConnection> | undefined
/** The NATS connection, or `null` when the NATS module is not linked (D13). */
export function nats(): Promise<NatsConnection> | null {
const env = getEnv()
const servers = env.NATS_URL
if (!modules(env).nats || !servers) return null
if (!connection) {
const pending = connect(env.NATS_TOKEN ? { servers, token: env.NATS_TOKEN } : { servers })
connection = pending
// A failed connect must not wedge the module: once it settles rejected, the
// next call connects again. The caller still sees the original rejection.
pending.catch(() => {
if (connection === pending) connection = undefined
})
}
return connection
}
+1
View File
@@ -0,0 +1 @@
export { logger, serverErrors, withRequest } from './server'
+95
View File
@@ -0,0 +1,95 @@
import { createServer, type IncomingMessage, type Server } from 'node:http'
import { afterEach, describe, expect, it, vi } from 'vitest'
// logger() wraps observe-next's getLogger() (showcase plan D23): undefined until register()
// starts the OTLP exporters, which only happens when the platform injects OTEL env.
describe('logger', () => {
afterEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
})
it('is undefined in local dev, without OTEL_EXPORTER_OTLP_ENDPOINT', async () => {
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', '')
vi.resetModules()
const { logger } = await import('@/shared/api/observe/server')
expect(logger()).toBeUndefined()
})
it('returns the logger of the OTLP exporters register() started, once OTEL_EXPORTER_OTLP_ENDPOINT is set', async () => {
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://app.example.com/api/otel')
vi.stubEnv('CONUSAI_API_URL', 'https://app.example.com/api')
vi.stubEnv('CONUSAI_API_TOKEN', 'dt_fixture')
vi.resetModules()
const { register } = await import('@conusai-sdk/observe-next')
const { logger } = await import('@/shared/api/observe/server')
const started = register({ dsn: 'https://pub123@errors.example.com/42', handlers: false })
expect(logger()).toBeDefined()
expect(logger()).toBe(started.otlp?.logger)
await started.otlp?.shutdown(0)
})
})
// Stage regression (deployment #41): register()'s gate and getLogger() both resolved fine, but
// nothing a server action logged ever reached the platform's /otel/v1/logs route -- no real fetch,
// no error, no retry, just silence. The two tests above would have stayed green through that
// regression: they only check that an Otlp instance exists, never that it sends anything. This
// test starts a real HTTP receiver (no mocked fetch, no faked timers) and proves a log a server
// action calls logger().info() with actually leaves the process and arrives within the exporter's
// 5 s batch interval, exactly as the "note.created, outcome: ok" log in create-note's action does.
describe('logger network delivery (D23 regression: a started logger must actually send)', () => {
afterEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
})
it(
'a log emitted through the logger register() started reaches a real receiver within 5 s',
async () => {
const posts: Array<{ authorization: string | undefined; body: string }> = []
const server: Server = createServer((req: IncomingMessage, res) => {
const chunks: Buffer[] = []
req.on('data', (chunk) => chunks.push(chunk))
req.on('end', () => {
posts.push({ authorization: req.headers.authorization, body: Buffer.concat(chunks).toString('utf8') })
res.writeHead(200, { 'content-type': 'application/json' }).end('{}')
})
})
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address() as { port: number }
try {
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', `http://127.0.0.1:${port}/otel`)
vi.stubEnv('CONUSAI_API_URL', `http://127.0.0.1:${port}`)
vi.stubEnv('CONUSAI_API_TOKEN', 'dt_fixture')
vi.resetModules()
const { register } = await import('@conusai-sdk/observe-next')
const { logger } = await import('@/shared/api/observe/server')
const started = register({ dsn: 'https://pub123@errors.example.com/42', handlers: false })
expect(started.otlp?.enabled).toBe(true)
logger()?.info('note.created', { 'note.id': 'note-e2e-1', outcome: 'ok' })
// No fake timers: BatchExporter's own real setInterval (default 5 s, exporter.ts) must fire.
await vi.waitFor(() => expect(posts).toHaveLength(1), { timeout: 7000, interval: 100 })
const [post] = posts
if (!post) throw new Error('unreachable: waitFor already asserted posts.length === 1')
expect(post.authorization).toBe('Bearer dt_fixture')
const doc = JSON.parse(post.body)
const [record] = doc.resourceLogs[0].scopeLogs[0].logRecords
expect(record.body).toEqual({ stringValue: 'note.created' })
expect(record.attributes).toEqual(
expect.arrayContaining([{ key: 'outcome', value: { stringValue: 'ok' } }, { key: 'note.id', value: { stringValue: 'note-e2e-1' } }]),
)
await started.otlp?.shutdown(1000)
} finally {
await new Promise((resolve) => server.close(resolve))
}
},
8000,
)
})
+29
View File
@@ -0,0 +1,29 @@
import 'server-only'
import { init, withRequest, type ErrorTrackingClient } from '@conusai-sdk/observe-node'
import { getLogger, type OtlpLogger } from '@conusai-sdk/observe-next'
import { getEnv } from '@/shared/config/env'
let client: ErrorTrackingClient | undefined
/**
* The server error client, bound to this environment's DSN and release (D8). Server
* errors leave with the request id of the request that raised them.
*/
export function serverErrors(): ErrorTrackingClient {
if (!client) {
const env = getEnv()
client = init({ dsn: env.CONUSAI_DSN, release: env.CONUSAI_RELEASE })
}
return client
}
export { withRequest }
/**
* The structured OTLP logger `register()` started in instrumentation.ts (showcase plan D23) —
* `undefined` in local dev, where the platform does not inject `OTEL_EXPORTER_OTLP_ENDPOINT`.
* Every Server Action logs through this, so a call never throws on its own account.
*/
export function logger(): OtlpLogger | undefined {
return getLogger()
}

Some files were not shown because too many files have changed in this diff Show More