@pikku/skills 0.12.4 → 0.12.8

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.
Files changed (65) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +74 -33
  4. package/skills/pikku-ai-agent/SKILL.md +197 -105
  5. package/skills/pikku-ai-vercel/SKILL.md +57 -18
  6. package/skills/pikku-ai-voice/SKILL.md +126 -52
  7. package/skills/pikku-audit/SKILL.md +35 -13
  8. package/skills/pikku-aws/SKILL.md +66 -16
  9. package/skills/pikku-backblaze/SKILL.md +44 -11
  10. package/skills/pikku-better-auth/SKILL.md +45 -10
  11. package/skills/pikku-cli/SKILL.md +67 -18
  12. package/skills/pikku-cli/references/complete-example.md +2 -0
  13. package/skills/pikku-concepts/SKILL.md +75 -10
  14. package/skills/pikku-config/SKILL.md +56 -14
  15. package/skills/pikku-cron/SKILL.md +13 -6
  16. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  17. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  18. package/skills/pikku-deploy-express/SKILL.md +40 -4
  19. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  20. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  21. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  22. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  23. package/skills/pikku-deps/SKILL.md +29 -8
  24. package/skills/pikku-emails/SKILL.md +36 -5
  25. package/skills/pikku-fabric/SKILL.md +27 -2
  26. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  27. package/skills/pikku-feature/SKILL.md +6 -1
  28. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  29. package/skills/pikku-http/SKILL.md +18 -5
  30. package/skills/pikku-http/references/http-options.md +10 -5
  31. package/skills/pikku-i18n/SKILL.md +18 -7
  32. package/skills/pikku-info/SKILL.md +18 -8
  33. package/skills/pikku-jose/SKILL.md +35 -6
  34. package/skills/pikku-knowledge/SKILL.md +50 -7
  35. package/skills/pikku-kysely/SKILL.md +78 -15
  36. package/skills/pikku-machine-auth/SKILL.md +36 -1
  37. package/skills/pikku-mcp/SKILL.md +159 -149
  38. package/skills/pikku-middleware/SKILL.md +17 -5
  39. package/skills/pikku-mongodb/SKILL.md +10 -2
  40. package/skills/pikku-n8n-import/SKILL.md +14 -6
  41. package/skills/pikku-permissions/SKILL.md +102 -22
  42. package/skills/pikku-pino/SKILL.md +12 -4
  43. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  44. package/skills/pikku-queue/SKILL.md +45 -16
  45. package/skills/pikku-react/SKILL.md +41 -14
  46. package/skills/pikku-react-query/SKILL.md +14 -10
  47. package/skills/pikku-realtime/SKILL.md +44 -22
  48. package/skills/pikku-redis/SKILL.md +12 -3
  49. package/skills/pikku-rpc/SKILL.md +23 -12
  50. package/skills/pikku-rtl/SKILL.md +21 -17
  51. package/skills/pikku-scenario/SKILL.md +141 -76
  52. package/skills/pikku-schedule/SKILL.md +39 -6
  53. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  54. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  55. package/skills/pikku-security/SKILL.md +54 -9
  56. package/skills/pikku-services/SKILL.md +49 -9
  57. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  58. package/skills/pikku-template-clone/SKILL.md +10 -5
  59. package/skills/pikku-trigger/SKILL.md +50 -6
  60. package/skills/pikku-versioning/SKILL.md +46 -17
  61. package/skills/pikku-websocket/SKILL.md +72 -44
  62. package/skills/pikku-workflow/SKILL.md +123 -11
  63. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  64. package/skills/pikku-workflows-client/SKILL.md +13 -6
  65. package/skills/pikku-ws/SKILL.md +44 -8
@@ -1,10 +1,11 @@
1
1
  ---
2
2
  name: pikku-deploy-azure
3
3
  description: >-
4
- Use when deploying a Pikku app to Azure Functions. Covers PikkuAzFunctionsLogger and
5
- PikkuAzTimerRequest for Azure Functions runtime. TRIGGER when: user asks about Azure Functions,
6
- Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when: user asks about AWS Lambda
7
- (use pikku-deploy-lambda) or Cloudflare Workers (use pikku-deploy-cloudflare).
4
+ Use when deploying a Pikku app to Azure Functions. Covers createAzureHandler for HTTP, storage
5
+ queue and timer triggers, plus AzInvocationLogger and PikkuAZTimerRequest. TRIGGER when: user
6
+ asks about Azure Functions, Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when:
7
+ user asks about AWS Lambda (use pikku-deploy-lambda) or Cloudflare Workers (use
8
+ pikku-deploy-cloudflare).
8
9
  ---
9
10
 
