@pikku/skills 0.12.37 → 0.12.39
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/multi-app.md +51 -76
- 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-knowledge/SKILL.md +21 -22
- package/skills/pikku-scenario/SKILL.md +9 -8
- package/skills/pikku-scenario/references/personas.md +65 -3
|
@@ -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:
|
|
@@ -3,16 +3,15 @@ name: pikku-knowledge
|
|
|
3
3
|
description: >-
|
|
4
4
|
Use when writing, reading, reorganising or validating a project's knowledge/ directory — the
|
|
5
5
|
notes that say what the app is, in the language its users use. Covers the Open Knowledge Format
|
|
6
|
-
note (path-as-identity markdown, YAML frontmatter, only `type` required), the
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
note), or to write a scenario test (use pikku-scenario).
|
|
6
|
+
note (path-as-identity markdown, YAML frontmatter, only `type` required), the app-project
|
|
7
|
+
profile's sections (milestones, entities, decisions, questions, wishlist) and what each answers,
|
|
8
|
+
milestone status/entities/gherkin rules, the `resource:` URI scheme tying a note to its code,
|
|
9
|
+
what is NOT a knowledge base, and `pikku knowledge validate|index`. TRIGGER when: user asks to
|
|
10
|
+
write down a decision, requirement, entity or open question; asks what the app does or is; asks
|
|
11
|
+
about knowledge/, notes, milestones, an index.md, or a diagram, callout or decision block; or
|
|
12
|
+
hands over a product brief to record. DO NOT TRIGGER when: user asks what functions, routes,
|
|
13
|
+
tables or permissions exist (that is `pikku meta` / `pikku info`, never a note), or to write a
|
|
14
|
+
scenario test (use pikku-scenario).
|
|
16
15
|
installGroups: [core]
|
|
17
16
|
agent:
|
|
18
17
|
tools: read, write, edit, bash, grep
|
|
@@ -76,7 +75,7 @@ Frontmatter fields:
|
|
|
76
75
|
|
|
77
76
|
| Field | Meaning |
|
|
78
77
|
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
-
| `type` | **The only required field.**
|
|
78
|
+
| `type` | **The only required field.** One of `milestone`, `entity`, `decision`, `note`, `overview`. Lowercase — every gate compares it literally, and `milestone` is the exact string `readMilestones` filters on, so a near-miss is a note no command can see. |
|
|
80
79
|
| `title` | What to call the note in a listing. Falls back to the first heading, then the filename. |
|
|
81
80
|
| `description` | One line, used as the note's subtitle in a section index. |
|
|
82
81
|
| `resource` | Comma-separated `<kind>:<id>` URIs — the code this note is about. See below. |
|
|
@@ -92,9 +91,9 @@ Plain markdown links between notes — `[revocation](../decisions/revocation-end
|
|
|
92
91
|
```
|
|
93
92
|
knowledge/
|
|
94
93
|
index.md # type: overview — the map
|
|
95
|
-
|
|
94
|
+
milestones/
|
|
96
95
|
index.md
|
|
97
|
-
01-the-daily-entry.md # type:
|
|
96
|
+
01-the-daily-entry.md # type: milestone
|
|
98
97
|
entities/
|
|
99
98
|
index.md
|
|
100
99
|
entry.md # type: entity
|
|
@@ -116,7 +115,7 @@ Each section answers exactly one question, which is what lets a reader find a no
|
|
|
116
115
|
|
|
117
116
|
| Section | The question it answers |
|
|
118
117
|
| --------------------- | ------------------------------------------------------------------ |
|
|
119
|
-
| `
|
|
118
|
+
| `milestones/` | What is one buildable piece of this app, and what proves it works? |
|
|
120
119
|
| `entities/` | What is this thing, in the words users use for it? |
|
|
121
120
|
| `decisions/` | What was chosen, and what does that rule out? |
|
|
122
121
|
| `decisions/security/` | Who may do what? |
|
|
@@ -125,13 +124,13 @@ Each section answers exactly one question, which is what lets a reader find a no
|
|
|
125
124
|
|
|
126
125
|
**Create a section the turn you have a note for it** — never a scaffold of empty directories, and never a section without its own `index.md`. A section index says in one line what belongs in it; that sentence is the reason the file exists, so `pikku knowledge index` writes only the note listing and leaves your prose alone.
|
|
127
126
|
|
|
128
|
-
##
|
|
127
|
+
## Milestones
|
|
129
128
|
|
|
130
|
-
A
|
|
129
|
+
A milestone is the one note type that is a piece of _work_ rather than a fact, so it alone carries state and size. It lives in `knowledge/milestones/` and nowhere else: `readMilestones` matches on that directory literally, so the same note under another section is invisible to every gate and command below.
|
|
131
130
|
|
|
132
131
|
````markdown
|
|
133
132
|
---
|
|
134
|
-
type:
|
|
133
|
+
type: milestone
|
|
135
134
|
title: The daily entry
|
|
136
135
|
description: An owner writes one entry per day, and sees it on the day.
|
|
137
136
|
status: proposed
|
|
@@ -151,9 +150,9 @@ And writing again replaces it rather than adding a second
|
|
|
151
150
|
```
|
|
152
151
|
````
|
|
153
152
|
|
|
154
|
-
- **`status`** is `
|
|
153
|
+
- **`status`** is `proposed` → `dispatched` → `built`. Nothing else — `MILESTONE_STATUSES` is those three, and every gate compares them literally, so an invented status fails `validate` rather than degrading. Only `proposed` is dispatchable. A profile may add one of its own ahead of `proposed` for work that is written down but must not be built yet; that is the profile's to define and validate, not core's.
|
|
155
154
|
- **`statusAt:` and `attempts:` are bookkeeping, not content — never hand-edit them.** A loop driving this base writes both. `statusAt:` is stamped by whatever moved the status, and is what makes "how long has this been building?" answerable; the file's mtime is not the transition time, because a note is edited after dispatch for all sorts of reasons. `attempts:` is `seat@hash` entries recording which seat has already tried to move this note forward, against the content it was trying to move — it is the loop's only brake, and clearing it by hand hands back a budget that exists to stop a note nothing can satisfy being rewritten forever. Rewriting the note's real content refunds that budget on its own, which is the point: an answer that changes the note is what unsticks it.
|
|
156
|
-
- **`entities`** lists what the
|
|
155
|
+
- **`entities`** lists what the milestone touches, **at most three**. Past three it is not one buildable piece — split it.
|
|
157
156
|
- **The scenario is a fenced `gherkin` block, in the third person.** `Given 'owner' has no entry` — never `Given I have no entry`. A quoted word _means a persona_, which is what lets a reader (and a test) tell who is acting. First person hides that, so it is rejected. The console draws the keywords as a column and each quoted persona as a chip, so a first-person scenario is visibly a block with no personas in it.
|
|
158
157
|
|
|
159
158
|
## Showing it
|
|
@@ -231,7 +230,7 @@ These are all things that exist somewhere better, so a note is always the copy t
|
|
|
231
230
|
| Do not write | Because it lives in |
|
|
232
231
|
| ----------------------------------- | ---------------------------------------------------------- |
|
|
233
232
|
| a `personas/` section | `definePersonas()` in the project's own code |
|
|
234
|
-
| a `scenarios/` section | the gherkin block inside the
|
|
233
|
+
| a `scenarios/` section | the gherkin block inside the milestone it belongs to |
|
|
235
234
|
| a `permissions/` section | a decision note under `decisions/security/` |
|
|
236
235
|
| a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |
|
|
237
236
|
| a changelog | `CHANGELOG.md` at the repo root |
|
|
@@ -250,7 +249,7 @@ pikku knowledge index # refresh every index.md
|
|
|
250
249
|
pikku knowledge index --check # report stale indexes without writing (CI gate)
|
|
251
250
|
```
|
|
252
251
|
|
|
253
|
-
`validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares,
|
|
252
|
+
`validate` reports: notes with no `type`, a missing `knowledge/index.md`, a section with no `index.md`, notes flat at the root, sections that duplicate what the project already declares, milestones with a bad or missing `status`, milestones over three entities, milestones with no gherkin block or a first-person one, `decision` fences that state no `chosen:` or rule nothing out, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.
|
|
254
253
|
|
|
255
254
|
`index` rewrites only the block between `<!-- pikku:knowledge-index -->` markers, creating a scaffolded `index.md` for a section that has none. It is idempotent — running it twice changes nothing.
|
|
256
255
|
|
|
@@ -312,4 +311,4 @@ The exception, and it is narrow: bookkeeping the loop owns. `statusAt:` and `att
|
|
|
312
311
|
|
|
313
312
|
OKF permits frontmatter fields a reader does not know, and the parser ignores them rather than failing. That is the extension point: a tool layered on Pikku can add its own sections and fields on top of everything above without forking the format.
|
|
314
313
|
|
|
315
|
-
Fabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — and a `design:` field on a
|
|
314
|
+
Fabric is the one that exists. It adds `decisions/design/` — rules about how the app looks and behaves — a `screens/` section, and a `design:` field on a milestone pointing at the design options it was built from. Both are Fabric's to validate; `pikku knowledge validate` passes them through untouched. Everything else in this skill is the same in both.
|
|
@@ -35,13 +35,13 @@ Use this skill as an execution checklist, not reference material.
|
|
|
35
35
|
This skill covers writing and running scenarios end to end. Four topics are one level down, and
|
|
36
36
|
each says when to open it:
|
|
37
37
|
|
|
38
|
-
| Read | For
|
|
39
|
-
| --------------------------- |
|
|
40
|
-
| `references/steps.md` | Authoring a `pikkuScenarioStep` — intent, witnesses, what a step is given
|
|
41
|
-
| `references/browser.md` | Browser bindings, and locating by message key in a translated app
|
|
42
|
-
| `references/personas.md` | Personas vs actors, `definePersonas`, and the human "Sign in as …" switcher |
|
|
43
|
-
| `references/coverage.md` | Live coverage, filling it, and unit tests for pure logic
|
|
44
|
-
| `references/persona-run.md` | Running a persona as a model-driven virtual user against a stage
|
|
38
|
+
| Read | For |
|
|
39
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
40
|
+
| `references/steps.md` | Authoring a `pikkuScenarioStep` — intent, witnesses, what a step is given |
|
|
41
|
+
| `references/browser.md` | Browser bindings, and locating by message key in a translated app |
|
|
42
|
+
| `references/personas.md` | Personas vs actors, `definePersonas`, upstream credentials for actors, and the human "Sign in as …" switcher |
|
|
43
|
+
| `references/coverage.md` | Live coverage, filling it, and unit tests for pure logic |
|
|
44
|
+
| `references/persona-run.md` | Running a persona as a model-driven virtual user against a stage |
|
|
45
45
|
|
|
46
46
|
## What a scenario is
|
|
47
47
|
|
|
@@ -245,7 +245,7 @@ suite survives its first redesign:
|
|
|
245
245
|
through the actor.
|
|
246
246
|
- **Steps describe intent, not actions** — `buys the £5 strawberry milkshake`, never
|
|
247
247
|
`clicks [data-testid=add]`. The clicking lives in plain utilities the step composes.
|
|
248
|
-
- **`then` bindings are witnesses, not alternatives** — a `then` runs
|
|
248
|
+
- **`then` bindings are witnesses, not alternatives** — a `then` runs _every_ binding it declares
|
|
249
249
|
and fails when they disagree, because "database right, user still watching a spinner" is the bug
|
|
250
250
|
nobody catches.
|
|
251
251
|
- **What language the prose is in** — identifiers are English in every project; `description`,
|
|
@@ -265,6 +265,7 @@ whole project — codegen builds the `PersonaId` union from it, materialises one
|
|
|
265
265
|
scenario actor per person, and seeds a user row each:
|
|
266
266
|
|
|
267
267
|
```ts snippet:definePersonas
|
|
268
|
+
|
|
268
269
|
```
|
|
269
270
|
|
|
270
271
|
`pikku.config.json` carries the settings around them — nothing about a person:
|
|
@@ -5,7 +5,7 @@ A **persona** is a person your product is for; an **actor** is one body that sig
|
|
|
5
5
|
- Two people of the same kind are two entries, not one persona with two logins — "you see yours, not theirs" is only testable with two customers.
|
|
6
6
|
- Never write an email address: each is derived from the persona id and `scenarios.emailDomain`, and a hand-written one signs in as somebody who was never created.
|
|
7
7
|
- `roles` is typechecked against `defineSystemRole`; an undeclared role is a build error.
|
|
8
|
-
- A person who is only ever acted
|
|
8
|
+
- A person who is only ever acted _upon_ — the account an admin bans — sets `runnable: false`: declared and seeded, never signed in, because a run as them would race the scenario that acts on them.
|
|
9
9
|
- A persona holds only what is true of that kind of person for the app's whole lifetime (`name`, `jobTitle`, `description`, `personality`, `roles`, `goals`, `disposition`). What someone is trying to get done, and the circumstances they are doing it in, belong to the **scenario**, not to them.
|
|
10
10
|
|
|
11
11
|
## Declaring personas in TypeScript
|
|
@@ -50,6 +50,32 @@ A project that never declares a persona keeps working: a scenario that names no
|
|
|
50
50
|
- `environments.<name>.apiUrl` is required. `signInPath` defaults to `/auth/sign-in/actor`, `rpcPath` to `/rpc`.
|
|
51
51
|
- **`SCENARIO_ACTOR_SECRET` is an environment variable and never goes in `pikku.config.json`.** It signs actors in. `pikku scenario run` throws without it; a server auto-building actors warns and runs without them.
|
|
52
52
|
|
|
53
|
+
## Actors that call a third-party API
|
|
54
|
+
|
|
55
|
+
An addon generated from an upstream API (Dolibarr, a CRM, a calendar) calls it
|
|
56
|
+
with the signed-in user's own credential. An actor has none until one is stored
|
|
57
|
+
for it, so every step that reaches the addon fails with `No <X> session`. Give
|
|
58
|
+
the `actor` plugin a `credentials` option, and each actor carries its upstream
|
|
59
|
+
credential into every session:
|
|
60
|
+
|
|
61
|
+
- `names: ['dolibarr']` — the credentials to carry
|
|
62
|
+
- `store: (name, value, userId) => credentialService.set(name, value, userId)`
|
|
63
|
+
- `remove: (name, userId) => credentialService.delete(name, userId)`
|
|
64
|
+
|
|
65
|
+
At each actor sign-in the plugin reads `ACTOR_CREDENTIAL_<PERSONA>_<NAME>` —
|
|
66
|
+
`dan` + `dolibarr` is `ACTOR_CREDENTIAL_DAN_DOLIBARR` — and stores it with
|
|
67
|
+
`credentialService.set` for that actor. A bare value is stored as `{ token }`
|
|
68
|
+
(what a delegated or bearer credential holds); a JSON object is stored as-is,
|
|
69
|
+
e.g. `{"apiKey":"…"}` for an API-key credential. Unset means the actor has no
|
|
70
|
+
upstream credential: `remove` drops one stored at an earlier sign-in.
|
|
71
|
+
|
|
72
|
+
- **Values live in `.env` (or CI secrets), never in `personas.ts` or code.**
|
|
73
|
+
Use a dedicated upstream test account per persona, not a real person's.
|
|
74
|
+
- `read` defaults to `process.env`; a Worker passes `(key) => variables.get(key)`.
|
|
75
|
+
- A malformed JSON value refuses the sign-in with a 500 that names the variable.
|
|
76
|
+
- A delegated token expires upstream like any other; re-signing the actor
|
|
77
|
+
in re-stores whatever the variable holds now.
|
|
78
|
+
|
|
53
79
|
## The same actors sign a human in
|
|
54
80
|
|
|
55
81
|
Declared actors are not only for automated runs. `signInPath` is Better Auth's
|
|
@@ -57,8 +83,8 @@ Declared actors are not only for automated runs. `signInPath` is Better Auth's
|
|
|
57
83
|
frontend gets a one-click "Sign in as …" switcher over the **same** list, and an
|
|
58
84
|
app can be reviewed as each kind of user without anyone knowing a seed password.
|
|
59
85
|
|
|
60
|
-
The
|
|
61
|
-
personas
|
|
86
|
+
The dev server bakes both halves into the frontend from the declared
|
|
87
|
+
personas — the sandbox's, or the template's `bun run dev`, never `pikku dev`: `VITE_DEV_ACTORS` (the JSON actor list) and `VITE_DEV_ACTOR_SECRETS`
|
|
62
88
|
(`{ email: credential }`, one per persona — `SCENARIO_ACTOR_SECRET` itself never
|
|
63
89
|
goes in a bundle; see **pikku-auth**). Neither var is set in a production
|
|
64
90
|
build, so the control renders nothing there — but gate the reads on your
|
|
@@ -85,3 +111,39 @@ When the switcher is there but signing in fails with `401 Invalid actor
|
|
|
85
111
|
secret`, check which server answered before checking the secret: a frontend
|
|
86
112
|
whose dev proxy (`VITE_API_PROXY`, default `http://localhost:3000`) points at
|
|
87
113
|
another project's API sends the sign-in there.
|
|
114
|
+
|
|
115
|
+
**A runner of your own that starts vite has to bake them itself**, from the
|
|
116
|
+
generated persona meta (`<outDir>/workflow/personas.gen.json`, which already
|
|
117
|
+
carries the derived `email`):
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const personas = Object.values(JSON.parse(readFileSync(personasPath, 'utf8')))
|
|
121
|
+
|
|
122
|
+
env.VITE_DEV_ACTORS = JSON.stringify(
|
|
123
|
+
personas.map(({ id, email, name, jobTitle }) => ({
|
|
124
|
+
key: id,
|
|
125
|
+
email,
|
|
126
|
+
name,
|
|
127
|
+
jobTitle: jobTitle ?? '',
|
|
128
|
+
}))
|
|
129
|
+
)
|
|
130
|
+
env.VITE_DEV_ACTOR_SECRETS = JSON.stringify(
|
|
131
|
+
Object.fromEntries(
|
|
132
|
+
await Promise.all(
|
|
133
|
+
personas.map(async ({ email }) => [
|
|
134
|
+
email,
|
|
135
|
+
await deriveActorSecret(env.SCENARIO_ACTOR_SECRET, email),
|
|
136
|
+
])
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Set `SCENARIO_ACTOR_SECRET` yourself**, at least 32 characters, in the
|
|
143
|
+
environment both processes read. Left unset, `pikku dev` mints an ephemeral root
|
|
144
|
+
for its own run that a separately spawned vite cannot see, so the two derive
|
|
145
|
+
from different roots: the switcher renders every persona and each click is
|
|
146
|
+
refused, which reads as a broken login rather than missing configuration. On a
|
|
147
|
+
brand-new project the persona file does not exist until the first `pikku dev`
|
|
148
|
+
codegen, after vite has baked an empty list — watch it and restart the frontend
|
|
149
|
+
when it changes.
|