@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-a11y/SKILL.md +59 -0
  5. package/skills/pikku-addon/SKILL.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +67 -316
  7. package/skills/pikku-agent/references/agents.md +299 -0
  8. package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
  9. package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
  10. package/skills/pikku-architect/SKILL.md +265 -0
  11. package/skills/pikku-auth/SKILL.md +89 -0
  12. package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +126 -34
  13. package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
  14. package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
  15. package/skills/pikku-auth/references/permissions.md +261 -0
  16. package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
  17. package/skills/pikku-build/SKILL.md +88 -0
  18. package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +76 -24
  19. package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
  20. package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +56 -1
  21. package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
  22. package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
  23. package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
  24. package/skills/{pikku-build-app → pikku-build}/references/ship.md +9 -3
  25. package/skills/pikku-concepts/SKILL.md +72 -7
  26. package/skills/pikku-concepts/references/concept-mapping.md +8 -8
  27. package/skills/pikku-deploy/SKILL.md +158 -0
  28. package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
  29. package/skills/pikku-deploy/references/cloudflare.md +104 -0
  30. package/skills/pikku-deploy/references/express.md +92 -0
  31. package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
  32. package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
  33. package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
  34. package/skills/pikku-deploy/references/uws.md +72 -0
  35. package/skills/pikku-deploy/references/ws.md +75 -0
  36. package/skills/pikku-emails/SKILL.md +3 -2
  37. package/skills/pikku-fabric/SKILL.md +47 -20
  38. package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
  39. package/skills/pikku-i18n/SKILL.md +62 -207
  40. package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
  41. package/skills/pikku-i18n/references/messages.md +218 -0
  42. package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
  43. package/skills/pikku-knowledge/SKILL.md +15 -0
  44. package/skills/pikku-kysely/SKILL.md +13 -13
  45. package/skills/pikku-list-query/SKILL.md +163 -0
  46. package/skills/pikku-meta/SKILL.md +58 -130
  47. package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
  48. package/skills/pikku-meta/references/meta.md +114 -0
  49. package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
  50. package/skills/pikku-middleware/SKILL.md +5 -5
  51. package/skills/pikku-n8n-import/SKILL.md +0 -1
  52. package/skills/pikku-permissions/SKILL.md +75 -229
  53. package/skills/pikku-react/SKILL.md +50 -298
  54. package/skills/pikku-react/references/client.md +313 -0
  55. package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
  56. package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
  57. package/skills/pikku-realtime/SKILL.md +110 -251
  58. package/skills/pikku-scenario/SKILL.md +60 -45
  59. package/skills/pikku-scenario/references/persona-run.md +148 -0
  60. package/skills/pikku-seo/SKILL.md +133 -0
  61. package/skills/pikku-service-backends/SKILL.md +154 -0
  62. package/skills/pikku-service-backends/references/aws.md +106 -0
  63. package/skills/pikku-service-backends/references/backblaze.md +57 -0
  64. package/skills/pikku-service-backends/references/mongodb.md +90 -0
  65. package/skills/pikku-service-backends/references/redis.md +75 -0
  66. package/skills/pikku-service-backends/references/schema.md +63 -0
  67. package/skills/pikku-services/SKILL.md +68 -291
  68. package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
  69. package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
  70. package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
  71. package/skills/pikku-services/references/services.md +272 -0
  72. package/skills/pikku-software-archaeology/README.md +5 -1
  73. package/skills/pikku-software-archaeology/SKILL.md +16 -2
  74. package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
  75. package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
  76. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  77. package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
  78. package/skills/pikku-webhook/SKILL.md +224 -0
  79. package/skills/pikku-wiring/SKILL.md +180 -0
  80. package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
  81. package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
  82. package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
  83. package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
  84. package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
  85. package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
  86. package/skills/pikku-wiring/references/realtime.md +265 -0
  87. package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
  88. package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
  89. package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
  90. package/skills/pikku-workflow/SKILL.md +39 -2
  91. package/skills/pikku-aws/SKILL.md +0 -161
  92. package/skills/pikku-backblaze/SKILL.md +0 -104
  93. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
  94. package/skills/pikku-deploy-express/SKILL.md +0 -122
  95. package/skills/pikku-deploy-uws/SKILL.md +0 -144
  96. package/skills/pikku-mongodb/SKILL.md +0 -113
  97. package/skills/pikku-product-second-opinion/README.md +0 -43
  98. package/skills/pikku-redis/SKILL.md +0 -99
  99. package/skills/pikku-schema-ajv/SKILL.md +0 -83
  100. package/skills/pikku-schema-cfworker/SKILL.md +0 -82
  101. package/skills/pikku-ws/SKILL.md +0 -87
  102. /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
  103. /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
  104. /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
  105. /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
  106. /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-deps` for the console's Security screen.
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: [pikkuActor({ secret: SCENARIO_ACTOR_SECRET })]
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, and it is the only hatch — there is no build-time option, because
511
- an option compiled into the bundle cannot be audited from the environment it
512
- runs in.
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-config`.
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