10
11
  # Pikku Azure Functions Deployment
@@ -29,43 +30,97 @@ yarn add @pikku/azure-functions @azure/functions
29
30
 
30
31
  ## API Reference
31
32
 
32
- ### `PikkuAzFunctionsLogger`
33
-
34
- Logger implementation that integrates with Azure Functions' built-in logging context.
35
-
36
- ### `PikkuAzTimerRequest`
37
-
38
- Timer trigger request handler for running Pikku scheduled functions as Azure Timer Triggers.
33
+ Exported from `@pikku/azure-functions`:
34
+
35
+ - `createAzureHandler(factories, handlerTypes)` — the entry point. Returns
36
+ `{ http?, queue?, timer? }` for the handler types you ask for.
37
+ - `createAzureWorkerHandler(factories)` — `createAzureHandler(factories, ['fetch'])`.
38
+ - `createAzureWebSocketHandler(factories)` — **a stub**: its `negotiate` always
39
+ answers `501 WebSocket via Azure Web PubSub not yet implemented`. Channels do
40
+ not work on Azure yet; do not plan a deployment around it.
41
+ - `AzInvocationLogger` — the logger. Note the name: there is no
42
+ `PikkuAzFunctionsLogger`.
43
+ - `PikkuAZTimerRequest` — `new PikkuAZTimerRequest(context, data)`, a
44
+ `PikkuRequest` carrying the data. The context argument is accepted and
45
+ ignored.
46
+ - `AzureQueueService`, `AzureDeploymentService`.
47
+
48
+ `factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
49
+ Services are built from `process.env` and cached in module scope across
50
+ invocations of the same instance.
39
51
 
40
52
  ## Usage Patterns
41
53
 
42
- ### HTTP Function
54
+ ### Registering handlers
43
55
 
44
56
  ```typescript
45
57
  import { app } from '@azure/functions'
46
- import { PikkuAzFunctionsLogger } from '@pikku/azure-functions'
58
+ import { createAzureHandler } from '@pikku/azure-functions'
59
+ import { createConfig, createSingletonServices } from './services.js'
60
+ import './.pikku/pikku-bootstrap.gen.js'
61
+
62
+ const handlers = createAzureHandler(
63
+ { createConfig, createSingletonServices },
64
+ ['fetch', 'queue', 'scheduled']
65
+ )
47
66
 
48
67
  app.http('api', {
49
- methods: ['GET', 'POST', 'PUT', 'DELETE'],
68
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
50
69
  route: '{*path}',
51
- handler: async (request, context) => {
52
- const logger = new PikkuAzFunctionsLogger(context)
53
- // Wire Pikku HTTP runner with Azure request/response
54
- },
70
+ handler: handlers.http as any,
55
71
  })
56
- ```
57
72
 
58
- ### Timer Trigger
59
-
60
- ```typescript
61
- import { app } from '@azure/functions'
62
- import { PikkuAzTimerRequest } from '@pikku/azure-functions'
73
+ app.storageQueue('queue', {
74
+ queueName: 'my-queue',
75
+ connection: 'AzureWebJobsStorage',
76
+ handler: handlers.queue as any,
77
+ })
63
78
 
64
79
  app.timer('scheduler', {
65
80
  schedule: '0 */5 * * * *',
66
- handler: async (timer, context) => {
67
- const request = new PikkuAzTimerRequest(timer)
68
- // Process scheduled Pikku functions
69
- },
81
+ handler: handlers.timer as any,
70
82
  })
