@pikku/cli 0.12.26 → 0.12.28

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 (91) hide show
  1. package/cli.schema.json +1 -1
  2. package/console-app/assets/index-Ca6xJwNm.js +229 -0
  3. package/console-app/assets/{index-C52h1B_L.css → index-DwUzVI5k.css} +1 -1
  4. package/console-app/index.html +2 -2
  5. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  7. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  8. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  12. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  16. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  17. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  18. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  19. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  20. package/dist/.pikku/function/pikku-functions-meta.gen.json +192 -170
  21. package/dist/.pikku/function/pikku-functions.gen.js +3 -1
  22. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  23. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  25. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  26. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  27. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  28. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  29. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  30. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  31. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  33. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  35. package/dist/.pikku/pikku-types.gen.js +1 -1
  36. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  37. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  38. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +4 -0
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  42. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +11 -10
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +13 -13
  47. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  48. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  49. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  50. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  51. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  52. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  53. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  54. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  55. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  56. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  57. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  58. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +8 -2
  59. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  60. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  61. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  62. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  63. package/dist/bin/pikku-bin.mjs +2 -2
  64. package/dist/src/fabric/functions/validate-core.js +6 -6
  65. package/dist/src/fabric/functions/validate.function.js +23 -7
  66. package/dist/src/functions/commands/tests-coverage.js +4 -2
  67. package/dist/src/functions/db/annotation-parser.d.ts +7 -7
  68. package/dist/src/functions/db/annotation-parser.js +61 -11
  69. package/dist/src/functions/db/db-codegen.d.ts +4 -0
  70. package/dist/src/functions/db/db-codegen.js +117 -15
  71. package/dist/src/functions/db/local-db.d.ts +6 -0
  72. package/dist/src/functions/db/local-db.js +134 -34
  73. package/dist/src/functions/db/postgres/postgres-introspector.d.ts +8 -2
  74. package/dist/src/functions/db/postgres/postgres-introspector.js +26 -14
  75. package/dist/src/functions/validate/workspace-validate.js +4 -1
  76. package/dist/src/functions/wirings/auth/pikku-command-auth.d.ts +1 -0
  77. package/dist/src/functions/wirings/auth/pikku-command-auth.js +22 -0
  78. package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +1 -0
  79. package/dist/src/functions/wirings/auth/serialize-auth-gen.js +115 -0
  80. package/dist/src/functions/workflows/all.workflow.js +1 -0
  81. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  82. package/dist/src/utils/pikku-cli-config.js +3 -0
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/package.json +7 -4
  85. package/skills/pikku-auth-js/SKILL.md +137 -117
  86. package/skills/pikku-middleware/SKILL.md +283 -0
  87. package/skills/pikku-permissions/SKILL.md +165 -0
  88. package/skills/pikku-security/SKILL.md +38 -177
  89. package/skills/pikku-services/SKILL.md +44 -7
  90. package/skills/pikku-tag-middleware/SKILL.md +13 -0
  91. package/console-app/assets/index-Ba9K10XZ.js +0 -232
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pikku-auth-js
3
- description: 'Use when integrating Auth.js (NextAuth) with a Pikku app. Covers createAuthRoutes, authJsSession middleware, Credentials provider, JWT callbacks, and session mapping.
4
- TRIGGER when: code uses createAuthRoutes, authJsSession, createAuthHandler, user asks about Auth.js, NextAuth, OAuth providers, login/logout, or @pikku/auth-js.
3
+ description: 'Use when integrating Auth.js (NextAuth) with a Pikku app. Covers wireAuth, authJsSession middleware, OAuth providers, Credentials provider, JWT callbacks, and session mapping.
4
+ TRIGGER when: code uses wireAuth, createAuthRoutes, authJsSession, createAuthHandler, user asks about Auth.js, NextAuth, OAuth providers, login/logout, or @pikku/auth-js.
5
5
  TRIGGER when: user asks about ANY form of authentication, login, logout, sessions, or user identity — always answer with this skill.
6
6
  DO NOT TRIGGER when: user asks about JWT middleware (use pikku-security) or custom session services (use pikku-services).'
7
7
  ---
@@ -45,101 +45,62 @@ yarn add @pikku/auth-js @auth/core
45
45
 
46
46
  Auth.js in Pikku has two independent concerns:
47
47
 
