@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.
- package/cli.schema.json +1 -1
- package/console-app/assets/index-Ca6xJwNm.js +229 -0
- package/console-app/assets/{index-C52h1B_L.css → index-DwUzVI5k.css} +1 -1
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +192 -170
- package/dist/.pikku/function/pikku-functions.gen.js +3 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +4 -0
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +11 -10
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +13 -13
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +8 -2
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/fabric/functions/validate-core.js +6 -6
- package/dist/src/fabric/functions/validate.function.js +23 -7
- package/dist/src/functions/commands/tests-coverage.js +4 -2
- package/dist/src/functions/db/annotation-parser.d.ts +7 -7
- package/dist/src/functions/db/annotation-parser.js +61 -11
- package/dist/src/functions/db/db-codegen.d.ts +4 -0
- package/dist/src/functions/db/db-codegen.js +117 -15
- package/dist/src/functions/db/local-db.d.ts +6 -0
- package/dist/src/functions/db/local-db.js +134 -34
- package/dist/src/functions/db/postgres/postgres-introspector.d.ts +8 -2
- package/dist/src/functions/db/postgres/postgres-introspector.js +26 -14
- package/dist/src/functions/validate/workspace-validate.js +4 -1
- package/dist/src/functions/wirings/auth/pikku-command-auth.d.ts +1 -0
- package/dist/src/functions/wirings/auth/pikku-command-auth.js +22 -0
- package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +1 -0
- package/dist/src/functions/wirings/auth/serialize-auth-gen.js +115 -0
- package/dist/src/functions/workflows/all.workflow.js +1 -0
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/utils/pikku-cli-config.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -4
- package/skills/pikku-auth-js/SKILL.md +137 -117
- package/skills/pikku-middleware/SKILL.md +283 -0
- package/skills/pikku-permissions/SKILL.md +165 -0
- package/skills/pikku-security/SKILL.md +38 -177
- package/skills/pikku-services/SKILL.md +44 -7
- package/skills/pikku-tag-middleware/SKILL.md +13 -0
- 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
|
|
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** (`
|
|
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 `
|
|
51
|
+
Both must be present and must share the same `AUTH_SECRET`.
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
## Standard Setup (
|
|
55
|
+
## Standard Setup (OAuth Providers)
|
|
56
56
|
|
|
57
57
|
### 1. Auth wiring — `wirings/auth.wiring.ts`
|
|
58
58
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
123
|
-
-
|
|
124
|
-
- The
|
|
125
|
-
- `
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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.
|
|
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
|
-
## `
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
+
```
|