@pikku/skills 0.12.2 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +56 -29
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +80 -34
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +82 -10
- package/skills/pikku-concepts/references/concept-mapping.md +2 -2
- package/skills/pikku-config/SKILL.md +134 -52
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +30 -5
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +12 -7
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +3 -3
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +285 -50
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-software-archaeology/README.md +16 -6
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +35 -1
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
|
@@ -55,15 +55,37 @@ await appServer.start()
|
|
|
55
55
|
|
|
56
56
|
**Methods:**
|
|
57
57
|
|
|
58
|
-
- `init(httpOptions
|
|
58
|
+
- `init(httpOptions?: RunHTTPWiringOptions): Promise<void>` — installs the body parsers, the cookie parser and the Pikku middleware
|
|
59
59
|
- `start(): Promise<void>` — Start listening
|
|
60
|
-
- `stop(): Promise<void>` — Graceful shutdown
|
|
61
|
-
- `enableExitOnSigInt(): Promise<void>` — SIGINT handler
|
|
60
|
+
- `stop(): Promise<void>` — Graceful shutdown; throws if the server was never started
|
|
61
|
+
- `enableExitOnSigInt(): Promise<void>` — SIGINT handler: stops the singleton services, then the server, then exits 0
|
|
62
62
|
- `enableCors(options): void` — Enable CORS
|
|
63
|
-
- `enableStaticAssets(): void` —
|
|
63
|
+
- `enableStaticAssets(): void` — serve `content.localFileUploadPath` under `content.assetUrlPrefix`
|
|
64
|
+
- `enableReaper(): void` — a `PUT /reaper/*path` upload sink for local development, path-traversal checked and bounded by `content.sizeLimit` (default `1mb`)
|
|
65
|
+
- `getHttpServer(): Server` — the underlying `http.Server`, e.g. to attach a WebSocket server; throws before `start()`
|
|
66
|
+
|
|
67
|
+
`enableStaticAssets` and `enableReaper` both throw when `content` is unset.
|
|
64
68
|
|
|
65
69
|
**Property:** `app: Express` — Direct access to Express instance for custom middleware.
|
|
66
70
|
|
|
71
|
+
### Ordering, and what `init` installs for you
|
|
72
|
+
|
|
73
|
+
The health check is registered in the **constructor**, so it answers before any
|
|
74
|
+
middleware you add and cannot be wrapped in auth. It defaults to
|
|
75
|
+
`/health-check`; override with `healthCheckPath`.
|
|
76
|
+
|
|
77
|
+
Everything else is installed by `init()`: `express.json`, `express.text` (for
|
|
78
|
+
`text/xml`), `express.urlencoded`, `cookie-parser`, then the Pikku middleware.
|
|
79
|
+
Call `enableCors` **before** `init` if you want CORS applied to Pikku's routes.
|
|
80
|
+
|
|
81
|
+
Express buffers the body before Pikku sees it, so the parser limit is the only
|
|
82
|
+
place an oversized request can actually be stopped. `httpOptions.maxBodySize`
|
|
83
|
+
therefore feeds those parser limits, with an explicit `config.limits` entry
|
|
84
|
+
(`json` / `xml` / `urlencoded`) still winning. Everything defaults to `1mb`.
|
|
85
|
+
|
|
86
|
+
`init` passes `logRoutes: true` and `loadSchemas: true` by default; your
|
|
87
|
+
`httpOptions` spread over them, so you can turn either off.
|
|
88
|
+
|
|
67
89
|
## Middleware (existing Express app)
|
|
68
90
|
|
|
69
91
|
```bash
|
|
@@ -76,11 +98,25 @@ import { pikkuExpressMiddleware } from '@pikku/express-middleware'
|
|
|
76
98
|
import './.pikku/pikku-bootstrap.gen.js'
|
|
77
99
|
|
|
78
100
|
const app = express()
|
|
101
|
+
app.use(express.json())
|
|
102
|
+
app.use(cookieParser())
|
|
79
103
|
app.use(
|
|
80
104
|
pikkuExpressMiddleware({
|
|
81
105
|
logger: singletonServices.logger,
|
|
82
106
|
logRoutes: true,
|
|
83
107
|
loadSchemas: true,
|
|
108
|
+
// plus any RunHTTPWiringOptions: maxBodySize, respondWith404, coerceDataFromSchema
|
|
84
109
|
})
|
|
85
110
|
)
|
|
86
111
|
```
|
|
112
|
+
|
|
113
|
+
Options beyond `logger` are all optional: `logRoutes` logs the wiring table once
|
|
114
|
+
at startup, `loadSchemas` compiles every schema up front, and the rest are
|
|
115
|
+
`RunHTTPWiringOptions` passed through per request.
|
|
116
|
+
|
|
117
|
+
On your own app **you** own the parser stack — the middleware reads
|
|
118
|
+
`req.body`, so a body parser and `cookie-parser` must be registered before it,
|
|
119
|
+
and `maxBodySize` alone will not stop an oversized request that your parser
|
|
120
|
+
already accepted. Unmatched requests fall through to `next()` (unless
|
|
121
|
+
`respondWith404` is set), so Pikku's routes coexist with your existing ones;
|
|
122
|
+
a streaming response is the exception and does not call `next()`.
|
|
@@ -47,10 +47,19 @@ await appServer.start()
|
|
|
47
47
|
|
|
48
48
|
**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`
|
|
49
49
|
|
|
50
|
-
**Methods:** `init(httpOptions
|
|
50
|
+
**Methods:** `init(httpOptions?: RunHTTPWiringOptions)`, `start()`, `stop()`, `enableExitOnSigInt()`
|
|
51
51
|
|
|
52
52
|
**Property:** `app: FastifyInstance` — Direct access to Fastify instance.
|
|
53
53
|
|
|
54
|
+
`enableCors` exists on the class but **throws `Method not implemented.`** — unlike
|
|
55
|
+
the Express server. Register `@fastify/cors` on `app` yourself before `init()`.
|
|
56
|
+
|
|
57
|
+
Unlike the Express server, the health check is registered by `init()`, not the
|
|
58
|
+
constructor, so nothing answers before `init` runs. `init` also passes
|
|
59
|
+
`logRoutes: true` and `loadSchemas: true`, which your `httpOptions` can override.
|
|
60
|
+
The Fastify instance is constructed with no options; reach for the plugin package
|
|
61
|
+
if you need `Fastify({ … })` of your own.
|
|
62
|
+
|
|
54
63
|
## Plugin (existing Fastify app)
|
|
55
64
|
|
|
56
65
|
```bash
|
|
@@ -68,6 +77,18 @@ app.register(pikkuFastifyPlugin, {
|
|
|
68
77
|
logger: singletonServices.logger,
|
|
69
78
|
logRoutes: true,
|
|
70
79
|
loadSchemas: true,
|
|
80
|
+
// plus any RunHTTPWiringOptions: maxBodySize, respondWith404, …
|
|
71
81
|
},
|
|
72
82
|
})
|
|
73
83
|
```
|
|
84
|
+
|
|
85
|
+
Every option other than `logger` is optional, and the rest of the `pikku` object
|
|
86
|
+
is `RunHTTPWiringOptions` passed straight through.
|
|
87
|
+
|
|
88
|
+
The plugin registers a catch-all `fastify.all('/*')`, so mount it on a
|
|
89
|
+
[Fastify prefix](https://fastify.dev/docs/latest/Reference/Plugins/) if the app
|
|
90
|
+
has routes of its own to keep.
|
|
91
|
+
|
|
92
|
+
Fastify buffers the body itself, so its `bodyLimit` is where an oversized request
|
|
93
|
+
is stopped. `maxBodySize` sets it — and left unset, Fastify's stricter 1MB default
|
|
94
|
+
stands rather than being loosened to Pikku's 10MB fallback.
|
|
@@ -44,30 +44,75 @@ export const coldStart = async () => {
|
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
If the deploy codegen generated your handlers, this caching is already done for
|
|
48
|
+
you by the factories in `@pikku/lambda` — `createLambdaHandler(factories,
|
|
49
|
+
handlerTypes)`, `createLambdaWorkerHandler(factories)` and
|
|
50
|
+
`createLambdaWebSocketHandler(factories)`. They build `variables`/`secrets` from
|
|
51
|
+
`process.env`, cache the singleton services in module scope, and return the
|
|
52
|
+
named exports (`handler`, `queue`, `scheduled`, or `connect`/`disconnect`/
|
|
53
|
+
`default`) that `serverless.yml` references. Hand-written handlers are for cases
|
|
54
|
+
the codegen does not cover.
|
|
55
|
+
|
|
47
56
|
## HTTP Handler
|
|
48
57
|
|
|
58
|
+
Pick the entry point that matches the API Gateway payload version — they take
|
|
59
|
+
different event types and are not interchangeable:
|
|
60
|
+
|
|
49
61
|
```typescript
|
|
50
|
-
import type {
|
|
51
|
-
import { runFetch } from '@pikku/lambda/http'
|
|
62
|
+
import type { APIGatewayEvent } from 'aws-lambda'
|
|
63
|
+
import { runFetch } from '@pikku/lambda/http' // REST API / payload v1
|
|
52
64
|
|
|
53
|
-
export const httpRoute = async (event:
|
|
65
|
+
export const httpRoute = async (event: APIGatewayEvent) => {
|
|
54
66
|
await coldStart()
|
|
55
67
|
return await runFetch(event)
|
|
56
68
|
}
|
|
57
69
|
```
|
|
58
70
|
|
|
71
|
+
```typescript
|
|
72
|
+
import type { APIGatewayProxyEventV2 } from 'aws-lambda'
|
|
73
|
+
import { runFetchV2 } from '@pikku/lambda/http' // HTTP API / payload v2
|
|
74
|
+
|
|
75
|
+
export const httpRoute = async (event: APIGatewayProxyEventV2) => {
|
|
76
|
+
await coldStart()
|
|
77
|
+
return await runFetchV2(event)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Both answer `OPTIONS` themselves before Pikku's wirings run, so a preflight
|
|
82
|
+
never reaches your middleware. Only `runFetchV2` echoes the request `Origin`
|
|
83
|
+
into `Access-Control-Allow-Origin`; `runFetch` sets allowed headers and methods
|
|
84
|
+
but **no origin header at all**, so v1 preflights fail in the browser unless
|
|
85
|
+
API Gateway or a CloudFront layer adds one.
|
|
86
|
+
|
|
87
|
+
They also differ on failure: `runFetchV2` logs and returns a JSON `500`, while
|
|
88
|
+
`runFetch` swallows the error and returns whatever status the response already
|
|
89
|
+
carried.
|
|
90
|
+
|
|
91
|
+
Neither takes `RunHTTPWiringOptions` — there is no `maxBodySize` or
|
|
92
|
+
`respondWith404` knob here; API Gateway's own payload limit is the bound.
|
|
93
|
+
|
|
59
94
|
## Scheduled Tasks
|
|
60
95
|
|
|
61
96
|
```typescript
|
|
62
97
|
import type { ScheduledHandler } from 'aws-lambda'
|
|
63
|
-
import {
|
|
98
|
+
import { runLambdaScheduled } from '@pikku/lambda/scheduled'
|
|
64
99
|
|
|
65
|
-
export const
|
|
100
|
+
export const scheduled: ScheduledHandler = async (event) => {
|
|
66
101
|
await coldStart()
|
|
67
|
-
await
|
|
102
|
+
await runLambdaScheduled(event)
|
|
68
103
|
}
|
|
69
104
|
```
|
|
70
105
|
|
|
106
|
+
`runLambdaScheduled` runs **every** scheduled task registered in the bundle,
|
|
107
|
+
each with its own `cron-<uuid>` traceId, and logs rather than rethrows a task
|
|
108
|
+
failure — so one bad task cannot fail the invocation or stop the others. The
|
|
109
|
+
event itself is ignored; which tasks run is decided by what the unit bundled,
|
|
110
|
+
not by which EventBridge rule fired.
|
|
111
|
+
|
|
112
|
+
Reach for `runScheduledTask({ name })` from `@pikku/core/scheduler` directly
|
|
113
|
+
only when one Lambda genuinely bundles several tasks that must fire on separate
|
|
114
|
+
schedules.
|
|
115
|
+
|
|
71
116
|
## SQS Queue Worker
|
|
72
117
|
|
|
73
118
|
```typescript
|
|
@@ -80,6 +125,25 @@ export const mySQSWorker: SQSHandler = async (event) => {
|
|
|
80
125
|
}
|
|
81
126
|
```
|
|
82
127
|
|
|
128
|
+
The worker returns an `SQSBatchResponse` listing the failed messages in
|
|
129
|
+
`batchItemFailures`, which SQS only honours when the event source mapping has
|
|
130
|
+
**`ReportBatchItemFailures`** enabled. Without it the whole batch is retried
|
|
131
|
+
when any one message fails, so successfully processed jobs run twice.
|
|
132
|
+
|
|
133
|
+
Records are processed in parallel, and the queue name is taken from the last
|
|
134
|
+
segment of `eventSourceARN` — it must match the name the worker was wired under.
|
|
135
|
+
A `QueueJobDiscardedError` counts as success (no retry); anything else is
|
|
136
|
+
reported as a failed item.
|
|
137
|
+
|
|
138
|
+
`waitForCompletion` throws on an SQS job: the transport is fire-and-forget.
|
|
139
|
+
|
|
140
|
+
On the producer side, `SQSQueueService` resolves each queue URL from the
|
|
141
|
+
constructor's `queueUrlMap` first, then from
|
|
142
|
+
`SQS_QUEUE_URL_<SCREAMING_SNAKE_NAME>`, and throws naming the missing variable
|
|
143
|
+
if neither has it. `supportsResults` is `false` and `getJob()` always throws —
|
|
144
|
+
use BullMQ or PgBoss if you need results. `delay` is milliseconds, rounded up to
|
|
145
|
+
whole seconds and capped at SQS's 900s ceiling.
|
|
146
|
+
|
|
83
147
|
## WebSocket (API Gateway v2)
|
|
84
148
|
|
|
85
149
|
```typescript
|
|
@@ -90,21 +154,37 @@ import {
|
|
|
90
154
|
LambdaEventHubService,
|
|
91
155
|
} from '@pikku/lambda/websocket'
|
|
92
156
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
return { statusCode: 200, body: '' }
|
|
157
|
+
const params = async (event) => {
|
|
158
|
+
const { channelStore } = await coldStart()
|
|
159
|
+
return { channelStore }
|
|
97
160
|
}
|
|
98
161
|
|
|
99
|
-
export const
|
|
100
|
-
|
|
101
|
-
return await disconnectWebsocket(event, params)
|
|
102
|
-
}
|
|
162
|
+
export const connectHandler = async (event) =>
|
|
163
|
+
await connectWebsocket(event, await params(event))
|
|
103
164
|
|
|
104
|
-
export const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
export const disconnectHandler = async (event) =>
|
|
166
|
+
await disconnectWebsocket(event, await params(event))
|
|
167
|
+
|
|
168
|
+
export const defaultHandler = async (event) =>
|
|
169
|
+
await processWebsocketMessage(event, await params(event))
|
|
108
170
|
```
|
|
109
171
|
|
|
110
|
-
|
|
172
|
+
All three take the same `{ channelStore }` and **return a complete
|
|
173
|
+
`APIGatewayProxyResult`** — return it. Discarding `connectWebsocket`'s result
|
|
174
|
+
and answering a hardcoded `200` accepts every connection, including the ones
|
|
175
|
+
your channel's auth rejected.
|
|
176
|
+
|
|
177
|
+
`channelStore` (e.g. `PgChannelStore`) must be a real shared store: each route
|
|
178
|
+
is a separate invocation, so nothing survives in memory between `$connect` and
|
|
179
|
+
`$default`.
|
|
180
|
+
|
|
181
|
+
`LambdaEventHubService` handles cross-connection messaging and takes
|
|
182
|
+
`(logger, event, channelStore, eventHubStore)` — the `event` is needed to derive
|
|
183
|
+
the API Gateway Management endpoint, so it is constructed per invocation, not
|
|
184
|
+
once at cold start. It also needs an `EventHubStore` alongside the channel
|
|
185
|
+
store.
|
|
186
|
+
|
|
187
|
+
Two behaviours to design around: **binary payloads throw** (`Binary data is not
|
|
188
|
+
supported on serverless lambdas`), and any `PostToConnection` failure removes
|
|
189
|
+
the connection from the channel store — a transient error drops a live client,
|
|
190
|
+
not just a stale one.
|
|
@@ -38,6 +38,17 @@ export const PATCH = pikkuAPIRequest
|
|
|
38
38
|
export const DELETE = pikkuAPIRequest
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
`pikkuAPIRequest` strips a leading `/api` from the pathname before routing, so
|
|
42
|
+
wirings are declared as `/todos`, not `/api/todos`, even though the route file
|
|
43
|
+
lives under `app/api`. Turn that off with `removeAPIPrefix(false)` from the same
|
|
44
|
+
generated file if your wirings really do carry the prefix.
|
|
45
|
+
|
|
46
|
+
It takes `(req, context)` to match Next's handler signature but ignores the
|
|
47
|
+
context — Pikku routes from the URL, so the catch-all segment name is yours to
|
|
48
|
+
choose. It also passes no `RunHTTPWiringOptions`: to set `maxBodySize` or
|
|
49
|
+
`respondWith404` you need your own handler over `new PikkuNextJS(...)` calling
|
|
50
|
+
`apiRequest(req, options)`.
|
|
51
|
+
|
|
41
52
|
## Server-Side Data Fetching
|
|
42
53
|
|
|
43
54
|
Use the generated `pikku()` helper in Server Components or Server Actions:
|
|
@@ -45,7 +56,7 @@ Use the generated `pikku()` helper in Server Components or Server Actions:
|
|
|
45
56
|
```typescript
|
|
46
57
|
import { pikku } from '@/pikku-nextjs.gen.js'
|
|
47
58
|
|
|
48
|
-
const { get, post, del, rpc, staticGet, staticPost, staticRPC } = pikku()
|
|
59
|
+
const { get, post, patch, del, rpc, staticGet, staticPost, staticRPC } = pikku()
|
|
49
60
|
|
|
50
61
|
// Dynamic (reads headers/cookies — requires request context)
|
|
51
62
|
const todos = await get('/todos')
|
|
@@ -60,8 +71,19 @@ const result = await rpc('calculateTax', { amount: 100, region: 'US' })
|
|
|
60
71
|
|
|
61
72
|
**Dynamic vs Static:**
|
|
62
73
|
|
|
63
|
-
- `get`, `post`, `del`, `rpc` —
|
|
64
|
-
|
|
74
|
+
- `get`, `post`, `patch`, `del`, `rpc` — read `next/headers` cookies and headers,
|
|
75
|
+
so they force the component dynamic
|
|
76
|
+
- `staticGet`, `staticPost`, `staticRPC` — no request context, safe for
|
|
77
|
+
precompile/ISR
|
|
78
|
+
|
|
79
|
+
The static variants pass `skipUserSession: true`, so a wiring that expects a
|
|
80
|
+
session sees none. That is the real difference — not just where they can run.
|
|
81
|
+
There is no `staticPatch` or `staticDel`; a mutation at build time is not a
|
|
82
|
+
thing the generated client offers.
|
|
83
|
+
|
|
84
|
+
Both paths run with `bubbleErrors: true`, so a failing wiring **throws** in your
|
|
85
|
+
Server Component rather than resolving to an error status. Wrap the call, or let
|
|
86
|
+
the Next.js error boundary take it.
|
|
65
87
|
|
|
66
88
|
## How It Works
|
|
67
89
|
|
|
@@ -73,6 +95,28 @@ import { PikkuNextJS } from '@pikku/next'
|
|
|
73
95
|
const pikku = new PikkuNextJS(createConfig, createSingletonServices)
|
|
74
96
|
```
|
|
75
97
|
|
|
76
|
-
**Constructor:** `new PikkuNextJS(createConfig
|
|
98
|
+
**Constructor:** `new PikkuNextJS(createConfig | undefined, createSingletonServices)`
|
|
99
|
+
|
|
100
|
+
Both arguments are positional and `createConfig` is only optional in the sense
|
|
101
|
+
that passing `undefined` substitutes an empty config —
|
|
102
|
+
`createSingletonServices` is required.
|
|
103
|
+
|
|
104
|
+
Initialization is memoized on a promise, so concurrent first requests share one
|
|
105
|
+
setup; a failed setup clears the promise, so the next request retries rather
|
|
106
|
+
than caching the failure forever.
|
|
107
|
+
|
|
108
|
+
The generated `pikku-nextjs.gen.ts` wraps this with full type safety from your
|
|
109
|
+
route definitions.
|
|
110
|
+
|
|
111
|
+
## Related exports
|
|
77
112
|
|
|
78
|
-
|
|
113
|
+
- **`PikkuNextJSWorkerRPC({ fetcher })`** — same surface as `PikkuNextJS`, but
|
|
114
|
+
every call is dispatched through a `Fetcher` (a Cloudflare service binding, a
|
|
115
|
+
local HTTP client, a fabric dispatcher) instead of loading function code
|
|
116
|
+
in-process. Use it to keep functions out of the SSR bundle. A non-2xx
|
|
117
|
+
response throws with the status and body text.
|
|
118
|
+
- **`toNextJsAuthHandler(auth)`** — wraps a better-auth instance or handler
|
|
119
|
+
function for an auth route. The three-argument form
|
|
120
|
+
`(pikkuAuthFactory, createConfig, createSingletonServices)` resolves a Pikku
|
|
121
|
+
better-auth factory lazily; it throws if you pass `createConfig` without
|
|
122
|
+
`createSingletonServices`. `nextCookies` is re-exported alongside it.
|
|
@@ -47,10 +47,52 @@ await appServer.start()
|
|
|
47
47
|
|
|
48
48
|
**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`
|
|
49
49
|
|
|
50
|
-
**Methods:** `init(httpOptions
|
|
50
|
+
**Methods:** `init(httpOptions?: RunHTTPWiringOptions)`, `start()`, `stop()`, `enableExitOnSigInt()`
|
|
51
51
|
|
|
52
52
|
**Property:** `app: uWS.App` — Direct access to uWebSockets app instance.
|
|
53
53
|
|
|
54
|
+
### What the server does and does not give you
|
|
55
|
+
|
|
56
|
+
`init()` registers three things in order: the health check (`healthCheckPath`,
|
|
57
|
+
default `/health-check`), a catch-all `app.any('/*')` HTTP handler, and a
|
|
58
|
+
catch-all `app.ws('/*')` websocket handler. Nothing is registered by the
|
|
59
|
+
constructor, so nothing answers before `init` runs.
|
|
60
|
+
|
|
61
|
+
**There is no `enableCors`, no static assets and no `content` support** — unlike
|
|
62
|
+
the Express server. The class is explicitly a prototyping convenience; for
|
|
63
|
+
anything that needs extra handlers, use `@pikku/uws-handler` directly and treat
|
|
64
|
+
`pikku-uws-server.ts` as the template (that is what its own JSDoc says).
|
|
65
|
+
|
|
66
|
+
`httpOptions` reaches the HTTP handler only. The websocket handler is
|
|
67
|
+
constructed with a fixed `{ logger, logRoutes: true }`, so per-request options
|
|
68
|
+
do not apply to the upgrade path. `loadSchemas` is also never passed by the
|
|
69
|
+
server, so schemas compile lazily on first use rather than at startup — pass
|
|
70
|
+
`loadSchemas: true` in `httpOptions` if you want the startup cost paid up front.
|
|
71
|
+
|
|
72
|
+
`stop()` closes the listen socket and then waits a fixed 2 seconds for
|
|
73
|
+
connections to drain. Called before `start()`, it throws a bare **string**, not
|
|
74
|
+
an `Error`, so `catch (e) { e.message }` reads `undefined`.
|
|
75
|
+
|
|
76
|
+
### Body limits
|
|
77
|
+
|
|
78
|
+
uWS hands over raw chunks with no limit of its own, so the handler counts the
|
|
79
|
+
bytes itself. A request over `maxBodySize` (default `DEFAULT_MAX_BODY_SIZE`) is
|
|
80
|
+
answered `413` with a `PayloadTooLargeError` body, and the chunks are dropped
|
|
81
|
+
rather than concatenated — an oversized request never accumulates in memory. A
|
|
82
|
+
`content-length` header that already exceeds the limit short-circuits before any
|
|
83
|
+
data arrives.
|
|
84
|
+
|
|
85
|
+
### Handlers directly (own uWS app)
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { pikkuHTTPHandler, pikkuWebsocketHandler } from '@pikku/uws-handler'
|
|
89
|
+
|
|
90
|
+
app.any('/*', pikkuHTTPHandler({ logger, logRoutes: true, loadSchemas: true }))
|
|
91
|
+
app.ws('/*', pikkuWebsocketHandler({ logger, logRoutes: true }))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Both take `{ logger, logRoutes?, loadSchemas? } & RunHTTPWiringOptions`.
|
|
95
|
+
|
|
54
96
|
## WebSocket Standalone (ws library)
|
|
55
97
|
|
|
56
98
|
For WebSocket-only servers using the `ws` library:
|
|
@@ -86,3 +128,14 @@ process.on('SIGINT', async () => {
|
|
|
86
128
|
process.exit(0)
|
|
87
129
|
})
|
|
88
130
|
```
|
|
131
|
+
|
|
132
|
+
`pikkuWebsocketHandler` takes `{ server, wss, logger, logRoutes?, loadSchemas? }`
|
|
133
|
+
plus `RunHTTPWiringOptions`, and there is no server class in `@pikku/ws` — the
|
|
134
|
+
handler attaches to a `Server` you own.
|
|
135
|
+
|
|
136
|
+
`noServer: true` is required, not stylistic: the handler listens for the HTTP
|
|
137
|
+
server's own `upgrade` event, opens the channel (running middleware and auth
|
|
138
|
+
first), and only then calls `wss.handleUpgrade`. A `WebSocketServer` bound to
|
|
139
|
+
the server would take the socket before any of that ran. An upgrade the channel
|
|
140
|
+
rejects gets the socket destroyed, and an auth failure is written as a real HTTP
|
|
141
|
+
response on the raw socket rather than a silent drop.
|
|
@@ -36,19 +36,29 @@ installGroups: [core]
|
|
|
36
36
|
|
|
37
37
|
- `pikku audit` — reports **security advisories** only.
|
|
38
38
|
- `pikku audit --outdated` — also reports **available dependency updates**.
|
|
39
|
-
- Package-manager detection is by **lockfile
|
|
40
|
-
|
|
39
|
+
- Package-manager detection is by **lockfile**, walking up to 12 levels to the
|
|
40
|
+
workspace root, checking in this order: `bun.lock`/`bun.lockb`,
|
|
41
|
+
`pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`. A project with several
|
|
42
|
+
lockfiles resolves as bun. Only **bun** runs a real audit (`bun audit --json` +
|
|
41
43
|
`bun outdated`, normalised into one `SecurityAuditReport` with per-severity /
|
|
42
44
|
per-update-level counts). Other PMs are detected but **stubbed** with a `note`
|
|
43
45
|
field until their shapes are normalised — issues/updates come back empty.
|
|
44
|
-
- `bun audit` exits non-zero when it *finds* advisories but still writes
|
|
45
|
-
|
|
46
|
+
- `bun audit` exits non-zero when it *finds* advisories but still writes the
|
|
47
|
+
payload to stdout, so a non-zero exit **with output** is data. A non-zero exit
|
|
48
|
+
with **no** output — or a launch failure, timeout, or a blown 32MB buffer —
|
|
49
|
+
throws, precisely so a failed run can't masquerade as "0 advisories".
|
|
46
50
|
|
|
47
51
|
## Console integration (@pikku/addon-console)
|
|
48
52
|
|
|
49
53
|
Three RPCs, all reading/writing the same artifact via the meta service. Shared
|
|
50
54
|
spawn/read helpers live in `lib/audit-exec.ts` (`readAuditReport`,
|
|
51
|
-
`runPikkuAudit`, `spawnProcess`, `findBin`)
|
|
55
|
+
`runPikkuAudit`, `spawnProcess`, `findBin`), alongside `lib/find-project-root.ts`
|
|
56
|
+
and `lib/resolve-package-manager.ts` (`resolvePackageManager`, `installArgs`,
|
|
57
|
+
`execPrefix`) — reuse them, don't re-implement. `resolvePackageManager` reads
|
|
58
|
+
package.json's corepack `packageManager` field first and only falls back to
|
|
59
|
+
lockfiles, because that field states intent before a lockfile exists and a
|
|
60
|
+
project can carry a stale one from another tool. Guessing wrong is not a soft
|
|
61
|
+
failure: the spawn dies with `Executable not found in $PATH`.
|
|
52
62
|
Like every console RPC these require an **authenticated session** (the console
|
|
53
63
|
is admin-only), so the host must have Better Auth wired — see `pikku-better-auth`.
|
|
54
64
|
|
|
@@ -84,15 +94,26 @@ is admin-only), so the host must have Better Auth wired — see `pikku-better-au
|
|
|
84
94
|
|
|
85
95
|
```ts
|
|
86
96
|
{
|
|
97
|
+
schemaVersion: number
|
|
87
98
|
tool: string // e.g. 'bun'
|
|
99
|
+
generatedAt: string // ISO timestamp
|
|
88
100
|
note?: string // set when the audit could NOT run (unsupported PM);
|
|
89
101
|
// render ONLY the note — never a reassuring "no vulnerabilities"
|
|
90
|
-
summary: {
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
summary: {
|
|
103
|
+
totalIssues, critical, high, moderate, low: number // no `info` bucket
|
|
104
|
+
totalUpdates, major, minor, patch: number
|
|
105
|
+
}
|
|
106
|
+
issues: SecurityAuditIssue[] // package, severity, title, advisoryId, url,
|
|
107
|
+
// vulnerableVersions, cwe[], cvssScore, recommendedVersion
|
|
93
108
|
updates: SecurityAuditUpdate[] // package, current, latest, level (major|minor|patch|unknown)
|
|
94
109
|
}
|
|
95
110
|
```
|
|
96
111
|
|
|
112
|
+
`severity` is one of `critical | high | moderate | low | info`, but `summary`
|
|
113
|
+
has no `info` count — an informational advisory raises `totalIssues` without
|
|
114
|
+
landing in a severity bucket, so don't sum the four to get the total. On an
|
|
115
|
+
issue, `url`, `cvssScore` and `recommendedVersion` are always present and
|
|
116
|
+
**nullable** rather than optional: check for `null`, not `undefined`.
|
|
117
|
+
|
|
97
118
|
When `note` is present the audit did not run — show only the note (an "Audit not
|
|
98
119
|
run" state), never the "no known vulnerabilities / up to date" copy.
|
|
@@ -23,7 +23,8 @@ generated output is never edited by hand.
|
|
|
23
23
|
|
|
24
24
|
## Agent Operating Procedure
|
|
25
25
|
|
|
26
|
-
1. Edit source files under `emailTemplatesDir` only. Never edit `.pikku/email/*`.
|
|
26
|
+
1. Edit source files under `emailTemplatesDir` only. Never edit `.pikku/email/*`. If the
|
|
27
|
+
directory does not exist yet, run `pikku emails init` rather than creating it by hand.
|
|
27
28
|
2. After any change run `pikku emails generate` (it is also part of `prebuild`, usually
|
|
28
29
|
`pikku bootstrap; pikku all; pikku emails generate`).
|
|
29
30
|
3. Validate by importing `renderEmailTemplate` and rendering with sample data, or run the
|
|
@@ -40,7 +41,14 @@ generated output is never edited by hand.
|
|
|
40
41
|
}
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
If `emailTemplatesDir` is unset the command is a no-op
|
|
44
|
+
If `emailTemplatesDir` is unset the command is a no-op — it logs
|
|
45
|
+
`Skipping emails (set emailTemplatesDir in pikku.config.json to enable).` and exits
|
|
46
|
+
cleanly, so a silent generate is a config problem, not a template problem.
|
|
47
|
+
|
|
48
|
+
`pikku emails init` scaffolds the directory (starter locales, theme, partials and a
|
|
49
|
+
hello-world template) **and** writes `emailTemplatesDir` into `pikku.config.json` for
|
|
50
|
+
you. Use it rather than hand-creating the tree; `--force` overwrites an existing
|
|
51
|
+
scaffold.
|
|
44
52
|
|
|
45
53
|
## Directory layout
|
|
46
54
|
|
|
@@ -97,8 +105,17 @@ import {
|
|
|
97
105
|
// { appName?: ...; inviteUrl?: ...; inviterName?: ...; organizationName?: ... }
|
|
98
106
|
```
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
Every extracted variable is emitted **optional** and typed `EmailTemplateValue`
|
|
109
|
+
(`string | number | boolean | null | undefined | object | array`). The type tells you
|
|
110
|
+
which variables a template can consume, not which ones it needs — there is no way to
|
|
111
|
+
mark one required, and a template that references none types as `Record<string, never>`.
|
|
112
|
+
Referencing a variable in the template body (rather than only in a locale string) is
|
|
113
|
+
what gets it into the type at all.
|
|
114
|
+
|
|
115
|
+
That matters because a placeholder with nothing behind it renders as the **empty
|
|
116
|
+
string** — no error, no leftover `{{…}}`. A typo'd variable name, a missing `data` key
|
|
117
|
+
and a value that isn't a string or number all produce the same silently blank output, so
|
|
118
|
+
render with sample data and read the result rather than trusting that it compiled.
|
|
102
119
|
|
|
103
120
|
## Rendering
|
|
104
121
|
|
|
@@ -111,7 +128,17 @@ const rendered = renderEmailTemplate({
|
|
|
111
128
|
// rendered: { name, locale, subject, html, text?, variables, hash }
|
|
112
129
|
```
|
|
113
130
|
|
|
131
|
+
It is synchronous, and it throws on an unknown template name or an unknown locale —
|
|
132
|
+
those are the only two failure modes; everything else degrades to blank output.
|
|
133
|
+
|
|
114
134
|
`hash` is a stable content hash (useful as an idempotency / dedupe key on outgoing mail).
|
|
135
|
+
The meta file also carries per-locale `htmlHash` / `subjectHash` / `textHash` if you need
|
|
136
|
+
to tell which part changed.
|
|
137
|
+
|
|
138
|
+
`{{locale}}` is in scope alongside `{{appName}}`, and placeholders are resolved by
|
|
139
|
+
repeated passes so a locale string containing `{{verifyUrl}}` expands. The loop stops
|
|
140
|
+
after 5 passes, which only becomes visible with placeholders nested more deeply than
|
|
141
|
+
that — a shape worth avoiding rather than working around.
|
|
115
142
|
|
|
116
143
|
## Sending through an EmailService
|
|
117
144
|
|
|
@@ -160,4 +187,8 @@ async send(input: SendEmailInput) {
|
|
|
160
187
|
reference it in this template to scope it in.
|
|
161
188
|
- Editing a locale string changes that template's content hash — expected; the hash covers
|
|
162
189
|
the strings the template uses.
|
|
163
|
-
- `layout.html` must contain `{{content}}` or the body is dropped.
|
|
190
|
+
- `layout.html` must contain `{{content}}` or the body is dropped. It is matched by the
|
|
191
|
+
partial name `layout`, so renaming the file opts every template out of the wrapper.
|
|
192
|
+
- A blank spot where a value should be is an unresolved placeholder, not a render
|
|
193
|
+
failure — check the key's spelling and that the value is a string or number (objects
|
|
194
|
+
and arrays resolve to empty).
|
|
@@ -96,6 +96,25 @@ Run migrations: `pikku db migrate`. It also regenerates `.pikku/db/schema.gen.ts
|
|
|
96
96
|
|
|
97
97
|
**NEVER hand-edit the generated schema** — write a migration and re-run.
|
|
98
98
|
|
|
99
|
+
### Dev seed data
|
|
100
|
+
|
|
101
|
+
Alongside the migrations sits `db/<engine>-dev-seed.sql` — `db/sqlite-dev-seed.sql`
|
|
102
|
+
or `db/postgres-dev-seed.sql`. There is no seed command. `pikku db reset` is the
|
|
103
|
+
only thing that applies it: wipe, migrate, seed. `--no-seed` stops after the
|
|
104
|
+
migration, for working on an empty-state or onboarding flow the test data hides.
|
|
105
|
+
|
|
106
|
+
Because reset always arrives at a database it has just wiped, **the seed file is
|
|
107
|
+
plain `INSERT`s** — no `INSERT OR IGNORE`, no `ON CONFLICT DO NOTHING`, no
|
|
108
|
+
`IF NOT EXISTS`. Nothing applies it twice, so it never has to defend itself. If
|
|
109
|
+
you find yourself reaching for an idempotent form, that's a sign the data wants
|
|
110
|
+
to be a migration instead.
|
|
111
|
+
|
|
112
|
+
This is **test data only**: enough rows that a fresh dev database isn't an empty
|
|
113
|
+
app. It never reaches staging or production — reset refuses `NODE_ENV=production`
|
|
114
|
+
and refuses a database outside the runtime directory. Anything a real environment
|
|
115
|
+
needs — accounts, role grants — is provisioning, not seeding, and belongs in
|
|
116
|
+
`pikku persona sync` or a migration.
|
|
117
|
+
|
|
99
118
|
A Better Auth app has a second constraint: the plugins you enable (`admin()`,
|
|
100
119
|
`actor()`, …) each declare columns, and `pikku db migrate` refuses to run while
|
|
101
120
|
the applied schema is missing any of them. `pikku db generate` writes the
|
|
@@ -143,7 +162,7 @@ packages/functions/
|
|
|
143
162
|
*.channel.ts # wireChannel
|
|
144
163
|
*.queue.ts # wireQueueWorker
|
|
145
164
|
*.schedule.ts # wireScheduler
|
|
146
|
-
*.mcp.ts #
|
|
165
|
+
*.mcp.ts # wireMCPResource / wireMCPPrompt (an MCP tool is just a function with `mcp: true`)
|
|
147
166
|
*.cli.ts # wireCLI
|
|
148
167
|
services.ts # pikkuServices factory (singleton)
|
|
149
168
|
middleware.ts # Shared middleware
|
|
@@ -152,6 +171,7 @@ packages/functions/
|
|
|
152
171
|
db/schema.gen.ts # Kysely types, written by `pikku db migrate` — NEVER hand-edit
|
|
153
172
|
apps/app/ # Frontend(s)
|
|
154
173
|
db/sqlite/ # Plain .sql migrations, numbered, gap-free (project root)
|
|
174
|
+
db/sqlite-dev-seed.sql # Dev-only test data, applied by `pikku db reset`
|
|
155
175
|
pikku.config.json # Pikku + deploy config (project root)
|
|
156
176
|
pikkufabric.config.json # Fabric project link + frontends (project root)
|
|
157
177
|
```
|
|
@@ -183,12 +203,17 @@ Links the repo to a Fabric project and declares its frontends:
|
|
|
183
203
|
```
|
|
184
204
|
|
|
185
205
|
- `projectId`: written by `pikku fabric init` / `link`. Templates ship the
|
|
186
|
-
`__PROJECT_ID__` placeholder — that is
|
|
206
|
+
`__PROJECT_ID__` placeholder — that is _not_ a link, and the CLI treats it as
|
|
187
207
|
unlinked.
|
|
188
208
|
- `production.domain`: optional custom domain. Production always maps to `main`;
|
|
189
209
|
without a domain it lives on the platform `*.pikkufabric.app` hostnames.
|
|
190
210
|
- `frontends`: each entry declares a frontend app with its dev command and port
|
|
191
211
|
|
|
212
|
+
Several CLI messages call this file `fabric.config.json` — `fabric init --force`,
|
|
213
|
+
`fabric link --apiUrl`, and the `domains` commands' "No fabric.config.json found".
|
|
214
|
+
The file the CLI actually reads and writes is `pikkufabric.config.json`; don't
|
|
215
|
+
create the shorter name to satisfy an error message.
|
|
216
|
+
|
|
192
217
|
## RPC is the default transport
|
|
193
218
|
|
|
194
219
|
In Fabric apps, most features don't need HTTP wirings. Just write the function with `expose: true` — Pikku generates an RPC client and React Query hooks automatically.
|
|
@@ -290,7 +315,7 @@ The output card shows whether any breaking changes were detected.
|
|
|
290
315
|
|
|
291
316
|
These apply in every Fabric app:
|
|
292
317
|
|
|
293
|
-
- **No `process.env`** — use `variables.get('NAME')` and `secrets.getSecret('NAME')`. Declare with `
|
|
318
|
+
- **No `process.env`** — use `variables.get('NAME')` and `secrets.getSecret('NAME')`. Declare with `defineVariable` / `defineSecret`.
|
|
294
319
|
- **No `as any`** — fix types properly.
|
|
295
320
|
- **No generic `Error`** — throw `NotFoundError`, `ConflictError`, `BadRequestError`, `UnauthorizedError` from `@pikku/core/errors`.
|
|
296
321
|
- **No auth checks in function bodies** — use `permissions:` field on the function config with a `pikkuPermission` factory.
|
|
@@ -311,8 +336,8 @@ Fix every `error` and `warn` in the output before continuing. Then:
|
|
|
311
336
|
1. **Replace the database layer**: swap PostgreSQL/MySQL queries for Kysely + libSQL. Convert schema to SQLite-compatible SQL migrations in `db/sqlite/`.
|
|
312
337
|
2. **Replace route handlers with pikkuFuncs**: extract business logic into `pikkuFunc`/`pikkuSessionlessFunc`, add `wireHTTP` or `expose: true` for transport.
|
|
313
338
|
3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.
|
|
314
|
-
4. **Replace `process.env` calls
|
|
339
|
+
4. **Replace `process.env` calls**: plain config becomes `defineVariable` + `variables.get()`, anything sensitive becomes `defineSecret` + `secrets.getSecret()`.
|
|
315
340
|
5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles`.
|
|
316
|
-
6. **Add `
|
|
341
|
+
6. **Add `pikkufabric.config.json`** at project root with `projectId`, `production.domain`, and `frontends` (production is always `main`, so there is no `production.branch`).
|
|
317
342
|
7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.
|
|
318
343
|
8. **Run `pikku fabric validate`** once more to confirm no structural issues remain.
|
|
@@ -63,6 +63,8 @@ pikku fabric metrics -b main # last 24h
|
|
|
63
63
|
pikku fabric metrics -b main --hours 2 --function createOrder
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
`--branch` is **required** here too; `--hours` defaults to 24.
|
|
67
|
+
|
|
66
68
|
Rows are `reqs= err= (rate%) avg= min= max=` per bucket. A single bad request
|
|
67
69
|
with a healthy error rate is a data problem; a climbing error rate is a
|
|
68
70
|
deployment or dependency problem. `--json` additionally returns a `wireTypes`
|
|
@@ -95,7 +97,9 @@ deploy stuck in flight, explains a whole class of "my fix did nothing".
|
|
|
95
97
|
`--since 15m` silently returns the same default window as no flag at all. Do
|
|
96
98
|
not conclude "nothing happened in the last 15 minutes" from it. Narrow by
|
|
97
99
|
`--level`, or by `--function` via `errors`, instead.
|
|
98
|
-
- **`--follow` is a 2-second client-side poll, not a server stream
|
|
100
|
+
- **`--follow` is a 2-second client-side poll, not a server stream** — despite
|
|
101
|
+
its own help text reading "Stream new logs (SSE)". Server-side SSE is planned;
|
|
102
|
+
the backend doesn't push natively today. It
|
|
99
103
|
dedups against what it already printed, so it behaves like `tail -f`, but new
|
|
100
104
|
entries can appear up to ~2s late and it holds the process open until killed.
|
|
101
105
|
|