48
- 1. **Route wiring** (`createAuthRoutes`) — mounts the Auth.js signin/signout/callback endpoints into Pikku's HTTP router.
48
+ 1. **Route wiring** (`wireAuth`) — mounts the Auth.js signin/signout/callback endpoints into Pikku's HTTP router. The CLI generates `auth.gen.ts` with provider imports, secret wires, and route setup.
49
49
  2. **Session middleware** (`authJsSession`) — reads the Auth.js JWT cookie on every request and populates the Pikku session object.
50
50
 
51
- Both must be present and must share the same `secret`.
51
+ Both must be present and must share the same `AUTH_SECRET`.
52
52
 
53
53
  ---
54
54
 
55
- ## Standard Setup (Credentials Provider)
55
+ ## Standard Setup (OAuth Providers)
56
56
 
57
57
  ### 1. Auth wiring — `wirings/auth.wiring.ts`
58
58
 
59
- ```typescript
60
- import Credentials from '@auth/core/providers/credentials'
61
- import { createAuthRoutes } from '@pikku/auth-js'
62
- import type { AuthConfigOrFactory } from '@pikku/auth-js'
63
- import { wireHTTPRoutes } from '#pikku'
64
-
65
- const DEV_AUTH_SECRET = 'dev-insecure-auth-secret-change-me'
66
-
67
- const configFactory: AuthConfigOrFactory = async (services) => {
68
- const secret = await services.secrets.getSecret('AUTH_SECRET').catch(() => null) ?? DEV_AUTH_SECRET
69
-
70
- return {
71
- providers: [
72
- Credentials({
73
- credentials: {
74
- email: { label: 'Email', type: 'email' },
75
- password: { label: 'Password', type: 'password' },
76
- },
77
- async authorize(credentials) {
78
- const email = (credentials?.email as string)?.toLowerCase()
79
- const password = credentials?.password as string
80
- if (!email || !password) return null
81
-
82
- // Look up user and verify password against your DB
83
- const user = await (services as any).kysely
84
- .selectFrom('appUser')
85
- .where('email', '=', email)
86
- .select(['userId', 'role', 'name', 'email', 'passwordHash'])
87
- .executeTakeFirst()
88
-
89
- if (!user || !user.passwordHash) return null
90
- // verifyPassword must be implemented in your app — use bcrypt or argon2.
91
- // See services/password.ts in seminarhof for a reference implementation.
92
- const ok = await verifyPassword(password, user.passwordHash)
93
- if (!ok) return null
94
-
95
- // Return shape is the Auth.js User — add any custom claims here
96
- return { id: user.userId, email: user.email, name: user.name, role: user.role }
97
- },
98
- }),
99
- ],
100
- // Embed custom claims into the JWT
101
- callbacks: {
102
- jwt({ token, user }: any) {
103
- if (user) token.role = user.role
104
- return token
105
- },
106
- session({ session, token }: any) {
107
- if (token) session.role = token.role
108
- return session
109
- },
110
- },
111
- session: { strategy: 'jwt' as const },
112
- secret,
113
- trustHost: true,
114
- basePath: '/auth',
115
- }
116
- }
59
+ Use `wireAuth` to declare which providers you need. The CLI reads this call and generates `auth.gen.ts` with all imports, secret declarations, and route wiring automatically.
117
60
 
118
- wireHTTPRoutes({ routes: { auth: createAuthRoutes(configFactory) as any } })
61
+ ```typescript
62
+ import { wireAuth } from '@pikku/auth-js'
63
+
64
+ wireAuth({
65
+ providers: ['github', 'google'],
66
+ callbacks: {
67
+ signIn: async (rpc, { user, account }) =>
68
+ rpc.invoke('auth:signIn', { userId: user.id, provider: account.provider }),
69
+ redirect: async (rpc, { url, baseUrl }) =>
70
+ rpc.invoke('auth:redirect', { url, baseUrl }),
71
+ },
72
+ })
119
73
  ```
120
74
 
121
75
  **Key points:**
122
- - `configFactory` is async and receives singleton servicesuse it to read `AUTH_SECRET` from the secrets service.
123
- - Always provide a `DEV_AUTH_SECRET` fallback with the same literal in both the wiring and the middleware (see below) so sign and verify agree during local dev without env vars.
124
- - The `jwt` + `session` callbacks are how you embed custom fields (e.g. `role`) into the token. Without them, only the standard Auth.js claims (`sub`, `name`, `email`) are available.
125
- - `trustHost: true` is required in non-Next.js deployments.
126
- - `basePath: '/auth'` must match the path your frontend hits.
76
+ - `providers` must be an array of string literals the CLI inspector reads them statically and generates the `auth.gen.ts` file.
77
+ - `callbacks` are standard Auth.js callbacks but receive `rpc` as a first argument. Use `rpc.invoke('funcName', data)` to delegate to typed pikku functions that have access to services and sessions.
78
+ - The generated `auth.gen.ts` file handles provider imports, Zod schemas, `wireSecret` declarations for all credentials and `AUTH_SECRET`, and the `createAuthRoutes` + `wireHTTPRoutes` call.
79
+ - Do NOT edit `auth.gen.ts` — re-run `pikku auth` (or `pikku all`) to regenerate.
127
80
 
128
- ### 2. Middleware `wirings/middleware.ts`
81
+ **Supported providers:** `github`, `google`, `discord`, `twitter`, `apple`, `facebook`, `linkedin`, `slack`, `spotify`, `twitch`, `gitlab`, `auth0`, `azure-ad`, `okta`
82
+
83
+ ### 2. Configure `pikku.config.json`
84
+
85
+ Add `authFile` pointing to where `auth.gen.ts` should be written (must be within `srcDirectories`):
86
+
87
+ ```json
88
+ {
89
+ "srcDirectories": ["src"],
90
+ "authFile": "src/wirings/auth.gen.ts"
91
+ }
92
+ ```
93
+
94
+ ### 3. Middleware — `wirings/middleware.ts`
129
95
 
130
96
  ```typescript
