@pikku/cli 0.12.26 → 0.12.27

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 (70) hide show
  1. package/console-app/assets/index-BERGDBO9.js +228 -0
  2. package/console-app/assets/{index-C52h1B_L.css → index-CQ29NRyR.css} +1 -1
  3. package/console-app/index.html +2 -2
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  8. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  9. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  10. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  14. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  15. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  16. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  17. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  18. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  19. package/dist/.pikku/function/pikku-functions-meta.gen.json +72 -72
  20. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  21. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  22. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  23. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  25. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  26. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  27. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  28. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  29. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  30. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  31. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  32. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  33. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-types.gen.js +1 -1
  35. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  36. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  37. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  38. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  39. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  40. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  41. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +3 -3
  42. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  43. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  44. package/dist/.pikku/schemas/register.gen.js +7 -7
  45. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  46. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  47. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  48. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  49. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  50. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  51. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  52. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  53. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  54. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  55. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  56. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  57. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  58. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  59. package/dist/bin/pikku-bin.mjs +2 -2
  60. package/dist/src/fabric/functions/validate-core.js +6 -6
  61. package/dist/src/functions/db/local-db.d.ts +1 -1
  62. package/dist/src/functions/db/local-db.js +4 -4
  63. package/dist/src/functions/validate/workspace-validate.js +4 -1
  64. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  65. package/package.json +3 -3
  66. package/skills/pikku-middleware/SKILL.md +283 -0
  67. package/skills/pikku-permissions/SKILL.md +165 -0
  68. package/skills/pikku-security/SKILL.md +38 -177
  69. package/skills/pikku-tag-middleware/SKILL.md +13 -0
  70. package/console-app/assets/index-Ba9K10XZ.js +0 -232
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.26
2
+ * This file was generated by @pikku/cli@0.12.27
3
3
  */
4
4
  import { WorkflowCancelledException } from '@pikku/core/workflow';
5
5
  import { template } from '@pikku/core/workflow';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.26
2
+ * This file was generated by @pikku/cli@0.12.27
3
3
  */
4
4
  import { pikkuState } from '@pikku/core/internal';
5
5
  import allWorkflowMeta from './meta/allWorkflow.gen.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.26
2
+ * This file was generated by @pikku/cli@0.12.27
3
3
  */
4
4
  import { addWorkflow } from '@pikku/core/workflow';
5
5
  import './pikku-workflow-wirings-meta.gen.js';
@@ -11,8 +11,8 @@ async function checkForUpdate() {
11
11
  })
12
12
  if (!res.ok) return
13
13
  const { version: latest } = await res.json()
14
- if (latest !== '0.12.26') {
15
- process.stderr.write(`\n Update available 0.12.26 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
14
+ if (latest !== '0.12.27') {
15
+ process.stderr.write(`\n Update available 0.12.27 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
16
16
  }
17
17
  } catch {}
18
18
  }
@@ -90,10 +90,10 @@ export async function runFabricValidate(startDir = process.cwd()) {
90
90
  e('missing-kysely-sqlite', 'services.ts imports @pikku/kysely-sqlite but it is not in root package.json', rootPkgPath, 'Add "@pikku/kysely-sqlite": "file:./vendor/pikku-kysely-sqlite.tgz" to dependencies');
91
91
  }
92
92
  }
93
- // db/migrations/ — presence, numbering and SQL dialect
94
- const migrationsDir = join(fnDir, 'db', 'migrations');
93
+ // db/sqlite/ — presence, numbering and SQL dialect
94
+ const migrationsDir = join(fnDir, 'db', 'sqlite');
95
95
  if (!existsSync(migrationsDir)) {
96
- e('migrations-dir-missing', 'packages/functions/db/migrations/ not found', migrationsDir, 'Create db/migrations/ and add numbered .sql files (e.g. 0001-init.sql) using SQLite-compatible syntax');
96
+ e('migrations-dir-missing', 'packages/functions/db/sqlite/ not found', migrationsDir, 'Create db/sqlite/ and add numbered .sql files (e.g. 0001-init.sql) using SQLite-compatible syntax');
97
97
  }
98
98
  else {
99
99
  try {
@@ -125,10 +125,10 @@ export async function runFabricValidate(startDir = process.cwd()) {
125
125
  // readdir failure — skip
126
126
  }
127
127
  }
128
- // db/seed.sql
129
- const seedPath = join(fnDir, 'db', 'seed.sql');
128
+ // db/sqlite-seed.sql
129
+ const seedPath = join(fnDir, 'db', 'sqlite-seed.sql');
130
130
  if (!existsSync(seedPath)) {
131
- e('seed-sql-missing', 'packages/functions/db/seed.sql not found', seedPath, 'Create db/seed.sql with idempotent INSERT OR IGNORE statements for demo/test data');
131
+ e('seed-sql-missing', 'packages/functions/db/sqlite-seed.sql not found', seedPath, 'Create db/sqlite-seed.sql with idempotent INSERT OR IGNORE statements for demo/test data');
132
132
  }
133
133
  }
134
134
  const appsDir = join(root, 'apps');
@@ -6,6 +6,7 @@ import { type SeedResult } from './sqlite/seed.js';
6
6
  import type { UserConfigShape } from '../commands/db-shared.js';
7
7
  interface ResolvedDbBase {
8
8
  migrationsDir: string;
9
+ seedFile: string;
9
10
  schemaFile: string;
10
11
  coercionFile: string;
11
12
  manifestFile: string;
@@ -16,7 +17,6 @@ export interface ResolvedSqliteDb extends ResolvedDbBase {
16
17
  dialect: 'sqlite';
17
18
  dbFile: string;
18
19
  runtimeDir: string;
19
- seedFile: string;
20
20
  }
21
21
  export interface ResolvedPostgresDb extends ResolvedDbBase {
22
22
  dialect: 'postgres';
@@ -17,8 +17,9 @@ import { PostgresIntrospector } from './postgres/postgres-introspector.js';
17
17
  * Returns null when neither sqliteDb nor postgresUrl is configured.
18
18
  */
19
19
  export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
20
- const base = (sub) => ({
20
+ const base = (sub, seedFileName) => ({
21
21
  migrationsDir: resolveAgainst(rootDir, sub),
22
+ seedFile: resolveAgainst(rootDir, seedFileName),
22
23
  schemaFile: join(outDir, 'db', 'schema.d.ts'),
23
24
  coercionFile: join(outDir, 'db', 'coercion.gen.ts'),
24
25
  manifestFile: join(outDir, 'db', 'classification.gen.ts'),
@@ -32,7 +33,7 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
32
33
  return {
33
34
  dialect: 'postgres',
34
35
  connectionString: userConfig.postgresUrl,
35
- ...base('db/postgres'),
36
+ ...base('db/postgres', 'db/postgres-seed.sql'),
36
37
  };
37
38
  }
38
39
  if (userConfig.sqliteDb) {
@@ -43,8 +44,7 @@ export function resolveDb(userConfig, rootDir, outDir, runtimeDir) {
43
44
  dialect: 'sqlite',
44
45
  dbFile: resolveAgainst(rootDir, userConfig.sqliteDb),
45
46
  runtimeDir: resolvedRuntimeDir,
46
- seedFile: resolveAgainst(rootDir, 'db/seed.sql'),
47
- ...base('db/sqlite'),
47
+ ...base('db/sqlite', 'db/sqlite-seed.sql'),
48
48
  };
49
49
  }
50
50
  return null;
@@ -142,10 +142,13 @@ export async function runWorkspaceValidate(startDir = process.cwd()) {
142
142
  if (!servicesText) {
143
143
  w('services-missing', 'packages/functions/src/services.ts not found', servicesPath, 'Create services.ts and export your service factory for the workspace');
144
144
  }
145
- const migrationsDir = join(fnDir, 'db', 'migrations');
146
145
  const authEnabled = await hasAuthSessionMiddleware(fnDir);
147
146
  const configText = await readTextSafe(join(fnDir, 'src', 'config.ts'));
148
147
  const hasConfiguredDevDb = /sqliteDb/.test(configText ?? '');
148
+ const hasPostgresUrl = /postgresUrl/.test(configText ?? '');
149
+ const migrationsDir = hasPostgresUrl
150
+ ? join(fnDir, 'db', 'postgres')
151
+ : join(fnDir, 'db', 'sqlite');
149
152
  let createsAppUser = false;
150
153
  let createsAuthVerificationToken = false;
151
154
  if (existsSync(migrationsDir)) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.26
2
+ * This file was generated by @pikku/cli@0.12.27
3
3
  */
4
4
  /**
5
5
  * Auto-generated remote internal RPC queue worker and HTTP endpoint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/cli",
3
- "version": "0.12.26",
3
+ "version": "0.12.27",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "imports": {
@@ -26,11 +26,11 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
29
- "@pikku/core": "^0.12.25",
29
+ "@pikku/core": "^0.12.26",
30
30
  "@pikku/deploy-cloudflare": "^0.12.3",
31
31
  "@pikku/fetch": "^0.12.2",
32
32
  "@pikku/inspector": "^0.12.13",
33
- "@pikku/kysely": "^0.12.12",
33
+ "@pikku/kysely": "^0.12.13",
34
34
  "@pikku/kysely-node-sqlite": "^0.12.1",
35
35
  "@pikku/node-http-server": "^0.12.1",
36
36
  "@pikku/openapi-parser": "^0.12.10",
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: pikku-middleware
3
+ description: 'Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware (including service-to-service bearer auth), per-route middleware, session-setting middleware, or understanding middleware execution order and priority.
4
+ TRIGGER when: user wants middleware on some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global interceptors, or middleware priority/order questions.
5
+ DO NOT TRIGGER when: user asks about permissions/authorization checks (use pikku-permissions), auth strategies like authBearer/authCookie (use pikku-security), or deployment.'
6
+ installGroups: [core]
7
+ ---
8
+
9
+ # Pikku Middleware
10
+
11
+ ## Agent Operating Procedure
12
+
13
+ 1. Discover before editing. Run `pikku info middleware --verbose` and `pikku info tags --json` to understand the existing middleware and tag landscape.
14
+ 2. Identify the source files that own the behavior — wirings files, not generated output.
15
+ 3. Register middleware at module load time — in a `wirings/*.ts` file, never inside a function body.
16
+ 4. Validate: run `pikku all` after adding or changing middleware; run `pikku tsc` to confirm type safety.
17
+
18
+ ## The `pikkuMiddleware` Factory
19
+
20
+ ```typescript
21
+ import { pikkuMiddleware } from '#pikku'
22
+
23
+ // Simple: just a function
24
+ const myMiddleware = pikkuMiddleware(async (services, wire, next) => {
25
+ // runs before the function
26
+ await next()
27
+ // runs after the function (optional)
28
+ })
29
+
30
+ // With metadata (name + priority)
31
+ const telemetryMiddleware = pikkuMiddleware({
32
+ name: 'my-telemetry',
33
+ priority: 'highest',
34
+ func: async (services, wire, next) => {
35
+ const start = performance.now()
36
+ try {
37
+ await next()
38
+ } finally {
39
+ services.logger.info({ duration: Math.round(performance.now() - start) })
40
+ }
41
+ },
42
+ })
43
+ ```
44
+
45
+ The `wire` object gives you:
46
+ - `wire.http` — inbound HTTP context (headers, URL, cookies)
47
+ - `wire.setSession(session)` — set the session for this request
48
+ - `wire.getSession()` — read the current session
49
+ - `wire.session` — the session set so far (may be undefined)
50
+
51
+ Throw a typed error to abort: `UnauthorizedError`, `ForbiddenError`, etc. from `@pikku/core/errors`.
52
+
53
+ ## Scoping: Five Levels
54
+
55
+ From broadest to narrowest:
56
+
57
+ ```typescript
58
+ // 1. Wire-agnostic global: all wire types (HTTP, Queue, Channel, Trigger, Workflow, ...)
59
+ addGlobalMiddleware([telemetryOuter()])
60
+
61
+ // 2. HTTP global: all HTTP routes
62
+ addHTTPMiddleware('*', [cors(), authBearer()])
63
+
64
+ // 3. Prefix-based: URL pattern
65
+ addHTTPMiddleware('/admin/*', [auditLog])
66
+
67
+ // 4. Tag-based: any wiring with matching tag
68
+ addTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire
69
+
70
+ // 5. Inline: per-wiring
71
+ wireHTTP({
72
+ route: '/books/:id',
73
+ func: getBook,
74
+ middleware: [cacheControl],
75
+ })
76
+ ```
77
+
78
+ ## Global Middleware (`addGlobalMiddleware`)
79
+
80
+ `addGlobalMiddleware` registers middleware that runs before everything else — across every wire type: HTTP, Queue, Channel, Trigger, Scheduler, Workflow, Agent, CLI, MCP. Use it for cross-cutting concerns like telemetry that must wrap every invocation regardless of transport.
81
+
82
+ ```typescript
83
+ import { addGlobalMiddleware } from '@pikku/core'
84
+ import { telemetryOuter, telemetryInner } from '@pikku/core/middleware'
85
+
86
+ // Outer telemetry: wraps the full call (highest priority)
87
+ addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })])
88
+
89
+ // Inner telemetry: closest to the function body (lowest priority)
90
+ addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })])
91
+ ```
92
+
93
+ `telemetryOuter` ships with `priority: 'highest'` and `telemetryInner` with `priority: 'lowest'` — so even if both are added in the same call, priority sorting places outer first regardless of array order.
94
+
95
+ ## HTTP & Prefix Middleware (`addHTTPMiddleware`)
96
+
97
+ ```typescript
98
+ import { addHTTPMiddleware } from '@pikku/core/http'
99
+ import { cors, authBearer } from '@pikku/core/middleware'
100
+
101
+ // All routes
102
+ addHTTPMiddleware('*', [cors({ origin: 'https://app.example.com', credentials: true })])
103
+
104
+ // Scoped to /api/* prefix
105
+ addHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])
106
+ ```
107
+
108
+ ## Tag Middleware (`addTagMiddleware`)
109
+
110
+ Tag middleware fires for any wiring (function or wire object) that carries a matching tag. This is the canonical approach for service-to-service bearer auth, rate limiting a group, or any cross-cutting concern scoped to a subset of routes.
111
+
112
+ ### Setting Tags
113
+
114
+ ```typescript
115
+ // On the function definition
116
+ export const myFunc = pikkuSessionlessFunc({
117
+ auth: false,
118
+ tags: ['machine-agent'],
119
+ func: async (services, input) => { ... },
120
+ })
121
+
122
+ // On the wire object
123
+ wireHTTP({
124
+ route: '/internal/action',
125
+ method: 'post',
126
+ auth: false,
127
+ tags: ['internal'],
128
+ func: myFunc,
129
+ })
130
+ ```
131
+
132
+ Tags from the function definition and the wire object are merged — middleware from both tag sets runs.
133
+
134
+ ### Registering Tag Middleware
135
+
136
+ ```typescript
137
+ import { addTagMiddleware } from '.pikku/pikku-types.gen.js'
138
+
139
+ addTagMiddleware('machine-agent', [machineAgentBearerAuth])
140
+ ```
141
+
142
+ Call at module load time — typically in the same `wirings/*.ts` file as the `wireHTTP` calls that use the tag.
143
+
144
+ ## Middleware Execution Order
145
+
146
+ **Scope resolution order (broadest → narrowest):**
147
+
148
+ ```
149
+ global → httpGroup/* → httpGroup/prefix → wiringTags → wiringMiddleware → funcTags → funcMiddleware → function body
150
+ ```
151
+
152
+ **Within each scope, sorted by priority:**
153
+
154
+ ```
155
+ highest → high → medium (default) → low → lowest
156
+ ```
157
+
158
+ Set priority using the config-object form of `pikkuMiddleware`:
159
+
160
+ ```typescript
161
+ const earlyMiddleware = pikkuMiddleware({
162
+ name: 'early',
163
+ priority: 'highest', // 'highest' | 'high' | 'medium' | 'low' | 'lowest'
164
+ func: async (services, wire, next) => { ... },
165
+ })
166
+ ```
167
+
168
+ Within the same priority level, registration order is preserved. Priority is the primary sort key — use it when a middleware must run before or after others regardless of registration order (e.g. telemetry wrapping everything, session extraction before auth checks).
169
+
170
+ ## Common Patterns
171
+
172
+ ### Service-to-Service Bearer Auth
173
+
174
+ The canonical pattern for a server that exposes RPCs only to a trusted caller (e.g. an API calling a machine-agent):
175
+
176
+ **On the server (the service being called):**
177
+
178
+ ```typescript
179
+ // lib/host-token.ts
180
+ let _token: string | null = null
181
+ export const setToken = (t: string) => { _token = t }
182
+ export const getToken = () => _token
183
+ ```
184
+
185
+ ```typescript
186
+ // wirings/http.wiring.ts
187
+ import { timingSafeEqual } from 'node:crypto'
188
+ import { addTagMiddleware, pikkuMiddleware } from '../../.pikku/pikku-types.gen.js'
189
+ import { UnauthorizedError } from '@pikku/core/errors'
190
+ import { getToken } from '../lib/host-token.js'
191
+
192
+ const bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {
193
+ const authHeader = http?.request?.header?.('authorization') || http?.request?.header?.('Authorization')
194
+ const token = getToken()
195
+ const expected = token ? `Bearer ${token}` : null
196
+ if (
197
+ !expected ||
198
+ !authHeader ||
199
+ authHeader.length !== expected.length ||
200
+ !timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))
201
+ ) {
202
+ throw new UnauthorizedError()
203
+ }
204
+ return next()
205
+ })
206
+
207
+ addTagMiddleware('machine-agent', [bearerAuth])
208
+ ```
209
+
210
+ ```typescript
211
+ // functions/my.function.ts
212
+ export const myFunc = pikkuSessionlessFunc({
213
+ expose: true,
214
+ auth: false,
215
+ tags: ['machine-agent'],
216
+ func: async (services, input) => { ... },
217
+ })
218
+ ```
219
+
220
+ **On the client (the caller):**
221
+
222
+ Use the generated `RPCInvoke` type from `.pikku/rpc/pikku-rpc-wirings-map.gen.d.ts` — never hand-write the input/output types:
223
+
224
+ ```typescript
225
+ import type { RPCInvoke } from '../../backends/my-service/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'
226
+
227
+ export function getServiceRPC(baseUrl: string, token: string): RPCInvoke {
228
+ return async (name: string, data?: unknown) => {
229
+ const res = await fetch(`${baseUrl}/rpc/${String(name)}`, {
230
+ method: 'POST',
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ Authorization: `Bearer ${token}`,
234
+ },
235
+ body: JSON.stringify({ data: data ?? {} }),
236
+ })
237
+ if (!res.ok) {
238
+ const text = await res.text().catch(() => '')
239
+ throw new Error(`rpc ${String(name)} failed: ${res.status} ${text}`)
240
+ }
241
+ return res.json()
242
+ } as RPCInvoke
243
+ }
244
+ ```
245
+
246
+ ### Session-Setting Middleware
247
+
248
+ ```typescript
249
+ const apiKeyAuth = pikkuMiddleware(async ({ kysely }, { http, setSession, session }, next) => {
250
+ if (session) return next() // already authenticated
251
+
252
+ const header = http?.request?.header?.('x-api-key')
253
+ if (!header) return next()
254
+
255
+ const row = await kysely.selectFrom('apiKey').select('userId').where('key', '=', header).executeTakeFirst()
256
+ if (row) setSession?.({ userId: row.userId })
257
+
258
+ return next()
259
+ })
260
+
261
+ addTagMiddleware('api-key-auth', [apiKeyAuth])
262
+ ```
263
+
264
+ Functions tagged `'api-key-auth'` with `auth: true` reject requests without a valid key; those with `auth: false` can inspect the session but won't reject.
265
+
266
+ ### Request Logging / Audit
267
+
268
+ ```typescript
269
+ const auditLog = pikkuMiddleware(async ({ logger, db }, wire, next) => {
270
+ const start = Date.now()
271
+ await next()
272
+ await db.createAuditLog({ duration: Date.now() - start })
273
+ })
274
+
275
+ addHTTPMiddleware('/admin/*', [auditLog])
276
+ ```
277
+
278
+ ## After Changes
279
+
280
+ ```bash
281
+ pikku all # regenerate metadata so new tags are picked up
282
+ pikku tsc # type-check
283
+ ```
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: pikku-permissions
3
+ description: 'Use when adding authorization checks to Pikku functions or routes — pikkuPermission, pikkuAuth, per-function permissions, pattern-based permissions, or understanding OR/AND permission logic.
4
+ TRIGGER when: user wants to restrict who can call a function, check resource ownership, add role-based access, or understand where permission checks belong.
5
+ DO NOT TRIGGER when: user asks about middleware or request interception (use pikku-middleware), authentication strategies (use pikku-security), or session management.'
6
+ installGroups: [core]
7
+ ---
8
+
9
+ # Pikku Permissions
10
+
11
+ ## The Rule
12
+
13
+ **ALWAYS put authorization checks in the `permissions` field of `pikkuFunc` or `pikkuSessionlessFunc` — NEVER inside the `func` body.**
14
+
15
+ This includes: org access checks, repo access checks, role checks, resource ownership, and any other authorization logic. The `permissions` field runs before `func`, is visible to the inspector, and is the only place Pikku enforces authorization.
16
+
17
+ ```typescript
18
+ // CORRECT
19
+ export const deleteBook = pikkuFunc({
20
+ func: async ({ db }, { bookId }) => {
21
+ await db.deleteBook(bookId)
22
+ },
23
+ permissions: {
24
+ owner: isBookOwner, // ← authorization here
25
+ },
26
+ })
27
+
28
+ // WRONG — permission check inside func body
29
+ export const deleteBook = pikkuFunc({
30
+ func: async ({ db }, { bookId }, { session }) => {
31
+ if (!session) throw new UnauthorizedError() // ← never do this
32
+ await db.deleteBook(bookId)
33
+ },
34
+ })
35
+ ```
36
+
37
+ ## Agent Operating Procedure
38
+
39
+ 1. Discover before editing. Run `pikku info permissions --verbose` and `pikku info functions --verbose` to understand what permissions are already defined and applied.
40
+ 2. Define permission checkers in a `src/permissions.ts` or domain-specific `src/lib/*-permissions.ts` file.
41
+ 3. Apply them via the `permissions` field on the function, or via `addHTTPPermission` / `addPermission` for pattern/tag-based application.
42
+ 4. Validate: run `pikku tsc` to confirm permission checker signatures are correct.
43
+
44
+ ## Permission Factories
45
+
46
+ ### `pikkuAuth(fn)` — Session-Only Checks
47
+
48
+ Use for checks that only need the session — no request data required.
49
+
50
+ ```typescript
51
+ import { pikkuAuth } from '#pikku'
52
+
53
+ export const isAuthenticated = pikkuAuth(
54
+ async (_services, session) => !!session
55
+ )
56
+
57
+ export const isAdmin = pikkuAuth(
58
+ async (_services, session) => session?.role === 'admin'
59
+ )
60
+ ```
61
+
62
+ ### `pikkuPermission(fn)` — Data-Aware Checks
63
+
64
+ Use when authorization depends on the actual request data (e.g., resource ownership).
65
+
66
+ ```typescript
67
+ import { pikkuPermission } from '#pikku'
68
+
69
+ export const isBookOwner = pikkuPermission(
70
+ async ({ db }, { bookId }, { session }) => {
71
+ const book = await db.getBook(bookId)
72
+ return book?.authorId === session?.userId
73
+ }
74
+ )
75
+
76
+ export const hasBookAccess = pikkuPermission(
77
+ async ({ db }, { bookId }, { session }) => {
78
+ return await db.hasAccess(session?.userId, bookId)
79
+ }
80
+ )
81
+ ```
82
+
83
+ ## OR / AND Logic
84
+
85
+ ```typescript
86
+ permissions: {
87
+ admin: isAdmin, // OR: admins can access
88
+ owner: isBookOwner, // OR: owners can access
89
+ reviewer: [isAuthenticated, hasBookAccess], // AND: both must pass
90
+ }
91
+ // Logic: admin OR owner OR (isAuthenticated AND hasBookAccess)
92
+ ```
93
+
94
+ Groups are OR'd. Entries within a group array are AND'd.
95
+
96
+ ## Where to Apply Permissions
97
+
98
+ ### Per-Function (preferred)
99
+
100
+ ```typescript
101
+ export const deleteBook = pikkuFunc({
102
+ func: async ({ db }, { bookId }) => {
103
+ await db.deleteBook(bookId)
104
+ },
105
+ permissions: {
106
+ admin: isAdmin,
107
+ owner: isBookOwner,
108
+ },
109
+ })
110
+ ```
111
+
112
+ ### Pattern-Based (`addHTTPPermission`)
113
+
114
+ ```typescript
115
+ import { addHTTPPermission } from '@pikku/core/http'
116
+
117
+ addHTTPPermission('/admin/*', { admin: isAdmin })
118
+ ```
119
+
120
+ ### Tag-Based (`addPermission`)
121
+
122
+ ```typescript
123
+ import { addPermission } from '.pikku/pikku-types.gen.js'
124
+
125
+ addPermission('internal', { machine: isMachineAgent })
126
+ ```
127
+
128
+ ## Complete Example
129
+
130
+ ```typescript
131
+ // src/permissions.ts
132
+ import { pikkuAuth, pikkuPermission } from '#pikku'
133
+
134
+ export const isAuthenticated = pikkuAuth(
135
+ async (_services, session) => !!session
136
+ )
137
+
138
+ export const isAdmin = pikkuAuth(
139
+ async (_services, session) => session?.role === 'admin'
140
+ )
141
+
142
+ export const isOrgMember = pikkuPermission(
143
+ async ({ db }, { orgId }, { session }) => {
144
+ return await db.isMember(session?.userId, orgId)
145
+ }
146
+ )
147
+
148
+ // src/functions/org.function.ts
149
+ export const deleteOrg = pikkuFunc({
150
+ func: async ({ db }, { orgId }) => {
151
+ await db.deleteOrg(orgId)
152
+ },
153
+ permissions: {
154
+ admin: isAdmin,
155
+ owner: [isAuthenticated, isOrgMember],
156
+ },
157
+ })
158
+ ```
159
+
160
+ ## After Changes
161
+
162
+ ```bash
163
+ pikku tsc # verify permission checker types are correct
164
+ pikku all # regenerate if wirings changed
165
+ ```