@pikku/skills 0.12.22 → 0.12.26
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/CHANGELOG.md +134 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-a11y/SKILL.md +59 -0
- package/skills/pikku-addon/SKILL.md +2 -2
- package/skills/pikku-agent/SKILL.md +67 -316
- package/skills/pikku-agent/references/agents.md +299 -0
- package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
- package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
- package/skills/pikku-architect/SKILL.md +265 -0
- package/skills/pikku-auth/SKILL.md +89 -0
- package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +126 -34
- package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
- package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
- package/skills/pikku-auth/references/permissions.md +261 -0
- package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
- package/skills/pikku-build/SKILL.md +88 -0
- package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +76 -24
- package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
- package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +56 -1
- package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
- package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
- package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
- package/skills/{pikku-build-app → pikku-build}/references/ship.md +9 -3
- package/skills/pikku-concepts/SKILL.md +72 -7
- package/skills/pikku-concepts/references/concept-mapping.md +8 -8
- package/skills/pikku-deploy/SKILL.md +158 -0
- package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
- package/skills/pikku-deploy/references/cloudflare.md +104 -0
- package/skills/pikku-deploy/references/express.md +92 -0
- package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
- package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
- package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
- package/skills/pikku-deploy/references/uws.md +72 -0
- package/skills/pikku-deploy/references/ws.md +75 -0
- package/skills/pikku-emails/SKILL.md +3 -2
- package/skills/pikku-fabric/SKILL.md +47 -20
- package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
- package/skills/pikku-i18n/SKILL.md +62 -207
- package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
- package/skills/pikku-i18n/references/messages.md +218 -0
- package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
- package/skills/pikku-knowledge/SKILL.md +15 -0
- package/skills/pikku-kysely/SKILL.md +13 -13
- package/skills/pikku-list-query/SKILL.md +163 -0
- package/skills/pikku-meta/SKILL.md +58 -130
- package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
- package/skills/pikku-meta/references/meta.md +114 -0
- package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-n8n-import/SKILL.md +0 -1
- package/skills/pikku-permissions/SKILL.md +75 -229
- package/skills/pikku-react/SKILL.md +50 -298
- package/skills/pikku-react/references/client.md +313 -0
- package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
- package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
- package/skills/pikku-realtime/SKILL.md +110 -251
- package/skills/pikku-scenario/SKILL.md +60 -45
- package/skills/pikku-scenario/references/persona-run.md +148 -0
- package/skills/pikku-seo/SKILL.md +133 -0
- package/skills/pikku-service-backends/SKILL.md +154 -0
- package/skills/pikku-service-backends/references/aws.md +106 -0
- package/skills/pikku-service-backends/references/backblaze.md +57 -0
- package/skills/pikku-service-backends/references/mongodb.md +90 -0
- package/skills/pikku-service-backends/references/redis.md +75 -0
- package/skills/pikku-service-backends/references/schema.md +63 -0
- package/skills/pikku-services/SKILL.md +68 -291
- package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
- package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
- package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
- package/skills/pikku-services/references/services.md +272 -0
- package/skills/pikku-software-archaeology/README.md +5 -1
- package/skills/pikku-software-archaeology/SKILL.md +16 -2
- package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
- package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
- package/skills/pikku-webhook/SKILL.md +224 -0
- package/skills/pikku-wiring/SKILL.md +180 -0
- package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
- package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
- package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
- package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
- package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
- package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
- package/skills/pikku-wiring/references/realtime.md +265 -0
- package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
- package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
- package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
- package/skills/pikku-workflow/SKILL.md +39 -2
- package/skills/pikku-aws/SKILL.md +0 -161
- package/skills/pikku-backblaze/SKILL.md +0 -104
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
- package/skills/pikku-deploy-express/SKILL.md +0 -122
- package/skills/pikku-deploy-uws/SKILL.md +0 -144
- package/skills/pikku-mongodb/SKILL.md +0 -113
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-redis/SKILL.md +0 -99
- package/skills/pikku-schema-ajv/SKILL.md +0 -83
- package/skills/pikku-schema-cfworker/SKILL.md +0 -82
- package/skills/pikku-ws/SKILL.md +0 -87
- /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
- /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
- /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
- /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
- /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pikku-auth
|
|
3
|
+
description: >-
|
|
4
|
+
Use for anything about identity in a Pikku app — authenticating a caller (login, logout,
|
|
5
|
+
sessions, cookies, bearer tokens, API keys, JWT, OAuth/social providers, MFA, Better Auth) and
|
|
6
|
+
authorizing one (pikkuPermission, pikkuAuth, scopes, defineScope, global permissions). Covers
|
|
7
|
+
which of the two a problem actually is, the built-in auth strategies, machine-to-machine auth
|
|
8
|
+
and `pikku login`, and the JWT service. TRIGGER when: user asks about login, logout, session,
|
|
9
|
+
cookie auth, bearer tokens, API keys, JWT, Better Auth, social providers, restricting who may
|
|
10
|
+
call a function, resource ownership, roles, scopes, or hits MissingScopeError or
|
|
11
|
+
InvalidSessionError. DO NOT TRIGGER when: user asks about middleware mechanics with no identity
|
|
12
|
+
involved (use pikku-middleware) or about secrets and env vars (use pikku-services).
|
|
13
|
+
installGroups: [core]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Pikku Auth
|
|
17
|
+
|
|
18
|
+
Signatures and option keys come from `pikku doc` — run `pikku doc --ai` for the
|
|
19
|
+
installed surface. This skill is the part the compiler cannot tell you: which of
|
|
20
|
+
the two problems you actually have, and what goes wrong in each.
|
|
21
|
+
|
|
22
|
+
## First: authentication or authorization?
|
|
23
|
+
|
|
24
|
+
They are separate gates, and confusing them is the most common mistake in a
|
|
25
|
+
Pikku app.
|
|
26
|
+
|
|
27
|
+
**Authentication** answers _who is calling_. It happens in middleware, before the
|
|
28
|
+
function runs, and ends in a call to `setSession`.
|
|
29
|
+
|
|
30
|
+
**Authorization** answers _may this caller do this_. It is declared on the
|
|
31
|
+
function — `scopes` and `permissions` — never checked inside its body.
|
|
32
|
+
|
|
33
|
+
A `permissions` entry that verifies a bearer token and returns `true` is
|
|
34
|
+
authentication wearing an authorization hat: it leaves the function sessionless,
|
|
35
|
+
so every body still has to work out who called it. Resolve the credential in
|
|
36
|
+
middleware instead.
|
|
37
|
+
|
|
38
|
+
## Pick the reference
|
|
39
|
+
|
|
40
|
+
| You are… | Read |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Restricting who may call a function — ownership, roles, scopes | `references/permissions.md` |
|
|
43
|
+
| Reading or setting the session, or wiring `authBearer`/`authCookie`/`authAPIKey` | `references/sessions.md` |
|
|
44
|
+
| Standing up user sign-in — OAuth, email+password, MFA, organizations | `references/better-auth.md` |
|
|
45
|
+
| Authenticating a CLI, agent, sandbox or worker — API keys, `pikku login` | `references/machine-auth.md` |
|
|
46
|
+
| Configuring the JWT service, or rotating a signing secret | `references/jose.md` |
|
|
47
|
+
|
|
48
|
+
## All authentication goes through `@pikku/better-auth`
|
|
49
|
+
|
|
50
|
+
There is no second auth story. A hand-rolled user table, a bespoke password
|
|
51
|
+
hash, a custom OAuth dance — all of them are the wrong answer, and the console
|
|
52
|
+
will not work against them. `references/better-auth.md` has the setup; the
|
|
53
|
+
built-in strategies in `references/sessions.md` are how a resolved credential
|
|
54
|
+
becomes a Pikku session, not a replacement for it.
|
|
55
|
+
|
|
56
|
+
## The three gates
|
|
57
|
+
|
|
58
|
+
Authorization is three independent gates, all of which must pass, in this order:
|
|
59
|
+
|
|
60
|
+
1. **Scopes** (`scopes`) — AND'd, checked before input validation, fails closed.
|
|
61
|
+
2. **Global permissions** (`addGlobalPermission`) — AND'd, an app-wide baseline.
|
|
62
|
+
3. **The function's own `permissions`** — OR'd groups of AND'd entries.
|
|
63
|
+
|
|
64
|
+
They are independent: a broad global gate can never satisfy a function's own
|
|
65
|
+
requirement, and a scope can only ever narrow access, never grant it.
|
|
66
|
+
|
|
67
|
+
## What NOT to do
|
|
68
|
+
|
|
69
|
+
- **Do not check authorization inside `func`.** The `permissions` field is
|
|
70
|
+
visible to the inspector; a check in the body is invisible to `pikku info
|
|
71
|
+
permissions` and to an audit. The one sanctioned exception is
|
|
72
|
+
`permissionsInBody`, which is purely declarative — see
|
|
73
|
+
`references/permissions.md`.
|
|
74
|
+
- **Do not write an "is signed in" permission.** `auth: true` (the default on
|
|
75
|
+
`pikkuFunc`) already requires the session. A checker returning `!!session`
|
|
76
|
+
gates nothing.
|
|
77
|
+
- **Do not put `scopes` on a `pikkuSessionlessFunc`.** Scopes fail closed and an
|
|
78
|
+
anonymous caller holds none, so it would reject every caller it exists to
|
|
79
|
+
serve. Gate those with `permissions`, which receive an optional session.
|
|
80
|
+
- **Do not expect the built-in strategies to authenticate a non-HTTP caller.**
|
|
81
|
+
`authBearer`, `authCookie` and `authAPIKey` all step aside when there is no
|
|
82
|
+
HTTP request — a queue job, a scheduled task or a channel message needs its
|
|
83
|
+
session set another way.
|
|
84
|
+
- **Do not share one header between the human and machine paths.**
|
|
85
|
+
`Authorization: Bearer` is the human session; `x-api-key` is the machine key.
|
|
86
|
+
Merging them reintroduces the ambiguity the split exists to remove.
|
|
87
|
+
- **Do not reach for wire-, tag- or route-level permissions.** They were removed
|
|
88
|
+
in #972. Permissions live on the function, plus the optional global gate; tags
|
|
89
|
+
are organizational only.
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-better-auth
|
|
3
|
-
description: >-
|
|
4
|
-
Use when integrating Better Auth with a Pikku app. Covers pikkuBetterAuth, betterAuth config,
|
|
5
|
-
the generated catch-all auth routes, betterAuthSession middleware, OAuth/social providers,
|
|
6
|
-
email+password credentials, database adapters, and session mapping. TRIGGER when: code uses
|
|
7
|
-
pikkuBetterAuth, betterAuth, betterAuthSession, createAuthHandler, user asks about Better Auth,
|
|
8
|
-
OAuth/social providers, MFA, organizations, login/logout, or @pikku/better-auth. TRIGGER when: user asks
|
|
9
|
-
about the actor plugin, /sign-in/actor, signing in as a scenario persona, or SCENARIO_ACTOR_SECRET. TRIGGER when:
|
|
10
|
-
user asks about ANY form of authentication, login, logout, sessions, or user identity — always
|
|
11
|
-
answer with this skill. DO NOT TRIGGER when: user asks about JWT middleware (use pikku-security)
|
|
12
|
-
or custom session services (use pikku-services).
|
|
13
|
-
installGroups: [fabric]
|
|
14
|
-
---
|
|
15
|
-
|
|
16
1
|
# Pikku Better Auth Integration
|
|
17
2
|
|
|
18
3
|
## ⚠️ MANDATORY RULE — READ FIRST
|
|
@@ -30,18 +15,6 @@ The only acceptable auth implementation in a Pikku app is the one described in t
|
|
|
30
15
|
|
|
31
16
|
---
|
|
32
17
|
|
|
33
|
-
## Agent Operating Procedure
|
|
34
|
-
|
|
35
|
-
Use this skill as an execution checklist, not reference material.
|
|
36
|
-
|
|
37
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
38
|
-
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, or build artifacts.
|
|
39
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated.
|
|
40
|
-
4. Validate with the narrowest relevant command first, then run `pikku all` when functions, wirings, schemas, or generated clients may have changed.
|
|
41
|
-
5. If validation fails, fix the source cause and rerun. Do not edit generated files.
|
|
42
|
-
|
|
43
|
-
`@pikku/better-auth` provides [Better Auth](https://better-auth.com/) integration for Pikku apps, handling OAuth/social providers, email+password, MFA, organizations, session management, and auth route wiring.
|
|
44
|
-
|
|
45
18
|
## Installation
|
|
46
19
|
|
|
47
20
|
```bash
|
|
@@ -61,7 +34,7 @@ You do NOT hand-write routes, the session middleware, or the secret wiring — `
|
|
|
61
34
|
|
|
62
35
|
### The console requires Better Auth
|
|
63
36
|
|
|
64
|
-
The Pikku console (`@pikku/addon-console`, enabled via `scaffold.console` in `pikku.config.json`) is an admin surface: **every console RPC now requires an authenticated session** (the functions are `pikkuFunc`; unauthenticated calls return `403`). So `scaffold.console` alone is **no longer the minimum** — you also need an auth strategy, and Better Auth is the supported one. `pikku all` **throws** if `scaffold.console` is set but no `pikkuBetterAuth(...)` is found in the project. Baseline is "must be logged in"; finer policy (admin-only, org scoping) is layered host-side via tag/HTTP middleware. See `pikku-
|
|
37
|
+
The Pikku console (`@pikku/addon-console`, enabled via `scaffold.console` in `pikku.config.json`) is an admin surface: **every console RPC now requires an authenticated session** (the functions are `pikkuFunc`; unauthenticated calls return `403`). So `scaffold.console` alone is **no longer the minimum** — you also need an auth strategy, and Better Auth is the supported one. `pikku all` **throws** if `scaffold.console` is set but no `pikkuBetterAuth(...)` is found in the project. Baseline is "must be logged in"; finer policy (admin-only, org scoping) is layered host-side via tag/HTTP middleware. See `pikku-meta` for the console's Security screen.
|
|
65
38
|
|
|
66
39
|
---
|
|
67
40
|
|
|
@@ -474,9 +447,14 @@ someone" means **a particular kind of user** rather than one fixed admin.
|
|
|
474
447
|
Register it explicitly — it is not automatic:
|
|
475
448
|
|
|
476
449
|
```typescript
|
|
477
|
-
import { pikkuActor } from '@pikku/better-auth'
|
|
478
|
-
|
|
479
|
-
plugins: [
|
|
450
|
+
import { ACTOR_SIGN_IN_OPT_IN_ENV, pikkuActor } from '@pikku/better-auth'
|
|
451
|
+
|
|
452
|
+
plugins: [
|
|
453
|
+
pikkuActor({
|
|
454
|
+
secret: SCENARIO_ACTOR_SECRET,
|
|
455
|
+
allowSignIn: await variables.get(ACTOR_SIGN_IN_OPT_IN_ENV),
|
|
456
|
+
}),
|
|
457
|
+
]
|
|
480
458
|
```
|
|
481
459
|
|
|
482
460
|
`POST ${basePath}/sign-in/actor` `{ email, secret, name? }` → 200 + the normal
|
|
@@ -507,9 +485,19 @@ itself instead.
|
|
|
507
485
|
A stage that genuinely must run scenarios opts in on purpose, with
|
|
508
486
|
`PIKKU_ALLOW_ACTOR_SIGN_IN=passwordless-actor-sign-in`. Any other value is
|
|
509
487
|
ignored and warned about, so the hatch cannot be opened by copying a `true` from
|
|
510
|
-
the line above
|
|
511
|
-
|
|
512
|
-
|
|
488
|
+
the line above.
|
|
489
|
+
|
|
490
|
+
**Pass it in on any runtime without a populated `process.env`.** The gate reads
|
|
491
|
+
the environment by default, which is enough for Node but not for a Worker: there
|
|
492
|
+
the opt-in arrives as a binding and reaches user code through the variables
|
|
493
|
+
service, so a gate left to `process.env` stays shut on exactly the stages a
|
|
494
|
+
deployment targets. `allowSignIn` takes the value the caller already read —
|
|
495
|
+
`await variables.get(ACTOR_SIGN_IN_OPT_IN_ENV)` — and is checked against the same
|
|
496
|
+
literal, near-miss warning included. It is deliberately a value and not a flag:
|
|
497
|
+
what opens the gate is still something the deployment set and an operator can
|
|
498
|
+
read back out of it, never something compiled into the bundle. A value passed
|
|
499
|
+
here is the one consulted, so the environment cannot quietly override what the
|
|
500
|
+
stage was configured with.
|
|
513
501
|
|
|
514
502
|
**Signing in and provisioning are separate powers.** An unknown address becomes
|
|
515
503
|
an `actor: true` row only under `pikku dev`. With the opt-in set, a stage signs
|
|
@@ -650,3 +638,107 @@ export const auth = pikkuBetterAuth(async ({ secrets, variables, kysely }) => be
|
|
|
650
638
|
- Export exactly ONE `pikkuBetterAuth` per project; the CLI generates a single catch-all worker for all auth routes.
|
|
651
639
|
- `betterAuthSession({ auth })` (generated) bridges the better-auth session into the Pikku session on every request — you never add it by hand.
|
|
652
640
|
- MFA, organizations, passkeys, etc. are better-auth plugins: add them to `betterAuth({ plugins: [...] })`. The catch-all route already forwards their endpoints.
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## Post-signup side effects
|
|
645
|
+
|
|
646
|
+
Anything that must happen after a user signs up — a welcome email, seeding a first
|
|
647
|
+
row, creating a personal organization — goes in `databaseHooks.user.create.after`
|
|
648
|
+
inside the `betterAuth({...})` config. Never a custom signup RPC that writes the
|
|
649
|
+
user itself: better-auth owns the `user` table, and a second write path desyncs it
|
|
650
|
+
from the session bridge.
|
|
651
|
+
|
|
652
|
+
## Two-factor (2FA / MFA)
|
|
653
|
+
|
|
654
|
+
TOTP authenticator apps, email/SMS OTP, backup codes and trusted devices are all
|
|
655
|
+
better-auth's `twoFactor()` plugin. Never hand-roll TOTP or OTP.
|
|
656
|
+
|
|
657
|
+
1. **Enable + migrate** — add `twoFactor({ issuer: '<AppName>' })` to the `plugins`
|
|
658
|
+
array in the `pikkuBetterAuth` factory, then generate its schema and apply it as a
|
|
659
|
+
migration like any other table change. Run the CLI at the version of better-auth the
|
|
660
|
+
project actually has installed (`npx @better-auth/cli@<that version> generate`) — a
|
|
661
|
+
bare `npx @better-auth/cli` resolves the latest release, which can emit a schema for
|
|
662
|
+
a library you are not running. `twoFactorSecret` lands on `user`.
|
|
663
|
+
2. **Client** — add `twoFactorClient({ onTwoFactorRedirect() { /* go to /2fa */ } })`
|
|
664
|
+
to `createAuthClient({ plugins: [...] })`, and expose thin wrappers
|
|
665
|
+
(`enable2FA`, `verifyTotp`, `disable2FA`, …) rather than leaking the raw
|
|
666
|
+
`authClient`, same as every other auth call.
|
|
667
|
+
3. **OTP delivery** — `otpOptions.sendOTP` goes through the injected email service
|
|
668
|
+
and a rendered template, not a raw `sendEmail`. Set `storeOTP: 'encrypted'`.
|
|
669
|
+
4. **Sign-in flow** — the challenge is raised on the three CREDENTIAL endpoints:
|
|
670
|
+
`signIn.email`, `signIn.username` and `signIn.phoneNumber`. Check
|
|
671
|
+
`context.data.twoFactorRedirect` in `onSuccess`; if true, route to a `/2fa` page
|
|
672
|
+
and verify via `verifyTotp`/`verifyOtp`/`verifyBackupCode` (`trustDevice: true`
|
|
673
|
+
for a 30-day trusted device). The response also carries `twoFactorMethods`
|
|
674
|
+
(`'totp'` only once that user has a verified secret, `'otp'` whenever
|
|
675
|
+
`otpOptions.sendOTP` is configured) — render the choice from it rather than
|
|
676
|
+
assuming TOTP. The session cookie is only created after verification: the
|
|
677
|
+
credential handler's session is deleted while the challenge is in flight, so a
|
|
678
|
+
hook reading `ctx.context.newSession` after sign-in must null-check it.
|
|
679
|
+
5. **UI** — a QR rendered from `data.totpURI` plus the `data.backupCodes` list.
|
|
680
|
+
Enabling, disabling and regenerating backup codes all require the user's
|
|
681
|
+
password.
|
|
682
|
+
|
|
683
|
+
TOTP secrets and backup codes are encrypted at rest with the auth secret, and
|
|
684
|
+
`/two-factor/*` is rate-limited (3/10s) out of the box.
|
|
685
|
+
|
|
686
|
+
**2FA gates credential sign-in only.** Magic link, email OTP and OAuth are not
|
|
687
|
+
matched by the plugin's hook, so a user with 2FA enabled who signs in through one
|
|
688
|
+
of them is NOT challenged. If every route into the app must be gated, either do not
|
|
689
|
+
offer the passwordless ones to 2FA users or add your own check — enabling the
|
|
690
|
+
plugin does not do it.
|
|
691
|
+
|
|
692
|
+
## Security hardening
|
|
693
|
+
|
|
694
|
+
The `pikkuBetterAuth` factory — where `betterAuth({...})` is built — is the one
|
|
695
|
+
place to harden. Everything below is a `betterAuth` option, not a pikku one.
|
|
696
|
+
|
|
697
|
+
- **Secret** — `BETTER_AUTH_SECRET` comes from the injected secrets service
|
|
698
|
+
(`await secrets.getSecret('BETTER_AUTH_SECRET')`), never `process.env`, a
|
|
699
|
+
literal, or a fallback default. 32+ chars, high entropy
|
|
700
|
+
(`openssl rand -base64 32`). Better Auth rejects placeholder secrets in
|
|
701
|
+
production.
|
|
702
|
+
- **Trusted origins** — the `baseURL` origin is auto-trusted, so a single-domain
|
|
703
|
+
app serving its API same-origin needs nothing. Add `trustedOrigins` (or a
|
|
704
|
+
comma-separated `BETTER_AUTH_TRUSTED_ORIGINS` variable; wildcards like
|
|
705
|
+
`*.example.com` allowed) ONLY when the browser origin differs from the API
|
|
706
|
+
origin — embedded, preview, or custom-domain deployments. An untrusted
|
|
707
|
+
`callbackURL`/`redirectTo`/`origin` is a 403.
|
|
708
|
+
- **CSRF** — keep it on (`advanced.disableCSRFCheck: false`, the default). A
|
|
709
|
+
proxy in front of the app must preserve the `/api/auth/*` prefix so origin
|
|
710
|
+
checks still work; do not disable the check to "fix" a redirect.
|
|
711
|
+
- **Rate limiting** — on by default in production (100/10s global, 3/10s on
|
|
712
|
+
sign-in/up/change-password). `storage: 'memory'` resets on restart, so a
|
|
713
|
+
deployed app wants `storage: 'database'`. Tighten sensitive routes with
|
|
714
|
+
`customRules`, e.g. `'/sign-in/email': { window: 60, max: 5 }` — the key is matched
|
|
715
|
+
against the path with the base path ALREADY STRIPPED, so a rule written as
|
|
716
|
+
`'/api/auth/sign-in/email'` matches nothing and silently leaves the route on the
|
|
717
|
+
default.
|
|
718
|
+
- **Cookies & sessions** — `httpOnly`, `sameSite: 'lax'` and `path: '/'` are
|
|
719
|
+
unconditional, but `secure` and the `__Secure-` name prefix are NOT: they follow a
|
|
720
|
+
`baseURL` on `https://` (or production, or an explicit
|
|
721
|
+
`advanced.useSecureCookies: true`). A deployment whose TLS terminates at a proxy
|
|
722
|
+
and passes an `http://` baseURL through therefore ships session cookies with no
|
|
723
|
+
`secure` flag — set `useSecureCookies` there rather than assuming. Defaults are
|
|
724
|
+
`session.expiresIn` 7d and
|
|
725
|
+
`updateAge` 1d. Add `freshAge` for sensitive actions, and
|
|
726
|
+
`cookieCache: { strategy: 'jwe' }` if the session carries sensitive data — see
|
|
727
|
+
the cookieCache section above, which you want enabled regardless. Only enable
|
|
728
|
+
`crossSubDomainCookies` if auth is genuinely shared across subdomains.
|
|
729
|
+
- **OAuth tokens** — set `account.encryptOAuthTokens: true` (AES-256-GCM) if you
|
|
730
|
+
store provider tokens to call their APIs later.
|
|
731
|
+
- **Audit** — drive auth events from `databaseHooks` (`session.create.after`,
|
|
732
|
+
`user.update.after` for email changes, `account.create.after` for links) into
|
|
733
|
+
whatever audit service the app injects, never a bespoke audit table wired into
|
|
734
|
+
a function body. Returning `false` from a `before` hook blocks the operation.
|
|
735
|
+
- **Background tasks** — on a serverless target, hand genuinely disposable work
|
|
736
|
+
(analytics, logging) to `advanced.backgroundTasks.handler` → `ctx.waitUntil(promise)`
|
|
737
|
+
so it does not delay the response. Not mail: the default handler is
|
|
738
|
+
`p.catch(() => {})`, so anything that must actually arrive — an invitation, a
|
|
739
|
+
password reset — is lost without a trace if the platform reaps the request first.
|
|
740
|
+
Better Auth sends its own through `runInBackgroundOrAwait`, which awaits when no
|
|
741
|
+
handler is configured; do the same for yours.
|
|
742
|
+
- **Enumeration** — handled already (generic "Invalid credentials", dummy work on
|
|
743
|
+
unknown users). Keep your own error copy generic too; never leak "user not
|
|
744
|
+
found".
|
|
@@ -1,27 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-jose
|
|
3
|
-
description: >-
|
|
4
|
-
Use when setting up JWT authentication with the jose library in a Pikku app. Covers
|
|
5
|
-
JoseJWTService constructor, secret rotation, token encoding/decoding/verification. TRIGGER when:
|
|
6
|
-
code uses JoseJWTService, user asks about JWT setup, token signing, token verification, or
|
|
7
|
-
@pikku/jose. DO NOT TRIGGER when: user asks about session middleware (use pikku-security) or
|
|
8
|
-
general service setup (use pikku-services).
|
|
9
|
-
installGroups: [fabric]
|
|
10
|
-
---
|
|
11
|
-
|
|
12
1
|
# Pikku Jose (JWT Service)
|
|
13
2
|
|
|
14
|
-
## Agent Operating Procedure
|
|
15
|
-
|
|
16
|
-
Use this skill as an execution checklist, not reference material.
|
|
17
|
-
|
|
18
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
19
|
-
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
|
|
20
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
21
|
-
4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
|
|
22
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
23
|
-
|
|
24
|
-
`@pikku/jose` provides JWT signing, verification, and decoding using the [jose](https://github.com/panva/jose) library. Implements the `JWTService` interface from `@pikku/core`.
|
|
25
3
|
|
|
26
4
|
## Installation
|
|
27
5
|
|
|
@@ -86,7 +64,7 @@ await jwt.init()
|
|
|
86
64
|
A signing key is a secret, so it comes from the secrets service rather than
|
|
87
65
|
`process.env` — and because `getSecrets` is a function called on demand, reading
|
|
88
66
|
it there (not once at construction) is what makes the re-init-on-unknown-kid path
|
|
89
|
-
above actually see a rotated key. See `pikku-
|
|
67
|
+
above actually see a rotated key. See `pikku-services`.
|
|
90
68
|
|
|
91
69
|
### Secret Rotation
|
|
92
70
|
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-machine-auth
|
|
3
|
-
description: >-
|
|
4
|
-
Use when authenticating a CLI/agent/service against a Pikku server, adding machine-to-machine
|
|
5
|
-
(M2M) auth, issuing scoped API keys for sandboxes/agents/workers, or wiring better-auth sessions
|
|
6
|
-
into Pikku middleware. Covers `pikku login` (device-authorization), the better-auth API Key
|
|
7
|
-
plugin, machine identities, and `betterAuthSession` with the api-key branch. TRIGGER when: user
|
|
8
|
-
asks about CLI login, `pikku login`, machine agents, service-to-service auth, API keys, client
|
|
9
|
-
credentials, sandbox/worker tokens, or resolving a better-auth session in a Pikku function. DO
|
|
10
|
-
NOT TRIGGER when: user asks about end-user HTTP session/cookie auth only (use pikku-http + the
|
|
11
|
-
app betterAuth config) or about WebSocket channel mechanics (use pikku-websocket).
|
|
12
|
-
installGroups: [fabric]
|
|
13
|
-
---
|
|
14
|
-
|
|
15
1
|
# Pikku Machine Auth
|
|
16
2
|
|
|
17
3
|
Unified authentication for humans **and** machines against a Pikku + better-auth
|
|
@@ -29,15 +15,6 @@ Both resolve to a Pikku `UserSession` through one middleware:
|
|
|
29
15
|
> better-auth's oidc-provider. The API Key plugin gives the same capability (a
|
|
30
16
|
> baked secret a service presents for scoped access), not the wire protocol.
|
|
31
17
|
|
|
32
|
-
## Agent Operating Procedure
|
|
33
|
-
|
|
34
|
-
1. Discover before editing — inspect the app's `betterAuth({ plugins: [...] })`
|
|
35
|
-
config and existing middleware wiring before adding anything.
|
|
36
|
-
2. Server changes go in the auth factory + a middleware wiring file; never put
|
|
37
|
-
auth checks in a function body (use `permissions`).
|
|
38
|
-
3. The API Key plugin contributes an `apikey` table — add the matching SQL
|
|
39
|
-
migration and regenerate DB types before relying on it.
|
|
40
|
-
4. Validate with the narrowest command, then `pikku all`.
|
|
41
18
|
|
|
42
19
|
## Human path — `pikku login`
|
|
43
20
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Pikku Permissions
|
|
2
|
+
|
|
3
|
+
## ⛔ FIRST: is the caller a machine with a token? ⛔
|
|
4
|
+
|
|
5
|
+
**Then this is NOT a permissions problem.** Resolve the token in `addHTTPMiddleware('*')` middleware that calls `setSession`, make the function a `pikkuFunc`, and gate it with `scopes`. A `permissions` check that verifies a bearer token and returns `true` is authentication wearing an authorization hat — and it leaves the function sessionless, so every body still has to work out who called it. See `references/machine-auth.md`. The only exception is a bootstrap endpoint whose caller has no identity yet (a shared-secret registration, a login): that one is sessionless and declares its gate here.
|
|
6
|
+
|
|
7
|
+
## The Rule
|
|
8
|
+
|
|
9
|
+
**ALWAYS put authorization checks in the `permissions` field of `pikkuFunc` or `pikkuSessionlessFunc` — NEVER inside the `func` body.**
|
|
10
|
+
|
|
11
|
+
This includes: org access checks, repo access checks, role checks, resource ownership, and any other authorization logic. The `permissions` field runs before `func` and is visible to the inspector, so the gate is declared rather than buried — which is what lets `pikku info permissions` and an audit see it at all. Alongside it sits `scopes` (see below) for grant-based gating; between them they are where Pikku enforces authorization. The one sanctioned exception is `permissionsInBody`, covered at the end.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// CORRECT
|
|
15
|
+
export const deleteBook = pikkuFunc({
|
|
16
|
+
func: async ({ db }, { bookId }) => {
|
|
17
|
+
await db.deleteBook(bookId)
|
|
18
|
+
},
|
|
19
|
+
permissions: {
|
|
20
|
+
owner: isBookOwner, // ← authorization here
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// WRONG — permission check inside func body
|
|
25
|
+
export const deleteBook = pikkuFunc({
|
|
26
|
+
func: async ({ db }, { bookId }, { session }) => {
|
|
27
|
+
if (!session) throw new UnauthorizedError() // ← never do this
|
|
28
|
+
await db.deleteBook(bookId)
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Permission Factories
|
|
35
|
+
|
|
36
|
+
### `pikkuAuth(fn)` — Session-Only Checks
|
|
37
|
+
|
|
38
|
+
Use for checks that read the session but need no request data — and that assert
|
|
39
|
+
something **beyond** merely having a session (a flag, a tier, a claim).
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { pikkuAuth } from '#pikku/auth'
|
|
43
|
+
|
|
44
|
+
// Good: a real gate on the session's contents, not just its existence.
|
|
45
|
+
export const isVerified = pikkuAuth(
|
|
46
|
+
async (_services, session) => !!session?.emailVerified
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Do NOT write an "is signed in" permission.** A checker that just returns
|
|
51
|
+
`!!session` is not authorization — it re-checks authentication, which the
|
|
52
|
+
function already enforces. A function that needs a signed-in user sets
|
|
53
|
+
`auth: true` (the default for `pikkuFunc`); it does not also carry a
|
|
54
|
+
`permissions: { signedIn }`.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// WRONG — redundant with auth: true; adds a permission that gates nothing.
|
|
58
|
+
export const isSignedIn = pikkuAuth(async (_s, session) => !!session)
|
|
59
|
+
pikkuFunc({ auth: true, permissions: { signedIn: isSignedIn } /* ... */ })
|
|
60
|
+
|
|
61
|
+
// RIGHT — auth: true already requires the session; permissions are for capability.
|
|
62
|
+
pikkuFunc({ auth: true /* ... */ })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A permission answers "_may this user do this?_" (role, ownership, tier) — never
|
|
66
|
+
"_is there a session?_".
|
|
67
|
+
|
|
68
|
+
### `pikkuPermission(fn)` — Data-Aware Checks
|
|
69
|
+
|
|
70
|
+
Use when authorization depends on the actual request data (e.g., resource ownership).
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { pikkuPermission } from '#pikku/auth'
|
|
74
|
+
|
|
75
|
+
export const isBookOwner = pikkuPermission(
|
|
76
|
+
async ({ db }, { bookId }, { session }) => {
|
|
77
|
+
const book = await db.getBook(bookId)
|
|
78
|
+
return book?.authorId === session?.userId
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
export const hasBookAccess = pikkuPermission(
|
|
83
|
+
async ({ db }, { bookId }, { session }) => {
|
|
84
|
+
return await db.hasAccess(session?.userId, bookId)
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## OR / AND Logic
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
permissions: {
|
|
93
|
+
verified: isVerified, // OR: verified users can access
|
|
94
|
+
owner: isBookOwner, // OR: owners can access
|
|
95
|
+
reviewer: [isVerified, hasBookAccess], // AND: both must pass
|
|
96
|
+
}
|
|
97
|
+
// Logic: verified OR owner OR (isVerified AND hasBookAccess)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Groups are OR'd. Entries within a group array are AND'd.
|
|
101
|
+
|
|
102
|
+
## Where to Apply Permissions
|
|
103
|
+
|
|
104
|
+
### Per-Function (preferred)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
export const deleteBook = pikkuFunc({
|
|
108
|
+
func: async ({ db }, { bookId }) => {
|
|
109
|
+
await db.deleteBook(bookId)
|
|
110
|
+
},
|
|
111
|
+
permissions: {
|
|
112
|
+
verified: isVerified,
|
|
113
|
+
owner: isBookOwner,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Global (`addGlobalPermission`) — App-Wide AND Gate
|
|
119
|
+
|
|
120
|
+
A global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever _narrow_ access — it never grants access a function's own `permissions` would deny.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { addGlobalPermission } from '#pikku/auth'
|
|
124
|
+
|
|
125
|
+
addGlobalPermission([isEmployee]) // every function now also requires an employee session
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Multiple `addGlobalPermission` calls accumulate and are AND'd together.
|
|
129
|
+
|
|
130
|
+
> Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP _middleware_ (`addTagMiddleware`, `addHTTPMiddleware`) for cross-cutting request handling, not authorization.
|
|
131
|
+
|
|
132
|
+
## Scopes — the AND Gate Above Permissions
|
|
133
|
+
|
|
134
|
+
Scopes answer "what was this session granted?" before permissions ask "may this
|
|
135
|
+
user do this to this resource?". They are AND-ed: every scope listed must be
|
|
136
|
+
held. Because they are checked first and fail closed, a scope can only ever
|
|
137
|
+
_narrow_ access — it never grants what `permissions` would deny.
|
|
138
|
+
|
|
139
|
+
Declare the scope tree once with `defineScope`. The body is a no-op that
|
|
140
|
+
tree-shakes away; the CLI reads the call by AST and generates a `ScopeId` union,
|
|
141
|
+
so a function naming an undeclared scope fails the build rather than silently
|
|
142
|
+
gating on nothing.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// src/scopes.ts
|
|
146
|
+
import { defineScope } from '#pikku/scopes'
|
|
147
|
+
|
|
148
|
+
defineScope({
|
|
149
|
+
admin: {
|
|
150
|
+
displayName: 'Administration',
|
|
151
|
+
description: 'Administrative access',
|
|
152
|
+
scopes: {
|
|
153
|
+
invoices: {
|
|
154
|
+
description: 'Invoice management',
|
|
155
|
+
scopes: {
|
|
156
|
+
create: { description: 'Create invoices' },
|
|
157
|
+
void: { description: 'Void invoices' },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
billing: {},
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Every node is grantable, keyed by segment: the above yields `admin`,
|
|
167
|
+
`admin:invoices`, `admin:invoices:create`, `admin:invoices:void` and `billing`.
|
|
168
|
+
Scopes may be declared across more than one file — the declarations merge.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
export const voidInvoice = pikkuFunc({
|
|
172
|
+
scopes: ['admin:invoices:void'],
|
|
173
|
+
permissions: { owner: isInvoiceOwner },
|
|
174
|
+
func: async ({ db }, { invoiceId }) => { ... },
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
A grant satisfies a required scope if it is the scope itself, an ancestor of it,
|
|
179
|
+
or a wildcard at any level — so a session holding `admin` satisfies
|
|
180
|
+
`admin:invoices:void`, and `admin:*` does too. A missing scope throws
|
|
181
|
+
`MissingScopeError` naming the first one that failed.
|
|
182
|
+
|
|
183
|
+
`scopes` requires a session and so is unavailable on `pikkuSessionlessFunc`:
|
|
184
|
+
scopes fail closed, an anonymous caller holds none, and a sessionless function
|
|
185
|
+
with scopes would reject every caller it exists to serve. Gate those with
|
|
186
|
+
`permissions`, which receive the optional session and may pass anonymous.
|
|
187
|
+
|
|
188
|
+
## The Three Gates
|
|
189
|
+
|
|
190
|
+
Authorization is three independent gates, evaluated in this order, all of which must pass:
|
|
191
|
+
|
|
192
|
+
1. **Scopes** (`scopes`) — AND'd, checked before input validation. Fails closed.
|
|
193
|
+
2. **Global permissions** (`addGlobalPermission`) — AND'd together. A broad baseline that can only narrow access.
|
|
194
|
+
3. **The function's own `permissions`** — OR'd groups (OR-of-ANDs), as above.
|
|
195
|
+
|
|
196
|
+
The gates are independent: a broad global (e.g. `isEmployee`) can **never** satisfy an admin-only function's own requirement. Each function still enforces its own `scopes` and `permissions` in full.
|
|
197
|
+
|
|
198
|
+
## The Sanctioned Exception: `permissionsInBody`
|
|
199
|
+
|
|
200
|
+
A few checks genuinely cannot be expressed as a permission — verifying a webhook
|
|
201
|
+
signature, a signed token, or an invite code, where the "identity" arrives in the
|
|
202
|
+
payload and there is no session to check. For those, declare
|
|
203
|
+
`permissionsInBody: true` on the function and keep the check in the body.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
export const handleStripeWebhook = pikkuSessionlessFunc({
|
|
207
|
+
permissionsInBody: true,
|
|
208
|
+
auth: false,
|
|
209
|
+
func: async ({ stripe }, data, { http }) => {
|
|
210
|
+
stripe.webhooks.constructEvent(
|
|
211
|
+
data.raw,
|
|
212
|
+
http.request.header('stripe-signature'),
|
|
213
|
+
secret
|
|
214
|
+
)
|
|
215
|
+
// ...
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This is a last resort, and it is purely declarative — it grants nothing and
|
|
221
|
+
enforces nothing. Its only job is to tell the auditor that this function's
|
|
222
|
+
apparent openness is deliberate, so asserting it falsely disables the very check
|
|
223
|
+
that would have caught the mistake. It requires `"allow": { "permissionsInBody": true }`
|
|
224
|
+
in `pikku.config.json`, which keeps the decision visible at the project level.
|
|
225
|
+
Prefer `permissions` whenever the check can be expressed as one — they are
|
|
226
|
+
declared, inspectable, and reusable.
|
|
227
|
+
|
|
228
|
+
## Complete Example
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// src/permissions.ts
|
|
232
|
+
import { pikkuAuth, pikkuPermission } from '#pikku/auth'
|
|
233
|
+
|
|
234
|
+
export const isVerified = pikkuAuth(
|
|
235
|
+
async (_services, session) => !!session?.emailVerified
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
export const isOrgMember = pikkuPermission(
|
|
239
|
+
async ({ db }, { orgId }, { session }) => {
|
|
240
|
+
return await db.isMember(session?.userId, orgId)
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// src/functions/org.function.ts
|
|
245
|
+
export const deleteOrg = pikkuFunc({
|
|
246
|
+
func: async ({ db }, { orgId }) => {
|
|
247
|
+
await db.deleteOrg(orgId)
|
|
248
|
+
},
|
|
249
|
+
permissions: {
|
|
250
|
+
verified: isVerified,
|
|
251
|
+
owner: [isVerified, isOrgMember],
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## After Changes
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
pikku all # regenerate if wirings changed
|
|
260
|
+
pikku all --tsc # regenerate, then verify permission checker types (fails on type errors)
|
|
261
|
+
```
|
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-security
|
|
3
|
-
description: >-
|
|
4
|
-
Use when adding authentication or session management to a Pikku app — pikkuAuth, session
|
|
5
|
-
lifecycle (setSession/clearSession), built-in auth strategies (authBearer, authCookie,
|
|
6
|
-
authAPIKey), or JWT setup. TRIGGER when: user asks about login, logout, session, bearer tokens,
|
|
7
|
-
cookie auth, API keys, or JWT. DO NOT TRIGGER when: user asks about middleware (use
|
|
8
|
-
pikku-middleware), permissions/authorization checks (use pikku-permissions), or secrets/env vars
|
|
9
|
-
(use pikku-config).
|
|
10
|
-
installGroups: [core]
|
|
11
|
-
---
|
|
12
|
-
|
|
13
1
|
# Pikku Security (Authentication & Sessions)
|
|
14
2
|
|
|
15
|
-
## Agent Operating Procedure
|
|
16
|
-
|
|
17
|
-
1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info functions --verbose` to understand existing auth setup.
|
|
18
|
-
2. Auth strategies live in wirings files — do not put `addHTTPMiddleware` calls inside function bodies.
|
|
19
|
-
3. Validate with `pikku all --tsc` after changes — it regenerates and then type-checks in one pass, and fails on type errors. Use `--tsc-summary` for a compact one-line-per-error report.
|
|
20
|
-
|
|
21
|
-
For **middleware** (including tag middleware and service-to-service bearer auth) see `pikku-middleware`.
|
|
22
|
-
For **permissions** (pikkuPermission, pikkuAuth, per-function authorization) see `pikku-permissions`.
|
|
23
3
|
|
|
24
4
|
## Session Management
|
|
25
5
|
|