@pikku/skills 0.12.37 → 0.12.38
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 +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +30 -0
- package/skills/pikku-addon/references/openapi.md +69 -38
- package/skills/pikku-auth/references/better-auth.md +17 -2
- package/skills/pikku-build/SKILL.md +80 -9
- package/skills/pikku-build/references/app.md +33 -11
- package/skills/pikku-build/references/design.md +16 -5
- package/skills/pikku-build/references/openapi.md +119 -0
- package/skills/pikku-build/references/platform.md +4 -0
- package/skills/pikku-build/references/quick.md +4 -3
- package/skills/pikku-scenario/SKILL.md +9 -8
- package/skills/pikku-scenario/references/personas.md +65 -3
package/package.json
CHANGED
|
@@ -359,6 +359,36 @@ export const myFunc = pikkuFunc({
|
|
|
359
359
|
})
|
|
360
360
|
```
|
|
361
361
|
|
|
362
|
+
### Wrap an addon function only to reshape it
|
|
363
|
+
|
|
364
|
+
A screen that shows the addon's data as the addon returns it calls the addon
|
|
365
|
+
function itself: name it in `wireAddon({ expose: ['listTodos'], auth: true })`
|
|
366
|
+
and the frontend calls `rpc.invoke('todos:listTodos', …)` (over HTTP,
|
|
367
|
+
`POST /rpc/todos:listTodos` with `{ "data": … }`), still behind the session.
|
|
368
|
+
Use `ref('todos:listTodos')` on an HTTP or MCP wiring only when the addon needs
|
|
369
|
+
a route of its own. Don't write an app function that calls `rpc.invoke` and
|
|
370
|
+
returns the result unchanged: it's a second name and a second schema for the
|
|
371
|
+
same thing, and it drifts. `expose: true` exposes only what the addon itself
|
|
372
|
+
declared `expose: true` — an OpenAPI-generated addon declares none, so list
|
|
373
|
+
the names.
|
|
374
|
+
|
|
375
|
+
Write your own function when the app needs the data narrowed, typed, or
|
|
376
|
+
combined (a flag the upstream sends as `"0"`, a total summed from several
|
|
377
|
+
calls, one field out of fifty), or when the app adds a permission of its own.
|
|
378
|
+
`wireAddon`'s `scopes` gate every function in the addon at once, and a wiring
|
|
379
|
+
carries middleware, not permissions, so a rule on one addon function lives in
|
|
380
|
+
the `permissions` of an app function that calls it. An addon called with the
|
|
381
|
+
user's own credential is already limited upstream to what that user may do, so
|
|
382
|
+
a data-aware `pikkuPermission` repeating that check (may they read *this*
|
|
383
|
+
invoice?) adds nothing. A session-only `pikkuAuth` (a role, a tier) is still
|
|
384
|
+
worth it: it can be checked before any input exists, so the functions a user
|
|
385
|
+
can't call drop out of the tools an MCP client, an agent or a workflow is
|
|
386
|
+
offered, instead of failing upstream when called. Name it for what the screen means
|
|
387
|
+
(`getMyProfile`), not after the upstream operation (`usersRetrieveInfo`), and
|
|
388
|
+
give it an `output:` schema of only what the app uses. For an OpenAPI-generated
|
|
389
|
+
addon this matters more: its outputs mirror the upstream's loose, oversized
|
|
390
|
+
payloads, and the wrapper is where they become the app's own shape.
|
|
391
|
+
|
|
362
392
|
### Wire to HTTP
|
|
363
393
|
|
|
364
394
|
```typescript
|
|
@@ -13,67 +13,81 @@ A JSON or YAML document with a top-level `openapi` key (3.x) or `swagger` key
|
|
|
13
13
|
(2.0), and a `paths` object. A URL ending in `openapi.json`, `swagger.json` or
|
|
14
14
|
`.yaml` is almost always one; open it and check the key before generating.
|
|
15
15
|
|
|
16
|
-
## 1 —
|
|
16
|
+
## 1 — Look at the spec first
|
|
17
17
|
|
|
18
|
-
`--openapi`
|
|
19
|
-
|
|
18
|
+
`--openapi` takes a path or a URL. A spec published only to signed-in callers
|
|
19
|
+
takes the key the way the API reads it: `--openapi-header "NAME: value"`
|
|
20
|
+
(repeatable), or in the URL's query string when the API reads it there
|
|
21
|
+
(Dolibarr's explorer takes `?DOLAPIKEY=`). A 401 while fetching says which.
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```
|
|
23
|
+
The generator warns loudly when the spec has fewer than five operations, or only
|
|
24
|
+
auth routes. That is almost always the public half of a spec that shows more
|
|
25
|
+
to an authenticated caller — fetch it again with the key, don't build on it.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Keep a copy in `specs/` as the record of what the addon was generated from.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
## 2 — Generate, from the app's root
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
|
-
bunx --bun pikku new addon <name> --openapi
|
|
32
|
+
bunx --bun pikku new addon <name> --openapi <path-or-url>
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
One command. Inside an app it writes `packages/addon-<name>` as
|
|
36
|
+
`@pikku/addon-<name>`, then installs it into the app:
|
|
37
|
+
|
|
38
|
+
- the dependency in the root and the functions `package.json`
|
|
39
|
+
- `src/addons/<name>.addon.ts` — `wireAddon` with `auth: true` and an explicit
|
|
40
|
+
`expose` list (every operation in per-user modes, only the `GET`s behind a
|
|
41
|
+
shared secret)
|
|
42
|
+
- the auth wiring in `src/auth.ts` for the chosen mode
|
|
43
|
+
- `<NAME>_BASE_URL` in `.env`
|
|
44
|
+
|
|
45
|
+
and then runs install and the addon's build. `--no-install` generates the
|
|
46
|
+
package alone.
|
|
47
|
+
|
|
48
|
+
| Flag | When |
|
|
49
|
+
| -------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
50
|
+
| `--auth user` (default) | Each user brings their own credential |
|
|
51
|
+
| `--auth shared` | One secret behind every user; locally it goes in `.env` |
|
|
52
|
+
| `--auth none` | The API really takes no auth |
|
|
53
|
+
| `--credential apikey\|bearer\|basic\|oauth2` | Override what the spec's `securitySchemes` declares |
|
|
54
|
+
| `--auth-config <file>` | Users sign in with their upstream login, or the spec gets auth wrong |
|
|
55
|
+
| `--tags a,b` / `--include` / `--exclude` | Keep part of a huge spec: tags, or globs on operationId, `/path`, `METHOD /path` |
|
|
56
|
+
| `--mcp` | The operations should also be MCP tools |
|
|
57
|
+
| `--camel-case` | The API's property names are snake_case and the app's are not |
|
|
58
|
+
|
|
59
|
+
The mode comes from the spec unless a flag says otherwise. A spec with no
|
|
60
|
+
machine-readable auth is refused rather than guessed: pass one of the flags the
|
|
61
|
+
error names. Which mode fits, and the auth-config format, are in the
|
|
62
|
+
`pikku-build` skill's `references/openapi.md`.
|
|
48
63
|
|
|
49
64
|
## 3 — Check what was generated
|
|
50
65
|
|
|
51
66
|
```
|
|
52
67
|
packages/addon-<name>/
|
|
68
|
+
├── <name>.svg # placeholder icon; replace with the real one
|
|
53
69
|
├── src/<name>-api.service.ts # one fetch wrapper, reads <NAME>_BASE_URL
|
|
54
|
-
├── src/<name>.variable.ts # <NAME>_BASE_URL,
|
|
70
|
+
├── src/<name>.variable.ts # <NAME>_BASE_URL: z.string().url(), the first server as default
|
|
55
71
|
├── src/functions/<op>.function.ts
|
|
56
72
|
├── src/functions/<op>.schemas.ts # the op's zod schemas — never import #pikku here
|
|
57
73
|
└── src/index.ts # re-exports every function
|
|
58
74
|
```
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
to `z.string().url()` so each deployment sets its own.
|
|
64
|
-
|
|
65
|
-
## 4 — Wire it into the app
|
|
76
|
+
An operation whose spec gives no response, or one too vague to validate
|
|
77
|
+
against, outputs `z.unknown()`. Tighten it in `<op>.schemas.ts` once §6 shows
|
|
78
|
+
what the API really returns.
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
An upstream 401 on a per-user credential throws `CredentialRejectedError`
|
|
81
|
+
(403, `reauth: 'sign-in' | 'connect'`); a UI shows the matching screen again
|
|
82
|
+
rather than a generic error.
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
```
|
|
84
|
+
## 4 — Call it from the app
|
|
73
85
|
|
|
74
|
-
|
|
86
|
+
Operations are reached by reference — `ref('<name>:<operationFn>')` in a
|
|
75
87
|
workflow, agent tool or HTTP wiring — the same way as any other addon. See
|
|
76
|
-
"Consuming an Addon" in the skill.
|
|
88
|
+
"Consuming an Addon" in the skill. An exposed operation is also callable from
|
|
89
|
+
the frontend at `POST /rpc/<name>:<operationFn>` with a body of
|
|
90
|
+
`{ "data": { … } }`, as the signed-in user.
|
|
77
91
|
|
|
78
92
|
## 5 — Verify
|
|
79
93
|
|
|
@@ -92,6 +106,23 @@ another shared package. Codegen then reads the app's
|
|
|
92
106
|
schemas with the wrong copy and fails on schemas that are correct. Pin one
|
|
93
107
|
version for the whole install, as the finding says, and reinstall.
|
|
94
108
|
|
|
109
|
+
## 6 — Check the spec against the real API
|
|
110
|
+
|
|
111
|
+
Specs are often wrong, and the generated schemas repeat every mistake. Before
|
|
112
|
+
building on the addon:
|
|
113
|
+
|
|
114
|
+
- **Call the reads you can.** The `GET`s the credential can reach, following ids
|
|
115
|
+
from lists into retrieves. Writes only if the user opts in.
|
|
116
|
+
- **Fix the addon, not the app**: the schema in the op's `<op>.schemas.ts`, or the
|
|
117
|
+
request shape in `src/<name>-api.service.ts`. Then rebuild it.
|
|
118
|
+
- **List each mismatch in `packages/addon-<name>/SPEC-ISSUES.md`**: a title and a
|
|
119
|
+
short description. No credentials or customer data.
|
|
120
|
+
|
|
121
|
+
Then tell the user in one line: "FYI, the spec deviates from the real API in
|
|
122
|
+
N ways: [SPEC-ISSUES.md](…)". Add to the file whenever a later call disagrees
|
|
123
|
+
with its schema. Ask before sending it to the API's maintainers, because an issue
|
|
124
|
+
on their tracker is a public post.
|
|
125
|
+
|
|
95
126
|
## Then
|
|
96
127
|
|
|
97
128
|
Go back to the mode you were building in (`pikku-build`). The addon is a
|
|
@@ -307,7 +307,7 @@ singleton a 403 that leaves no platform user behind.
|
|
|
307
307
|
|
|
308
308
|
```typescript
|
|
309
309
|
pikkuDelegatedAuth({
|
|
310
|
-
authenticate: async ({ email, password, apiKey }) => upstream.login(...),
|
|
310
|
+
authenticate: async ({ login, email, password, apiKey }) => upstream.login(...),
|
|
311
311
|
storeCredential: (userId, identity) =>
|
|
312
312
|
credentialService.set('acme', identity.credential, userId),
|
|
313
313
|
defaultRole: 'member',
|
|
@@ -318,7 +318,10 @@ pikkuDelegatedAuth({
|
|
|
318
318
|
```
|
|
319
319
|
|
|
320
320
|
`POST /sign-in/delegated` forwards the credentials the user already has to
|
|
321
|
-
`authenticate`.
|
|
321
|
+
`authenticate`. The body takes `email`, `login` or `username` with `password`
|
|
322
|
+
(or `apiKey`); whichever identifier was sent reaches `authenticate` as
|
|
323
|
+
`credentials.login`, and `email` as well when it was one. Upstreams that sign in
|
|
324
|
+
with a username — most ERPs — need nothing more. On success it JIT-provisions a real user row (email-keyed and
|
|
322
325
|
`emailVerified` — the upstream just verified them), links it via an `account`
|
|
323
326
|
row (`providerId: 'delegated'`, `accountId: externalId`), persists the upstream
|
|
324
327
|
token **before** minting the session, and returns a normal session cookie.
|
|
@@ -333,6 +336,18 @@ a warning and the user still gets in.
|
|
|
333
336
|
`storeCredential` failing, by contrast, **fails the sign-in**: every proxied
|
|
334
337
|
call would be dead anyway.
|
|
335
338
|
|
|
339
|
+
An upstream user with no email gets one made up from the login, and the
|
|
340
|
+
identity says so with `syntheticEmail: true`. A made-up address never links to
|
|
341
|
+
an existing user row, so it cannot take over someone else's account.
|
|
342
|
+
|
|
343
|
+
For an addon generated from an OpenAPI spec, none of this is written by hand:
|
|
344
|
+
`pikku new addon --openapi … --auth-config <file>` generates
|
|
345
|
+
`authenticate<Name>Upstream` in the addon and wires this plugin, the stored
|
|
346
|
+
credential and the actor credentials into `src/auth.ts`. The config format is
|
|
347
|
+
in the `pikku-build` skill's `references/openapi.md`. When the upstream later
|
|
348
|
+
refuses the stored token, the addon throws `CredentialRejectedError` (403,
|
|
349
|
+
`reauth: 'sign-in'`): the UI shows the sign-in again.
|
|
350
|
+
|
|
336
351
|
#### `pikkuFabric()` — control-plane operator sign-in
|
|
337
352
|
|
|
338
353
|
```typescript
|
|
@@ -44,12 +44,14 @@ plus more effort" — it is App plus a deliberate surface checklist, so read the
|
|
|
44
44
|
base first and follow it in full rather than blending the two into one plan.
|
|
45
45
|
|
|
46
46
|
The supporting references belong to whichever mode sends you to them:
|
|
47
|
-
`references/multi-app.md` (a second frontend), `references/design.md` (
|
|
48
|
-
|
|
49
|
-
the screens realise it — read before the first screen is built, not
|
|
50
|
-
last), `references/theming.md`
|
|
47
|
+
`references/multi-app.md` (a second frontend), `references/design.md` (showing
|
|
48
|
+
a picture of the screens first, committing to a design direction, and judging
|
|
49
|
+
whether the screens realise it — read before the first screen is built, not
|
|
50
|
+
after the last), `references/theming.md`
|
|
51
51
|
(authoring the theme),
|
|
52
|
-
`references/ship.md` (deploying, and the Fabric-readiness contract)
|
|
52
|
+
`references/ship.md` (deploying, and the Fabric-readiness contract),
|
|
53
|
+
`references/openapi.md` (an app on an OpenAPI spec: the auth mode, the
|
|
54
|
+
auth-config format, and the sign-in or connect screen it implies).
|
|
53
55
|
|
|
54
56
|
## Bootstrap before anything else
|
|
55
57
|
|
|
@@ -67,10 +69,10 @@ while still planning. Those failures look alarming and are nothing but this.
|
|
|
67
69
|
When the request comes with a file or a URL, look at it before planning
|
|
68
70
|
anything. Two kinds are converted first and then built on:
|
|
69
71
|
|
|
70
|
-
| Handed | Say, then do
|
|
71
|
-
| ------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
-
| An **OpenAPI / Swagger spec** — top-level `openapi` or `swagger` key, a `paths` object | "This is an OpenAPI spec — I'll turn it into an addon first."
|
|
73
|
-
| An **n8n export** — an object with `nodes` and `connections`, an array of them, or a `{ workflows: [...] }` wrapper | "This is an n8n workflow — I'll import it first." Follow `pikku-n8n-import`.
|
|
72
|
+
| Handed | Say, then do |
|
|
73
|
+
| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| An **OpenAPI / Swagger spec** — top-level `openapi` or `swagger` key, a `paths` object | "This is an OpenAPI spec — I'll turn it into an addon first." Pick the auth mode in `references/openapi.md`, then follow the `pikku-addon` skill's OpenAPI reference. |
|
|
75
|
+
| An **n8n export** — an object with `nodes` and `connections`, an array of them, or a `{ workflows: [...] }` wrapper | "This is an n8n workflow — I'll import it first." Follow `pikku-n8n-import`. |
|
|
74
76
|
|
|
75
77
|
Say it at once, in one line, and start: this is the obvious first move, not a
|
|
76
78
|
question for the user. Generate the whole spec, however large.
|
|
@@ -103,6 +105,75 @@ generated functions through `ref()`.
|
|
|
103
105
|
`pikku knowledge plan progress` measures the build against it from the
|
|
104
106
|
generated meta. You plan it and you build it; what you never do is edit the
|
|
105
107
|
plan afterwards to match what you built — that is grading yourself.
|
|
108
|
+
- **Print the links whenever the stack comes up, and in every hand-over.** Full,
|
|
109
|
+
clickable URLs, with the ports taken from what `bun run dev` actually printed:
|
|
110
|
+
- **App** — the frontend's URL (`http://localhost:7104` in the template; each
|
|
111
|
+
frontend in `pikkufabric.config.json` has its own port)
|
|
112
|
+
- **API** — `http://localhost:3000`
|
|
113
|
+
- **Console** — `http://localhost:3000/console`, plus a deep link to each
|
|
114
|
+
page that shows what this turn produced (the paths are below)
|
|
115
|
+
|
|
116
|
+
A person who has to go hunting for the port assumes the app did not start.
|
|
117
|
+
|
|
118
|
+
## Keep a BUILD-REPORT.md
|
|
119
|
+
|
|
120
|
+
Whenever pikku or a skill costs you time — a command that failed on a fresh
|
|
121
|
+
tree, a skill that described a flag the CLI does not have, generated code you
|
|
122
|
+
had to fix by hand — add an entry to `BUILD-REPORT.md` at the repo root as it
|
|
123
|
+
happens: what you ran, what you expected, what happened, and the workaround.
|
|
124
|
+
Leave out secrets, tokens and customer data.
|
|
125
|
+
|
|
126
|
+
At hand-over, show the file and ask the user whether to send it. Only with
|
|
127
|
+
their okay, send each entry with `pikku fabric report --stdin` (JSON on stdin;
|
|
128
|
+
`"kind": "product"` when pikku behaved wrongly, `"kind": "harness"` with
|
|
129
|
+
`"skill"` and `"passage"` when a skill misled you). The `pikku-report` skill
|
|
130
|
+
has the fields. When the CLI is not signed in to Fabric, the report is queued
|
|
131
|
+
locally rather than sent: say so, and that `pikku fabric findings flush` sends
|
|
132
|
+
the queue once they sign in. Do not retry or file it twice.
|
|
133
|
+
|
|
134
|
+
## Who you are talking to
|
|
135
|
+
|
|
136
|
+
The prompt asks first how technical the person is: **not technical**,
|
|
137
|
+
**technical, no code**, or **developer** (the default when unsaid). Whenever the
|
|
138
|
+
build makes or changes something the console can show, give the
|
|
139
|
+
`http://localhost:<port>/console/...` link instead of describing it.
|
|
140
|
+
|
|
141
|
+
Until the app is deployed that is the local open-source console, on the port
|
|
142
|
+
`pikku dev` printed. Once it is on Fabric, link the Fabric console for the stage
|
|
143
|
+
you are talking about instead; the `pikku-fabric` skill says which.
|
|
144
|
+
|
|
145
|
+
| Level | Links | Code in the conversation |
|
|
146
|
+
| ------------------ | --------------------------- | ------------------------ |
|
|
147
|
+
| Not technical | Product pages only | Never |
|
|
148
|
+
| Technical, no code | Product and technical pages | Never |
|
|
149
|
+
| Developer | Product and technical pages | As normal |
|
|
150
|
+
|
|
151
|
+
"Never" includes snippets and command lines; say what changed in the person's
|
|
152
|
+
words and link to where they can see it.
|
|
153
|
+
|
|
154
|
+
**Product pages** — the only ones a non-technical person gets:
|
|
155
|
+
|
|
156
|
+
| Shows | Path |
|
|
157
|
+
| --------------------- | -------------------------------------------------------------------------- |
|
|
158
|
+
| Knowledge, plans | `/console/knowledge`, `/console/knowledge?id=<note path>` |
|
|
159
|
+
| Personas | `/console/personas`, `/console/virtual-users?persona=<id>` |
|
|
160
|
+
| Roles and permissions | `/console/roles`, `/console/scopes` (only with `@pikku/addon-admin` wired) |
|
|
161
|
+
| Scenarios and runs | `/console/scenarios?id=<id>`, `/console/scenarios?view=runs&run=<run id>` |
|
|
162
|
+
| Workflows | `/console/workflow?id=<id>` |
|
|
163
|
+
| Agents | `/console/agents`, `/console/agents/playground?id=<agent id>` |
|
|
164
|
+
|
|
165
|
+
**Technical pages** — never for a non-technical person: `/console/overview`,
|
|
166
|
+
`/console/functions`, `/console/surface`, `/console/database`,
|
|
167
|
+
`/console/changes`, `/console/wires/http`, `/console/wires/channel`,
|
|
168
|
+
`/console/wires/mcp`, `/console/wires/cli`, `/console/wires/gateway`,
|
|
169
|
+
`/console/async/scheduler`, `/console/async/queue`, `/console/async/trigger`,
|
|
170
|
+
`/console/runtime`, `/console/emails`, `/console/webhooks`, `/console/secrets`,
|
|
171
|
+
`/console/variables`, `/console/security`, `/console/auth-providers`,
|
|
172
|
+
`/console/addons`, `/console/analytics`, `/console/credentials`,
|
|
173
|
+
`/console/users`, `/console/audit`, `/console/flags`, `/console/scorers`.
|
|
174
|
+
|
|
175
|
+
These come from `packages/console/src/App.tsx`; do not link a path that is not
|
|
176
|
+
listed here.
|
|
106
177
|
|
|
107
178
|
## What NOT to do
|
|
108
179
|
|
|
@@ -24,6 +24,9 @@ project shaped so `pikku fabric init` later adopts it with zero rework.
|
|
|
24
24
|
wirings, schemas or generated clients may have changed.
|
|
25
25
|
4. If validation fails, fix the source cause and rerun. Do not paper over
|
|
26
26
|
generated errors by editing generated files.
|
|
27
|
+
5. Report at the person's level (SKILL.md, "Who you are talking to"): a console
|
|
28
|
+
link for everything the console can show, and no code unless they are a
|
|
29
|
+
developer.
|
|
27
30
|
|
|
28
31
|
## 0. Bootstrap, before anything else
|
|
29
32
|
|
|
@@ -62,11 +65,18 @@ in one message. Then stop; do not interview the user.
|
|
|
62
65
|
a reference (brand guide, screenshots, a site whose register they want); or
|
|
63
66
|
their own design agent/prompt, whose output you take as the direction.
|
|
64
67
|
- **Do they want to see the screens before you build them?** Offer it here, in
|
|
65
|
-
this same round,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
this same round, with yes marked recommended, in the words of
|
|
69
|
+
`references/design.md` — "a picture of the main screens so you can say 'yes,
|
|
70
|
+
like that' or 'no, move this'", never "mock" or "wireframe". Behind it is one
|
|
71
|
+
HTML page mocking the main screens, a few minutes of work. On a yes, or no
|
|
72
|
+
answer at all, `references/design.md` owns what to make and what it then binds — the
|
|
68
73
|
approved page becomes source of truth for the screens, and the theme is written
|
|
69
|
-
before it so what they approve is what ships.
|
|
74
|
+
before it so what they approve is what ships. Only an explicit no skips it.
|
|
75
|
+
- **May I write test records into the system it talks to?** Ask only when the
|
|
76
|
+
app reads a live system through an addon (an ERP, a CRM) and a milestone needs
|
|
77
|
+
data that isn't there yet: an unpaid invoice, a closed ticket. Say what you
|
|
78
|
+
would create and that it will be marked "Test". A no means building those
|
|
79
|
+
screens against their empty states.
|
|
70
80
|
- **What language should the app speak, and what language does the team work
|
|
71
81
|
in?** Two answers, not one — see §1a, which is where they go. Ask only if the
|
|
72
82
|
request is not obviously English; a brief written in English about an English
|
|
@@ -343,8 +353,9 @@ What a milestone is:
|
|
|
343
353
|
persona. If you cannot write the gherkin, you cannot build it yet — that is a
|
|
344
354
|
`questions/` note, not a milestone.
|
|
345
355
|
|
|
346
|
-
If §1's screen mock was made
|
|
347
|
-
screen on that page belongs to some milestone,
|
|
356
|
+
If §1's screen mock was made — approved, or drawn because nobody answered — the
|
|
357
|
+
milestones are read off it: every screen on that page belongs to some milestone,
|
|
358
|
+
and a screen no milestone builds
|
|
348
359
|
is a hole in this plan. Say which milestone covers which screen.
|
|
349
360
|
|
|
350
361
|
How to order them:
|
|
@@ -363,8 +374,17 @@ How to order them:
|
|
|
363
374
|
Number the files (`01-…`, `02-…`) so the order is visible in the tree. Then
|
|
364
375
|
`knowledge index && knowledge validate` before you write a line of code.
|
|
365
376
|
|
|
366
|
-
**
|
|
367
|
-
|
|
377
|
+
**One approval, then build to the end.** Show the picture of the screens and
|
|
378
|
+
the milestone list together, in one message, as the plan: which milestone builds
|
|
379
|
+
which screen, in what order. That is the only approval you ask for. It is the
|
|
380
|
+
last cheap moment to reorder: after §6 the migrations are numbered and the order
|
|
381
|
+
is concrete.
|
|
382
|
+
|
|
383
|
+
Once they approve it, or don't answer, build every milestone in order without
|
|
384
|
+
stopping to ask between them. Post one line as each milestone closes, with its
|
|
385
|
+
console links, and carry on. Stop only for what is theirs to decide: a
|
|
386
|
+
credential you don't have, spending money, posting in public, deleting or
|
|
387
|
+
overwriting their data, or a finding that changes the plan.
|
|
368
388
|
|
|
369
389
|
## 5a. The technical plan — one milestone at a time, before you build it
|
|
370
390
|
|
|
@@ -399,6 +419,8 @@ no plan, and everything after the current milestone is still allowed to move.
|
|
|
399
419
|
|
|
400
420
|
## 6. Implement milestones, one at a time
|
|
401
421
|
|
|
422
|
+
All of them, one after another, on the one approval from §5.
|
|
423
|
+
|
|
402
424
|
**Per milestone** — plan it (§5a), set its note to `status: dispatched`, do the
|
|
403
425
|
six steps, close it out (§6a), set it to `built`. Do not start the next one
|
|
404
426
|
until §6a passes, §7 is green for this one _and §7a shows its functions
|
|
@@ -498,8 +520,8 @@ Rules that are not optional:
|
|
|
498
520
|
`pikkuSessionlessFunc`. `pikkuFunc` with `auth: false` still answers
|
|
499
521
|
`MissingSessionError` over `/rpc` to a caller with no session.
|
|
500
522
|
- Better Auth already owns the `user`, `session`, `account` and `verification`
|
|
501
|
-
tables. A domain table with one of those names — a class
|
|
502
|
-
|
|
523
|
+
tables. A domain table with one of those names — a class _session_, a drop-in
|
|
524
|
+
_session_ — collides in the migration. Name it for the domain instead
|
|
503
525
|
(`evening`, `class_meeting`) and keep the word in the UI copy.
|
|
504
526
|
- The template's `/` redirects to `/app`, so the login screen — and its "Sign in
|
|
505
527
|
as …" switcher — is what a signed-out visitor sees first. Replace `/` with a
|
|
@@ -544,7 +566,7 @@ start the frontend on its own (say :3000 is taken by another project), you owe
|
|
|
544
566
|
it three things: the two `VITE_DEV_*` values the dev script would have computed,
|
|
545
567
|
and `VITE_API_PROXY` pointing at your API — the dev proxy defaults to
|
|
546
568
|
`http://localhost:3000`, so beside another project's server your sign-ins go to
|
|
547
|
-
|
|
569
|
+
_its_ API and come back `401 Invalid actor secret`, which reads like a bad
|
|
548
570
|
credential rather than the wrong server.
|
|
549
571
|
|
|
550
572
|
The `--bun` in `bunx --bun pikku …` is load-bearing — keep it. Without it the
|
|
@@ -54,13 +54,18 @@ allowed is landing on one because it was nearest to hand.
|
|
|
54
54
|
## Offer to draw the screens before you build them
|
|
55
55
|
|
|
56
56
|
Before the first milestone, **ask** whether they want to see the screens first.
|
|
57
|
-
One question, in §1's round, not a gate of its own
|
|
57
|
+
One question, in §1's round, not a gate of its own, with yes marked
|
|
58
|
+
recommended every time:
|
|
58
59
|
|
|
59
|
-
>
|
|
60
|
-
>
|
|
61
|
-
> than a built
|
|
60
|
+
> Before I build, shall I show you a picture of the main screens so you can say
|
|
61
|
+
> "yes, like that" or "no, move this"? (Recommended: it takes a few minutes and
|
|
62
|
+
> changing a picture is much cheaper than changing a built app.)
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
Never say "mock", "mockup" or "wireframe" to the person; most people do not
|
|
65
|
+
know the words. They stay the technical terms in this file only.
|
|
66
|
+
|
|
67
|
+
If they do not answer, draw it anyway and build from it. Only an explicit no
|
|
68
|
+
skips it; then build, and the direction in words is enough to be accountable to.
|
|
64
69
|
If they accept, this is the cheapest decision in the project — a picture of eight
|
|
65
70
|
screens costs a fraction of eight built screens, and it is the only point where
|
|
66
71
|
"that is not what I meant" is free.
|
|
@@ -105,6 +110,12 @@ first. Whatever your host offers for showing a page is how you show it: an
|
|
|
105
110
|
Artifact, a file they open, a preview server. The page is the deliverable; how it
|
|
106
111
|
gets in front of them is not this file's business.
|
|
107
112
|
|
|
113
|
+
**Say it is a picture, on the page and in the message.** A well-drawn screen
|
|
114
|
+
reads as a finished app, and a person who thinks it is already built asks why
|
|
115
|
+
nothing works. The page opens with a banner that stays in view: "A picture of
|
|
116
|
+
the planned screens. Nothing is built yet." The message that shows it says the
|
|
117
|
+
same, then says what happens next: "Once you're happy with it, I'll build it."
|
|
118
|
+
|
|
108
119
|
Write it to `knowledge/decisions/design/screens.html` and treat it as **source of
|
|
109
120
|
truth for the screens** once they approve it. That has consequences worth
|
|
110
121
|
stating:
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# An app on top of an OpenAPI spec
|
|
2
|
+
|
|
3
|
+
The spec becomes an addon first — the `pikku-addon` skill's
|
|
4
|
+
`references/openapi.md` covers generating, verifying and checking it against
|
|
5
|
+
the real API. This reference is the decision that shapes the app around it:
|
|
6
|
+
**whose credentials reach the upstream**, and what the person signs in with.
|
|
7
|
+
|
|
8
|
+
## Pick the auth mode
|
|
9
|
+
|
|
10
|
+
| The upstream… | Mode | Command | Who signs in, and how |
|
|
11
|
+
| ------------------------------------------------------------------------ | ----------------- | ----------------------------- | ------------------------------------------------------- |
|
|
12
|
+
| Is where the users already have accounts, and has a login endpoint | Delegated | `--auth-config <file>` | Users sign in with their upstream login; no new account |
|
|
13
|
+
| Takes a per-user API key, bearer token or basic auth | Per-user key | (the default) | App account, then a Connect screen for the key |
|
|
14
|
+
| Uses OAuth2 | Per-user OAuth | (the default) | App account, then Connect via the OAuth consent |
|
|
15
|
+
| Is used on everyone's behalf with one key the business owns | Shared secret | `--auth shared` | App account only; the key is a secret |
|
|
16
|
+
| Takes no auth | None | `--auth none` | App account only |
|
|
17
|
+
|
|
18
|
+
Per-user is the default because the upstream then enforces each person's own
|
|
19
|
+
permissions. A shared secret acts with one identity for everyone, so the
|
|
20
|
+
install exposes only its reads; widen that deliberately, never by default.
|
|
21
|
+
|
|
22
|
+
The mode comes from the spec's `securitySchemes` unless a flag overrides it.
|
|
23
|
+
Many real specs describe auth only in prose. The generator then refuses rather
|
|
24
|
+
than guessing. Read the API's docs and pass the flag that is true.
|
|
25
|
+
|
|
26
|
+
**Delegated is the right call whenever the upstream is the system of record for
|
|
27
|
+
who the users are** — an ERP, a CRM, a helpdesk the whole team already logs in
|
|
28
|
+
to. The app then has no separate sign-up, the upstream token is stored per user
|
|
29
|
+
at sign-in, and every call acts as that user.
|
|
30
|
+
|
|
31
|
+
## The auth-config file
|
|
32
|
+
|
|
33
|
+
JSON, passed with `--auth-config`. Every field is optional except where noted.
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"headerName": "DOLAPIKEY",
|
|
38
|
+
"headerFormat": "raw",
|
|
39
|
+
"extraHeaders": { "Origin": "https://tenant.example.com" },
|
|
40
|
+
"delegated": {
|
|
41
|
+
"loginPath": "/login",
|
|
42
|
+
"loginMethod": "post",
|
|
43
|
+
"credentials": ["login", "password"],
|
|
44
|
+
"fields": { "login": "login", "password": "password" },
|
|
45
|
+
"encoding": "json",
|
|
46
|
+
"tokenPath": "success.token",
|
|
47
|
+
"expiresAtPath": "success.expires",
|
|
48
|
+
"identity": { "path": "/users/info", "method": "get" },
|
|
49
|
+
"claims": {
|
|
50
|
+
"source": "identity",
|
|
51
|
+
"externalId": "id",
|
|
52
|
+
"email": "email",
|
|
53
|
+
"name": ["firstname", "lastname"],
|
|
54
|
+
"role": "admin",
|
|
55
|
+
"tenantId": "entity"
|
|
56
|
+
},
|
|
57
|
+
"emailTemplate": "{login}@{host}",
|
|
58
|
+
"roles": { "1": "dolibarr-admin" }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Top level — how every call authenticates:
|
|
64
|
+
|
|
65
|
+
| Field | Meaning |
|
|
66
|
+
| -------------- | ---------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `headerName` | The header the API reads the token or key from. Setting it alone means a per-user API key |
|
|
68
|
+
| `headerFormat` | `raw` sends the bare value, `bearer` prefixes `Bearer `. Default: bearer for `Authorization`, else raw |
|
|
69
|
+
| `extraHeaders` | Static headers on every request, login included — for upstreams that route on a header |
|
|
70
|
+
| `delegated` | Present when users sign in with their upstream login |
|
|
71
|
+
|
|
72
|
+
`delegated`:
|
|
73
|
+
|
|
74
|
+
| Field | Meaning |
|
|
75
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `loginPath` | Required. The spec-relative login operation |
|
|
77
|
+
| `loginMethod` | Default `post` |
|
|
78
|
+
| `credentials` | What the sign-in form collects: `login` (a username or email), `email`, `password`, `apiKey`. Default `["email","password"]` |
|
|
79
|
+
| `fields` | The upstream's name for each credential, e.g. `{ "login": "username" }` |
|
|
80
|
+
| `encoding` | `json`, `form` or `query` — how the login fields travel. Default `json` |
|
|
81
|
+
| `apiKeyHeader` | The header an `apiKey` credential is sent in. Default `x-api-key` |
|
|
82
|
+
| `tokenPath` | Required. Dot-path to the token in the login response |
|
|
83
|
+
| `expiresAtPath` | Dot-path to an epoch-seconds expiry. Defaults to the JWT's `exp` when claims come from the JWT |
|
|
84
|
+
| `identity` | An operation that returns the signed-in user, called with the new token — for logins that return only a token |
|
|
85
|
+
| `claims.source` | `jwt`, `response` or `identity`. Default `identity` when `identity` is set, else `response` |
|
|
86
|
+
| `claims.*` | Dot-paths to `externalId`, `email`, `name` (one path or several joined with a space), `role`, `tenantId` |
|
|
87
|
+
| `emailTemplate` | Builds an email for an upstream user who has none: `{login}`, `{externalId}`, `{host}`. Such an address never claims an existing user |
|
|
88
|
+
| `roles` | Maps the raw `claims.role` value onto an app role; an unmapped value gets no role |
|
|
89
|
+
|
|
90
|
+
Check it before building on it: start `pikku dev`, sign in once with
|
|
91
|
+
`POST /api/auth/sign-in/delegated` as a real upstream user, and call an exposed
|
|
92
|
+
operation with the session. A wrong password must be refused. Keep real
|
|
93
|
+
credentials in the environment, never in the config or a test.
|
|
94
|
+
|
|
95
|
+
What the install wires for delegated mode — `pikkuDelegatedAuth` in
|
|
96
|
+
`src/auth.ts`, the token stored per user, actors carrying it into scenarios — is
|
|
97
|
+
in the `pikku-auth` skill's `references/better-auth.md`.
|
|
98
|
+
|
|
99
|
+
## The screens
|
|
100
|
+
|
|
101
|
+
**If the app has a UI, build the sign-in or connect screen that matches the chosen auth mode (Sign in with <X> for delegated, a Connect <X> screen for per-user keys/OAuth, nothing for a shared secret), labelling fields in the upstream's terms (e.g. 'Dolibarr login', not 'Email').**
|
|
102
|
+
|
|
103
|
+
- **Delegated** — the sign-in page posts to `POST /api/auth/sign-in/delegated`
|
|
104
|
+
with the fields named in `credentials` (`login` or `username`, `password`).
|
|
105
|
+
It replaces email sign-up; there is no "create account".
|
|
106
|
+
- **Per-user key or OAuth** — a Connect screen after sign-in, and wherever a
|
|
107
|
+
call fails with `missing_credential`.
|
|
108
|
+
- A `credential_rejected` error (`CredentialRejectedError`, 403) means the
|
|
109
|
+
upstream refused the stored token: show the sign-in again (`reauth:
|
|
110
|
+
'sign-in'`) or the Connect screen (`reauth: 'connect'`), not a generic error.
|
|
111
|
+
|
|
112
|
+
## Scenarios
|
|
113
|
+
|
|
114
|
+
A persona that signs in through the upstream needs an upstream credential to
|
|
115
|
+
act with. The install adds `credentials` to `pikkuActor` in `src/auth.ts`, and
|
|
116
|
+
at sign-in each actor stores `ACTOR_CREDENTIAL_<PERSONA>_<NAME>` from the
|
|
117
|
+
environment (e.g. `ACTOR_CREDENTIAL_SALES_REP_DOLIBARR`). Without it every
|
|
118
|
+
scenario step that reaches the upstream fails with `missing_credential`. The
|
|
119
|
+
`pikku-scenario` skill's `references/personas.md` has the details.
|
|
@@ -16,6 +16,10 @@ failed; a showcase where every surface is a stub has also failed. The bar for
|
|
|
16
16
|
each surface below: **it does something the app genuinely needs, and a scenario
|
|
17
17
|
proves it.** A cron job that logs "tick" is not a schedule — it is a comment.
|
|
18
18
|
|
|
19
|
+
Each surface lands with its console link, filtered by the person's level
|
|
20
|
+
(SKILL.md, "Who you are talking to"): a non-technical person sees the workflow
|
|
21
|
+
or agent page, never the queue, scheduler or wire pages behind it.
|
|
22
|
+
|
|
19
23
|
Budget the extra surfaces at one milestone each. They are not free, and a
|
|
20
24
|
half-wired workflow engine is worse than no workflow engine.
|
|
21
25
|
|
|
@@ -223,9 +223,10 @@ bunx --bun pikku scenario run local --spawn
|
|
|
223
223
|
|
|
224
224
|
## 6. Hand it over honestly
|
|
225
225
|
|
|
226
|
-
Tell the user, in one short paragraph
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
Tell the user, in one short paragraph and at their level (SKILL.md, "Who you
|
|
227
|
+
are talking to"), with console links rather than descriptions: what runs, what
|
|
228
|
+
it is seeded with, and that this is a quick build — no knowledge base, no
|
|
229
|
+
milestones, no design pass, access control clicked-through rather than proven.
|
|
229
230
|
|
|
230
231
|
**Upgrading to a real build is additive, not a rewrite.** If they want it, switch
|
|
231
232
|
to `references/app.md` and do this, in order:
|