71
83
  ```
84
+
85
+ Note the key names: `handlerTypes` uses **`scheduled`**, but the handler it
86
+ returns is **`timer`**.
87
+
88
+ ### HTTP
89
+
90
+ The handler buffers the whole body, converts to a standard `Request`, and
91
+ returns the response body as **text** — a streaming or binary response is
92
+ flattened. It takes no `RunHTTPWiringOptions`, so there is no `maxBodySize` or
93
+ `respondWith404` here; Azure's own request limits are the bound. A thrown error
94
+ is logged to `console.error` and whatever the response already holds is
95
+ returned.
96
+
97
+ ### Queue
98
+
99
+ The queue name comes from the message's own `queueName`, falling back to
100
+ `context.triggerMetadata.queueTrigger` and then `'unknown'` — a name that does
101
+ not match a wired queue means the job has no handler. `attemptsMade` is read
102
+ from `dequeueCount`, and `waitForCompletion` throws: Azure Storage Queues are
103
+ fire-and-forget. A failing job throws out of the handler, so retries and the
104
+ poison queue are governed by `host.json`, not by Pikku.
105
+
106
+ Producer side, `AzureQueueService(connectionString?)` falls back to
107
+ `AzureWebJobsStorage` and throws at construction if neither is set. Messages are
108
+ base64-encoded (Azure requires it), `delay` is milliseconds mapped to
109
+ `visibilityTimeout` in whole seconds capped at 7 days, `supportsResults` is
110
+ `false` and `getJob()` always throws. The queue name is remapped through
111
+ `AZURE_QUEUE_NAME_<SCREAMING_SNAKE>` when that variable exists, otherwise used
112
+ as-is.
113
+
114
+ ### Timer
115
+
116
+ The timer handler runs **every** scheduled task registered in the bundle,
117
+ ignoring both the `Timer` argument and each task's own cron expression. Unlike
118
+ the Lambda equivalent it does not catch per-task failures, so the first task
119
+ that throws aborts the ones after it — keep one schedule per function app, or
120
+ guard the task bodies yourself.
121
+
122
+ ### Logging
123
+
124
+ `new AzInvocationLogger(context)` forwards to the invocation context's
125
+ `info`/`warn`/`error`/`debug`/`trace`. `setLevel()` is a **no-op**: every level
126
+ is emitted and filtering has to be done in Azure's own logging configuration.
@@ -26,57 +26,99 @@ yarn add @pikku/cloudflare
26
26
 
27
27
  ## Worker Entry
28
28
 
29
+ `@pikku/cloudflare` ships the handler factories the deploy codegen emits — use
30
+ them rather than hand-rolling an `ExportedHandler`. Each returns a
31
+ `WorkerEntrypoint` class that sets services up on every invocation (cached after
32
+ the first) and adds an RPC-callable `runRpc(name, args)`:
33
+
29
34
  ```typescript
30
- import { runFetch, runScheduled } from '@pikku/cloudflare'
31
- import { setupServices } from './setup-services.js'
35
+ import { createCloudflareHandler } from '@pikku/cloudflare'
36
+ import { createConfig, createSingletonServices } from './services.js'
32
37
  import './.pikku/pikku-bootstrap.gen.js'
33
38
 
34
- export default {
35
- async scheduled(controller, env) {
36
- await setupServices(env)
37
- await runScheduled(controller)
38
- },
39
-
40
- async fetch(request, env): Promise<Response> {
41
- await setupServices(env)
42
- return await runFetch(request as unknown as Request)
43
- },
44
- } satisfies ExportedHandler<Record<string, string>>
39
+ export default createCloudflareHandler(
40
+ { createConfig, createSingletonServices },
41
+ ['fetch', 'scheduled']
42
+ )
45
43
  ```
46
44
 
45
+ | Factory | For |
46
+ | --- | --- |
47
+ | `createCloudflareHandler(factories, handlerTypes)` | combined `fetch`/`queue`/`scheduled` |
48
+ | `createCloudflareWorkerHandler(factories)` | HTTP / agent / RPC / workflow-orchestrator units |
49
+ | `createCloudflareCronHandler(factories)` | cron units |
50
+ | `createCloudflareQueueHandler(factories)` | queue-consumer and workflow-step units |
51
+ | `createCloudflareMCPHandler(factories)` | MCP units (same HTTP transport) |
52
+ | `createCloudflareWebSocketHandler(factories)` | channel units, delegating to the DO |
53
+
54
+ `factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
55
+
47
56
  ## Service Setup
48
57
 
49
- Cloudflare passes env variables per-request — wrap them with Pikku services:
58
+ Cloudflare passes env bindings per-request, so services are built from `env`
59
+ rather than at module load. `setupServices(env, factories)` is exported from
60
+ `@pikku/cloudflare` and is what the factories call:
50
61
 
51
62
  ```typescript
52
- // setup-services.ts
53
- import { LocalVariablesService, LocalSecretService } from '@pikku/core/services'
54
- import { createConfig, createSingletonServices } from './services.js'
63
+ import { setupServices } from '@pikku/cloudflare'
55
64
 
56
- export const setupServices = async (
57
- env: Record<string, string | undefined>
58
- ) => {
59
- const localVariables = new LocalVariablesService(env)
60
- const config = await createConfig(localVariables)
61
- const localSecrets = new LocalSecretService(localVariables)
62
- return await createSingletonServices(config, {
63
- variables: localVariables,
64
- secrets: localSecrets,
65
- })
66
- }
65
+ const services = await setupServices(env, {
66
+ createConfig,
67
+ createSingletonServices,
68
+ })
67
69
  ```
68
70
 
