@pikku/skills 0.12.57 → 0.12.59
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 +10 -7
- package/skills/pikku-a11y/SKILL.md +5 -3
- package/skills/pikku-addon/SKILL.md +8 -9
- package/skills/pikku-agent/SKILL.md +16 -13
- package/skills/pikku-agent/references/runner-vercel.md +1 -1
- package/skills/pikku-architect/SKILL.md +7 -10
- package/skills/pikku-auth/SKILL.md +16 -20
- package/skills/pikku-auth/references/better-auth.md +74 -37
- package/skills/pikku-auth/references/sessions.md +8 -0
- package/skills/pikku-build/SKILL.md +36 -68
- package/skills/pikku-build/references/app.md +27 -828
- package/skills/pikku-build/references/feature.md +3 -5
- package/skills/pikku-build/references/phase-1-what-it-is.md +167 -0
- package/skills/pikku-build/references/phase-2-people-and-apps.md +133 -0
- package/skills/pikku-build/references/phase-3-plan.md +62 -0
- package/skills/pikku-build/references/phase-4-build.md +249 -0
- package/skills/pikku-build/references/phase-5-prove-and-ship.md +148 -0
- package/skills/pikku-build/references/platform.md +5 -6
- package/skills/pikku-build/references/quick.md +2 -2
- package/skills/pikku-build/references/ship.md +4 -5
- package/skills/pikku-changes/SKILL.md +13 -7
- package/skills/pikku-concepts/SKILL.md +19 -258
- package/skills/pikku-concepts/references/cli.md +63 -0
- package/skills/pikku-concepts/references/functions.md +117 -0
- package/skills/pikku-concepts/references/language.md +5 -5
- package/skills/pikku-concepts/references/packages.md +6 -4
- package/skills/pikku-concepts/references/testing.md +14 -0
- package/skills/pikku-convert-screen/SKILL.md +7 -1
- package/skills/pikku-deploy/SKILL.md +29 -30
- package/skills/pikku-deploy/references/ws.md +1 -1
- package/skills/pikku-emails/SKILL.md +7 -3
- package/skills/pikku-guide/SKILL.md +7 -10
- package/skills/pikku-i18n/SKILL.md +18 -19
- package/skills/pikku-i18n/references/messages.md +6 -8
- package/skills/pikku-knowledge/SKILL.md +42 -45
- package/skills/pikku-kysely/SKILL.md +50 -13
- package/skills/pikku-list-query/SKILL.md +27 -14
- package/skills/pikku-meta/SKILL.md +13 -16
- package/skills/pikku-meta/references/audit.md +1 -45
- package/skills/pikku-meta/references/versioning.md +1 -1
- package/skills/pikku-middleware/SKILL.md +23 -13
- package/skills/pikku-middleware/references/middleware-patterns.md +2 -2
- package/skills/pikku-n8n-import/SKILL.md +7 -8
- package/skills/pikku-permissions/SKILL.md +14 -8
- package/skills/pikku-react/SKILL.md +7 -6
- package/skills/pikku-realtime/SKILL.md +16 -11
- package/skills/pikku-report/SKILL.md +8 -10
- package/skills/pikku-scenario/SKILL.md +15 -18
- package/skills/pikku-scenario/references/steps.md +3 -4
- package/skills/pikku-scenario/references/virtual-users.md +1 -1
- package/skills/pikku-seo/SKILL.md +16 -8
- package/skills/pikku-service-backends/SKILL.md +26 -28
- package/skills/pikku-services/SKILL.md +8 -9
- package/skills/pikku-services/references/services.md +1 -1
- package/skills/pikku-software-archaeology/README.md +12 -1
- package/skills/pikku-software-archaeology/SKILL.md +9 -2
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +1 -1
- package/skills/pikku-software-archaeology/references/sources/express.md +75 -0
- package/skills/pikku-software-archaeology/references/sources/laravel.md +77 -0
- package/skills/pikku-software-archaeology/references/sources/nestjs.md +73 -0
- package/skills/pikku-software-archaeology/references/sources/rails.md +72 -0
- package/skills/pikku-software-archaeology/references/tests-to-scenarios.md +72 -0
- package/skills/pikku-tailwind/SKILL.md +15 -16
- package/skills/pikku-theme/SKILL.md +15 -17
- package/skills/pikku-ui/SKILL.md +4 -3
- package/skills/pikku-webhook/SKILL.md +10 -11
- package/skills/pikku-wiring/SKILL.md +45 -47
- package/skills/pikku-wiring/references/channel.md +1 -1
- package/skills/pikku-wiring/references/scheduler.md +1 -1
- package/skills/pikku-wiring/references/trigger.md +24 -1
- package/skills/pikku-workflow/SKILL.md +7 -8
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-kysely
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Writing Kysely queries (select, join, aggregate, insert, update, delete, transactions, sql
|
|
5
|
+
template, jsonArrayFrom) in Pikku function bodies, collapsing sequential awaits into fewer round
|
|
6
|
+
trips, and @pikku/kysely service setup (channel stores, workflow, secret, AI storage, deployment).
|
|
7
|
+
TRIGGER when: writing a non-trivial kysely query, a function body awaits several queries, the
|
|
8
|
+
injected `kysely` service or
|
|
9
|
+
PikkuKysely/KyselyChannelStore/KyselyWorkflowService/KyselySecretService is used, or asked about
|
|
10
|
+
SQL setup. DO NOT TRIGGER for MongoDB/Redis services (use pikku-service-backends) or the
|
|
11
|
+
pagination contract (use pikku-list-query).
|
|
7
12
|
installGroups: [core]
|
|
8
13
|
---
|
|
9
14
|
|
|
@@ -119,15 +124,27 @@ just stop queuing behind each other.
|
|
|
119
124
|
|
|
120
125
|
```typescript
|
|
121
126
|
// Three hops, in series
|
|
122
|
-
const item = await kysely
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
const item = await kysely
|
|
128
|
+
.selectFrom('item')
|
|
129
|
+
.where('id', '=', id)
|
|
130
|
+
.selectAll()
|
|
131
|
+
.executeTakeFirst()
|
|
132
|
+
const bins = await kysely
|
|
133
|
+
.selectFrom('bin')
|
|
134
|
+
.where('warehouseId', '=', wid)
|
|
135
|
+
.selectAll()
|
|
136
|
+
.execute()
|
|
137
|
+
const moves = await kysely
|
|
138
|
+
.selectFrom('stockMove')
|
|
139
|
+
.where('itemId', '=', id)
|
|
140
|
+
.selectAll()
|
|
141
|
+
.execute()
|
|
125
142
|
|
|
126
143
|
// One hop's worth of latency
|
|
127
144
|
const [item, bins, moves] = await Promise.all([
|
|
128
|
-
kysely.selectFrom('item').where('id','=',id).selectAll().executeTakeFirst(),
|
|
129
|
-
kysely.selectFrom('bin').where('warehouseId','=',wid).selectAll().execute(),
|
|
130
|
-
kysely.selectFrom('stockMove').where('itemId','=',id).selectAll().execute(),
|
|
145
|
+
kysely.selectFrom('item').where('id', '=', id).selectAll().executeTakeFirst(),
|
|
146
|
+
kysely.selectFrom('bin').where('warehouseId', '=', wid).selectAll().execute(),
|
|
147
|
+
kysely.selectFrom('stockMove').where('itemId', '=', id).selectAll().execute(),
|
|
131
148
|
])
|
|
132
149
|
```
|
|
133
150
|
|
|
@@ -142,7 +159,11 @@ wants.
|
|
|
142
159
|
// N+1 — one extra hop per warehouse
|
|
143
160
|
const warehouses = await kysely.selectFrom('warehouse').selectAll().execute()
|
|
144
161
|
for (const w of warehouses) {
|
|
145
|
-
w.bins = await kysely
|
|
162
|
+
w.bins = await kysely
|
|
163
|
+
.selectFrom('bin')
|
|
164
|
+
.where('warehouseId', '=', w.id)
|
|
165
|
+
.selectAll()
|
|
166
|
+
.execute()
|
|
146
167
|
}
|
|
147
168
|
// One hop — see NESTED DATA above
|
|
148
169
|
```
|
|
@@ -158,7 +179,11 @@ and what comes back tells you which branch happened.
|
|
|
158
179
|
|
|
159
180
|
```typescript
|
|
160
181
|
// Two hops and a race
|
|
161
|
-
const existing = await kysely
|
|
182
|
+
const existing = await kysely
|
|
183
|
+
.selectFrom('item')
|
|
184
|
+
.where('sku', '=', sku)
|
|
185
|
+
.select('id')
|
|
186
|
+
.executeTakeFirst()
|
|
162
187
|
if (existing) throw new ConflictError()
|
|
163
188
|
await kysely.insertInto('item').values({ sku, name }).execute()
|
|
164
189
|
|
|
@@ -183,8 +208,20 @@ never have crossed the wire.
|
|
|
183
208
|
|
|
184
209
|
```typescript
|
|
185
210
|
// Two hops — the ids are only ever used as a filter
|
|
186
|
-
const ids = await kysely
|
|
187
|
-
|
|
211
|
+
const ids = await kysely
|
|
212
|
+
.selectFrom('bin')
|
|
213
|
+
.where('warehouseId', '=', wid)
|
|
214
|
+
.select('id')
|
|
215
|
+
.execute()
|
|
216
|
+
const stock = await kysely
|
|
217
|
+
.selectFrom('stock')
|
|
218
|
+
.where(
|
|
219
|
+
'binId',
|
|
220
|
+
'in',
|
|
221
|
+
ids.map((b) => b.id)
|
|
222
|
+
)
|
|
223
|
+
.selectAll()
|
|
224
|
+
.execute()
|
|
188
225
|
|
|
189
226
|
// One hop
|
|
190
227
|
const stock = await kysely
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-list-query
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
TRIGGER when:
|
|
6
|
-
|
|
4
|
+
Paginated or infinite-scroll lists: pikkuListFunc, the ListInput/ListOutput cursor contract, the
|
|
5
|
+
filter tree, the generated usePikkuInfiniteQuery hook. TRIGGER when: asked for infinite scroll,
|
|
6
|
+
"load more", a paginated table/list/grid, or a list that could outgrow one page. DO NOT TRIGGER
|
|
7
|
+
for small fixed lists (a plain pikkuFunc plus usePikkuQuery is correct) or general React provider
|
|
8
|
+
and hook setup (use pikku-react).
|
|
7
9
|
installGroups: [core, client]
|
|
8
10
|
---
|
|
9
11
|
|
|
@@ -22,7 +24,10 @@ installGroups: [core, client]
|
|
|
22
24
|
A list function is a normal `pikkuFunc`/`pikkuSessionlessFunc` whose input/output conform to two shared shapes from `@pikku/core`:
|
|
23
25
|
|
|
24
26
|
```typescript
|
|
25
|
-
interface ListInput<
|
|
27
|
+
interface ListInput<
|
|
28
|
+
F extends Record<string, unknown> = {},
|
|
29
|
+
S extends string = never,
|
|
30
|
+
> {
|
|
26
31
|
cursor?: string // opaque — echo back whatever you returned as nextCursor
|
|
27
32
|
limit?: number // page size; server may cap it
|
|
28
33
|
sort?: Array<{ column: S; direction: 'asc' | 'desc' }>
|
|
@@ -57,7 +62,10 @@ export const listItems = pikkuListFunc<{ status?: string }, Item>({
|
|
|
57
62
|
func: async ({ kysely }, input, { session }) => {
|
|
58
63
|
// `limit` is caller-supplied on an exposed RPC, so it is CAPPED, not trusted —
|
|
59
64
|
// `ListInput` says "server may cap" and this is where that happens.
|
|
60
|
-
const limit = Math.min(
|
|
65
|
+
const limit = Math.min(
|
|
66
|
+
Math.max(Math.trunc(input.limit ?? 20) || 20, 1),
|
|
67
|
+
100
|
|
68
|
+
)
|
|
61
69
|
const parsed = input.cursor ? Number(input.cursor) : 0
|
|
62
70
|
const offset = Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0
|
|
63
71
|
|
|
@@ -67,7 +75,11 @@ export const listItems = pikkuListFunc<{ status?: string }, Item>({
|
|
|
67
75
|
query = query.where('status', '=', status)
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
const rows = await query
|
|
78
|
+
const rows = await query
|
|
79
|
+
.orderBy('createdAt', 'desc')
|
|
80
|
+
.offset(offset)
|
|
81
|
+
.limit(limit)
|
|
82
|
+
.execute()
|
|
71
83
|
const nextOffset = offset + rows.length
|
|
72
84
|
const totalCount = await query
|
|
73
85
|
.select((eb) => eb.fn.countAll<number>().as('count'))
|
|
@@ -92,10 +104,11 @@ Generated automatically alongside `usePikkuQuery`/`usePikkuMutation` once `react
|
|
|
92
104
|
import { usePikkuInfiniteQuery } from '.pikku/pikku-react-query.gen'
|
|
93
105
|
|
|
94
106
|
function ItemList() {
|
|
95
|
-
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
|
108
|
+
usePikkuInfiniteQuery(
|
|
109
|
+
'listItems',
|
|
110
|
+
{ limit: 20 } // never pass cursor here — the hook manages it
|
|
111
|
+
)
|
|
99
112
|
|
|
100
113
|
const rows = data?.pages.flatMap((page) => page.rows) ?? []
|
|
101
114
|
|
|
@@ -142,10 +155,10 @@ Read exactly the shape you support, and refuse the rest rather than ignoring it:
|
|
|
142
155
|
import type { Filter } from '@pikku/core/function'
|
|
143
156
|
|
|
144
157
|
/** The one shape this endpoint accepts: a single-key leaf with a plain value. */
|
|
145
|
-
function leafEquals<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
): F[K] | undefined {
|
|
158
|
+
function leafEquals<
|
|
159
|
+
F extends Record<string, unknown>,
|
|
160
|
+
K extends keyof F & string,
|
|
161
|
+
>(filter: Filter<F> | undefined, field: K): F[K] | undefined {
|
|
149
162
|
if (!filter || Array.isArray(filter)) return undefined
|
|
150
163
|
const keys = Object.keys(filter)
|
|
151
164
|
if (keys.length !== 1 || keys[0] !== field) return undefined
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-meta
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
deserves, dependency vulnerabilities, the console Security screen, or upgrading Pikku. DO NOT
|
|
12
|
-
TRIGGER when: user is writing a new function or wiring (use the wiring skill) or asking about
|
|
13
|
-
Pikku concepts (use pikku-concepts).
|
|
4
|
+
Inspect or evolve a project you did not just write: `pikku meta`/`pikku info` for functions,
|
|
5
|
+
schemas, wires, workflows, middleware, permissions; `pikku meta apply`; `pikku versions`/`pikku
|
|
6
|
+
release` for contract hashes, breaking changes and semver; `pikku audit`/`pikku update` for
|
|
7
|
+
advisories and upgrades. TRIGGER when: asked what functions or routes exist, a function's
|
|
8
|
+
input/output shape, retagging, API versioning, breaking changes, release semver, dependency
|
|
9
|
+
vulnerabilities or upgrading Pikku. DO NOT TRIGGER when writing a new function or wiring (use the
|
|
10
|
+
wiring skill) or on Pikku concepts (use pikku-concepts).
|
|
14
11
|
installGroups: [core]
|
|
15
12
|
allowed-tools: Bash(yarn pikku meta *), Bash(yarn pikku info *)
|
|
16
13
|
argument-hint: '[context|functions|schemas|workflows|middleware|permissions|wires|apply|versions|release|audit|update]'
|
|
@@ -23,11 +20,11 @@ and change it through the write path rather than by hand.
|
|
|
23
20
|
|
|
24
21
|
## Pick the reference
|
|
25
22
|
|
|
26
|
-
| You are…
|
|
27
|
-
|
|
|
28
|
-
| Asking what exists, or setting config on a declaration
|
|
23
|
+
| You are… | Read |
|
|
24
|
+
| ----------------------------------------------------------- | -------------------------- |
|
|
25
|
+
| Asking what exists, or setting config on a declaration | `references/meta.md` |
|
|
29
26
|
| Versioning a contract, or versioning and shipping a release | `references/versioning.md` |
|
|
30
|
-
| Chasing a dependency advisory, or upgrading Pikku
|
|
27
|
+
| Chasing a dependency advisory, or upgrading Pikku | `references/audit.md` |
|
|
31
28
|
|
|
32
29
|
## Start with `pikku meta context`
|
|
33
30
|
|
|
@@ -48,7 +45,7 @@ that asymmetry built in, so let it decide rather than eyeballing a diff.
|
|
|
48
45
|
|
|
49
46
|
- **Do not infer a function's input or output by reading its body**, and do not
|
|
50
47
|
cast a call site to make it compile. The schema is the type; `pikku meta
|
|
51
|
-
|
|
48
|
+
functions get <id>` has it.
|
|
52
49
|
- **Do not expect an unversioned function to be promoted for you.** Without an
|
|
53
50
|
explicit `version: 2` it is version 1 of its contract, collides with the
|
|
54
51
|
pinned `@v1`, and `pikku versions check` reports the published contract as
|
|
@@ -63,5 +60,5 @@ that asymmetry built in, so let it decide rather than eyeballing a diff.
|
|
|
63
60
|
companions come from `@pikku/core`; the CLI writes it, the addon reads it, the
|
|
64
61
|
UI renders it.
|
|
65
62
|
- **Do not treat a failed audit run as a clean one.** `bun audit` exits non-zero
|
|
66
|
-
when it
|
|
63
|
+
when it _finds_ advisories and still writes its payload, so non-zero with
|
|
67
64
|
output is data; non-zero with no output throws on purpose.
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
- [Agent Operating Procedure](#agent-operating-procedure)
|
|
6
6
|
- [The `pikku audit` command](#the-pikku-audit-command)
|
|
7
7
|
- [The `pikku update` command](#the-pikku-update-command)
|
|
8
|
-
- [Console integration (@pikku/addon-console)](#console-integration-pikkuaddon-console)
|
|
9
|
-
- [Console UI (@pikku/console)](#console-ui-pikkuconsole)
|
|
10
8
|
- [Report shape (SecurityAuditReport)](#report-shape-securityauditreport)
|
|
11
9
|
|
|
12
10
|
## Agent Operating Procedure
|
|
@@ -14,7 +12,7 @@
|
|
|
14
12
|
1. The audit is a generated artifact, not live state. `pikku audit` writes the
|
|
15
13
|
normalised report to `.pikku/audit.json` (config `outDir`), so it rides the
|
|
16
14
|
same meta pipeline as every other codegen output — uploaded on deploy,
|
|
17
|
-
readable by
|
|
15
|
+
readable by any tooling. Read it via
|
|
18
16
|
`metaService.readFile('audit.json')`, never by shelling out to the package
|
|
19
17
|
manager from a function.
|
|
20
18
|
2. One source of truth for the shape: `SecurityAuditReport` (and
|
|
@@ -81,48 +79,6 @@ the point is what the target needs. An @pikku peer the same run already brings
|
|
|
81
79
|
forward is not reported, and an unsatisfied _optional_ peer the project never
|
|
82
80
|
declared is skipped.
|
|
83
81
|
|
|
84
|
-
## Console integration (@pikku/addon-console)
|
|
85
|
-
|
|
86
|
-
Three RPCs, all reading/writing the same artifact via the meta service. Shared
|
|
87
|
-
spawn/read helpers live in `lib/audit-exec.ts` (`readAuditReport`,
|
|
88
|
-
`runPikkuAudit`, `spawnProcess`, `findBin`), alongside `lib/find-project-root.ts`
|
|
89
|
-
and `lib/resolve-package-manager.ts` (`resolvePackageManager`, `installArgs`,
|
|
90
|
-
`execPrefix`) — reuse them, don't re-implement. `resolvePackageManager` reads
|
|
91
|
-
package.json's corepack `packageManager` field first and only falls back to
|
|
92
|
-
lockfiles, because that field states intent before a lockfile exists and a
|
|
93
|
-
project can carry a stale one from another tool. Guessing wrong is not a soft
|
|
94
|
-
failure: the spawn dies with `Executable not found in $PATH`.
|
|
95
|
-
Like every console RPC these require an **authenticated session** (the console
|
|
96
|
-
is admin-only), so the host must have Better Auth wired — see `pikku-auth`.
|
|
97
|
-
|
|
98
|
-
- `getSecurityAudit` — reads `.pikku/audit.json`, returns the report (or `null`).
|
|
99
|
-
- `runSecurityAudit` — runs `pikku audit --outdated` server-side (regenerates the
|
|
100
|
-
artifact) then returns the fresh report. Same shape as the Run Tests action.
|
|
101
|
-
- `updateDependency({ package, version })` — bumps the package in `package.json`
|
|
102
|
-
(preserving the `^`/`~` range prefix), runs `bun install`, re-audits, and
|
|
103
|
-
returns the fresh report. Throws if the package is not a direct dependency.
|
|
104
|
-
NOTE: `bun install` must be scoped to a standalone project — do not run it
|
|
105
|
-
inside a yarn/bun monorepo member (it resolves the whole workspace).
|
|
106
|
-
|
|
107
|
-
## Console UI (@pikku/console)
|
|
108
|
-
|
|
109
|
-
- `SecurityPage` — the page: **Run audit** button (`lead`) + responsive
|
|
110
|
-
`ShellHeader` (structured `search` + `selection` for the Issues/Dependencies
|
|
111
|
-
lens; never cram raw controls into the non-collapsing `filters`/`view` escape
|
|
112
|
-
hatch). Empty state until an audit has run.
|
|
113
|
-
- `SecurityAuditView` — exported presentational component. Two lenses
|
|
114
|
-
(Issues grouped by severity; Dependencies table). Each finding row carries its
|
|
115
|
-
actions **right-aligned in the row header** (`Accordion.Control` sibling, so a
|
|
116
|
-
click acts instead of toggling): "View advisory" + a per-finding
|
|
117
|
-
**remediation slot**.
|
|
118
|
-
- `renderRemediation({ pkg, version, issue })` — the extension seam. OSS default
|
|
119
|
-
is `UpdateDependencyButton` (the free bump + `bun install`). Downstream
|
|
120
|
-
consoles pass their own sandbox-verified action here — replace the
|
|
121
|
-
action, keep the view.
|
|
122
|
-
- Hooks: `useSecurityAudit` (read), `useRunSecurityAudit` (run),
|
|
123
|
-
`useUpdateDependency` (bump). All are `useMutation`/`useQuery` — surface
|
|
124
|
-
`mutation.error`, never hand-roll loading/error state or swallow the error.
|
|
125
|
-
|
|
126
82
|
## Report shape (SecurityAuditReport)
|
|
127
83
|
|
|
128
84
|
```ts
|
|
@@ -208,7 +208,7 @@ A release with neither says it holds internal changes only. Branch names are con
|
|
|
208
208
|
while it waits to ship.
|
|
209
209
|
|
|
210
210
|
Functions and wirings defined under a `scaffold/` directory or in a generated
|
|
211
|
-
`*.gen.*` file are platform plumbing (auth,
|
|
211
|
+
`*.gen.*` file are platform plumbing (auth, a host's injected shims)
|
|
212
212
|
and are left out of the surface, so the same app diffs the same locally and in
|
|
213
213
|
a platform's build.
|
|
214
214
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-middleware
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Middleware in a Pikku app: global (addGlobalMiddleware), HTTP/prefix (addHTTPMiddleware),
|
|
5
|
+
tag-scoped (addTagMiddleware), per-wiring, session-setting; execution order and priority; the
|
|
6
|
+
pikkuMiddleware factory; resolving a token into a session (HTTP, cron); service-to-service bearer
|
|
7
|
+
gates. TRIGGER when: middleware on some or all routes, tag-scoped concerns, global interceptors,
|
|
8
|
+
priority/order questions, or middleware calling setInitialSession for a machine caller. DO NOT TRIGGER
|
|
9
|
+
for choosing an auth strategy (authBearer, authCookie, Better Auth, API keys, `pikku login`),
|
|
10
|
+
permissions or scopes (use pikku-permissions), or deployment.
|
|
7
11
|
installGroups: [core]
|
|
8
12
|
---
|
|
9
13
|
|
|
@@ -41,7 +45,8 @@ const telemetryMiddleware = pikkuMiddleware({
|
|
|
41
45
|
The `wire` object gives you:
|
|
42
46
|
|
|
43
47
|
- `wire.http` — inbound HTTP context (headers, URL, cookies)
|
|
44
|
-
- `wire.
|
|
48
|
+
- `wire.setInitialSession(session)` — establish who the caller is for this request; nothing is persisted and no cookie is re-issued. What auth middleware calls.
|
|
49
|
+
- `wire.setSession(session)` / `wire.clearSession()` — sign the caller in or out: marks the session changed (so `authCookie` re-issues the cookie) and writes the session store. Same meaning as in a function body; await it.
|
|
45
50
|
- `wire.getSession()` — read the current session
|
|
46
51
|
- `wire.session` — the session set so far (may be undefined)
|
|
47
52
|
|
|
@@ -77,7 +82,7 @@ wireHTTP({
|
|
|
77
82
|
Runs before everything else, across every wire type: HTTP, Queue, Channel, Trigger, Scheduler, Workflow, Agent, CLI, MCP. Use it for cross-cutting concerns (e.g. telemetry) that must wrap every invocation regardless of transport.
|
|
78
83
|
|
|
79
84
|
```typescript
|
|
80
|
-
import { addGlobalMiddleware } from '@pikku/core'
|
|
85
|
+
import { addGlobalMiddleware } from '@pikku/core/middleware'
|
|
81
86
|
import { telemetryOuter, telemetryInner } from '@pikku/core/middleware'
|
|
82
87
|
|
|
83
88
|
addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call
|
|
@@ -177,20 +182,20 @@ Within the same priority level, the collection order above is preserved. Use pri
|
|
|
177
182
|
|
|
178
183
|
## ⛔ MACHINE AUTH: THE TOKEN BECOMES A SESSION. ⛔
|
|
179
184
|
|
|
180
|
-
**A caller that has an identity — a sandbox, a deployed stage, a pool host, a device — is authenticated ONCE, in middleware, which calls `
|
|
185
|
+
**A caller that has an identity — a sandbox, a deployed stage, a pool host, a device — is authenticated ONCE, in middleware, which calls `setInitialSession`. The function is then an ordinary `pikkuFunc` gated with `scopes`. It reads `session`. It NEVER re-derives who the caller is.**
|
|
181
186
|
|
|
182
187
|
Either a function is sessionless (genuinely public) or it has a session. Anything in between — a token verified inside `func`, a token verified in a `permissions` check that returns `true`, an identity passed in the input schema, the same resolver memoised per request so N functions can each call it — is the anti-pattern this section exists to kill.
|
|
183
188
|
|
|
184
189
|
```typescript
|
|
185
190
|
// middleware.ts — resolve the bearer ONCE, for every route
|
|
186
191
|
const sandboxBearerAuth = pikkuMiddleware<SingletonServices>(
|
|
187
|
-
async ({ kysely, auth }, { http, getSession,
|
|
192
|
+
async ({ kysely, auth }, { http, getSession, setInitialSession }, next) => {
|
|
188
193
|
if (await getSession?.()) return next()
|
|
189
194
|
const header = http?.request?.header?.('authorization')
|
|
190
195
|
if (!header?.startsWith('Bearer ')) return next()
|
|
191
196
|
const sandbox = await resolveSandboxSession(kysely, auth, header.slice(7).trim())
|
|
192
197
|
if (sandbox) {
|
|
193
|
-
|
|
198
|
+
setInitialSession?.({ userId: sandbox.createdByUserId ?? sandbox.sandboxInstanceId,
|
|
194
199
|
orgId: sandbox.organizationId, scopes: ['machine:sandbox'], sandbox } as UserSession)
|
|
195
200
|
}
|
|
196
201
|
return next()
|
|
@@ -218,17 +223,22 @@ An unresolved token leaves the session unset and the function throws `MissingSes
|
|
|
218
223
|
|
|
219
224
|
### It MUST be `addHTTPMiddleware`, never `addTagMiddleware`
|
|
220
225
|
|
|
221
|
-
**A session set in tag middleware is invisible to the function when the call arrives over `POST /rpc/:rpcName`.** Tag middleware runs inside `runPikkuFunc`, and the RPC dispatch calls it without a `sessionService`, so `invocationWire.session` is never re-read after your `
|
|
226
|
+
**A session set in tag middleware is invisible to the function when the call arrives over `POST /rpc/:rpcName`.** Tag middleware runs inside `runPikkuFunc`, and the RPC dispatch calls it without a `sessionService`, so `invocationWire.session` is never re-read after your `setInitialSession` — the function sees the session the OUTER wire had, which is none. `addHTTPMiddleware('*')` runs on the `/rpc` route itself, before its handler, and that session is the one the dispatched function inherits. Tag middleware is still right for a gate that only says yes/no.
|
|
222
227
|
|
|
223
228
|
### A cron is a machine identity too — set it in the task's own middleware
|
|
224
229
|
|
|
225
230
|
A scheduled task has no caller and no header, but it is still a machine principal, and without a session it cannot invoke a gated RPC or be attributed in anything it writes. Give it one the same way, in the task's own `middleware`:
|
|
226
231
|
|
|
227
232
|
```typescript
|
|
228
|
-
const cronSession = pikkuMiddleware(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
233
|
+
const cronSession = pikkuMiddleware(
|
|
234
|
+
async (_services, { scheduledTask, setInitialSession }, next) => {
|
|
235
|
+
setInitialSession?.({
|
|
236
|
+
userId: `cron:${scheduledTask?.name}`,
|
|
237
|
+
scopes: ['machine:cron'],
|
|
238
|
+
} as UserSession)
|
|
239
|
+
return next()
|
|
240
|
+
}
|
|
241
|
+
)
|
|
232
242
|
|
|
233
243
|
wireScheduler({
|
|
234
244
|
name: 'tickVirtualUserSchedules',
|
|
@@ -32,7 +32,7 @@ export function getServiceRPC(baseUrl: string, token: string): RPCInvoke {
|
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
34
|
const apiKeyAuth = pikkuMiddleware(
|
|
35
|
-
async ({ kysely }, { http,
|
|
35
|
+
async ({ kysely }, { http, setInitialSession, session }, next) => {
|
|
36
36
|
if (session) return next() // already authenticated
|
|
37
37
|
|
|
38
38
|
const header = http?.request?.header?.('x-api-key')
|
|
@@ -43,7 +43,7 @@ const apiKeyAuth = pikkuMiddleware(
|
|
|
43
43
|
.select('userId')
|
|
44
44
|
.where('key', '=', header)
|
|
45
45
|
.executeTakeFirst()
|
|
46
|
-
if (row)
|
|
46
|
+
if (row) setInitialSession?.({ userId: row.userId })
|
|
47
47
|
|
|
48
48
|
return next()
|
|
49
49
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-n8n-import
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
pikku-addon), or for authoring workflows from scratch (use pikku-workflow).
|
|
4
|
+
Import an n8n workflow JSON export into a runnable Pikku workflow: run `pikku import n8n`, triage
|
|
5
|
+
unmapped nodes, fill each stub, report missing `@pikku/addon-*` integrations, verify it compiles
|
|
6
|
+
with no stubs left. TRIGGER when: asked to import/convert an n8n workflow, pointed at an n8n .json
|
|
7
|
+
export or directory, or continuing after the import left throwing stubs (`STUB — generated from
|
|
8
|
+
n8n …`, `— implement me`) or a `*.integrations.json` manifest. DO NOT TRIGGER for hand-written
|
|
9
|
+
addon wiring unrelated to n8n (use pikku-addon) or authoring workflows from scratch (use
|
|
10
|
+
pikku-workflow).
|
|
12
11
|
installGroups: [core]
|
|
13
12
|
metadata:
|
|
14
13
|
version: 1.0.0
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-permissions
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Deciding WHO may call a function: resource ownership, role gates, admin-only actions, "only their
|
|
5
|
+
own rows" rules; the `permissions` field, pikkuPermission, pikkuAuth, scopes/defineScope,
|
|
6
|
+
addGlobalPermission, permissionsInBody. TRIGGER when: writing or reviewing a function that touches
|
|
7
|
+
a user-owned row, gating on a role, building the permissions half of a contract in build PHASE 2,
|
|
8
|
+
about to write an `if` in a body deciding whether the caller is allowed, or hitting
|
|
9
|
+
MissingScopeError. DO NOT TRIGGER for how a caller proves identity: sign-in, sessions, tokens, API
|
|
10
|
+
keys, seeding a virtual user (use pikku-auth), or paginated lists (use pikku-list-query).
|
|
7
11
|
installGroups: [core]
|
|
8
12
|
---
|
|
9
13
|
|
|
@@ -12,7 +16,7 @@ installGroups: [core]
|
|
|
12
16
|
## First: is the caller a machine with a token?
|
|
13
17
|
|
|
14
18
|
Then this is NOT a permissions problem. Resolve the token in `addHTTPMiddleware('*')`
|
|
15
|
-
middleware that calls `
|
|
19
|
+
middleware that calls `setInitialSession`, make the function a `pikkuFunc`, and gate it with
|
|
16
20
|
`scopes` (pikku-auth, `../pikku-auth/references/machine-auth.md`). A `permissions` check that verifies a
|
|
17
21
|
bearer token and returns `true` is authentication wearing an authorization hat, and it
|
|
18
22
|
leaves the function sessionless. The only exception is a bootstrap endpoint whose caller
|
|
@@ -54,8 +58,8 @@ user's row by passing its id. Every function that takes a row id needs BOTH: `au
|
|
|
54
58
|
for the session, and a `permissions` entry for the ownership.
|
|
55
59
|
|
|
56
60
|
Equally: do NOT write an `isSignedIn` permission that returns `!!session`. That re-checks
|
|
57
|
-
authentication, which `auth: true` already did. A permission answers
|
|
58
|
-
|
|
61
|
+
authentication, which `auth: true` already did. A permission answers _may this user do
|
|
62
|
+
this_ — role, ownership, tier — never _is there a session_.
|
|
59
63
|
|
|
60
64
|
## Single row vs list — where ownership actually goes
|
|
61
65
|
|
|
@@ -88,11 +92,13 @@ export const isBookOwner = pikkuPermission(
|
|
|
88
92
|
.where('bookId', '=', bookId)
|
|
89
93
|
.executeTakeFirst()
|
|
90
94
|
return book?.ownerId === session?.userId
|
|
91
|
-
}
|
|
95
|
+
}
|
|
92
96
|
)
|
|
93
97
|
|
|
94
98
|
// Session-only: no input needed. Use for role and flag gates.
|
|
95
|
-
export const isAdmin = pikkuAuth(
|
|
99
|
+
export const isAdmin = pikkuAuth(
|
|
100
|
+
async (_services, session) => session?.role === 'admin'
|
|
101
|
+
)
|
|
96
102
|
```
|
|
97
103
|
|
|
98
104
|
## OR and AND
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-react
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
data, wiring PikkuProvider, paginating,
|
|
8
|
-
backend (use pikku-wiring), defining the workflow
|
|
9
|
-
dates (use pikku-tailwind)
|
|
4
|
+
React frontend talking to a Pikku backend: PikkuProvider/createPikku, generated React Query hooks
|
|
5
|
+
(usePikkuQuery, usePikkuMutation, usePikkuInfiniteQuery), usePikkuRPC/usePikkuFetch, realtime
|
|
6
|
+
subscriptions, agent and workflow hooks. TRIGGER when: writing a React component that fetches or
|
|
7
|
+
mutates backend data, wiring PikkuProvider, paginating, or running/tracking a workflow from the
|
|
8
|
+
client. DO NOT TRIGGER for backend work (use pikku-wiring), defining the workflow (use
|
|
9
|
+
pikku-workflow), rendering fetched values such as dates (use pikku-tailwind) or user-facing copy
|
|
10
|
+
(use pikku-i18n).
|
|
10
11
|
installGroups: [client]
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-realtime
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
pikku-workflow, or pikku-wiring's queue and scheduler references), or for the full wiring
|
|
12
|
-
reference of channels, SSE routes and the `/events` scaffold (use pikku-wiring).
|
|
4
|
+
Making a view live in a Pikku app (board, shared list, dashboard, ticker, bidding room, live
|
|
5
|
+
count) or adding two-way chat/presence: the default event-hub SSE path (`eventHub.publish`,
|
|
6
|
+
`PikkuRealtime.subscribeToTopic`) and the WebSocket channel. TRIGGER when: asked for live updates,
|
|
7
|
+
realtime, "update without refresh", presence or chat, or data several signed-in users can change
|
|
8
|
+
should reflect to others. DO NOT TRIGGER when a one-shot refetch is enough, for background jobs
|
|
9
|
+
(use pikku-workflow, or pikku-wiring's queue/scheduler references), or for the full
|
|
10
|
+
channel/SSE/`/events` wiring reference (use pikku-wiring).
|
|
13
11
|
installGroups: [core, client]
|
|
14
12
|
---
|
|
15
13
|
|
|
@@ -115,8 +113,15 @@ import { pikkuChannelFunc, wireChannel } from '#pikku/channel'
|
|
|
115
113
|
|
|
116
114
|
export const onMessage = pikkuChannelFunc<{ text: string }>({
|
|
117
115
|
func: async ({ eventHub }, input, { channel, session }) => {
|
|
118
|
-
const message = {
|
|
119
|
-
|
|
116
|
+
const message = {
|
|
117
|
+
id: crypto.randomUUID(),
|
|
118
|
+
text: input.text,
|
|
119
|
+
userId: session!.userId,
|
|
120
|
+
}
|
|
121
|
+
await eventHub.publish('room', channel.channelId, {
|
|
122
|
+
topic: 'room',
|
|
123
|
+
data: message,
|
|
124
|
+
})
|
|
120
125
|
return message
|
|
121
126
|
},
|
|
122
127
|
})
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-report
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
not,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
is in the app you are building (fix it there), you are debugging a deployed stage,
|
|
12
|
-
or you are tempted to patch pikku's source (never do that from an app).
|
|
4
|
+
Reporting Pikku itself when it cost you time (wrong generated types, a check that passes when it
|
|
5
|
+
should not, quietly wrong output, a misleading skill) or when asked to file a framework bug:
|
|
6
|
+
`pikku cloud report`, product-vs-harness kinds, the workaround-first ladder, asking the user at
|
|
7
|
+
hand-over whether to send findings. TRIGGER when: the framework fought you, codegen is broken, a
|
|
8
|
+
skill told you to run something that does not exist, the user says "report this to pikku" / "file
|
|
9
|
+
a finding", or you are handing over a build. DO NOT TRIGGER for bugs in the app you are building,
|
|
10
|
+
debugging a deployed stage, or patching pikku's source (never from an app).
|
|
13
11
|
installGroups: [core]
|
|
14
12
|
---
|
|
15
13
|
|
|
@@ -62,7 +60,7 @@ baseline noise that was already failing before you started.
|
|
|
62
60
|
- `--kind harness` — a skill misled you: it told you to run something that does
|
|
63
61
|
not exist, described a flag that is spelled differently, or contradicted what
|
|
64
62
|
the CLI actually did. Pass `--skill <name>` and `--passage "<the line or
|
|
65
|
-
|
|
63
|
+
section>"`. This is the most useful kind to file, because it is fixable
|
|
66
64
|
immediately — so file it even when the cost was small.
|
|
67
65
|
|
|
68
66
|
## The command
|