@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.37",
3
+ "version": "0.12.38",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -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 — Put the spec in the repo
16
+ ## 1 — Look at the spec first
17
17
 
18
- `--openapi` reads a local path, not a URL. Download it to `specs/`, where it
19
- stays as the record of what the addon was generated from:
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
- ```bash
22
- mkdir -p specs
23
- curl -fsSL <url> -o specs/<name>.openapi.json
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
- ## 2 — Generate
27
+ Keep a copy in `specs/` as the record of what the addon was generated from.
27
28
 
28
- Inside an app, the addon is a workspace package under `packages/`:
29
+ ## 2 — Generate, from the app's root
29
30
 
30
31
  ```bash
31
- bunx --bun pikku new addon <name> --openapi specs/<name>.openapi.json --dir packages
32
+ bunx --bun pikku new addon <name> --openapi <path-or-url>
32
33
  ```
33
34
 
34
- This writes `packages/addon-<name>` as `@pikku/addon-<name>`, installs it and
35
- builds it. Inside a workspace the generated test app depends on it as
36
- `workspace:*`, so nothing is published.
37
-
38
- | Flag | When |
39
- | ----------------------------- | ----------------------------------------------------------------------------------------- |
40
- | `--credential apikey\|bearer` | The spec's `securitySchemes` is an API key or a bearer token — each user brings their own |
41
- | `--credential oauth2` | The spec's `securitySchemes` is OAuth2 |
42
- | `--auth-config <file>` | The spec gets auth wrong or leaves it out: a custom header, a delegated login endpoint |
43
- | `--mcp` | The operations should also be MCP tools |
44
- | `--camel-case` | The API's property names are snake_case and the app's are not |
45
-
46
- Read the spec's `securitySchemes` to pick the credential. Leave the flag off
47
- only when the API really takes no auth.
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, an enum of the spec's servers
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
- `<NAME>_BASE_URL` is an enum of the spec's `servers`. When those are
61
- placeholders or a per-tenant host (`https://{tenant}.example.com`, or a server
62
- list that is only an example), change its schema in `src/<name>.variable.ts`
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
- ```typescript
68
- // src/addons.ts
69
- import { wireAddon } from '#pikku/addon'
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
- wireAddon({ name: '<name>', package: '@pikku/addon-<name>' })
72
- ```
84
+ ## 4 — Call it from the app
73
85
 
74
- Then call operations by reference — `ref('<name>:<operationFn>')` in a
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`. On success it JIT-provisions a real user row (email-keyed and
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` (offering
48
- to mock the screens first, committing to a design direction, and judging whether
49
- the screens realise it — read before the first screen is built, not after the
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." Follow the `pikku-addon` skill's OpenAPI reference. |
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, as a question and not a gate: one HTML page mocking the main
66
- screens, a few minutes, far cheaper to change than built screens. If they say
67
- yes, `references/design.md` owns what to make and what it then binds — the
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. If they say no, build.
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 and approved, the milestones are read off it: every
347
- screen on that page belongs to some milestone, and a screen no milestone builds
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
- **Show the user the list before building.** This is the last cheap moment to
367
- reorder — after §6 the migrations are numbered and the order is concrete.
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 *session*, a drop-in
502
- *session* — collides in the migration. Name it for the domain instead
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
- *its* API and come back `401 Invalid actor secret`, which reads like a bad
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
- > Want me to mock the main screens as a page you can look at before I build
60
- > anything? It takes a few minutes and it is much cheaper to change a picture
61
- > than a built screen.
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
- If they decline, build; the direction in words is enough to be accountable to.
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: what runs, what it is seeded with, and
227
- that this is a quick build — no knowledge base, no milestones, no design pass,
228
- access control clicked-through rather than proven.
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: