@pikku/skills 0.12.4 → 0.12.8
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 +74 -33
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +45 -10
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +75 -10
- package/skills/pikku-config/SKILL.md +56 -14
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +27 -2
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +6 -1
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +50 -7
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +141 -76
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +123 -11
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
|
@@ -45,15 +45,28 @@ await jwt.init()
|
|
|
45
45
|
|
|
46
46
|
**Constructor Parameters:**
|
|
47
47
|
|
|
48
|
-
- `getSecrets` — Async function returning an array of `{ id, value }` key pairs.
|
|
48
|
+
- `getSecrets` — Async function returning an array of `{ id, value }` key pairs. The **first** entry signs; every entry can verify.
|
|
49
49
|
- `logger` — Optional logger instance.
|
|
50
50
|
|
|
51
51
|
**Methods:**
|
|
52
52
|
|
|
53
53
|
- `init(): Promise<void>` — Fetch and cache secrets. Call at startup.
|
|
54
|
-
- `encode<T>(expiresIn: RelativeTimeInput, payload: T): Promise<string>` — Create a signed JWT.
|
|
55
|
-
- `decode<T>(token: string): Promise<T>` —
|
|
56
|
-
- `verify(token: string): Promise<void>` —
|
|
54
|
+
- `encode<T>(expiresIn: RelativeTimeInput, payload: T): Promise<string>` — Create a signed JWT, stamping the signing key's `id` as the token's `kid` header.
|
|
55
|
+
- `decode<T>(token: string): Promise<T>` — **Verifies** the signature and expiry, then returns the payload.
|
|
56
|
+
- `verify(token: string): Promise<void>` — The same check, discarding the payload.
|
|
57
|
+
|
|
58
|
+
`decode` is not an unchecked read: both methods run `jose.jwtVerify` and both
|
|
59
|
+
throw on a bad signature or an expired token. There is no way to inspect an
|
|
60
|
+
untrusted payload through this service — reach for `jose.decodeJwt` directly if
|
|
61
|
+
you genuinely need that, and treat the result as unauthenticated input.
|
|
62
|
+
|
|
63
|
+
Tokens are signed **HS256** with a symmetric secret. The algorithm is fixed and
|
|
64
|
+
pinned on verification, so a token arriving with any other `alg` is rejected —
|
|
65
|
+
but it also means this service has no asymmetric (RS256/ES256) mode.
|
|
66
|
+
|
|
67
|
+
`init()` is not strictly required: `encode` calls it lazily on first use. Call it
|
|
68
|
+
at startup anyway so a missing or unreachable secret fails at boot rather than
|
|
69
|
+
on the first request that needs a token.
|
|
57
70
|
|
|
58
71
|
## Usage Patterns
|
|
59
72
|
|
|
@@ -63,15 +76,20 @@ await jwt.init()
|
|
|
63
76
|
import { JoseJWTService } from '@pikku/jose'
|
|
64
77
|
|
|
65
78
|
const jwt = new JoseJWTService(
|
|
66
|
-
async () => [{ id: 'key-1', value:
|
|
79
|
+
async () => [{ id: 'key-1', value: await secrets.getSecret('JWT_SECRET') }],
|
|
67
80
|
logger
|
|
68
81
|
)
|
|
69
82
|
await jwt.init()
|
|
70
83
|
```
|
|
71
84
|
|
|
85
|
+
A signing key is a secret, so it comes from the secrets service rather than
|
|
86
|
+
`process.env` — and because `getSecrets` is a function called on demand, reading
|
|
87
|
+
it there (not once at construction) is what makes the re-init-on-unknown-kid path
|
|
88
|
+
above actually see a rotated key. See `pikku-config`.
|
|
89
|
+
|
|
72
90
|
### Secret Rotation
|
|
73
91
|
|
|
74
|
-
Supply multiple keys. The first
|
|
92
|
+
Supply multiple keys. The first signs; the rest stay available for verification:
|
|
75
93
|
|
|
76
94
|
```typescript
|
|
77
95
|
const jwt = new JoseJWTService(async () => [
|
|
@@ -80,6 +98,17 @@ const jwt = new JoseJWTService(async () => [
|
|
|
80
98
|
])
|
|
81
99
|
```
|
|
82
100
|
|
|
101
|
+
Verification resolves the key by the token's `kid` header rather than trying each
|
|
102
|
+
secret in turn — which is why `encode` stamps the signing key's `id` there, and
|
|
103
|
+
why the ids must stay stable across a rotation. Keep an id in the list for as
|
|
104
|
+
long as tokens bearing it can still be in flight.
|
|
105
|
+
|
|
106
|
+
When a `kid` isn't in the cache, the service re-runs `getSecrets()` once before
|
|
107
|
+
giving up with `Missing secret for id: <kid>`. That is what lets a running server
|
|
108
|
+
pick up a newly added key without a restart, provided `getSecrets` reads from
|
|
109
|
+
something live (a secret store) rather than a value captured at boot. A token
|
|
110
|
+
with no `kid` at all falls back to the current signing key.
|
|
111
|
+
|
|
83
112
|
### With Pikku Services
|
|
84
113
|
|
|
85
114
|
```typescript
|
|
@@ -5,13 +5,14 @@ description: >-
|
|
|
5
5
|
notes that say what the app is, in the language its users use. Covers the Open Knowledge Format
|
|
6
6
|
note (path-as-identity markdown, YAML frontmatter, only `type` required), the sections of the
|
|
7
7
|
app-project profile (slices, entities, decisions, questions, wishlist) and the one question each
|
|
8
|
-
answers, slice status/entities/gherkin rules, the `resource:` URI scheme
|
|
8
|
+
answers, slice status/entities/gherkin rules, the `resource:` URI scheme tying a note to the
|
|
9
9
|
code it is about, the shapes that are NOT a knowledge base, and the `pikku knowledge
|
|
10
|
-
validate|index` commands. TRIGGER when: user asks to write down a decision,
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
validate|index` commands. TRIGGER when: user asks to write down a decision, requirement, entity
|
|
11
|
+
or open question; asks what the app does or is; asks about knowledge/, notes, slices,
|
|
12
|
+
an index.md, or a diagram, callout or decision block; or hands over a product
|
|
13
|
+
brief to record. DO NOT TRIGGER when: user asks what
|
|
13
14
|
functions, routes, tables or permissions exist (that is `pikku meta` / `pikku info`, never a
|
|
14
|
-
note), or
|
|
15
|
+
note), or to write a scenario test (use pikku-scenario).
|
|
15
16
|
installGroups: [core]
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -142,7 +143,49 @@ And writing again replaces it rather than adding a second
|
|
|
142
143
|
|
|
143
144
|
- **`status`** is `proposed` → `dispatched` → `built`. Nothing else. Every gate compares it literally.
|
|
144
145
|
- **`entities`** lists what the slice touches, **at most three**. Past three it is not one buildable piece — split it.
|
|
145
|
-
- **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.
|
|
146
|
+
- **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.
|
|
147
|
+
|
|
148
|
+
## Showing it
|
|
149
|
+
|
|
150
|
+
A note is markdown, and four kinds of block are **drawn** rather than printed. Every one of them degrades to something readable — a diagram falls back to its source, a callout to a blockquote, a decision to a code block — so writing one costs nothing where it is not rendered.
|
|
151
|
+
|
|
152
|
+
None of this changes the governing rule. A diagram of the schema is still a copy of `pikku meta` that drifts, and it drifts while looking more authoritative than prose would. These are for the part no generator can derive.
|
|
153
|
+
|
|
154
|
+
**```mermaid — when the relationship is the point.** Prose is bad at graphs: "an entry belongs to a day, a day belongs to an owner, and a grant lets another owner read a day" is a sentence a reader has to re-read twice and draw themselves. Reach for one when a note is about how several things relate, an order of steps across time, or a state machine. Do not draw one thing, or two things and an arrow — that is a sentence.
|
|
155
|
+
|
|
156
|
+
````markdown
|
|
157
|
+
```mermaid
|
|
158
|
+
flowchart LR
|
|
159
|
+
owner -->|writes| entry
|
|
160
|
+
entry -->|belongs to| day
|
|
161
|
+
owner -->|grants read on| day
|
|
162
|
+
```
|
|
163
|
+
````
|
|
164
|
+
|
|
165
|
+
**`> [!NOTE]` — when a line must survive skimming.** Five kinds: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`. Use one for the thing a reader who skips the paragraph must still not miss — a trap, a constraint that is easy to violate, an assumption the rest of the note rests on. Two callouts in a note is normal; six means the note has no prose left and nothing stands out.
|
|
166
|
+
|
|
167
|
+
```markdown
|
|
168
|
+
> [!WARNING]
|
|
169
|
+
> A grant is checked on every request, not cached. A permission change is
|
|
170
|
+
> immediate everywhere, and there is no invalidation step to forget.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**```decision — the answer a decision note owes.** `decisions/` answers "what was chosen, and what does that rule out?", and the second half is the half that gets dropped. The fence makes it checkable: `pikku knowledge validate` warns when a fence says what was chosen and never says what it closes off.
|
|
174
|
+
|
|
175
|
+
````markdown
|
|
176
|
+
```decision
|
|
177
|
+
chosen: A revoked grant stops working immediately, everywhere.
|
|
178
|
+
rules-out:
|
|
179
|
+
- A "revoked but valid until midnight" state
|
|
180
|
+
- A scheduled cleanup job
|
|
181
|
+
because: Two people disagreeing about who can see today is worse than one of
|
|
182
|
+
them losing access mid-session.
|
|
183
|
+
```
|
|
184
|
+
````
|
|
185
|
+
|
|
186
|
+
It is a **summary, not the note** — the argument continues in prose underneath. `rules-out:` takes one line or a `- item` block, and any value too long for one line wraps onto indented lines under it, as `because:` does above. A decision genuinely argued in prose needs no fence, and validate never asks for one; what it does ask is that a fence you did write is complete.
|
|
187
|
+
|
|
188
|
+
**Fences of any other language are code** — highlighted and copyable, which is right for a snippet and wrong for a scenario or a decision, so do not put either in a bare fence.
|
|
146
189
|
|
|
147
190
|
## `resource:` — tying a note to the code
|
|
148
191
|
|
|
@@ -196,7 +239,7 @@ pikku knowledge index # refresh every index.md
|
|
|
196
239
|
pikku knowledge index --check # report stale indexes without writing (CI gate)
|
|
197
240
|
```
|
|
198
241
|
|
|
199
|
-
`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, slices with a bad or missing `status`, slices over three entities, slices with no gherkin block or a first-person one, and every `resource:` that no longer resolves. Errors fail the command; warnings do not.
|
|
242
|
+
`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, slices with a bad or missing `status`, slices over three entities, slices 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.
|
|
200
243
|
|
|
201
244
|
`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.
|
|
202
245
|
|
|
@@ -28,7 +28,7 @@ Use this skill as an execution checklist, not reference material.
|
|
|
28
28
|
|
|
29
29
|
## Writing Queries — the Kysely query builder
|
|
30
30
|
|
|
31
|
-
In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly.
|
|
31
|
+
In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Every connection factory below wires the **CamelCasePlugin** by default, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. If a project opted out (`createNodeSqliteKysely({ camelCase: false })`), that inverts — check how the instance was built before assuming. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
34
|
import { sql } from 'kysely'
|
|
@@ -92,12 +92,19 @@ await kysely.transaction().execute(async (trx) => {
|
|
|
92
92
|
})
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
Pikku provides SQL database services through
|
|
95
|
+
Pikku provides SQL database services through six packages:
|
|
96
96
|
|
|
97
|
-
- `@pikku/kysely` — Base service implementations (database-agnostic)
|
|
98
|
-
- `@pikku/kysely-postgres` — PostgreSQL-specific implementations + `PikkuKysely` connection wrapper
|
|
97
|
+
- `@pikku/kysely` — Base service implementations (database-agnostic), the serialize plugins, `createAuditedKysely` and the `pikkuSchemas` helpers
|
|
98
|
+
- `@pikku/kysely-postgres` — PostgreSQL-specific implementations + the `PikkuKysely` connection wrapper and `PgEventHubService` (LISTEN/NOTIFY-backed)
|
|
99
99
|
- `@pikku/kysely-mysql` — MySQL-specific implementations
|
|
100
|
-
- `@pikku/kysely-sqlite` — SQLite-specific implementations
|
|
100
|
+
- `@pikku/kysely-sqlite` — SQLite-specific implementations, `createSQLiteKysely`, and the `LibsqlWebDialect`
|
|
101
|
+
- `@pikku/kysely-node-sqlite` — `createNodeSqliteKysely` over `node:sqlite`, plus user-defined SQL functions and the coercion plugin
|
|
102
|
+
- `@pikku/kysely-bun-sqlite` — the same over `bun:sqlite`
|
|
103
|
+
|
|
104
|
+
The last two are runtime adapters rather than service sets: they build the
|
|
105
|
+
`Kysely<DB>` you inject into functions, while the dialect packages above supply
|
|
106
|
+
Pikku's own stores. They differ in one place — `bun:sqlite` cannot register
|
|
107
|
+
scalar functions, so `createBunSqliteKysely` throws if you pass `functions`.
|
|
101
108
|
|
|
102
109
|
All implement standard Pikku interfaces from `@pikku/core`.
|
|
103
110
|
|
|
@@ -105,9 +112,11 @@ All implement standard Pikku interfaces from `@pikku/core`.
|
|
|
105
112
|
|
|
106
113
|
```bash
|
|
107
114
|
# Pick your database
|
|
108
|
-
yarn add @pikku/kysely @pikku/kysely-postgres
|
|
109
|
-
yarn add @pikku/kysely @pikku/kysely-mysql
|
|
110
|
-
yarn add @pikku/kysely @pikku/kysely-sqlite
|
|
115
|
+
yarn add @pikku/kysely @pikku/kysely-postgres # PostgreSQL
|
|
116
|
+
yarn add @pikku/kysely @pikku/kysely-mysql # MySQL
|
|
117
|
+
yarn add @pikku/kysely @pikku/kysely-sqlite # SQLite (stores)
|
|
118
|
+
yarn add @pikku/kysely-node-sqlite # SQLite on Node
|
|
119
|
+
yarn add @pikku/kysely-bun-sqlite # SQLite on Bun
|
|
111
120
|
```
|
|
112
121
|
|
|
113
122
|
## API Reference
|
|
@@ -120,7 +129,8 @@ import { PikkuKysely } from '@pikku/kysely-postgres'
|
|
|
120
129
|
const db = new PikkuKysely<DB>(
|
|
121
130
|
logger: Logger,
|
|
122
131
|
connectionOrConfig: postgres.Sql | postgres.Options | string,
|
|
123
|
-
defaultSchemaName?: string
|
|
132
|
+
defaultSchemaName?: string,
|
|
133
|
+
poolConfig?: PostgresConfig // maxPool, connectTimeout, idleTimeout, maxLifetime, prepare, statementTimeout
|
|
124
134
|
)
|
|
125
135
|
|
|
126
136
|
await db.init()
|
|
@@ -128,14 +138,39 @@ db.kysely // Kysely<DB> instance for queries
|
|
|
128
138
|
await db.close()
|
|
129
139
|
```
|
|
130
140
|
|
|
131
|
-
|
|
141
|
+
It builds a postgres.js-backed Kysely with the CamelCasePlugin. Pass an existing
|
|
142
|
+
`postgres.Sql` when something else owns the pool — the wrapper then leaves it
|
|
143
|
+
open on `close()`. `poolConfig` keys are only forwarded when set, so postgres.js
|
|
144
|
+
keeps its own defaults for the rest, and it is ignored entirely when you hand in
|
|
145
|
+
an already-constructed connection.
|
|
146
|
+
|
|
147
|
+
### SQLite factories
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { createNodeSqliteKysely } from '@pikku/kysely-node-sqlite'
|
|
151
|
+
|
|
152
|
+
// Your application DB — CamelCasePlugin on by default
|
|
153
|
+
const kysely = createNodeSqliteKysely<DB>({
|
|
154
|
+
filename: 'app.db', // or ':memory:'
|
|
155
|
+
camelCase: true,
|
|
156
|
+
plugins: [], // layered on top
|
|
157
|
+
functions: {}, // scalar UDFs, registered as deterministic (Node only)
|
|
158
|
+
})
|
|
159
|
+
```
|
|
132
160
|
|
|
133
161
|
```typescript
|
|
134
162
|
import { createSQLiteKysely } from '@pikku/kysely-sqlite'
|
|
135
163
|
|
|
136
|
-
|
|
164
|
+
// Pikku's own tables — returns Kysely<KyselyPikkuDB>, not your DB
|
|
165
|
+
const pikkuDb = createSQLiteKysely(database: SqliteDatabase | (() => Promise<SqliteDatabase>))
|
|
137
166
|
```
|
|
138
167
|
|
|
168
|
+
These two are not interchangeable. `createSQLiteKysely` is typed to
|
|
169
|
+
`KyselyPikkuDB` and wires the `SerializePlugin` (JSON columns in and out) rather
|
|
170
|
+
than the CamelCasePlugin, because it exists to back the stores below. Reach for
|
|
171
|
+
`createNodeSqliteKysely` / `createBunSqliteKysely` for the instance your
|
|
172
|
+
functions query.
|
|
173
|
+
|
|
139
174
|
### Available Services
|
|
140
175
|
|
|
141
176
|
Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLite`, or base `Kysely`):
|
|
@@ -151,24 +186,52 @@ Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLi
|
|
|
151
186
|
| `*AgentRunService` | `AgentRunService` | Agent execution tracking |
|
|
152
187
|
| `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
|
|
153
188
|
|
|
189
|
+
A handful more live only on the base package — there is no `Pg`/`MySQL`/`SQLite`
|
|
190
|
+
variant to reach for, you import them from `@pikku/kysely` whatever the engine:
|
|
191
|
+
|
|
192
|
+
| Service | Purpose |
|
|
193
|
+
| -------------------------- | --------------------------------------------- |
|
|
194
|
+
| `KyselySessionStore` | Persisted user sessions |
|
|
195
|
+
| `KyselyScopeService` | Scope and role storage |
|
|
196
|
+
| `KyselyWebhookService` | Webhook registrations and deliveries |
|
|
197
|
+
| `KyselyCredentialService` | Encrypted third-party credentials |
|
|
198
|
+
| `KyselyAIRunStateService` | AI run state (also implemented by AIStorage) |
|
|
199
|
+
| `KyselyWorkflowMirror` | Mirrors workflow runs into queryable tables |
|
|
200
|
+
| `KyselyAuditService` | Durable audit sink (see `pikku-audit`) |
|
|
201
|
+
|
|
154
202
|
All services take a `Kysely<KyselyPikkuDB>` instance in their constructor and have an `init()` method that creates tables if needed.
|
|
155
203
|
|
|
156
204
|
### Secret Service
|
|
157
205
|
|
|
206
|
+
Envelope encryption: each secret gets its own DEK, wrapped by a KEK derived from
|
|
207
|
+
`key` plus a stored per-version salt. Keeping `previousKey` around is what makes
|
|
208
|
+
rotation possible — `rotateKEK` re-wraps every secret from the old key to the
|
|
209
|
+
current one and returns the new version, and it throws if no `previousKey` is
|
|
210
|
+
configured.
|
|
211
|
+
|
|
158
212
|
```typescript
|
|
159
213
|
import { PgKyselySecretService } from '@pikku/kysely-postgres'
|
|
160
214
|
|
|
161
215
|
const secrets = new PgKyselySecretService(db.kysely, {
|
|
162
|
-
|
|
163
|
-
|
|
216
|
+
key: 'your-key-encryption-passphrase',
|
|
217
|
+
keyVersion: 2, // defaults to 1
|
|
218
|
+
previousKey: 'the-passphrase-you-are-rotating-away-from',
|
|
219
|
+
audit: true, // log write/delete/rotate through the audit sink
|
|
220
|
+
auditReads: false, // reads too — noisy, off by default
|
|
164
221
|
})
|
|
165
222
|
await secrets.init()
|
|
166
223
|
|
|
167
224
|
await secrets.setSecret('api-key', { key: 'sk-...' })
|
|
168
|
-
const
|
|
169
|
-
await secrets.
|
|
225
|
+
const secret = await secrets.getSecret<{ key: string }>('api-key')
|
|
226
|
+
await secrets.hasSecret('api-key')
|
|
227
|
+
await secrets.deleteSecret('api-key')
|
|
228
|
+
const newVersion = await secrets.rotateKEK()
|
|
170
229
|
```
|
|
171
230
|
|
|
231
|
+
`getSecret` hands back a `SecretValue<T>`, not the bare value — it serializes as
|
|
232
|
+
`[secret]` until something reveals it, which is what stops a secret drifting into
|
|
233
|
+
a log line or an audit row. See `pikku-config` for the reveal rules.
|
|
234
|
+
|
|
172
235
|
## Usage Patterns
|
|
173
236
|
|
|
174
237
|
### PostgreSQL Setup
|
|
@@ -157,7 +157,42 @@ addHTTPMiddleware([
|
|
|
157
157
|
|
|
158
158
|
When the api-key header is present it is authoritative — the middleware never
|
|
159
159
|
falls through to `getSession` (a bare mock session would shadow the scoped one).
|
|
160
|
-
When it is absent, the human `getSession` path runs as normal.
|
|
160
|
+
When it is absent, the human `getSession` path runs as normal. Either way the
|
|
161
|
+
middleware bails out entirely if a session is already set, and it checks the
|
|
162
|
+
*live* session rather than the wire's construction-time snapshot, so it can't
|
|
163
|
+
clobber one an earlier middleware resolved.
|
|
164
|
+
|
|
165
|
+
### Restricting a key below its owner
|
|
166
|
+
|
|
167
|
+
Set `scopes` on the session `mapKey` returns and that set is **authoritative** —
|
|
168
|
+
including an empty one. It is never widened back out to everything the owning
|
|
169
|
+
service user holds:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
mapKey: async (key) => ({
|
|
173
|
+
userId: 'sandbox-runtime',
|
|
174
|
+
scopes: ['sandbox:read'], // this key can do only this
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Leave `scopes` unset for a key that acts with its owner's full rights. This is
|
|
179
|
+
what makes one stable service user safely able to own keys of very different
|
|
180
|
+
power — the restriction lives on the key, not on a proliferation of identities.
|
|
181
|
+
|
|
182
|
+
### Failure handling is deliberately split
|
|
183
|
+
|
|
184
|
+
A key that fails to verify is logged and treated as an ordinary "not
|
|
185
|
+
authenticated" — an unusable credential is not an outage. A failure *inside*
|
|
186
|
+
`mapKey` (your scope store is down) propagates as a real error instead. That
|
|
187
|
+
asymmetry is on purpose: a scope lookup that silently failed would serve the
|
|
188
|
+
request anonymously, which is exactly the wrong direction to fail in.
|
|
189
|
+
|
|
190
|
+
### `betterAuthStatelessSession` has no machine path
|
|
191
|
+
|
|
192
|
+
The lean cookie-cache middleware (`betterAuthStatelessSession` — no
|
|
193
|
+
`services.auth()`, no DB) handles only the human path. Machine auth needs
|
|
194
|
+
`betterAuthSession`, because `verifyApiKey` is a server call there is no
|
|
195
|
+
stateless equivalent of. Both accept an `impersonation` option.
|
|
161
196
|
|
|
162
197
|
### WebSocket channels authenticate on the upgrade handshake
|
|
163
198
|
|