131
97
  import { addHTTPMiddleware } from '#pikku'
132
98
  import { authJsSession } from '@pikku/auth-js'
133
- import { sessionCookieMiddleware } from '../middleware/session-cookie.js'
134
99
 
135
- // Order is load-bearing: sessionCookieMiddleware MUST run before authJsSession.
136
- // If you have a custom DB session middleware it must go first, otherwise
137
- // authJsSession's post-check throws when the session is set inside next().
138
100
  addHTTPMiddleware('*', [
139
- sessionCookieMiddleware, // custom session (if present) — always first
140
101
  authJsSession({
141
102
  secretId: 'AUTH_SECRET',
142
- mapSession: (claims) => ({ userId: claims.sub as string, role: claims.role as string }),
103
+ mapSession: (claims) => ({ userId: claims.sub as string }),
143
104
  }),
144
105
  ])
145
106
  ```
@@ -163,7 +124,56 @@ cors({
163
124
  })
164
125
  ```
165
126
 
166
- ### 3. Auth-protected functions
127
+ ---
128
+
129
+ ## Credentials Provider (Username/Password)
130
+
131
+ Use `wireAuth` with the `credentials` option. The `authorize` callback receives `rpc` as a first argument so you can delegate to a typed Pikku function:
132
+
133
+ ```typescript
134
+ import { wireAuth } from '@pikku/auth-js'
135
+
136
+ wireAuth({
137
+ credentials: {
138
+ fields: {
139
+ email: { label: 'Email', type: 'email' },
140
+ password: { label: 'Password', type: 'password' },
141
+ },
142
+ authorize: async (rpc, { email, password }) =>
143
+ rpc.invoke('auth:login', { email, password }),
144
+ },
145
+ callbacks: {
146
+ jwt: async (_rpc, { token, user }) => {
147
+ if (user) token.role = user.role
148
+ return token
149
+ },
150
+ },
151
+ })
152
+ ```
153
+
154
+ The `auth:login` function handles password verification and returns the Auth.js `User` shape (with `id` required), or `null` to reject the credentials:
155
+
156
+ ```typescript
157
+ export const login = pikkuSessionlessFunc({
158
+ func: async ({ kysely }, { email, password }) => {
159
+ const user = await kysely
160
+ .selectFrom('appUser')
161
+ .where('email', '=', email.toLowerCase())
162
+ .select(['userId', 'role', 'name', 'email', 'passwordHash'])
163
+ .executeTakeFirst()
164
+
165
+ if (!user || !user.passwordHash) return null
166
+ const ok = await verifyPassword(password, user.passwordHash)
167
+ if (!ok) return null
168
+
169
+ return { id: user.userId, email: user.email, name: user.name, role: user.role }
170
+ },
171
+ })
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Auth-Protected Functions
167
177
 
168
178
  Functions that require a session use `pikkuFunc` — anonymous callers are rejected automatically:
169
179
 
@@ -245,23 +255,58 @@ The Pikku SDK does **not** wrap these — call them directly or use `@auth/core`
245
255
 
246
256
  ## Secret Management
247
257
 
248
- Both the auth config factory and `authJsSession` must use the same `AUTH_SECRET` value they resolve it through the secrets service in both cases.
258
+ All auth secrets are managed through the secrets service. `wireAuth` reads `AUTH_SECRET` and each provider's credentials object at request time using `services.secrets.getSecrets(keys)`.
249
259
 
250
- **In `auth.wiring.ts`** — read via the services factory (falls back to a dev literal if the secret is absent):
251
- ```typescript
252
- const secret = await services.secrets.getSecret('AUTH_SECRET').catch(() => null) ?? DEV_AUTH_SECRET
253
- ```
260
+ **`AUTH_SECRET`** — a random string used to sign all JWT session tokens. Required.
261
+
262
+ **Provider credentials** each provider (e.g. `GITHUB_OAUTH`, `GOOGLE_OAUTH`) stores a JSON object with `clientId` and `clientSecret`.
263
+
264
+ Both are registered in `auth.gen.ts` via `wireSecret`, which makes them visible in the Pikku console for secret management.
254
265
 
255
266
  **In `middleware.ts`** — use `secretId`, resolved from the secrets service at request time:
256
267
  ```typescript
257
268
  authJsSession({ secretId: 'AUTH_SECRET', mapSession: ... })
258
269
  ```
259
270
 
260
- Do **not** pass `secret: process.env.AUTH_SECRET` or any string value directly to `authJsSession`. The `secret` option no longer exists — `secretId` is the only accepted form. This ensures the secret is always fetched through the secrets service rather than leaked into the process environment.
271
+ Do **not** pass `secret: process.env.AUTH_SECRET` or any string value directly to `authJsSession`. The `secret` option no longer exists — `secretId` is the only accepted form.
261
272
 
262
273
  ---
263
274
 
264
- ## `createAuthRoutes` API
275
+ ## `wireAuth` API
276
+
277
+ ```typescript
278
+ import { wireAuth } from '@pikku/auth-js'
279
+ import type { WireAuthOptions } from '@pikku/auth-js'
280
+
281
+ wireAuth({
282
+ providers: ['github', 'google'], // optional — string literals read by CLI at build time
283
+ credentials: { // optional — Credentials provider (username/password)
284
+ fields: { // optional — defines what form fields to show
285
+ email: { label: 'Email', type: 'email' },
286
+ password: { label: 'Password', type: 'password' },
287
+ },
288
+ authorize: async (rpc, credentials) =>
289
+ rpc.invoke('auth:login', { email: credentials.email, password: credentials.password }),
290
+ },
291
+ basePath: '/auth', // optional, defaults to '/auth'
292
+ callbacks: { // optional — all standard Auth.js callbacks
293
+ signIn: async (rpc, data) => rpc.invoke('auth:signIn', data),
294
+ redirect: async (rpc, { url }) => url,
295
+ session: async (rpc, data) => data,
296
+ jwt: async (rpc, data) => data,
297
+ },
298
+ })
299
+ ```
300
+
301
+ - `providers` and `credentials` are both optional — use one, both, or neither.
302
+ - `rpc.invoke(funcName, data)` calls any registered Pikku function with full service injection. The return type is typed from your function definition.
303
+ - `credentials.authorize` returns the Auth.js `User` object on success, or `null` on failure.
304
+
305
+ ---
306
+
307
+ ## `createAuthRoutes` API (low-level escape hatch)
308
+
309
+ Use this only when you need full manual control, e.g. for the Credentials provider with custom `authorize` logic.
265
310
 
266
311
  ```typescript
267
312
  import { createAuthRoutes } from '@pikku/auth-js'
@@ -283,37 +328,12 @@ wireHTTPRoutes({ routes: { auth: routes as any } })
283
328
 
284
329
  ## Adding Custom Claims (e.g. `role`)
285
330
 
