@pikku/skills 0.12.52 → 0.12.54
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 +2 -2
- package/package.json +1 -1
- package/skills/pikku-a11y/SKILL.md +22 -15
- package/skills/pikku-addon/SKILL.md +5 -5
- package/skills/pikku-addon/references/addon-package-manifest.md +7 -0
- package/skills/pikku-addon/references/openapi.md +11 -0
- package/skills/pikku-admin-to-fabric/SKILL.md +7 -21
- package/skills/pikku-agent/SKILL.md +6 -6
- package/skills/pikku-agent/references/agents.md +8 -1
- package/skills/pikku-agent/references/runner-vercel.md +2 -2
- package/skills/pikku-agent/references/voice.md +7 -1
- package/skills/pikku-architect/SKILL.md +1 -1
- package/skills/pikku-architect/references/plan-defects.md +2 -0
- package/skills/pikku-auth/SKILL.md +15 -36
- package/skills/pikku-auth/references/better-auth.md +22 -45
- package/skills/pikku-auth/references/jose.md +6 -0
- package/skills/pikku-auth/references/machine-auth.md +6 -0
- package/skills/pikku-auth/references/sessions.md +6 -0
- package/skills/pikku-blueprint-to-fabric/SKILL.md +6 -21
- package/skills/pikku-blueprint-to-fabric/scripts/inventory.mjs +1 -1
- package/skills/pikku-build/SKILL.md +29 -26
- package/skills/pikku-build/references/app.md +60 -41
- package/skills/pikku-build/references/design.md +31 -19
- package/skills/pikku-build/references/feature.md +14 -0
- package/skills/pikku-build/references/mocks.md +23 -0
- package/skills/pikku-build/references/multi-app.md +9 -2
- package/skills/pikku-build/references/native-app.md +9 -0
- package/skills/pikku-build/references/openapi.md +7 -0
- package/skills/pikku-build/references/platform.md +20 -10
- package/skills/pikku-build/references/quick.md +14 -4
- package/skills/pikku-build/references/scenarios.md +12 -1
- package/skills/pikku-build/references/ship.md +6 -1
- package/skills/pikku-build/references/theming.md +74 -125
- package/skills/pikku-changes/SKILL.md +6 -6
- package/skills/pikku-concepts/SKILL.md +8 -17
- package/skills/pikku-concepts/references/concept-mapping.md +18 -0
- package/skills/pikku-convert-screen/SKILL.md +45 -0
- package/skills/pikku-deploy/references/cloudflare.md +8 -0
- package/skills/pikku-deploy/references/lambda.md +8 -0
- package/skills/pikku-deploy/references/uws.md +1 -1
- package/skills/pikku-deploy/references/ws.md +3 -3
- package/skills/pikku-emails/SKILL.md +3 -9
- package/skills/pikku-fabric/SKILL.md +7 -295
- package/skills/pikku-fabric/references/database.md +128 -0
- package/skills/pikku-fabric/references/debugging.md +9 -5
- package/skills/pikku-fabric/references/deploy.md +179 -0
- package/skills/pikku-guide/SKILL.md +10 -10
- package/skills/pikku-i18n/SKILL.md +7 -7
- package/skills/pikku-i18n/references/enum-labels.md +9 -0
- package/skills/pikku-i18n/references/messages.md +16 -3
- package/skills/pikku-i18n/references/rtl.md +18 -38
- package/skills/pikku-knowledge/SKILL.md +9 -10
- package/skills/pikku-kysely/SKILL.md +3 -21
- package/skills/pikku-list-query/SKILL.md +2 -2
- package/skills/pikku-meta/references/audit.md +9 -0
- package/skills/pikku-meta/references/meta.md +6 -0
- package/skills/pikku-meta/references/versioning.md +10 -0
- package/skills/pikku-middleware/SKILL.md +7 -16
- package/skills/pikku-n8n-import/SKILL.md +9 -1
- package/skills/pikku-n8n-import/references/addon-mapping.md +6 -0
- package/skills/pikku-n8n-import/references/code-translation.md +7 -0
- package/skills/pikku-permissions/SKILL.md +41 -10
- package/skills/pikku-permissions/references/scopes.md +59 -0
- package/skills/pikku-react/SKILL.md +2 -2
- package/skills/pikku-react/references/client.md +1 -34
- package/skills/pikku-react/references/react-query.md +11 -0
- package/skills/pikku-react/references/workflows.md +11 -0
- package/skills/pikku-realtime/SKILL.md +9 -3
- package/skills/pikku-report/SKILL.md +10 -9
- package/skills/pikku-scenario/SKILL.md +11 -17
- package/skills/pikku-scenario/references/persona-run.md +11 -1
- package/skills/pikku-scenario/references/personas.md +7 -1
- package/skills/pikku-seo/SKILL.md +12 -16
- package/skills/pikku-service-backends/SKILL.md +0 -10
- package/skills/pikku-service-backends/references/aws.md +6 -0
- package/skills/pikku-service-backends/references/mongodb.md +7 -0
- package/skills/pikku-service-backends/references/redis.md +6 -0
- package/skills/pikku-services/references/audit.md +10 -0
- package/skills/pikku-services/references/config.md +6 -0
- package/skills/pikku-services/references/services.md +8 -1
- package/skills/pikku-software-archaeology/SKILL.md +9 -15
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +7 -7
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
- package/skills/pikku-software-archaeology/references/second-opinion.md +13 -1
- package/skills/pikku-tailwind/SKILL.md +111 -0
- package/skills/pikku-theme/SKILL.md +187 -0
- package/skills/pikku-theme/references/from-a-reference.md +44 -0
- package/skills/pikku-theme/references/presets.md +40 -0
- package/skills/pikku-ui/SKILL.md +41 -0
- package/skills/pikku-wiring/SKILL.md +13 -12
- package/skills/pikku-wiring/references/channel.md +7 -1
- package/skills/pikku-wiring/references/cli.md +7 -1
- package/skills/pikku-wiring/references/gateway-slack.md +6 -0
- package/skills/pikku-wiring/references/http.md +10 -3
- package/skills/pikku-wiring/references/mcp.md +8 -0
- package/skills/pikku-wiring/references/queue.md +6 -0
- package/skills/pikku-wiring/references/realtime.md +34 -4
- package/skills/pikku-wiring/references/rpc.md +6 -0
- package/skills/pikku-wiring/references/scheduler.md +6 -0
- package/skills/pikku-wiring/references/trigger.md +8 -0
- package/skills/pikku-workflow/SKILL.md +8 -6
- package/skills/pikku-auth/references/permissions.md +0 -261
- package/skills/pikku-mantine/SKILL.md +0 -80
- package/skills/pikku-wiring/references/realtime-other-routes.md +0 -23
|
@@ -3,14 +3,15 @@ name: pikku-wiring
|
|
|
3
3
|
description: >-
|
|
4
4
|
Use when exposing a Pikku function over a transport — HTTP routes and SSE, WebSocket channels,
|
|
5
5
|
typed realtime pub/sub, internal and exposed RPC, queue workers, cron schedules, event triggers,
|
|
6
|
-
MCP tools/resources/prompts, CLI commands, or a Slack gateway. Covers choosing the wiring,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
MCP tools/resources/prompts, CLI commands, or a Slack gateway. Covers choosing the wiring, what
|
|
7
|
+
every wiring shares, and what differs: function type, session source, and which calls throw
|
|
8
|
+
instead of returning. TRIGGER when: code uses wireHTTP, defineHTTPRoutes, wireChannel,
|
|
9
|
+
wireQueueWorker, wireScheduler, wireTrigger, wireTriggerSource, wireCLI, wireMCPResource,
|
|
10
|
+
wireMCPPrompt, `mcp: true`, `sse: true`, `expose: true`, rpc.invoke, SlackGatewayAdapter, or the
|
|
11
|
+
user asks how to expose, route, schedule, queue, stream or publish a function. DO NOT TRIGGER
|
|
12
|
+
when: writing the function body (use pikku-concepts), authorization (use pikku-permissions), serving on
|
|
13
|
+
a runtime (use pikku-deploy), building a live-updating view (use pikku-realtime), or sending
|
|
14
|
+
outgoing webhooks (use pikku-webhook).
|
|
14
15
|
installGroups: [core]
|
|
15
16
|
---
|
|
16
17
|
|
|
@@ -53,7 +54,7 @@ Three consequences that hold for every wiring below:
|
|
|
53
54
|
are merged into the function's `data` argument.
|
|
54
55
|
- **Permissions are never declared on the wiring.** Wire-level permissions were
|
|
55
56
|
removed in #972 — declare them on the function (`pikkuFunc({ permissions })`,
|
|
56
|
-
see `pikku-
|
|
57
|
+
see `pikku-permissions`) or app-wide via `addGlobalPermission`. Tags and
|
|
57
58
|
patterns now target *middleware* only.
|
|
58
59
|
- **The wire is the third argument**, not a service. `channel`, `rpc`, `session`,
|
|
59
60
|
`setSession`, `mcp`, `queue`, `scheduledTask` and `cli` all live there.
|
|
@@ -83,15 +84,15 @@ subpath it exempts — the service implementations bootstrap picks.
|
|
|
83
84
|
|
|
84
85
|
| Reach for | When | Reference |
|
|
85
86
|
| --- | --- | --- |
|
|
86
|
-
| **HTTP** | REST endpoints, web APIs, and SSE streams (`sse: true`, `get` only) | `references/http.md` |
|
|
87
|
+
| **HTTP** | REST endpoints, web APIs, and SSE streams (`sse: true`, `get` only) | `references/http.md`; every field in `references/http-options.md` |
|
|
87
88
|
| **Channel** | A hand-designed WebSocket protocol with your own action routing | `references/channel.md` |
|
|
88
|
-
| **Realtime** | Typed pub/sub push — the scaffolded `/events` channel and SSE topics | `references/realtime.md` |
|
|
89
|
+
| **Realtime** | Typed pub/sub push — the scaffolded `/events` channel and SSE topics, plus subscribing to any other `sse: true` route or channel from the client | `references/realtime.md` |
|
|
89
90
|
| **RPC** | One function calling another, or dispatching a name from outside | `references/rpc.md` |
|
|
90
91
|
| **Queue** | Reliable background work that must survive a crash and retry | `references/queue.md` |
|
|
91
92
|
| **Scheduler** | Recurring work on a cron expression | `references/scheduler.md` |
|
|
92
93
|
| **Trigger** | Reacting in-process to an external event source (Redis pub/sub, PG LISTEN) | `references/trigger.md` |
|
|
93
94
|
| **MCP** | Exposing functions to an AI assistant as tools, resources or prompts | `references/mcp.md` |
|
|
94
|
-
| **CLI** | A terminal program with commands, subcommands and options | `references/cli.md` |
|
|
95
|
+
| **CLI** | A terminal program with commands, subcommands and options | `references/cli.md`; end-to-end walkthrough in `references/cli-complete-example.md` |
|
|
95
96
|
| **Gateway** | An inbound integration from a third-party product — Slack is the shipped adapter | `references/gateway-slack.md` |
|
|
96
97
|
|
|
97
98
|
Realtime and Channel are the pair most often confused. If the shape is "server
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Pikku WebSocket Wiring
|
|
2
2
|
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- API Reference
|
|
6
|
+
- Usage Patterns
|
|
7
|
+
- Complete Example
|
|
8
|
+
|
|
3
9
|
## API Reference
|
|
4
10
|
|
|
5
11
|
### `wireChannel(config)`
|
|
@@ -33,7 +39,7 @@ wireChannel({
|
|
|
33
39
|
|
|
34
40
|
Note there is **no `permissions` key on a message wiring** — wire-level
|
|
35
41
|
permissions were removed in #972. Authorization lives on the function's own
|
|
36
|
-
`permissions` field (see `pikku-
|
|
42
|
+
`permissions` field (see `pikku-permissions`).
|
|
37
43
|
|
|
38
44
|
### `pikkuChannelMiddleware(fn)`
|
|
39
45
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Pikku CLI Wiring
|
|
2
2
|
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- API Reference
|
|
6
|
+
- Usage Patterns
|
|
7
|
+
- Complete Example
|
|
8
|
+
|
|
3
9
|
## API Reference
|
|
4
10
|
|
|
5
11
|
### `wireCLI(config)`
|
|
@@ -212,4 +218,4 @@ session for local runs. Don't hand-write or edit the generated channel file.
|
|
|
212
218
|
|
|
213
219
|
## Complete Example
|
|
214
220
|
|
|
215
|
-
For a full functions + renderers + nested-subcommand wiring walkthrough, see
|
|
221
|
+
For a full functions + renderers + nested-subcommand wiring walkthrough, see the CLI complete example listed in `SKILL.md`.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Pikku HTTP Wiring
|
|
2
2
|
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- API Reference
|
|
6
|
+
- Data Flow
|
|
7
|
+
- Usage Patterns
|
|
8
|
+
- Complete Example
|
|
9
|
+
|
|
3
10
|
## API Reference
|
|
4
11
|
|
|
5
12
|
All three come from `#pikku/http` (the generated `.pikku/http/index.ts`), which
|
|
@@ -14,7 +21,7 @@ Function input/output types come from the function's own `input:`/`output:` zod
|
|
|
14
21
|
|
|
15
22
|
Config cascading across groups: `basePath` concatenates down the chain, `tags` merge (union), `auth` child overrides parent.
|
|
16
23
|
|
|
17
|
-
For the full option tables (every `wireHTTP` field, the `defineHTTPRoutes`/`wireHTTPRoutes` config shape), read `
|
|
24
|
+
For the full option tables (every `wireHTTP` field, the `defineHTTPRoutes`/`wireHTTPRoutes` config shape), read the HTTP options reference listed in `SKILL.md`.
|
|
18
25
|
|
|
19
26
|
### `addHTTPMiddleware(pattern, middlewares)`
|
|
20
27
|
|
|
@@ -23,7 +30,7 @@ addHTTPMiddleware('*', [authBearer()]) // All routes
|
|
|
23
30
|
addHTTPMiddleware('/api/*', [rateLimit()]) // Pattern match
|
|
24
31
|
```
|
|
25
32
|
|
|
26
|
-
> HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-
|
|
33
|
+
> HTTP-route-level permissions (`addHTTPPermission`, a `permissions` field on the wiring) were removed in #972. Declare authorization on the function definition (`pikkuFunc({ permissions })`, see `pikku-permissions`), or app-wide via `addGlobalPermission`. Tags/patterns are for _middleware_ only now.
|
|
27
34
|
|
|
28
35
|
## Data Flow
|
|
29
36
|
|
|
@@ -86,7 +93,7 @@ wireHTTP({ method: 'get', route: '/books', func: listBooks, auth: false })
|
|
|
86
93
|
wireHTTP({ method: 'delete', route: '/books/:bookId', func: deleteBook })
|
|
87
94
|
```
|
|
88
95
|
|
|
89
|
-
Authorization is not a wiring concern — declare it on the function via `permissions` (see `pikku-
|
|
96
|
+
Authorization is not a wiring concern — declare it on the function via `permissions` (see `pikku-permissions`), or app-wide via `addGlobalPermission`.
|
|
90
97
|
|
|
91
98
|
### Middleware
|
|
92
99
|
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Pikku Realtime
|
|
2
2
|
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- 1. Declare your topics
|
|
6
|
+
- 2. Enable the server side
|
|
7
|
+
- 3. Generate the typed client
|
|
8
|
+
- 4. Publish events from a function
|
|
9
|
+
- 5. Wire it up — share fetch with PikkuRPC
|
|
10
|
+
- 6. Subscribe from React
|
|
11
|
+
- Other SSE / WebSocket routes
|
|
12
|
+
- When to pick which transport
|
|
13
|
+
- What NOT to do
|
|
14
|
+
|
|
3
15
|
## 1. Declare your topics
|
|
4
16
|
|
|
5
17
|
In your project's types file (e.g. `types/eventhub-topics.d.ts`):
|
|
@@ -95,7 +107,7 @@ export class PikkuRealtime {
|
|
|
95
107
|
handler: (data: EventHubTopics[K]) => void
|
|
96
108
|
): { close: () => void }
|
|
97
109
|
|
|
98
|
-
// generic escape hatches — see
|
|
110
|
+
// generic escape hatches — see "Other SSE / WebSocket routes" below
|
|
99
111
|
subscribeToSSE<T>(
|
|
100
112
|
path: string,
|
|
101
113
|
handler: (data: T) => void
|
|
@@ -236,9 +248,27 @@ function TodoList() {
|
|
|
236
248
|
|
|
237
249
|
## Other SSE / WebSocket routes
|
|
238
250
|
|
|
239
|
-
The same client also
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
The same client also handles generic SSE + channel routes (not just
|
|
252
|
+
`/events` topics). Use the path; the base URL is inherited from `PikkuFetch`.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// Any `sse: true` HTTP route
|
|
256
|
+
const sub = realtime.subscribeToSSE<{ progress: number }>(
|
|
257
|
+
`/workflow-run/${runId}/stream`,
|
|
258
|
+
(event) => setProgress(event.progress)
|
|
259
|
+
)
|
|
260
|
+
// later: sub.close()
|
|
261
|
+
|
|
262
|
+
// Any wireChannel — open a raw socket, wrap in PikkuWebSocket for typed I/O
|
|
263
|
+
const ws = realtime.connectToChannel('/ws/kanban')
|
|
264
|
+
const typed = new PikkuWebSocket<'kanban-live'>(ws)
|
|
265
|
+
typed.getRoute('command').subscribe('message', (data) => {
|
|
266
|
+
/* ... */
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Discover what's available with `pikku meta clients --json` — `channels` and any HTTP
|
|
271
|
+
`sse: true` routes are listed there.
|
|
242
272
|
|
|
243
273
|
## When to pick which transport
|
|
244
274
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-workflow
|
|
3
3
|
description: >-
|
|
4
|
-
Use when building multi-step
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
Use when building multi-step, durable workflows with Pikku — pikkuWorkflowFunc,
|
|
5
|
+
pikkuWorkflowGraph, workflow.do / sleep / suspend / approval, retries, compensate, sub-workflows,
|
|
6
|
+
and starting or resuming runs. Includes deciding whether something should be a workflow at all.
|
|
7
|
+
TRIGGER when: code uses pikkuWorkflowFunc or pikkuWorkflowGraph, or the user asks about
|
|
8
|
+
workflows, multi-step processes, durable execution, suspend/resume, approval gates, sagas or DAG
|
|
9
|
+
orchestration. DO NOT TRIGGER when: the user wants a single background job (use the queue
|
|
10
|
+
reference in pikku-wiring) or a cron task (use the scheduler reference in pikku-wiring), or is
|
|
11
|
+
converting an n8n export (use pikku-n8n-import).
|
|
10
12
|
installGroups: [core]
|
|
11
13
|
---
|
|
12
14
|
|
|
@@ -1,261 +0,0 @@
|
|
|
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,80 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-mantine
|
|
3
|
-
description: >-
|
|
4
|
-
Use when building a Mantine UI on top of a Pikku backend — rendering dates that came back from a
|
|
5
|
-
generated client, keeping layout flow-relative so the app survives an RTL locale, and branching on
|
|
6
|
-
colour scheme without hardcoding a shade. TRIGGER when: putting a value from usePikkuQuery or an
|
|
7
|
-
RPC response on screen, writing margins/padding/alignment in Mantine props or CSS, choosing a date
|
|
8
|
-
input, or handling light/dark. DO NOT TRIGGER when: the data does not come from a Pikku client
|
|
9
|
-
(this is only about what the generated clients hand you), or for user-facing copy (use pikku-i18n).
|
|
10
|
-
installGroups: [client]
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Mantine on a Pikku client
|
|
14
|
-
|
|
15
|
-
## Dates — always format before rendering
|
|
16
|
-
|
|
17
|
-
The generated clients run `transformDates`, which revives **fully-zoned ISO-8601 instants** —
|
|
18
|
-
`2026-03-14T08:12:00Z`, `2026-03-14T08:12:00.000+01:00` — into `Date` objects and touches nothing
|
|
19
|
-
else. A bare `2026-03-14`, a zoneless `2026-03-14T08:12:00` and an impossible `2026-02-31T00:00:00Z`
|
|
20
|
-
all stay the strings the server sent. So a field's runtime type follows the VALUE, not the schema:
|
|
21
|
-
one column can arrive as a `Date` from one row and a string from the next.
|
|
22
|
-
|
|
23
|
-
Two consequences, and both compile:
|
|
24
|
-
|
|
25
|
-
- **A string method on one white-screens the page.** `row.createdAt.split('T')[0]` type-checks
|
|
26
|
-
against nothing useful and blows up at runtime. There is no string to slice.
|
|
27
|
-
- **A raw `Date` dropped into JSX crashes the route.** `<Text>{row.createdAt}</Text>`, a table cell,
|
|
28
|
-
a `<Badge>` — React throws `Objects are not valid as a React child (found: [object Date])` and the
|
|
29
|
-
page falls into its error boundary. Nothing catches it before the screen is white, which makes it
|
|
30
|
-
the most common broken page in a build.
|
|
31
|
-
|
|
32
|
-
**Format with dayjs.** It is Mantine's own date library, already shipped alongside `@mantine/dates`,
|
|
33
|
-
and it takes either a `Date` or a string. Never `toLocaleDateString`, `date-fns` or `luxon`.
|
|
34
|
-
|
|
35
|
-
```tsx
|
|
36
|
-
import dayjs from 'dayjs'
|
|
37
|
-
|
|
38
|
-
<Text>{dayjs(row.dueOn).format('D MMM YYYY')}</Text> // 15 Jun 2026
|
|
39
|
-
<Text>{dayjs(row.createdAt).format('D MMM YYYY, HH:mm')}</Text>
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
A relative "2 days ago" via dayjs `relativeTime` is fine. Coercing instead of formatting
|
|
43
|
-
(`` `${d}` ``, `String(d)`, `d + ''`) does not crash but prints
|
|
44
|
-
`Mon Jun 15 2026 02:00:00 GMT+0200` — a different bug, equally wrong.
|
|
45
|
-
|
|
46
|
-
Date **inputs** are `@mantine/dates` — `DatePickerInput`, `DatePicker`, `Calendar` — never a raw
|
|
47
|
-
`<TextInput type="date">`. Those are pickers and they are not a schedule: Mantine ships no
|
|
48
|
-
week/time-grid component, so a diary, rota, timetable or booking week is a grid you build, not a
|
|
49
|
-
`Calendar` with the time-of-day left out. `Calendar` with `renderDay` IS right for "a few things on
|
|
50
|
-
each day of a month", like a content calendar or a holiday planner.
|
|
51
|
-
|
|
52
|
-
All of this applies to stub and fixture dates exactly as it does to real data.
|
|
53
|
-
|
|
54
|
-
## RTL-safe styles
|
|
55
|
-
|
|
56
|
-
Write layout styles flow-relative so the UI works in both LTR and RTL languages. Pikku's i18n ships
|
|
57
|
-
Arabic, Hebrew, Farsi and Urdu support, and a physical margin is what breaks under it.
|
|
58
|
-
|
|
59
|
-
| Avoid | Use instead |
|
|
60
|
-
| ----------------------------- | ------------------------------------------ |
|
|
61
|
-
| `ml`, `mr`, `pl`, `pr` | `ms`, `me`, `ps`, `pe` |
|
|
62
|
-
| `text-align: left/right` | `text-align: start/end` |
|
|
63
|
-
| `margin-left`, `margin-right` | `margin-inline-start`, `margin-inline-end` |
|
|
64
|
-
| `flex-direction: row-reverse` | `dir` attribute or logical properties |
|
|
65
|
-
|
|
66
|
-
Mantine shorthand: `ms` = margin-inline-start, `me` = margin-inline-end, `ps` = padding-inline-start,
|
|
67
|
-
`pe` = padding-inline-end.
|
|
68
|
-
|
|
69
|
-
## Dark mode
|
|
70
|
-
|
|
71
|
-
Use Mantine's `light-dark()` utility or `useMantineColorScheme`, and only with colours that already
|
|
72
|
-
come from the theme — never introduce a literal colour or a shade string for one scheme.
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
75
|
-
// Correct
|
|
76
|
-
<Box bg="var(--mantine-color-body)">
|
|
77
|
-
|
|
78
|
-
// Wrong — scheme branch with hardcoded Mantine shades
|
|
79
|
-
<Box bg={theme.colorScheme === 'dark' ? 'dark.6' : 'gray.0'}>
|
|
80
|
-
```
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Subscribing to other SSE / WebSocket routes
|
|
2
|
-
|
|
3
|
-
The same `PikkuRealtime` client also handles generic SSE + channel routes (not just
|
|
4
|
-
`/events` topics). Use the path; the base URL is inherited from `PikkuFetch`.
|
|
5
|
-
|
|
6
|
-
```ts
|
|
7
|
-
// Any `sse: true` HTTP route
|
|
8
|
-
const sub = realtime.subscribeToSSE<{ progress: number }>(
|
|
9
|
-
`/workflow-run/${runId}/stream`,
|
|
10
|
-
(event) => setProgress(event.progress)
|
|
11
|
-
)
|
|
12
|
-
// later: sub.close()
|
|
13
|
-
|
|
14
|
-
// Any wireChannel — open a raw socket, wrap in PikkuWebSocket for typed I/O
|
|
15
|
-
const ws = realtime.connectToChannel('/ws/kanban')
|
|
16
|
-
const typed = new PikkuWebSocket<'kanban-live'>(ws)
|
|
17
|
-
typed.getRoute('command').subscribe('message', (data) => {
|
|
18
|
-
/* ... */
|
|
19
|
-
})
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Discover what's available with `pikku meta clients --json` — `channels` and any HTTP
|
|
23
|
-
`sse: true` routes are listed there.
|