71
+ **Do not hand-roll this.** Beyond building `LocalVariablesService` /
72
+ `LocalSecretService` and caching the result, it calls `setSingletonServices()` —
73
+ and the core runners (`fetchData`, `runQueueJob`, `runScheduled`) resolve
74
+ services through that global slot, *not* through the value you were returned. A
75
+ setup function that only returns the services leaves every request throwing
76
+ "Singleton services not initialized" as a CF `1101`. It also stashes the env via
77
+ `setCloudflareEnv`, which `getCloudflareEnv()` reads for bindings.
78
+
79
+ ## HTTP
80
+
81
+ `runFetch(request, websocketHibernationServer?, options?)`:
82
+
83
+ - A `GET` with `Upgrade: websocket` is routed to the hibernation server. Without
84
+ one passed in it answers **426**, so a channel worker that forgets the second
85
+ argument fails every upgrade while plain HTTP keeps working.
86
+ - `CF-Ray` becomes the traceId when present, so a Cloudflare trace and a Pikku
87
+ trace line up without extra wiring.
88
+ - `options.exposeErrors` defaults to **`false`** — error detail is withheld from
89
+ responses unless you opt in.
90
+
91
+ ## Scheduled Tasks
92
+
93
+ `runScheduled(controller)` matches registered tasks against
94
+ `controller.cron` and **returns after the first match**. Two tasks sharing one
95
+ cron expression means only one of them ever runs — give each its own expression,
96
+ or invoke `runScheduledTask({ name })` per task yourself.
97
+
69
98
  ## WebSocket (Durable Objects)
70
99
 
100
+ The ready-made DO class is exported; re-export it under the binding name and
101
+ point the worker at it:
102
+
71
103
  ```typescript
72
- import { CloudflareWebSocketHibernationServer } from '@pikku/cloudflare'
73
-
74
- export class WebSocketHibernationServer extends CloudflareWebSocketHibernationServer {
75
- protected async getParams() {
76
- const singletonServices = await setupServices(this.env)
77
- return { singletonServices }
78
- }
79
- }
104
+ export { PikkuWebSocketHibernationServer as WebSocketHibernationServer } from '@pikku/cloudflare'
105
+ export default createCloudflareWebSocketHandler({
106
+ createConfig,
107
+ createSingletonServices,
108
+ })
80
109
  ```
81
110
 
82
- Register the Durable Object in `wrangler.toml` and export from the worker entry.
111
+ Subclass `CloudflareWebSocketHibernationServer` only when you need something
112
+ `getParams()` cannot express — it is abstract with one method returning
113
+ `{ singletonServices, createWireServices? }`. The channel store
114
+ (`CloudflareWebsocketStore` over the DO's own storage), the event hub and the
115
+ channel handler factory are all built by the base class; do not supply them.
116
+
117
+ The router looks up the DO through the **`WEBSOCKET_HIBERNATION_SERVER`**
118
+ binding and answers `503` naming it if the binding is missing, so declare it in
119
+ `wrangler.toml` under exactly that name.
120
+
121
+ A throw during `onConnect` closes the socket with `1008` and answers `403
122
+ Forbidden` with a deliberately generic body — an auth denial and a genuine fault
123
+ look identical to the client. The real reason is on the logger, so read the
124
+ worker logs rather than the status code.
@@ -55,15 +55,37 @@ await appServer.start()
55
55
 
56
56
  **Methods:**
57
57
 
58
- - `init(httpOptions?): Promise<void>` — Register middleware and routes
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` — Serve static files (requires `content` config)
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?)`, `start()`, `stop()`, `enableExitOnSigInt()`
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 { APIGatewayProxyEvent } from 'aws-lambda'
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: APIGatewayProxyEvent) => {
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 { runScheduledTask } from '@pikku/core/scheduler'
98
+ import { runLambdaScheduled } from '@pikku/lambda/scheduled'
64
99
 
65
- export const myScheduledTask: ScheduledHandler = async () => {
100
+ export const scheduled: ScheduledHandler = async (event) => {
66
101
  await coldStart()
67
- await runScheduledTask({ name: 'myScheduledTask' })
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
- export const connectHandler = async (event) => {
94
- const params = await getParams(event)
95
- await connectWebsocket(event, params)
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 disconnectHandler = async (event) => {
100
- const params = await getParams(event)
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 defaultHandler = async (event) => {
105
- const params = await getParams(event)
106
- return await processWebsocketMessage(event, params)
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
- WebSocket requires a `ChannelStore` (e.g., `PgChannelStore`) and `LambdaEventHubService` for cross-connection messaging.
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` — access headers/cookies, use in dynamic Server Components
64
- - `staticGet`, `staticPost`, `staticRPC` — no request context, safe for precompile/ISR
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?, createSingletonServices)`
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
- The generated `pikku-nextjs.gen.ts` wraps this with full type safety from your route definitions.
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.