286
- 1. Return extra fields from `authorize()` in your Credentials provider (Auth.js `User` type is open).
287
- 2. Copy them into the JWT token in the `jwt` callback (`token.role = user.role`).
288
- 3. Expose them in `mapSession` in `authJsSession` (`role: claims.role`).
289
- 4. They are now available on every `session` object in your Pikku functions.
290
-
291
- ---
292
-
293
- ## Adding OAuth Providers (GitHub, Google, etc.)
294
-
295
- With `strategy: 'jwt'` no database adapter is needed — tokens are self-contained.
296
-
297
- ```typescript
298
- import GitHub from '@auth/core/providers/github'
299
- import Google from '@auth/core/providers/google'
300
-
301
- const configFactory: AuthConfigOrFactory = async (services) => {
302
- const secret = await services.secrets.getSecret('AUTH_SECRET').catch(() => null) ?? DEV_AUTH_SECRET
303
- const github = await services.secrets.getSecretJSON('GITHUB_OAUTH').catch(() => null)
304
- const google = await services.secrets.getSecretJSON('GOOGLE_OAUTH').catch(() => null)
305
-
306
- return {
307
- providers: [
308
- GitHub({ clientId: github?.clientId, clientSecret: github?.clientSecret }),
309
- Google({ clientId: google?.clientId, clientSecret: google?.clientSecret }),
310
- ],
311
- session: { strategy: 'jwt' as const },
312
- secret,
313
- trustHost: true,
314
- basePath: '/auth',
315
- }
316
- }
317
- ```
331
+ When using `wireAuth` with callbacks:
332
+ 1. Return extra fields from your `signIn` callback.
333
+ 2. Handle them in the `jwt` callback: `jwt: async (rpc, { token, user }) => { if (user) token.role = user.role; return token }`.
334
+ 3. Expose them in `mapSession` in `authJsSession`: `role: claims.role`.
318
335
 
