@pikku/skills 0.12.2 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +56 -29
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +80 -34
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +82 -10
- package/skills/pikku-concepts/references/concept-mapping.md +2 -2
- package/skills/pikku-config/SKILL.md +134 -52
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +30 -5
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +12 -7
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +3 -3
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +285 -50
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-software-archaeology/README.md +16 -6
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +35 -1
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
|
@@ -54,7 +54,7 @@ Better Auth owns its own HTTP surface, database tables, and session cookie. The
|
|
|
54
54
|
1. **`pikkuBetterAuth(factory)`** — you export ONE `pikkuBetterAuth` call whose factory returns a configured `betterAuth({...})` instance. The pikku CLI inspects this export and generates everything else.
|
|
55
55
|
2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router. The enabled providers and plugins are written to `auth/pikku-auth-meta.gen.json` (read by the console SSO page via `getAuthProviders`).
|
|
56
56
|
3. **Generated session middleware** — with `session.cookieCache` enabled (recommended), a separate `auth-middleware.gen.ts` adds the lean stateless `betterAuthStatelessSession()`; without it, `auth.gen.ts` adds the stateful `betterAuthSession()` that bundles the full server into every unit. See "Stateless session" below.
|
|
57
|
-
4. **Generated `auth-secrets.gen.ts`** — a `
|
|
57
|
+
4. **Generated `auth-secrets.gen.ts`** — a `defineSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `defineVariable` for any non-secret provider config (e.g. `tenantId`).
|
|
58
58
|
|
|
59
59
|
You do NOT hand-write routes, the session middleware, or the secret wiring — `pikkuBetterAuth` + the CLI generate all of it. Re-run `pikku all` to regenerate.
|
|
60
60
|
|
|
@@ -86,7 +86,12 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
|
|
|
86
86
|
secret: BETTER_AUTH_SECRET,
|
|
87
87
|
// memoryAdapter needs an array per model — `{}` throws "Model user not found"
|
|
88
88
|
// at runtime. Swap for the Kysely adapter in production (see below).
|
|
89
|
-
database: memoryAdapter({
|
|
89
|
+
database: memoryAdapter({
|
|
90
|
+
user: [],
|
|
91
|
+
session: [],
|
|
92
|
+
account: [],
|
|
93
|
+
verification: [],
|
|
94
|
+
}),
|
|
90
95
|
emailAndPassword: { enabled: true },
|
|
91
96
|
// ALWAYS enable for deployed apps — see "Stateless session" below.
|
|
92
97
|
session: { cookieCache: { enabled: true } },
|
|
@@ -98,7 +103,8 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
|
|
|
98
103
|
```
|
|
99
104
|
|
|
100
105
|
**Key points:**
|
|
101
|
-
|
|
106
|
+
|
|
107
|
+
- `socialProviders` keys must be string literals — the CLI reads them statically to emit a `defineSecret` per provider. Provider keys mirror better-auth's built-in ids exactly (e.g. `microsoft`, NOT `microsoft-entra-id`; `cognito`; `github`).
|
|
102
108
|
- The factory runs lazily on the first auth request, so it pulls secrets/DB off the injected `services`.
|
|
103
109
|
- The default `basePath` is `/api/auth`. Override it by passing `basePath` to `betterAuth`.
|
|
104
110
|
- **Enable `session: { cookieCache: { enabled: true } }`** so non-auth units tree-shake the better-auth server out (see below).
|
|
@@ -115,36 +121,42 @@ Enabling `session: { cookieCache: { enabled: true } }` makes the CLI split out a
|
|
|
115
121
|
|
|
116
122
|
**Customizing the session bridge (`mapSession`, `impersonation`, `apiKey`, …):** you do NOT chain a second middleware on top of the generated one — register your OWN global session middleware and the CLI steps aside (it stops generating its default). This works on both paths and is detected the same way:
|
|
117
123
|
|
|
118
|
-
- **Stateless (cookieCache on):** register `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI sees the global registration and skips emitting `auth-middleware.gen.ts` (pikkujs/pikku#754), so you keep cookieCache's lean bundles
|
|
124
|
+
- **Stateless (cookieCache on):** register `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI sees the global registration and skips emitting `auth-middleware.gen.ts` (pikkujs/pikku#754), so you keep cookieCache's lean bundles _and_ your custom fields.
|
|
119
125
|
- **Stateful (cookieCache off):** register `betterAuthSession({ mapSession, impersonation })` **globally**. The CLI detects it (`hasUserSessionMiddleware`) and omits its own `addHTTPMiddleware('*', [betterAuthSession()])` from `auth.gen.ts` — so there's exactly one session bridge in the chain, yours.
|
|
120
126
|
|
|
121
127
|
In both cases a **route-scoped** registration (`addHTTPMiddleware('/some/path', [...])`) does NOT count — only a global one suppresses the generated default. The generated middleware in a `.gen.ts` file is also ignored by the detector, so regeneration never self-suppresses.
|
|
122
128
|
|
|
123
129
|
### Admin capabilities are scopes, not a role
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
| `
|
|
136
|
-
|
|
|
131
|
+
Scopes are the source of truth for what an admin may do; nothing in pikku reads
|
|
132
|
+
a `role`. A role is not a permission: "who may impersonate" and "who may rebind a
|
|
133
|
+
shared credential" are different capabilities one user can hold independently,
|
|
134
|
+
which a single `role` string cannot express. Every gate the package owns
|
|
135
|
+
resolves the caller's scopes through the registered `ScopeService` and checks the
|
|
136
|
+
`admin:*` tree (`ADMIN_SCOPES` exports the ids so you never spell them as bare
|
|
137
|
+
strings):
|
|
138
|
+
|
|
139
|
+
| Gate | Scope required |
|
|
140
|
+
| -------------------------------------------------------------------- | ------------------------ |
|
|
141
|
+
| `impersonation` (`betterAuthSession` / `betterAuthStatelessSession`) | `admin:impersonate` |
|
|
142
|
+
| `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
|
|
143
|
+
| the console's user directory | `admin:users:list` |
|
|
144
|
+
| create a user out of band | `admin:users:create` |
|
|
145
|
+
| ban / unban | `admin:users:ban` |
|
|
146
|
+
| delete a user and their data | `admin:users:remove` |
|
|
147
|
+
| revoke a user's sessions | `admin:users:sessions` |
|
|
148
|
+
| set a user's password | `admin:users:password` |
|
|
137
149
|
|
|
138
150
|
Holding the bare `admin` scope satisfies all of them — a parent grant covers
|
|
139
151
|
everything nested beneath it — so `admin` is the direct replacement for the old
|
|
140
152
|
`role === 'admin'`.
|
|
141
153
|
|
|
142
|
-
Declare the tree in your own `
|
|
154
|
+
Declare the tree in your own `defineScope` (the CLI extracts it by AST, so it must
|
|
143
155
|
be an inline literal; `ADMIN_SCOPE_TREE` is exported from `@pikku/better-auth`
|
|
144
156
|
as the reference shape). Apps wiring `@pikku/addon-console` inherit it already.
|
|
145
157
|
|
|
146
158
|
```typescript
|
|
147
|
-
|
|
159
|
+
defineScope({
|
|
148
160
|
admin: {
|
|
149
161
|
displayName: 'Administration',
|
|
150
162
|
description: 'Capabilities that act on the application as a whole',
|
|
@@ -158,7 +170,14 @@ wireScope({
|
|
|
158
170
|
},
|
|
159
171
|
users: {
|
|
160
172
|
description: 'The user directory',
|
|
161
|
-
scopes: {
|
|
173
|
+
scopes: {
|
|
174
|
+
list: { description: 'List and search users' },
|
|
175
|
+
create: { description: 'Create users out of band' },
|
|
176
|
+
ban: { description: 'Ban and unban users' },
|
|
177
|
+
remove: { description: 'Delete users and all their data' },
|
|
178
|
+
sessions: { description: "Revoke a user's sessions" },
|
|
179
|
+
password: { description: "Set a user's password" },
|
|
180
|
+
},
|
|
162
181
|
},
|
|
163
182
|
},
|
|
164
183
|
},
|
|
@@ -172,9 +191,22 @@ a scope, so nothing is authorized, and the denial is logged at `warn` because
|
|
|
172
191
|
that is a configuration bug rather than a permissions decision. Pass your own
|
|
173
192
|
`canImpersonate` / `canLinkSingleton` to override the default entirely.
|
|
174
193
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
### If you do wire better-auth's `admin()` plugin
|
|
195
|
+
|
|
196
|
+
The last five capabilities in the table are implemented by better-auth's own
|
|
197
|
+
`admin()` endpoints, which authorize against `user.role` — a column pikku
|
|
198
|
+
otherwise ignores. Rather than making you maintain two grant systems,
|
|
199
|
+
`syncProjectedAdminRole` keeps that column as a *projection* of the scope set:
|
|
200
|
+
at the session boundary it writes `role = 'admin'` when the user holds any of
|
|
201
|
+
`admin:users:{create,ban,remove,sessions,password}`, and the plugin's
|
|
202
|
+
`defaultRole` otherwise. `projectedAdminRole(scopes, defaultRole)` computes the
|
|
203
|
+
value if you need it yourself.
|
|
204
|
+
|
|
205
|
+
The projection is deliberately not "any `admin:*` scope": `impersonate` and
|
|
206
|
+
`users:list` are pikku's own gates, and rolling them in would hand ban and delete
|
|
207
|
+
rights to someone granted only the ability to look. The plugin is auto-detected
|
|
208
|
+
from the live instance, so an app without it never writes to a column that does
|
|
209
|
+
not exist.
|
|
178
210
|
|
|
179
211
|
### 2. Production database adapter
|
|
180
212
|
|
|
@@ -184,9 +216,9 @@ For real deployments swap `memoryAdapter` for the Kysely adapter backed by an in
|
|
|
184
216
|
import { kyselyAdapter } from 'better-auth/adapters/kysely'
|
|
185
217
|
|
|
186
218
|
export const auth = pikkuBetterAuth(async ({ secrets, kysely }) => {
|
|
187
|
-
const { BETTER_AUTH_SECRET } = await secrets.getSecrets<{
|
|
188
|
-
|
|
189
|
-
])
|
|
219
|
+
const { BETTER_AUTH_SECRET } = await secrets.getSecrets<{
|
|
220
|
+
BETTER_AUTH_SECRET: string
|
|
221
|
+
}>(['BETTER_AUTH_SECRET'])
|
|
190
222
|
return betterAuth({
|
|
191
223
|
secret: BETTER_AUTH_SECRET,
|
|
192
224
|
database: kyselyAdapter(kysely, { type: 'postgres' }),
|
|
@@ -204,7 +236,7 @@ If you place `auth.ts` under `srcDirectories` it is inspected automatically. The
|
|
|
204
236
|
|
|
205
237
|
## Social Providers needing extra config
|
|
206
238
|
|
|
207
|
-
Some providers require non-secret config alongside the OAuth secret — the CLI emits a `
|
|
239
|
+
Some providers require non-secret config alongside the OAuth secret — the CLI emits a `defineVariable` for these:
|
|
208
240
|
|
|
209
241
|
- `microsoft` → `MICROSOFT_TENANT_ID` (or `"common"`)
|
|
210
242
|
- `cognito` → `COGNITO_DOMAIN`, `COGNITO_REGION`, `COGNITO_USER_POOL_ID`
|
|
@@ -221,7 +253,12 @@ export const auth = pikkuBetterAuth(async ({ secrets, variables }) => {
|
|
|
221
253
|
|
|
222
254
|
return betterAuth({
|
|
223
255
|
secret: BETTER_AUTH_SECRET,
|
|
224
|
-
database: memoryAdapter({
|
|
256
|
+
database: memoryAdapter({
|
|
257
|
+
user: [],
|
|
258
|
+
session: [],
|
|
259
|
+
account: [],
|
|
260
|
+
verification: [],
|
|
261
|
+
}),
|
|
225
262
|
socialProviders: {
|
|
226
263
|
microsoft: { ...MICROSOFT_OAUTH, tenantId: MICROSOFT_TENANT_ID },
|
|
227
264
|
},
|
|
@@ -258,18 +295,27 @@ For public endpoints that optionally vary by viewer, use `pikkuSessionlessFunc`
|
|
|
258
295
|
|
|
259
296
|
Better Auth serves everything under `basePath` (default `/api/auth`). Call these directly — the Pikku SDK does not wrap them.
|
|
260
297
|
|
|
261
|
-
| Action
|
|
262
|
-
|
|
263
|
-
| Sign up
|
|
264
|
-
| Log in
|
|
265
|
-
| Session
|
|
266
|
-
| Social sign-in | `POST /api/auth/sign-in/social` `{ provider, callbackURL }` | 200 `{ url, redirect }` (authorize URL)
|
|
267
|
-
| Sign out
|
|
298
|
+
| Action | Request | Result |
|
|
299
|
+
| -------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
300
|
+
| Sign up | `POST /api/auth/sign-up/email` `{ name, email, password }` | 200 + `better-auth.session_token` cookie |
|
|
301
|
+
| Log in | `POST /api/auth/sign-in/email` `{ email, password }` | 200 + cookie; wrong creds → 401 `{ code: "INVALID_EMAIL_OR_PASSWORD" }` |
|
|
302
|
+
| Session | `GET /api/auth/get-session` | `{ session, user }` or `null` |
|
|
303
|
+
| Social sign-in | `POST /api/auth/sign-in/social` `{ provider, callbackURL }` | 200 `{ url, redirect }` (authorize URL) |
|
|
304
|
+
| Sign out | `POST /api/auth/sign-out` | 200, clears cookie |
|
|
268
305
|
|
|
269
306
|
**`Origin` header on state-changing POSTs:** better-auth enforces an `Origin` header matching `baseURL` on POSTs such as sign-out — omit it and you get `403`. Browsers send it automatically; server-to-server callers must set it.
|
|
270
307
|
|
|
271
308
|
The session cookie is `better-auth.session_token` (dev) / `__Secure-better-auth.session_token` (prod).
|
|
272
309
|
|
|
310
|
+
### Dev quick login
|
|
311
|
+
|
|
312
|
+
Set `PIKKU_DEV_QUICK_LOGIN=true` and `${basePath}/dev/quick-login` signs in a
|
|
313
|
+
fixed dev admin (`admin@pikku.dev`), creating the user idempotently and granting
|
|
314
|
+
it the bare `admin` scope. It is guarded twice — the env var *and* a localhost
|
|
315
|
+
hostname check — because a one-request path to an admin session is exactly the
|
|
316
|
+
thing that must not survive a deploy. An app that has not declared the `admin`
|
|
317
|
+
scope still gets a session, with a warning, since a scopeless dev user is useful.
|
|
318
|
+
|
|
273
319
|
---
|
|
274
320
|
|
|
275
321
|
## Secret Management
|
|
@@ -36,19 +36,23 @@ See `pikku-concepts` for the core mental model.
|
|
|
36
36
|
|
|
37
37
|
### `wireCLI(config)`
|
|
38
38
|
|
|
39
|
+
All three factories come from `#pikku` (the generated types re-export
|
|
40
|
+
`cli/pikku-cli-types.gen.js`). Importing them from `@pikku/core/cli` compiles but
|
|
41
|
+
loses your project's service and middleware types.
|
|
42
|
+
|
|
39
43
|
```typescript
|
|
40
|
-
import { wireCLI } from '
|
|
44
|
+
import { wireCLI } from '#pikku'
|
|
41
45
|
|
|
42
46
|
wireCLI({
|
|
43
47
|
program: string, // Program name (e.g. 'todos')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
short?: string, // Single char alias (e.g. 'v')
|
|
48
|
-
default?: any,
|
|
49
|
-
}
|
|
50
|
-
},
|
|
48
|
+
description?: string,
|
|
49
|
+
summary?: string,
|
|
50
|
+
options?: CLIOptions, // Global options — see below
|
|
51
51
|
render?: PikkuCLIRender, // Default renderer for all commands
|
|
52
|
+
middleware?: PikkuMiddleware[],
|
|
53
|
+
tags?: string[], // Targets tag middleware
|
|
54
|
+
errors?: string[],
|
|
55
|
+
auth?: boolean, // Only affects the websocket backend, not local runs
|
|
52
56
|
commands: {
|
|
53
57
|
[name: string]: PikkuCLICommand | {
|
|
54
58
|
description: string,
|
|
@@ -65,24 +69,49 @@ import { pikkuCLICommand } from '#pikku'
|
|
|
65
69
|
|
|
66
70
|
pikkuCLICommand({
|
|
67
71
|
parameters?: string, // Positional args (e.g. '<text>', '<username> <email>')
|
|
68
|
-
func
|
|
72
|
+
func?: PikkuFunc, // Business logic function — omit on a pure command group
|
|
73
|
+
title?: string,
|
|
69
74
|
description?: string,
|
|
70
75
|
render?: PikkuCLIRender, // Custom output renderer
|
|
71
|
-
options?:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
},
|
|
76
|
+
options?: CLIOptions,
|
|
77
|
+
subcommands?: { [name: string]: PikkuCLICommand }, // nests to any depth
|
|
78
|
+
middleware?: PikkuMiddleware[],
|
|
79
|
+
permissions?: PermissionGroup,
|
|
80
|
+
auth?: boolean,
|
|
81
|
+
isDefault?: boolean, // Runs when the group is invoked with no subcommand
|
|
79
82
|
})
|
|
80
83
|
```
|
|
81
84
|
|
|
85
|
+
`parameters` is checked against the func's input at compile time — a name that is
|
|
86
|
+
not a key of the input makes the type `never`, so a typo'd positional fails to
|
|
87
|
+
build rather than arriving as `undefined`.
|
|
88
|
+
|
|
89
|
+
### Options
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
{
|
|
93
|
+
description: string,
|
|
94
|
+
short?: string, // Single char alias (e.g. 'v')
|
|
95
|
+
default?: any,
|
|
96
|
+
choices?: any[], // Restrict to these values
|
|
97
|
+
array?: boolean, // Collect every value up to the next flag
|
|
98
|
+
required?: boolean,
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
How the parser reads them, which is worth knowing before you name one:
|
|
103
|
+
|
|
104
|
+
- **Flag names are camel-cased**, so `--api-url` and `--apiUrl` both fill `apiUrl`.
|
|
105
|
+
- **`--no-x` negation only works when `x` has a boolean `default`.** Without one,
|
|
106
|
+
`--no-x` parses as an option literally named `noX` — which is why boolean flags
|
|
107
|
+
should always declare their default.
|
|
108
|
+
- Short flags cluster (`-abc`), and only the last in a cluster may take a value.
|
|
109
|
+
- An unknown `--flag` warns rather than throwing.
|
|
110
|
+
|
|
82
111
|
### `pikkuCLIRender(fn)`
|
|
83
112
|
|
|
84
113
|
```typescript
|
|
85
|
-
import { pikkuCLIRender } from '
|
|
114
|
+
import { pikkuCLIRender } from '#pikku'
|
|
86
115
|
|
|
87
116
|
const renderer = pikkuCLIRender<OutputType>((services, data) => {
|
|
88
117
|
// Format and print output to terminal
|
|
@@ -90,6 +119,15 @@ const renderer = pikkuCLIRender<OutputType>((services, data) => {
|
|
|
90
119
|
})
|
|
91
120
|
```
|
|
92
121
|
|
|
122
|
+
### Wire object (`wire.cli`)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
wire.cli.program // program name
|
|
126
|
+
wire.cli.command // string[] — the resolved command path
|
|
127
|
+
wire.cli.data // all positionals and options, merged
|
|
128
|
+
wire.cli.channel // the channel when served remotely (see below)
|
|
129
|
+
```
|
|
130
|
+
|
|
93
131
|
## Usage Patterns
|
|
94
132
|
|
|
95
133
|
### Basic Commands
|
|
@@ -193,6 +231,17 @@ wireCLI({
|
|
|
193
231
|
|
|
194
232
|
The func's input is the positional `parameters` plus `options`, merged (e.g. `parameters: '<username> <email>'` + an `admin` option → func input `{ username, email, admin }`).
|
|
195
233
|
|
|
234
|
+
A renderer's full signature is `(services, data, session?)`. It returns nothing —
|
|
235
|
+
printing is its job.
|
|
236
|
+
|
|
237
|
+
### Running the program over a websocket
|
|
238
|
+
|
|
239
|
+
Codegen emits a `<program>-channel.gen.ts` beside your wiring: a `wireChannel`
|
|
240
|
+
that serves the same commands remotely, so a local binary and a hosted session
|
|
241
|
+
run identical code. `auth` on `wireCLI` guards **that channel only** — a locally
|
|
242
|
+
executed CLI has no connection to authenticate, so it is not a way to require a
|
|
243
|
+
session for local runs. Don't hand-write or edit the generated channel file.
|
|
244
|
+
|
|
196
245
|
## Complete Example
|
|
197
246
|
|
|
198
247
|
For a full functions + renderers + nested-subcommand wiring walkthrough, see `references/complete-example.md`.
|
|
@@ -32,6 +32,8 @@ export const deleteUser = pikkuFunc({
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
// wirings/cli.wiring.ts
|
|
35
|
+
import { wireCLI, pikkuCLICommand, pikkuCLIRender } from '#pikku'
|
|
36
|
+
|
|
35
37
|
const userRenderer = pikkuCLIRender<{ user: User }>((_services, { user }) => {
|
|
36
38
|
console.log(`Created user: ${user.username} (${user.email}) [${user.role}]`)
|
|
37
39
|
})
|
|
@@ -28,7 +28,8 @@ Pikku is a TypeScript framework that separates business logic from transport mec
|
|
|
28
28
|
For deep-dive on each topic, see the dedicated skills:
|
|
29
29
|
|
|
30
30
|
- **Wiring**: `pikku-http`, `pikku-websocket`, `pikku-rpc`, `pikku-mcp`, `pikku-queue`, `pikku-cron`, `pikku-trigger`, `pikku-cli`, `pikku-ai-agent`, `pikku-workflow`
|
|
31
|
-
- **
|
|
31
|
+
- **Authorization**: `pikku-security` (authentication/sessions), `pikku-permissions` (permission checks, scopes), `pikku-middleware` (global/tag/route middleware)
|
|
32
|
+
- **Infrastructure**: `pikku-services`, `pikku-config`
|
|
32
33
|
- **Project introspection**: `pikku-info`
|
|
33
34
|
|
|
34
35
|
## Core Mental Model
|
|
@@ -58,7 +59,7 @@ The function never imports Express, never reads `req.body`, never touches `ws.se
|
|
|
58
59
|
|
|
59
60
|
## Concept Mapping: Generic Backend → Pikku
|
|
60
61
|
|
|
61
|
-
Controllers/routes → `pikkuFunc`;
|
|
62
|
+
Controllers/routes → `pikkuFunc`; auth/sessions → `pikku-security`; authorization checks → `pikku-permissions`; request interception → `pikku-middleware`; DI → `pikku-services`; transports (HTTP/WS/queue/cron) → their `wire*` + skill. For the full Generic Backend → Pikku mapping table (with side-by-side code examples), read `references/concept-mapping.md`.
|
|
62
63
|
|
|
63
64
|
## Functions
|
|
64
65
|
|
|
@@ -95,22 +96,53 @@ Services can be destructured inline in the `func` signature (e.g. `async ({ logg
|
|
|
95
96
|
|
|
96
97
|
```typescript
|
|
97
98
|
pikkuFunc({
|
|
99
|
+
// Identity and documentation
|
|
98
100
|
title?: string, // Human-readable name
|
|
99
101
|
description?: string, // What the function does
|
|
100
|
-
version?: number, // Contract version (see pikku-
|
|
102
|
+
version?: number, // Contract version (see pikku-versioning)
|
|
103
|
+
override?: string, // Logical name override, so several exports share a versioned base
|
|
101
104
|
tags?: string[], // For grouping and middleware targeting
|
|
105
|
+
|
|
106
|
+
// Contract
|
|
107
|
+
input?: ZodSchema, // Input validation schema
|
|
108
|
+
output?: ZodSchema, // Output validation schema
|
|
109
|
+
errors?: Array<typeof PikkuError>, // Errors this function may throw
|
|
110
|
+
|
|
111
|
+
// Reachability
|
|
102
112
|
expose?: boolean, // Allow external RPC calls (see pikku-rpc)
|
|
103
113
|
remote?: boolean, // Allow remote RPC calls
|
|
104
114
|
mcp?: boolean, // Expose as MCP tool (see pikku-mcp)
|
|
115
|
+
readonly?: boolean, // Declares the function performs no writes
|
|
116
|
+
deploy?: 'serverless' | 'server' | 'auto',
|
|
117
|
+
|
|
118
|
+
// Authorization — see pikku-permissions
|
|
105
119
|
auth?: boolean, // Override default auth requirement
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
middleware?: PikkuMiddleware[], // See pikku-
|
|
120
|
+
scopes?: ScopeId[], // AND-ed, checked before permissions; session required
|
|
121
|
+
permissions?: PermissionGroup, // OR-ed pool
|
|
122
|
+
permissionsInBody?: boolean, // Last resort; needs allow.permissionsInBody in config
|
|
123
|
+
middleware?: PikkuMiddleware[], // See pikku-middleware
|
|
124
|
+
|
|
125
|
+
// Agent tooling — see pikku-ai-agent
|
|
126
|
+
approvalRequired?: boolean,
|
|
127
|
+
approvalDescription?: (services, data) => Promise<string>,
|
|
128
|
+
|
|
129
|
+
// Workflow step behavior — see pikku-workflow
|
|
130
|
+
workflowQueued?: boolean, // Dispatch via queue instead of inline
|
|
131
|
+
workflowRetries?: number,
|
|
132
|
+
workflowTimeout?: string, // e.g. '30s', '5m'
|
|
133
|
+
|
|
134
|
+
audit?: boolean | { durability?: 'best-effort' | 'transactional' },
|
|
135
|
+
|
|
110
136
|
func: async (services, data, wire) => { ... },
|
|
111
137
|
})
|
|
112
138
|
```
|
|
113
139
|
|
|
140
|
+
`scopes` is the one option `pikkuSessionlessFunc` does not accept, and the
|
|
141
|
+
omission is deliberate: scopes are AND-ed and fail closed, so an anonymous
|
|
142
|
+
caller holds none and satisfies none — a sessionless function with scopes would
|
|
143
|
+
reject every caller it exists to serve. Gate those with `permissions`, which
|
|
144
|
+
receive the optional session and may pass anonymous.
|
|
145
|
+
|
|
114
146
|
**Generics XOR `input`/`output` — never both.** A function's data and return
|
|
115
147
|
types come from *one* source: either the `input`/`output` schemas (preferred —
|
|
116
148
|
they double as runtime validation and OpenAPI) or type generics
|
|
@@ -145,7 +177,35 @@ Schemas serve triple duty: runtime validation, TypeScript types, and OpenAPI doc
|
|
|
145
177
|
|
|
146
178
|
## Server Bootstrap
|
|
147
179
|
|
|
148
|
-
|
|
180
|
+
There are two ways to start a Pikku app. Pick based on whether you need to own the HTTP server.
|
|
181
|
+
|
|
182
|
+
**1. Let Pikku own the server (preferred when you don't need a specific runtime)**
|
|
183
|
+
|
|
184
|
+
`pikku dev` and `pikku serve` create the config and singleton services, start the server, and shut it down cleanly. You write no bootstrap code at all — startup and shutdown work goes in lifecycle hooks:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// src/lifecycle.ts
|
|
188
|
+
import { pikkuServerLifecycle } from '@pikku/core'
|
|
189
|
+
import type { SingletonServices } from '../types/application-types.js'
|
|
190
|
+
|
|
191
|
+
export const lifecycle = pikkuServerLifecycle<SingletonServices>({
|
|
192
|
+
beforeStart: async ({ kysely }) => {
|
|
193
|
+
await runMigrations(kysely)
|
|
194
|
+
},
|
|
195
|
+
afterStart: async ({ logger }) => {
|
|
196
|
+
logger.info('accepting traffic')
|
|
197
|
+
},
|
|
198
|
+
beforeStop: async ({ queueService }) => {
|
|
199
|
+
await queueService.drain()
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Export exactly one `pikkuServerLifecycle` from anywhere in `srcDirectories` — the inspector finds it by the wrapper call. Every hook is optional and receives the already-created singleton services. See pikku-services for the ordering and the `afterStop` caveat.
|
|
205
|
+
|
|
206
|
+
**2. Bootstrap it yourself (required for a specific runtime)**
|
|
207
|
+
|
|
208
|
+
Express, Fastify, uWS, Lambda, Cloudflare and Next.js need their own entrypoint, because Pikku is embedded in a server you own:
|
|
149
209
|
|
|
150
210
|
```typescript
|
|
151
211
|
import '../../functions/.pikku/pikku-bootstrap.gen.js' // Generated — registers all wirings
|
|
@@ -168,6 +228,10 @@ await server.init()
|
|
|
168
228
|
await server.start()
|
|
169
229
|
```
|
|
170
230
|
|
|
231
|
+
**Lifecycle hooks do not run on this path** — only `pikku dev` and `pikku serve` invoke them. Do your startup work directly in the entrypoint instead.
|
|
232
|
+
|
|
233
|
+
`pikku workspace validate` warns when a project starts a server by hand *and* depends on no runtime adapter, since that combination means path 1 was available and unused. Silence it with `"lint": { "customServerBootstrap": "off" }` in `pikku.config.json`.
|
|
234
|
+
|
|
171
235
|
## Code Generation
|
|
172
236
|
|
|
173
237
|
Run `npx pikku all` to generate:
|
|
@@ -175,7 +239,7 @@ Run `npx pikku all` to generate:
|
|
|
175
239
|
- `pikku-types.gen.ts` — Typed function factories and wiring functions
|
|
176
240
|
- `pikku-fetch.gen.ts` — Type-safe HTTP client
|
|
177
241
|
- `pikku-websocket.gen.ts` — Type-safe WebSocket client
|
|
178
|
-
- `pikku-bootstrap.gen.
|
|
242
|
+
- `pikku-bootstrap.gen.ts` — Runtime initialization (auto-imports all wirings)
|
|
179
243
|
- `pikku-services.gen.ts` — Service factory types
|
|
180
244
|
|
|
181
245
|
Config lives in `pikku.config.json`:
|
|
@@ -203,12 +267,13 @@ src/
|
|
|
203
267
|
│ └── queue.wiring.ts
|
|
204
268
|
├── schemas.ts # Zod/Valibot schemas
|
|
205
269
|
├── services.ts # Service factories (see pikku-services)
|
|
270
|
+
├── lifecycle.ts # Server lifecycle hooks (pikku dev/serve only)
|
|
206
271
|
├── middleware.ts # Middleware definitions (see pikku-security)
|
|
207
272
|
├── permissions.ts # Permission definitions (see pikku-security)
|
|
208
273
|
└── .pikku/ # Generated (gitignored)
|
|
209
274
|
├── pikku-types.gen.ts
|
|
210
275
|
├── pikku-fetch.gen.ts
|
|
211
|
-
└── pikku-bootstrap.gen.
|
|
276
|
+
└── pikku-bootstrap.gen.ts
|
|
212
277
|
```
|
|
213
278
|
|
|
214
279
|
## Environment Variables
|
|
@@ -221,6 +286,13 @@ const apiKey = services.variables.get('API_KEY')
|
|
|
221
286
|
|
|
222
287
|
`process.env` belongs in server bootstrap code (`start.ts`) only.
|
|
223
288
|
|
|
289
|
+
## Secrets
|
|
290
|
+
|
|
291
|
+
`secrets` is not part of a function's services. It is available only in
|
|
292
|
+
`pikkuServices`, `pikkuWireServices`, addon service factories and middleware —
|
|
293
|
+
read it there, give the value to a service, and have the function ask that
|
|
294
|
+
service. Reaching for it through a cast throws at runtime.
|
|
295
|
+
|
|
224
296
|
## Testing
|
|
225
297
|
|
|
226
298
|
Functions are easily testable because they're pure:
|
|
@@ -18,8 +18,8 @@ Authoritative mapping table plus side-by-side code examples showing how common b
|
|
|
18
18
|
| **Cron / Scheduled tasks** | `wireScheduler` | `pikku-cron` |
|
|
19
19
|
| **Module / Feature grouping** | Tags + wiring files | `pikku-concepts` |
|
|
20
20
|
| **Error handling** | Throw typed errors (`NotFoundError`, `ForbiddenError`) | `pikku-concepts` |
|
|
21
|
-
| **Type-safe API client** | `npx pikku all` generates clients
|
|
22
|
-
| **Secrets / Config** | `
|
|
21
|
+
| **Type-safe API client** | `npx pikku all` generates clients | `pikku-concepts` |
|
|
22
|
+
| **Secrets / Config** | `defineSecret`, `defineVariable`, `services.variables` | `pikku-config` |
|
|
23
23
|
|
|
24
24
|
## Route Handler / Controller → pikkuFunc
|
|
25
25
|
|