@pikku/skills 0.12.10 → 0.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +819 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +4 -4
- package/skills/pikku-addon/SKILL.md +17 -11
- package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
- package/skills/pikku-agent/SKILL.md +4 -5
- package/skills/pikku-audit/SKILL.md +28 -13
- package/skills/pikku-aws/SKILL.md +2 -2
- package/skills/pikku-better-auth/SKILL.md +97 -17
- package/skills/pikku-build-app/SKILL.md +621 -0
- package/skills/pikku-build-app/references/multi-app.md +117 -0
- package/skills/pikku-build-app/references/ship.md +98 -0
- package/skills/pikku-build-app/references/theming.md +70 -0
- package/skills/pikku-build-platform/SKILL.md +239 -0
- package/skills/pikku-build-quick/SKILL.md +238 -0
- package/skills/pikku-cli/SKILL.md +7 -7
- package/skills/pikku-cli/references/complete-example.md +1 -1
- package/skills/pikku-concepts/SKILL.md +5 -2
- package/skills/pikku-concepts/references/concept-mapping.md +1 -1
- package/skills/pikku-config/SKILL.md +5 -3
- package/skills/pikku-deploy-azure/SKILL.md +5 -4
- package/skills/pikku-emails/SKILL.md +28 -7
- package/skills/pikku-fabric/SKILL.md +27 -3
- package/skills/pikku-fabric-debug/SKILL.md +1 -1
- package/skills/pikku-feature/SKILL.md +5 -4
- package/skills/pikku-http/SKILL.md +4 -4
- package/skills/pikku-http/references/http-options.md +13 -13
- package/skills/pikku-i18n/SKILL.md +2 -1
- package/skills/pikku-info/SKILL.md +1 -1
- package/skills/pikku-knowledge/SKILL.md +13 -13
- package/skills/pikku-mcp/SKILL.md +4 -4
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
- package/skills/pikku-n8n-import/SKILL.md +12 -12
- package/skills/pikku-n8n-import/SPEC.md +3 -0
- package/skills/pikku-n8n-import/references/code-translation.md +26 -22
- package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
- package/skills/pikku-paraglide/SKILL.md +11 -6
- package/skills/pikku-permissions/SKILL.md +19 -15
- package/skills/pikku-product-second-opinion/README.md +3 -3
- package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
- package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
- package/skills/pikku-queue/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +53 -13
- package/skills/pikku-realtime/SKILL.md +52 -21
- package/skills/pikku-rpc/SKILL.md +4 -2
- package/skills/pikku-rtl/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +131 -20
- package/skills/pikku-schedule/SKILL.md +6 -1
- package/skills/pikku-schema-ajv/SKILL.md +2 -2
- package/skills/pikku-schema-cfworker/SKILL.md +1 -1
- package/skills/pikku-security/SKILL.md +9 -5
- package/skills/pikku-services/SKILL.md +27 -18
- package/skills/pikku-services/references/audit-wire-service.md +14 -8
- package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
- package/skills/pikku-tag-middleware/SKILL.md +1 -0
- package/skills/pikku-template-clone/SKILL.md +2 -1
- package/skills/pikku-trigger/SKILL.md +3 -3
- package/skills/pikku-websocket/SKILL.md +4 -3
- package/skills/pikku-workflow/SKILL.md +2 -2
- package/skills/pikku-workflow/references/workflow-reference.md +24 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/skills",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.12",
|
|
4
4
|
"description": "The Pikku agent skills — the instruction set coding agents read to build, wire and deploy Pikku projects",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"skills"
|
|
27
27
|
],
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@types/node": "^24.
|
|
29
|
+
"@types/node": "^24.13.3",
|
|
30
30
|
"typescript": "^6.0.3",
|
|
31
|
-
"yaml": "^2.
|
|
31
|
+
"yaml": "^2.9.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=24"
|
|
35
35
|
}
|
|
36
|
-
}
|
|
36
|
+
}
|
|
@@ -40,7 +40,7 @@ See `pikku-concepts` for the core mental model.
|
|
|
40
40
|
Register an addon in the consuming project:
|
|
41
41
|
|
|
42
42
|
```typescript
|
|
43
|
-
import { wireAddon } from '#pikku'
|
|
43
|
+
import { wireAddon } from '#pikku/addon'
|
|
44
44
|
|
|
45
45
|
wireAddon({
|
|
46
46
|
name: string, // Namespace for addon functions (e.g. 'todos')
|
|
@@ -123,7 +123,7 @@ Type-safe reference to a function — local or addon — for use in any wiring.
|
|
|
123
123
|
returns a function config that proxies the call via RPC at runtime:
|
|
124
124
|
|
|
125
125
|
```typescript
|
|
126
|
-
import { ref } from '#pikku'
|
|
126
|
+
import { ref } from '#pikku/function'
|
|
127
127
|
|
|
128
128
|
ref('todos:addTodo') // namespace:functionName for an addon function
|
|
129
129
|
ref('myLocalFunc') // a local function by name
|
|
@@ -134,7 +134,7 @@ There is no `addon()` helper; `ref()` covers both. For an addon that publishes
|
|
|
134
134
|
`refChannel` and `refCLI`, which carry the addon's own route/config metadata:
|
|
135
135
|
|
|
136
136
|
```typescript
|
|
137
|
-
import { refHTTP } from '#pikku'
|
|
137
|
+
import { refHTTP } from '#pikku/function'
|
|
138
138
|
|
|
139
139
|
wireHTTP(refHTTP('todos:listTodos', { basePath: '/api' }))
|
|
140
140
|
```
|
|
@@ -146,7 +146,7 @@ second argument is always present — an addon never falls back to its own logge
|
|
|
146
146
|
variables or secrets; the consuming app supplies them:
|
|
147
147
|
|
|
148
148
|
```typescript
|
|
149
|
-
import { pikkuAddonServices } from '#pikku'
|
|
149
|
+
import { pikkuAddonServices } from '#pikku/addon/setup'
|
|
150
150
|
|
|
151
151
|
export const createSingletonServices = pikkuAddonServices(
|
|
152
152
|
async (config, { secrets, logger }) => {
|
|
@@ -167,7 +167,7 @@ config object.
|
|
|
167
167
|
Define per-request services for an addon package (created fresh per HTTP request, queue job, etc.):
|
|
168
168
|
|
|
169
169
|
```typescript
|
|
170
|
-
import { pikkuAddonWireServices } from '#pikku'
|
|
170
|
+
import { pikkuAddonWireServices } from '#pikku/addon/setup'
|
|
171
171
|
|
|
172
172
|
export const createWireServices = pikkuAddonWireServices(
|
|
173
173
|
async (singletonServices, wire) => {
|
|
@@ -195,7 +195,7 @@ This generates `package.json` (exports `.pikku/*` + `dist/`), `pikku.config.json
|
|
|
195
195
|
|
|
196
196
|
```typescript
|
|
197
197
|
// src/services.ts
|
|
198
|
-
import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku'
|
|
198
|
+
import { pikkuAddonServices, pikkuAddonWireServices } from '#pikku/addon/setup'
|
|
199
199
|
import { TodoStore } from './todo-store.service.js'
|
|
200
200
|
|
|
201
201
|
export const createSingletonServices = pikkuAddonServices(async () => {
|
|
@@ -213,10 +213,14 @@ export const createWireServices = pikkuAddonWireServices(
|
|
|
213
213
|
|
|
214
214
|
### Functions
|
|
215
215
|
|
|
216
|
+
An addon generates its whole tree under `#pikku/addon/*`, so it authors against
|
|
217
|
+
`#pikku/addon/function`, `#pikku/addon/http` and so on. An application's leaves
|
|
218
|
+
stay flat, which is what stops a linked addon resolving against its host.
|
|
219
|
+
|
|
216
220
|
```typescript
|
|
217
221
|
// src/functions/addTodo.function.ts
|
|
218
222
|
import { z } from 'zod'
|
|
219
|
-
import { pikkuSessionlessFunc } from '#pikku'
|
|
223
|
+
import { pikkuSessionlessFunc } from '#pikku/addon/function'
|
|
220
224
|
|
|
221
225
|
const AddTodoInput = z.object({ title: z.string() })
|
|
222
226
|
const AddTodoOutput = z.object({ id: z.string(), title: z.string() })
|
|
@@ -269,7 +273,7 @@ yarn add @my-org/addon-todos
|
|
|
269
273
|
|
|
270
274
|
```typescript
|
|
271
275
|
// wirings/todos.wirings.ts
|
|
272
|
-
import { wireAddon } from '#pikku'
|
|
276
|
+
import { wireAddon } from '#pikku/addon'
|
|
273
277
|
|
|
274
278
|
wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
|
|
275
279
|
```
|
|
@@ -290,7 +294,8 @@ export const myFunc = pikkuFunc({
|
|
|
290
294
|
### Wire to HTTP
|
|
291
295
|
|
|
292
296
|
```typescript
|
|
293
|
-
import { wireHTTP
|
|
297
|
+
import { wireHTTP } from '#pikku/http'
|
|
298
|
+
import { ref } from '#pikku/function'
|
|
294
299
|
|
|
295
300
|
wireHTTP({
|
|
296
301
|
method: 'get',
|
|
@@ -303,7 +308,8 @@ wireHTTP({
|
|
|
303
308
|
Or batch multiple addon routes with `defineHTTPRoutes` + `wireHTTPRoutes`:
|
|
304
309
|
|
|
305
310
|
```typescript
|
|
306
|
-
import { wireHTTPRoutes, defineHTTPRoutes
|
|
311
|
+
import { wireHTTPRoutes, defineHTTPRoutes } from '#pikku/http'
|
|
312
|
+
import { ref } from '#pikku/function'
|
|
307
313
|
|
|
308
314
|
const todoRoutes = defineHTTPRoutes({
|
|
309
315
|
tags: ['todos'],
|
|
@@ -321,7 +327,7 @@ wireHTTPRoutes({ basePath: '/api', routes: { todos: todoRoutes } })
|
|
|
321
327
|
|
|
322
328
|
```typescript
|
|
323
329
|
import { pikkuAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
324
|
-
import { ref } from '#pikku'
|
|
330
|
+
import { ref } from '#pikku/function'
|
|
325
331
|
|
|
326
332
|
export const todoAgent = pikkuAgent({
|
|
327
333
|
name: 'todo-agent',
|
|
@@ -40,8 +40,8 @@ my-addon/
|
|
|
40
40
|
{
|
|
41
41
|
"name": "@my-org/addon-todos",
|
|
42
42
|
"imports": {
|
|
43
|
-
"#pikku": "./.pikku
|
|
44
|
-
"#pikku/*": "./.pikku
|
|
43
|
+
"#pikku/*.js": "./.pikku/*.ts",
|
|
44
|
+
"#pikku/*": "./.pikku/*/index.ts"
|
|
45
45
|
},
|
|
46
46
|
"exports": {
|
|
47
47
|
".": { "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" },
|
|
@@ -41,7 +41,7 @@ Import it from the generated agent types file — `#pikku` does not re-export it
|
|
|
41
41
|
|
|
42
42
|
```typescript
|
|
43
43
|
import { pikkuAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
44
|
-
import { ref } from '#pikku/
|
|
44
|
+
import { ref } from '#pikku/function'
|
|
45
45
|
|
|
46
46
|
pikkuAgent({
|
|
47
47
|
name: string, // Unique agent identifier
|
|
@@ -179,7 +179,7 @@ than folding it into the parent's transcript.
|
|
|
179
179
|
|
|
180
180
|
```typescript
|
|
181
181
|
import { pikkuAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
182
|
-
import { ref } from '#pikku/
|
|
182
|
+
import { ref } from '#pikku/function'
|
|
183
183
|
|
|
184
184
|
export const todoAgent = pikkuAgent({
|
|
185
185
|
name: 'todo-agent',
|
|
@@ -201,8 +201,7 @@ export const todoAgent = pikkuAgent({
|
|
|
201
201
|
### Scaffold the HTTP surface
|
|
202
202
|
|
|
203
203
|
```bash
|
|
204
|
-
pikku enable agent
|
|
205
|
-
pikku enable agent --noAuth # public
|
|
204
|
+
pikku enable agent
|
|
206
205
|
```
|
|
207
206
|
|
|
208
207
|
The next `pikku all` generates `agent.gen.ts` — run/stream/approve/resume
|
|
@@ -283,7 +282,7 @@ export const completeTodo = pikkuFunc({
|
|
|
283
282
|
|
|
284
283
|
// agents/todo-assistant.agent.ts
|
|
285
284
|
import { pikkuAgent } from '#pikku/agent/pikku-agent-types.gen.js'
|
|
286
|
-
import { ref } from '#pikku/
|
|
285
|
+
import { ref } from '#pikku/function'
|
|
287
286
|
|
|
288
287
|
export const todoAssistant = pikkuAgent({
|
|
289
288
|
name: 'todo-assistant',
|
|
@@ -38,11 +38,13 @@ An event only persists when the function opts in with **`audit: true`** — othe
|
|
|
38
38
|
```typescript
|
|
39
39
|
import { NoopAuditService, createInvocationAudit } from '@pikku/core/services'
|
|
40
40
|
|
|
41
|
-
export const createSingletonServices = pikkuServices(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
41
|
+
export const createSingletonServices = pikkuServices(
|
|
42
|
+
async (config, existing) => {
|
|
43
|
+
// Prod platforms may inject a queue-backed sink as existing.audit.
|
|
44
|
+
const audit = existing?.audit ?? new NoopAuditService()
|
|
45
|
+
return { ...existing, config, /* ... */ audit }
|
|
46
|
+
}
|
|
47
|
+
)
|
|
46
48
|
|
|
47
49
|
// auditLog is created per invocation from the sink. Returned unconditionally so
|
|
48
50
|
// a write from a function that forgot `audit: true` warns instead of vanishing.
|
|
@@ -67,12 +69,17 @@ Mark the function `audit: true` and call `auditLog?.write(...)`. Domain history
|
|
|
67
69
|
|
|
68
70
|
```typescript
|
|
69
71
|
export const cancelInvoice = pikkuFunc({
|
|
70
|
-
audit: true,
|
|
72
|
+
audit: true, // REQUIRED — else write() is a no-op
|
|
71
73
|
input: CancelInvoiceInput,
|
|
72
74
|
output: CancelInvoiceOutput,
|
|
73
75
|
func: async ({ kysely, auditLog }, { invoiceId }, { session }) => {
|
|
74
|
-
const inv = await kysely
|
|
75
|
-
|
|
76
|
+
const inv = await kysely
|
|
77
|
+
.selectFrom('invoice') /* ... */
|
|
78
|
+
.executeTakeFirstOrThrow()
|
|
79
|
+
await kysely
|
|
80
|
+
.updateTable('invoice')
|
|
81
|
+
.set({ status: 'cancelled' }) /* ... */
|
|
82
|
+
.execute()
|
|
76
83
|
|
|
77
84
|
await auditLog?.write({
|
|
78
85
|
type: 'invoice.update',
|
|
@@ -108,7 +115,10 @@ import { createAuditedKysely } from '@pikku/kysely'
|
|
|
108
115
|
export const createWireServices = pikkuWireServices(async (services, wire) => {
|
|
109
116
|
if (!services.audit) return {}
|
|
110
117
|
const auditLog = createInvocationAudit(services.audit, wire)
|
|
111
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
auditLog,
|
|
120
|
+
kysely: createAuditedKysely(services.kysely, { audit: auditLog }),
|
|
121
|
+
}
|
|
112
122
|
})
|
|
113
123
|
```
|
|
114
124
|
|
|
@@ -172,15 +182,20 @@ const rows = await kysely
|
|
|
172
182
|
|
|
173
183
|
```typescript
|
|
174
184
|
type AuditEvent = {
|
|
175
|
-
type: string
|
|
185
|
+
type: string // e.g. 'invoice.update'
|
|
176
186
|
source: 'auto' | 'explicit'
|
|
177
|
-
occurredAt: string
|
|
187
|
+
occurredAt: string // auto-filled by auditLog
|
|
178
188
|
eventId?: string
|
|
179
189
|
outcome?: 'success' | 'failed' | 'denied'
|
|
180
|
-
functionId
|
|
190
|
+
functionId?
|
|
191
|
+
wireType?
|
|
192
|
+
wireId?
|
|
193
|
+
traceId?
|
|
194
|
+
transactionId?
|
|
195
|
+
queryId? // auto
|
|
181
196
|
userIdentity?: { userId?; orgId?; pikkuUserId? } // auto from wire session
|
|
182
197
|
input?: unknown
|
|
183
|
-
metadata?: Record<string, unknown>
|
|
198
|
+
metadata?: Record<string, unknown> // your domain payload
|
|
184
199
|
}
|
|
185
200
|
```
|
|
186
201
|
|
|
@@ -64,12 +64,12 @@ provision an S3 bucket per logical bucket — the config takes only one.
|
|
|
64
64
|
|
|
65
65
|
### Behaviours worth knowing before you rely on them
|
|
66
66
|
|
|
67
|
-
- **`signURL` fails open.** A signing error is logged and the
|
|
67
|
+
- **`signURL` fails open.** A signing error is logged and the _unsigned_ URL is
|
|
68
68
|
returned rather than thrown. If your CloudFront distribution is private the
|
|
69
69
|
client then gets a 403; if it isn't, you have just handed out an unrestricted
|
|
70
70
|
link. Check that `signConfig` is a valid CloudFront key pair at boot.
|
|
71
71
|
- **`signContentKey` builds `https://<bucketName>/<bucket>/<contentKey>`** — it
|
|
72
|
-
uses `bucketName` as the
|
|
72
|
+
uses `bucketName` as the _host_. For signed content the value must therefore be
|
|
73
73
|
your CloudFront domain, not a plain bucket name, which also means the same
|
|
74
74
|
config field is doing two jobs.
|
|
75
75
|
- **Presigned upload URLs expire after a fixed 3600s.** It is not configurable
|
|
@@ -5,7 +5,8 @@ description: >-
|
|
|
5
5
|
the generated catch-all auth routes, betterAuthSession middleware, OAuth/social providers,
|
|
6
6
|
email+password credentials, database adapters, and session mapping. TRIGGER when: code uses
|
|
7
7
|
pikkuBetterAuth, betterAuth, betterAuthSession, createAuthHandler, user asks about Better Auth,
|
|
8
|
-
OAuth/social providers, MFA, organizations, login/logout, or @pikku/better-auth. TRIGGER when:
|
|
8
|
+
OAuth/social providers, MFA, organizations, login/logout, or @pikku/better-auth. TRIGGER when: user asks
|
|
9
|
+
about the actor plugin, /sign-in/actor, signing in as a scenario persona, or SCENARIO_ACTOR_SECRET. TRIGGER when:
|
|
9
10
|
user asks about ANY form of authentication, login, logout, sessions, or user identity — always
|
|
10
11
|
answer with this skill. DO NOT TRIGGER when: user asks about JWT middleware (use pikku-security)
|
|
11
12
|
or custom session services (use pikku-services).
|
|
@@ -146,6 +147,11 @@ strings):
|
|
|
146
147
|
| delete a user and their data | `admin:users:remove` |
|
|
147
148
|
| revoke a user's sessions | `admin:users:sessions` |
|
|
148
149
|
| set a user's password | `admin:users:password` |
|
|
150
|
+
| read credential values and who holds them | `admin:credentials:read` |
|
|
151
|
+
| set and delete credentials | `admin:credentials:manage` |
|
|
152
|
+
| view declared scopes, roles, and who holds them | `admin:scopes:read` |
|
|
153
|
+
| create roles, change their scopes, grant them | `admin:scopes:manage` |
|
|
154
|
+
| read the audit trail | `admin:audit:read` |
|
|
149
155
|
|
|
150
156
|
Holding the bare `admin` scope satisfies all of them — a parent grant covers
|
|
151
157
|
everything nested beneath it — so `admin` is the direct replacement for the old
|
|
@@ -166,6 +172,8 @@ defineScope({
|
|
|
166
172
|
description: 'Application-wide credentials',
|
|
167
173
|
scopes: {
|
|
168
174
|
link: { description: 'Bind a shared credential for every user' },
|
|
175
|
+
read: { description: 'Read credential values and who holds them' },
|
|
176
|
+
manage: { description: 'Set and delete credentials' },
|
|
169
177
|
},
|
|
170
178
|
},
|
|
171
179
|
users: {
|
|
@@ -179,6 +187,27 @@ defineScope({
|
|
|
179
187
|
password: { description: "Set a user's password" },
|
|
180
188
|
},
|
|
181
189
|
},
|
|
190
|
+
scopes: {
|
|
191
|
+
description: 'Authorization management',
|
|
192
|
+
scopes: {
|
|
193
|
+
read: {
|
|
194
|
+
description: 'View declared scopes, roles, and who holds them',
|
|
195
|
+
},
|
|
196
|
+
manage: {
|
|
197
|
+
description:
|
|
198
|
+
'Create and delete roles, change their scopes, and grant roles to users',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
audit: {
|
|
203
|
+
description: 'The audit trail',
|
|
204
|
+
scopes: {
|
|
205
|
+
read: {
|
|
206
|
+
description:
|
|
207
|
+
'Read the audit trail — every recorded action, and which user took it',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
182
211
|
},
|
|
183
212
|
},
|
|
184
213
|
})
|
|
@@ -191,22 +220,32 @@ a scope, so nothing is authorized, and the denial is logged at `warn` because
|
|
|
191
220
|
that is a configuration bug rather than a permissions decision. Pass your own
|
|
192
221
|
`canImpersonate` / `canLinkSingleton` to override the default entirely.
|
|
193
222
|
|
|
194
|
-
###
|
|
223
|
+
### Do not wire better-auth's `admin()` plugin
|
|
224
|
+
|
|
225
|
+
Every capability in the table is pikku's own, gated by the scope next to it and
|
|
226
|
+
implemented against better-auth's internal adapter — `createAuthUser`,
|
|
227
|
+
`setAuthUserPassword`, `setAuthUserBanned`, `deleteAuthUser` and
|
|
228
|
+
`revokeAuthUserSessions`, all exported from `@pikku/better-auth`.
|
|
195
229
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`admin:users:{create,ban,remove,sessions,password}`, and the plugin's
|
|
202
|
-
`defaultRole` otherwise. `projectedAdminRole(scopes, defaultRole)` computes the
|
|
203
|
-
value if you need it yourself.
|
|
230
|
+
`admin()` would add a second gate on a `user.role` column that pikku otherwise
|
|
231
|
+
ignores, which means maintaining two grant systems that have to agree — and the
|
|
232
|
+
column loses, since the scope store is what the rest of the framework reads.
|
|
233
|
+
Pikku used to project scopes onto it for exactly that reason; dropping the
|
|
234
|
+
plugin dropped the projection with it.
|
|
204
235
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
236
|
+
Banning is the one capability with a schema requirement, and it has its own
|
|
237
|
+
small plugin:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { ban } from '@pikku/better-auth'
|
|
241
|
+
|
|
242
|
+
betterAuth({ plugins: [ban()] })
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`ban()` adds `banned`, `banReason` and `banExpires` to `user` and refuses to
|
|
246
|
+
create a session for a banned user, lapsing an expired ban as it goes. It makes
|
|
247
|
+
no authorization decision — who may ban is decided by `admin:users:ban` — so it
|
|
248
|
+
never needs to know about scopes or roles.
|
|
210
249
|
|
|
211
250
|
### 2. Production database adapter
|
|
212
251
|
|
|
@@ -273,7 +312,7 @@ export const auth = pikkuBetterAuth(async ({ secrets, variables }) => {
|
|
|
273
312
|
Functions that require a session use `pikkuFunc` — anonymous callers are rejected automatically. `betterAuthSession` has already bridged better-auth's session into `session`:
|
|
274
313
|
|
|
275
314
|
```typescript
|
|
276
|
-
import { pikkuFunc } from '#pikku'
|
|
315
|
+
import { pikkuFunc } from '#pikku/function'
|
|
277
316
|
|
|
278
317
|
export const me = pikkuFunc({
|
|
279
318
|
expose: true,
|
|
@@ -311,11 +350,52 @@ The session cookie is `better-auth.session_token` (dev) / `__Secure-better-auth.
|
|
|
311
350
|
|
|
312
351
|
Set `PIKKU_DEV_QUICK_LOGIN=true` and `${basePath}/dev/quick-login` signs in a
|
|
313
352
|
fixed dev admin (`admin@pikku.dev`), creating the user idempotently and granting
|
|
314
|
-
it the bare `admin` scope. It is guarded twice — the env var
|
|
353
|
+
it the bare `admin` scope. It is guarded twice — the env var _and_ a localhost
|
|
315
354
|
hostname check — because a one-request path to an admin session is exactly the
|
|
316
355
|
thing that must not survive a deploy. An app that has not declared the `admin`
|
|
317
356
|
scope still gets a session, with a warning, since a scopeless dev user is useful.
|
|
318
357
|
|
|
358
|
+
### Actor sign-in (`actor` plugin)
|
|
359
|
+
|
|
360
|
+
A different thing from dev quick login, and the one to reach for when "sign in as
|
|
361
|
+
someone" means **a particular kind of user** rather than one fixed admin.
|
|
362
|
+
Register it explicitly — it is not automatic:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { actor } from '@pikku/better-auth'
|
|
366
|
+
|
|
367
|
+
plugins: [actor({ secret: SCENARIO_ACTOR_SECRET })]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`POST ${basePath}/sign-in/actor` `{ email, secret, name? }` → 200 + the normal
|
|
371
|
+
session cookie. `secret` may also be a (possibly async) function, so it can come
|
|
372
|
+
off the secrets service instead of a captured value.
|
|
373
|
+
|
|
374
|
+
**`SCENARIO_ACTOR_SECRET` is a credential as powerful as the most privileged
|
|
375
|
+
persona.** `pikku persona sync` grants declared roles to actor accounts, so an
|
|
376
|
+
`admin` persona is an actor holding real admin — anyone with the secret can take
|
|
377
|
+
a session as one. Keep the endpoint **off outside development and sandbox
|
|
378
|
+
deployments**: leave the secret unset on any stage that should not run scenarios,
|
|
379
|
+
which is the supported switch (`Actor sign-in is not configured`), rather than
|
|
380
|
+
conditionally registering the plugin. Do not treat "actors only" as a licence to
|
|
381
|
+
enable it in production.
|
|
382
|
+
|
|
383
|
+
Within that boundary, three properties bound the damage:
|
|
384
|
+
|
|
385
|
+
- **It only ever signs in actors.** The plugin adds a `user.actor` boolean
|
|
386
|
+
column; an email matching a row without it is refused with `User is not an
|
|
387
|
+
actor`. So the secret cannot take over a **real user's** account — the blast
|
|
388
|
+
radius is the actor accounts and whatever roles they were granted.
|
|
389
|
+
- **Unknown emails are created**, flagged `actor: true`, so a scenario that
|
|
390
|
+
declares a new persona needs no seed step. Note the flip side: the secret
|
|
391
|
+
mints accounts, it does not merely use existing ones.
|
|
392
|
+
- **The comparison is constant-time and length-hiding**, so a wrong secret leaks
|
|
393
|
+
neither the length nor a prefix of the right one.
|
|
394
|
+
|
|
395
|
+
This is the endpoint `pikku scenario` signs its actors in through, and the one
|
|
396
|
+
the frontend dev switcher posts to — see `pikku-scenario` for declaring the
|
|
397
|
+
actors and `pikku-react` for `useDevActors()`.
|
|
398
|
+
|
|
319
399
|
---
|
|
320
400
|
|
|
321
401
|
## Secret Management
|