319
- Each OAuth provider needs its client ID and secret registered in the secrets service. No adapter or DB changes required when using JWT sessions.
336
+ When using `createAuthRoutes` directly:
337
+ 1. Return extra fields from `authorize()` in your Credentials provider.
338
+ 2. Copy them into the JWT token in the `jwt` callback.
339
+ 3. Expose them in `mapSession` in `authJsSession`.
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: pikku-middleware
3
+ description: 'Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware (including service-to-service bearer auth), per-route middleware, session-setting middleware, or understanding middleware execution order and priority.
4
+ TRIGGER when: user wants middleware on some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global interceptors, or middleware priority/order questions.
5
+ DO NOT TRIGGER when: user asks about permissions/authorization checks (use pikku-permissions), auth strategies like authBearer/authCookie (use pikku-security), or deployment.'
6
+ installGroups: [core]
7
+ ---
8
+
9
+ # Pikku Middleware
10
+
11
+ ## Agent Operating Procedure
12
+
13
+ 1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info tags --json` to understand the existing middleware and tag landscape.
14
+ 2. Identify the source files that own the behavior — wirings files, not generated output.
15
+ 3. Register middleware at module load time — in a `wirings/*.ts` file, never inside a function body.
16
+ 4. Validate: run `pikku all` after adding or changing middleware; run `pikku tsc` to confirm type safety.
17
+
18
+ ## The `pikkuMiddleware` Factory
19
+
20
+ ```typescript
21
+ import { pikkuMiddleware } from '#pikku'
22
+
23
+ // Simple: just a function
24
+ const myMiddleware = pikkuMiddleware(async (services, wire, next) => {
25
+ // runs before the function
26
+ await next()
27
+ // runs after the function (optional)
28
+ })
29
+
30
+ // With metadata (name + priority)
31
+ const telemetryMiddleware = pikkuMiddleware({
32
+ name: 'my-telemetry',
33
+ priority: 'highest',
34
+ func: async (services, wire, next) => {
35
+ const start = performance.now()
36
+ try {
37
+ await next()
38
+ } finally {
39
+ services.logger.info({ duration: Math.round(performance.now() - start) })
40
+ }
41
+ },
42
+ })
43
+ ```
44
+
45
+ The `wire` object gives you:
46
+ - `wire.http` — inbound HTTP context (headers, URL, cookies)
47
+ - `wire.setSession(session)` — set the session for this request
48
+ - `wire.getSession()` — read the current session
49
+ - `wire.session` — the session set so far (may be undefined)
50
+
51
+ Throw a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `@pikku/core/errors`.
52
+
53
+ ## Scoping: Five Levels
54
+
55
+ From broadest to narrowest:
56
+
57
+ ```typescript
58
+ // 1. Wire-agnostic global: all wire types (HTTP, Queue, Channel, Trigger, Workflow, ...)
59
+ addGlobalMiddleware([telemetryOuter()])
60
+
61
+ // 2. HTTP global: all HTTP routes
62
+ addHTTPMiddleware('*', [cors(), authBearer()])
63
+
64
+ // 3. Prefix-based: URL pattern
65
+ addHTTPMiddleware('/admin/*', [auditLog])
66
+
67
+ // 4. Tag-based: any wiring with matching tag
68
+ addTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire
69
+
70
+ // 5. Inline: per-wiring
71
+ wireHTTP({
72
+ route: '/books/:id',
73
+ func: getBook,
74
+ middleware: [cacheControl],
75
+ })
76
+ ```
77
+
78
+ ## Global Middleware (`addGlobalMiddleware`)
79
+
80
+ `addGlobalMiddleware` registers middleware that runs before everything else — across every wire type: HTTP, Queue, Channel, Trigger, Scheduler, Workflow, Agent, CLI, MCP. Use it for cross-cutting concerns like telemetry that must wrap every invocation regardless of transport.
81
+
82
+ ```typescript
83
+ import { addGlobalMiddleware } from '@pikku/core'
84
+ import { telemetryOuter, telemetryInner } from '@pikku/core/middleware'
85
+
86
+ // Outer telemetry: wraps the full call (highest priority)
87
+ addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })])
88
+
89
+ // Inner telemetry: closest to the function body (lowest priority)
90
+ addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })])
91
+ ```
92
+
93
+ `telemetryOuter` ships with `priority: 'highest'` and `telemetryInner` with `priority: 'lowest'` — so even if both are added in the same call, priority sorting places outer first regardless of array order.
94
+
95
+ ## HTTP & Prefix Middleware (`addHTTPMiddleware`)
96
+
97
+ ```typescript
98
+ import { addHTTPMiddleware } from '@pikku/core/http'
99
+ import { cors, authBearer } from '@pikku/core/middleware'
100
+
101
+ // All routes
102
+ addHTTPMiddleware('*', [cors({ origin: 'https://app.example.com', credentials: true })])
103
+
104
+ // Scoped to /api/* prefix
105
+ addHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])
106
+ ```
107
+
108
+ ## Tag Middleware (`addTagMiddleware`)
109
+
110
+ Tag middleware fires for any wiring (function or wire object) that carries a matching tag. This is the canonical approach for service-to-service bearer auth, rate limiting a group, or any cross-cutting concern scoped to a subset of routes.
111
+
112
+ ### Setting Tags
113
+
114
+ ```typescript
115
+ // On the function definition
116
+ export const myFunc = pikkuSessionlessFunc({
117
+ auth: false,
118
+ tags: ['machine-agent'],
119
+ func: async (services, input) => { ... },
120
+ })
121
+
122
+ // On the wire object
123
+ wireHTTP({
124
+ route: '/internal/action',
125
+ method: 'post',
126
+ auth: false,
127
+ tags: ['internal'],
128
+ func: myFunc,
129
+ })
130
+ ```
131
+
132
+ Tags from the function definition and the wire object are merged — middleware from both tag sets runs.
133
+
134
+ ### Registering Tag Middleware
135
+
136
+ ```typescript
137
+ import { addTagMiddleware } from '.pikku/pikku-types.gen.js'
138
+
139
+ addTagMiddleware('machine-agent', [machineAgentBearerAuth])
140
+ ```
141
+
142
+ Call at module load time — typically in the same `wirings/*.ts` file as the `wireHTTP` calls that use the tag.
143
+
144
+ ## Middleware Execution Order
145
+
146
+ **Scope resolution order (broadest → narrowest):**
147
+
148
+ ```text
149
+ global → httpGroup/* → httpGroup/prefix → wiringTags → wiringMiddleware → funcTags → funcMiddleware → function body
150
+ ```
151
+
152
+ **Within each scope, sorted by priority:**
153
+
154
+ ```text
155
+ highest → high → medium (default) → low → lowest
156
+ ```
157
+
158
+ Set priority using the config-object form of `pikkuMiddleware`:
159
+
160
+ ```typescript
161
+ const earlyMiddleware = pikkuMiddleware({
162
+ name: 'early',
163
+ priority: 'highest', // 'highest' | 'high' | 'medium' | 'low' | 'lowest'
164
+ func: async (services, wire, next) => { ... },
165
+ })
166
+ ```
167
+
168
+ Within the same priority level, registration order is preserved. Priority is the primary sort key — use it when a middleware must run before or after others regardless of registration order (e.g. telemetry wrapping everything, session extraction before auth checks).
169
+
170
+ ## Common Patterns
171
+
172
+ ### Service-to-Service Bearer Auth
173
+
174
+ The canonical pattern for a server that exposes RPCs only to a trusted caller (e.g. an API calling a machine-agent):
175
+
176
+ **On the server (the service being called):**
177
+
178
+ ```typescript
179
+ // lib/host-token.ts
180
+ let _token: string | null = null
181
+ export const setToken = (t: string) => { _token = t }
182
+ export const getToken = () => _token
183
+ ```
184
+
185
+ ```typescript
186
+ // wirings/http.wiring.ts
187
+ import { timingSafeEqual } from 'node:crypto'
188
+ import { addTagMiddleware, pikkuMiddleware } from '../../.pikku/pikku-types.gen.js'
189
+ import { UnauthorizedError } from '@pikku/core/errors'
190
+ import { getToken } from '../lib/host-token.js'
191
+
192
+ const bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {
193
+ const authHeader = http?.request?.header?.('authorization') || http?.request?.header?.('Authorization')
194
+ const token = getToken()
195
+ const expected = token ? `Bearer ${token}` : null
196
+ if (
197
+ !expected ||
198
+ !authHeader ||
199
+ authHeader.length !== expected.length ||
200
+ !timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))
201
+ ) {
202
+ throw new UnauthorizedError()
203
+ }
204
+ return next()
205
+ })
206
+
207
+ addTagMiddleware('machine-agent', [bearerAuth])
208
+ ```
209
+
210
+ ```typescript
211
+ // functions/my.function.ts
212
+ export const myFunc = pikkuSessionlessFunc({
213
+ expose: true,
214
+ auth: false,
215
+ tags: ['machine-agent'],
216
+ func: async (services, input) => { ... },
217
+ })
218
+ ```
219
+
220
+ **On the client (the caller):**
221
+
222
+ Use the generated `RPCInvoke` type from `.pikku/rpc/pikku-rpc-wirings-map.gen.d.ts` — never hand-write the input/output types:
223
+
224
+ ```typescript
225
+ import type { RPCInvoke } from '../../backends/my-service/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'
226
+
227
+ export function getServiceRPC(baseUrl: string, token: string): RPCInvoke {
228
+ return async (name: string, data?: unknown) => {
229
+ const res = await fetch(`${baseUrl}/rpc/${String(name)}`, {
230
+ method: 'POST',
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ Authorization: `Bearer ${token}`,
234
+ },
235
+ body: JSON.stringify({ data: data ?? {} }),
236
+ })
237
+ if (!res.ok) {
238
+ const text = await res.text().catch(() => '')
239
+ throw new Error(`rpc ${String(name)} failed: ${res.status} ${text}`)
240
+ }
241
+ return res.json()
242
+ } as RPCInvoke
243
+ }
244
+ ```
245
+
246
+ ### Session-Setting Middleware
247
+
248
+ ```typescript
249
+ const apiKeyAuth = pikkuMiddleware(async ({ kysely }, { http, setSession, session }, next) => {
250
+ if (session) return next() // already authenticated
251
+
252
+ const header = http?.request?.header?.('x-api-key')
253
+ if (!header) return next()
254
+
255
+ const row = await kysely.selectFrom('apiKey').select('userId').where('key', '=', header).executeTakeFirst()
256
+ if (row) setSession?.({ userId: row.userId })
257
+
258
+ return next()
259
+ })
260
+
261
+ addTagMiddleware('api-key-auth', [apiKeyAuth])
262
+ ```
263
+
264
+ Functions tagged `'api-key-auth'` with `auth: true` reject requests without a valid key; those with `auth: false` can inspect the session but won't reject.
265
+
266
+ ### Request Logging / Audit
267
+
268
+ ```typescript
269
+ const auditLog = pikkuMiddleware(async ({ logger, db }, wire, next) => {
270
+ const start = Date.now()
271
+ await next()
272
+ await db.createAuditLog({ duration: Date.now() - start })
273
+ })
274
+
275
+ addHTTPMiddleware('/admin/*', [auditLog])
276
+ ```
277
+
278
+ ## After Changes
279
+
280
+ ```bash
281
+ pikku all # regenerate metadata so new tags are picked up
282
+ pikku tsc # type-check
283
+ ```