@pikku/skills 0.12.21 → 0.12.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +10 -9
- package/skills/pikku-agent/SKILL.md +67 -316
- package/skills/pikku-agent/references/agents.md +299 -0
- package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
- package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
- package/skills/pikku-architect/SKILL.md +264 -0
- package/skills/pikku-auth/SKILL.md +89 -0
- package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +42 -47
- package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
- package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
- package/skills/{pikku-permissions/SKILL.md → pikku-auth/references/permissions.md} +5 -24
- package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +3 -23
- package/skills/pikku-build/SKILL.md +87 -0
- package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +77 -25
- package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
- package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +1 -1
- package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
- package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
- package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +6 -22
- package/skills/{pikku-build-app → pikku-build}/references/ship.md +7 -1
- package/skills/pikku-concepts/SKILL.md +75 -8
- package/skills/pikku-concepts/references/concept-mapping.md +8 -8
- package/skills/pikku-deploy/SKILL.md +158 -0
- package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
- package/skills/pikku-deploy/references/cloudflare.md +104 -0
- package/skills/pikku-deploy/references/express.md +92 -0
- package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
- package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
- package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
- package/skills/pikku-deploy/references/uws.md +72 -0
- package/skills/pikku-deploy/references/ws.md +75 -0
- package/skills/pikku-emails/SKILL.md +3 -2
- package/skills/pikku-fabric/SKILL.md +20 -10
- package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
- package/skills/pikku-i18n/SKILL.md +60 -207
- package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
- package/skills/pikku-i18n/references/messages.md +218 -0
- package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
- package/skills/pikku-knowledge/SKILL.md +14 -0
- package/skills/pikku-kysely/SKILL.md +13 -13
- package/skills/pikku-meta/SKILL.md +58 -130
- package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
- package/skills/pikku-meta/references/meta.md +114 -0
- package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
- package/skills/pikku-middleware/SKILL.md +8 -8
- package/skills/pikku-n8n-import/SKILL.md +0 -1
- package/skills/pikku-react/SKILL.md +50 -298
- package/skills/pikku-react/references/client.md +293 -0
- package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
- package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
- package/skills/pikku-scenario/SKILL.md +64 -49
- package/skills/pikku-scenario/references/persona-run.md +148 -0
- package/skills/pikku-service-backends/SKILL.md +154 -0
- package/skills/pikku-service-backends/references/aws.md +106 -0
- package/skills/pikku-service-backends/references/backblaze.md +57 -0
- package/skills/pikku-service-backends/references/mongodb.md +90 -0
- package/skills/pikku-service-backends/references/redis.md +75 -0
- package/skills/pikku-service-backends/references/schema.md +63 -0
- package/skills/pikku-services/SKILL.md +68 -291
- package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
- package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
- package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
- package/skills/pikku-services/references/services.md +272 -0
- package/skills/pikku-software-archaeology/README.md +5 -1
- package/skills/pikku-software-archaeology/SKILL.md +15 -2
- package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
- package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
- package/skills/pikku-webhook/SKILL.md +199 -0
- package/skills/pikku-wiring/SKILL.md +180 -0
- package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
- package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
- package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
- package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +4 -40
- package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
- package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
- package/skills/{pikku-realtime/SKILL.md → pikku-wiring/references/realtime.md} +2 -25
- package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
- package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
- package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
- package/skills/pikku-workflow/SKILL.md +3 -3
- package/skills/pikku-aws/SKILL.md +0 -161
- package/skills/pikku-backblaze/SKILL.md +0 -104
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
- package/skills/pikku-deploy-express/SKILL.md +0 -122
- package/skills/pikku-deploy-uws/SKILL.md +0 -144
- package/skills/pikku-mongodb/SKILL.md +0 -113
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-redis/SKILL.md +0 -99
- package/skills/pikku-schema-ajv/SKILL.md +0 -83
- package/skills/pikku-schema-cfworker/SKILL.md +0 -82
- package/skills/pikku-ws/SKILL.md +0 -87
- /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
- /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
- /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
- /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
- /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Pikku Services (Dependency Injection)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Before You Start
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pikku info functions --verbose # See which services existing functions use
|
|
8
|
+
pikku info tags --verbose # Understand project organization
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API Reference
|
|
12
|
+
|
|
13
|
+
### `pikkuServices(factory)` — singleton services (created once at startup)
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { pikkuServices } from '#pikku/setup'
|
|
17
|
+
import { ConsoleLogger } from '@pikku/core/services'
|
|
18
|
+
import { JoseJWTService } from '@pikku/jose'
|
|
19
|
+
|
|
20
|
+
export const createSingletonServices = pikkuServices(
|
|
21
|
+
async (config, existingServices?) => {
|
|
22
|
+
// config: your CoreConfig object
|
|
23
|
+
// existingServices: optional, for chaining factories
|
|
24
|
+
const logger = new ConsoleLogger()
|
|
25
|
+
const database = new DatabasePool(config.database)
|
|
26
|
+
await database.connect()
|
|
27
|
+
const jwt = new JoseJWTService(
|
|
28
|
+
async () => [{ id: 'my-key', value: config.jwtSecret }],
|
|
29
|
+
logger
|
|
30
|
+
)
|
|
31
|
+
return { config, logger, database, jwt, books: new BookService() }
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### `pikkuWireServices(factory)` — per-request services (fresh per HTTP request, queue job, CLI command, etc.)
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { pikkuWireServices } from '#pikku/setup'
|
|
40
|
+
|
|
41
|
+
export const createWireServices = pikkuWireServices(
|
|
42
|
+
async (singletonServices, wire) => {
|
|
43
|
+
// singletonServices: all singleton services
|
|
44
|
+
// wire: transport context (session, channel, etc.)
|
|
45
|
+
// Pikku merges these with singleton services automatically
|
|
46
|
+
return {
|
|
47
|
+
userSession: createUserSessionService(wire),
|
|
48
|
+
dbTransaction: new DatabaseTransaction(singletonServices.database),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `pikkuServerLifecycle(hooks)` — startup and shutdown work
|
|
55
|
+
|
|
56
|
+
A service factory should **construct** services, not run startup side effects. Seeding a database, warming a cache, starting a background consumer or draining a queue belongs in lifecycle hooks, which receive the singleton services after they are built:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// src/lifecycle.ts
|
|
60
|
+
import { pikkuServerLifecycle } from '@pikku/core'
|
|
61
|
+
import type { SingletonServices } from '../types/application-types.js'
|
|
62
|
+
|
|
63
|
+
export const lifecycle = pikkuServerLifecycle<SingletonServices>({
|
|
64
|
+
beforeStart: async ({ kysely }) => {
|
|
65
|
+
await runMigrations(kysely) // before the port opens
|
|
66
|
+
},
|
|
67
|
+
afterStart: async (services) => {
|
|
68
|
+
await seedDevData(services) // server is accepting traffic
|
|
69
|
+
},
|
|
70
|
+
beforeStop: async ({ queueService }) => {
|
|
71
|
+
await queueService.drain() // services are still alive here
|
|
72
|
+
},
|
|
73
|
+
afterStop: async () => {
|
|
74
|
+
await releaseExternalLock() // services are ALREADY stopped
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Every hook is optional. Order is `beforeStart` → server starts → `afterStart`, then on SIGINT `beforeStop` → services stopped → server stopped → `afterStop`.
|
|
80
|
+
|
|
81
|
+
**`afterStop` runs after the singleton services have been stopped.** It still receives the services object, but the services inside it are shut down — using one there is a use-after-close bug. Anything that needs a live service goes in `beforeStop`.
|
|
82
|
+
|
|
83
|
+
Export **exactly one** `pikkuServerLifecycle` from anywhere in `srcDirectories`; the inspector finds it by the wrapper call, so the filename is free (`src/lifecycle.ts` by convention). It must be an exported `const` initialized with a direct call to `pikkuServerLifecycle` — a re-export or a conditional wrapper is invisible to the inspector.
|
|
84
|
+
|
|
85
|
+
**Only `pikku dev` and `pikku serve` run these hooks.** If you bootstrap your own server (Express, Fastify, uWS, Lambda, Cloudflare, Next.js), no runtime adapter invokes them — put the work in your entrypoint instead.
|
|
86
|
+
|
|
87
|
+
### Auto-Generated Service Manifest
|
|
88
|
+
|
|
89
|
+
After `npx pikku all`, Pikku generates `.pikku/pikku-services.gen.ts`, a manifest of which services are actually used by wired functions:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
export const requiredSingletonServices = {
|
|
93
|
+
database: true, // used by getUser, deleteUser
|
|
94
|
+
audit: true, // used by deleteUser
|
|
95
|
+
cache: false, // not used by any wired function
|
|
96
|
+
jwt: true, // used by auth middleware
|
|
97
|
+
} as const
|
|
98
|
+
|
|
99
|
+
export type RequiredSingletonServices = Pick<
|
|
100
|
+
SingletonServices,
|
|
101
|
+
'database' | 'audit' | 'jwt'
|
|
102
|
+
> &
|
|
103
|
+
Partial<Omit<SingletonServices, 'database' | 'audit' | 'jwt'>>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Usage Patterns
|
|
107
|
+
|
|
108
|
+
### Using Services in Functions
|
|
109
|
+
|
|
110
|
+
**Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` (`PKU410`) and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// ✅ Correct — inline destructure, no cast
|
|
114
|
+
const getUser = pikkuFunc({
|
|
115
|
+
title: 'Get User',
|
|
116
|
+
func: async ({ db, logger, jwt }, { userId }) => {
|
|
117
|
+
logger.info('Fetching user', { userId })
|
|
118
|
+
return { user: await db.getUser(userId) }
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// ❌ Wrong — named param + body cast; inspector warns + tree-shaking breaks
|
|
123
|
+
const getUser = pikkuFunc({
|
|
124
|
+
func: async (services, { userId }) => {
|
|
125
|
+
const { db } = services as typeof services & { db: DbService }
|
|
126
|
+
// ...
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Services Are Never Optional Inside a Function
|
|
132
|
+
|
|
133
|
+
**Never write a `if (!service) throw ...` existence guard in a function body.** It is dead code, and it defeats the platform.
|
|
134
|
+
|
|
135
|
+
Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means _"this may not be created"_, not _"this may be missing at call time"_. A service is optional precisely because **nothing destructures it**, and the generated `requiredSingletonServices` manifest therefore never marks it for creation. The moment any wired function destructures it, Pikku creates it and guarantees it is there.
|
|
136
|
+
|
|
137
|
+
The types enforce this rather than merely documenting it. The inspector records the services destructured by every wired `func`, `permissions` **and** `middleware`, and emits them as `RequiredSingletonServices`. The generated function types then default their service parameter to:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
export type WiredSingletonServices = RequiredSingletonServices &
|
|
141
|
+
SingletonServices
|
|
142
|
+
export type WiredServices = SecretlessServices<
|
|
143
|
+
RequiredSingletonServices & Services
|
|
144
|
+
>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The `SecretlessServices<...>` wrapper is why `secrets` never appears in a
|
|
148
|
+
function's services: it is stripped at the type level, not merely omitted by
|
|
149
|
+
convention. Read secrets in a service factory or middleware and hand the value
|
|
150
|
+
to a service instead.
|
|
151
|
+
|
|
152
|
+
so a service that is `foo?: Foo` in `SingletonServices` arrives as a non-optional `Foo` in every function, permission and middleware that uses it. There is nothing to guard against.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// ✅ Correct — destructure and use; creation is guaranteed by the manifest
|
|
156
|
+
const listThreads = pikkuFunc({
|
|
157
|
+
func: async ({ agentRunService }, { threadId }) => {
|
|
158
|
+
return await agentRunService.getThreadMessages(threadId)
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// ❌ Wrong — unreachable guard; signals a misunderstanding of service wiring
|
|
163
|
+
const listThreads = pikkuFunc({
|
|
164
|
+
func: async ({ agentRunService }, { threadId }) => {
|
|
165
|
+
if (!agentRunService) throw new MissingServiceError('agentRunService')
|
|
166
|
+
return await agentRunService.getThreadMessages(threadId)
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
If a service really is conditional at runtime (e.g. an optional integration a deployment may not configure), that is a **configuration** concern: branch on config, or fail fast at startup in `services.ts` — not per-request in every function.
|
|
172
|
+
|
|
173
|
+
### Dynamic Import Optimization
|
|
174
|
+
|
|
175
|
+
Use the generated manifest to conditionally import heavy dependencies — only the services actually wired get instantiated:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { requiredSingletonServices } from '.pikku/pikku-services.gen.js'
|
|
179
|
+
|
|
180
|
+
const createSingletonServices = pikkuServices(async (config) => {
|
|
181
|
+
const logger = new ConsoleLogger()
|
|
182
|
+
|
|
183
|
+
let jwt: JWTService | undefined
|
|
184
|
+
if (requiredSingletonServices.jwt) {
|
|
185
|
+
const { JoseJWTService } = await import('@pikku/jose')
|
|
186
|
+
jwt = new JoseJWTService(keys, logger)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let database: Database | undefined
|
|
190
|
+
if (requiredSingletonServices.database) {
|
|
191
|
+
database = await createDatabase(config.databaseUrl)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { config, logger, jwt, database }
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Audit Wire Service
|
|
199
|
+
|
|
200
|
+
`createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and Fabric notes, read `references/audit-wire-service.md`.
|
|
201
|
+
|
|
202
|
+
### Built-in Services
|
|
203
|
+
|
|
204
|
+
| Service | Package | Purpose |
|
|
205
|
+
| ----------------------- | ---------------------- | --------------------------------------- |
|
|
206
|
+
| `ConsoleLogger` | `@pikku/core/services` | Console-based logging |
|
|
207
|
+
| `JoseJWTService` | `@pikku/jose` | JWT sign/verify via jose |
|
|
208
|
+
| `LocalSecretService` | `@pikku/core/services` | Local development secrets |
|
|
209
|
+
| `LocalVariablesService` | `@pikku/core/services` | Local environment variables |
|
|
210
|
+
| `PinoLogger` | `@pikku/pino` | Structured logging via Pino |
|
|
211
|
+
| `createInvocationAudit` | `@pikku/core/services` | Per-request audit buffer |
|
|
212
|
+
| `createAuditedKysely` | `@pikku/kysely` | Auto-capture DB queries as audit events |
|
|
213
|
+
|
|
214
|
+
## Complete Example
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// services.ts
|
|
218
|
+
import { pikkuServices, pikkuWireServices } from '#pikku/setup'
|
|
219
|
+
import { ConsoleLogger } from '@pikku/core/services'
|
|
220
|
+
import { JoseJWTService } from '@pikku/jose'
|
|
221
|
+
|
|
222
|
+
// Custom service
|
|
223
|
+
class TodoStore {
|
|
224
|
+
private todos: Map<string, Todo> = new Map()
|
|
225
|
+
async create(title: string, priority: string) {
|
|
226
|
+
const todo = { id: crypto.randomUUID(), title, priority, completed: false }
|
|
227
|
+
this.todos.set(todo.id, todo)
|
|
228
|
+
return todo
|
|
229
|
+
}
|
|
230
|
+
async get(id: string) {
|
|
231
|
+
return this.todos.get(id)
|
|
232
|
+
}
|
|
233
|
+
async list() {
|
|
234
|
+
return [...this.todos.values()]
|
|
235
|
+
}
|
|
236
|
+
async delete(id: string) {
|
|
237
|
+
this.todos.delete(id)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const createSingletonServices = pikkuServices(async (config) => {
|
|
242
|
+
const logger = new ConsoleLogger()
|
|
243
|
+
const jwt = new JoseJWTService(
|
|
244
|
+
async () => [{ id: 'my-key', value: config.jwtSecret }],
|
|
245
|
+
logger
|
|
246
|
+
)
|
|
247
|
+
return {
|
|
248
|
+
config,
|
|
249
|
+
logger,
|
|
250
|
+
jwt,
|
|
251
|
+
secrets: new LocalSecretService(),
|
|
252
|
+
variables: new LocalVariablesService(),
|
|
253
|
+
todoStore: new TodoStore(),
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
export const createWireServices = pikkuWireServices(
|
|
258
|
+
async (singletonServices, wire) => ({
|
|
259
|
+
scopedLogger: new ScopedLogger(wire.session?.userId),
|
|
260
|
+
})
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
// functions/todos.functions.ts — services are auto-injected
|
|
264
|
+
export const createTodo = pikkuFunc({
|
|
265
|
+
title: 'Create Todo',
|
|
266
|
+
func: async ({ todoStore, logger }, { title, priority }) => {
|
|
267
|
+
const todo = await todoStore.create(title, priority)
|
|
268
|
+
logger.info('Created todo', { id: todo.id })
|
|
269
|
+
return { todo }
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
```
|
|
@@ -74,7 +74,11 @@ pikku-software-archaeology/
|
|
|
74
74
|
├── README.md # this file
|
|
75
75
|
├── references/
|
|
76
76
|
│ ├── blueprint.schema.json # the output contract (JSON Schema)
|
|
77
|
-
│
|
|
77
|
+
│ ├── pikku-mapping.md # blueprint → Pikku primitives
|
|
78
|
+
│ ├── second-opinion.md # blueprint → owner-facing report: method, voice, red flags
|
|
79
|
+
│ └── second-opinion-report-template.md # the layered structure to fill in
|
|
80
|
+
├── example/
|
|
81
|
+
│ └── second-opinion-sample-report.md # worked example (competitor-tracking area, founder voice)
|
|
78
82
|
└── scripts/
|
|
79
83
|
└── validate.mjs # schema + cross-file validation (node, no deps)
|
|
80
84
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-software-archaeology
|
|
3
|
-
description: 'Use when reverse-engineering an existing repository into a Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app). TRIGGER when: user says "extract a blueprint", "reverse engineer this app", "what does this codebase actually do as a product", "prepare this repo for a rewrite/migration",
|
|
3
|
+
description: 'Use when reverse-engineering an existing repository into a Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app) — and when turning that blueprint into a plain-language second opinion for the non-technical owner who holds the app. TRIGGER when: user says "extract a blueprint", "reverse engineer this app", "what does this codebase actually do as a product", "prepare this repo for a rewrite/migration", points at a legacy repo (any language — JS, TS, Ruby, Python, PHP, Go) and asks for its domains, workflows, business rules or a rebuild plan, or asks "explain how my app works" / "what would you do differently" / "is this built well?" for a founder, PM or operator audience. DO NOT TRIGGER for: documenting code structure, generating API docs from an already-clean codebase, or an engineer-facing code review.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Software Archaeology
|
|
@@ -132,7 +132,7 @@ Run each lens over the surveyed material. Rules that counter the classic failure
|
|
|
132
132
|
|
|
133
133
|
**Frontend** (`frontend.json` + `frontend-routes.json` + `frontend-components.json`, optional) — the web UI, which needs its own treatment because frontends vary wildly (framework, router, styling, state, data, auth) and the rebuild target is opinionated: **everything in one component system (Mantine), one data layer, one auth**.
|
|
134
134
|
|
|
135
|
-
- `frontend.json` records the stack as FACTS: framework (e.g. TanStack Start), rendering (SSR/streaming/SPA), router, styling/design system, `designSystemConsistency`, state management, data layer (e.g. pikku-react
|
|
135
|
+
- `frontend.json` records the stack as FACTS: framework (e.g. TanStack Start), rendering (SSR/streaming/SPA), router, styling/design system, `designSystemConsistency`, state management, data layer (e.g. pikku-react vs REST helpers), auth (e.g. better-auth), i18n. Name the real technologies — the second-opinion skill weighs their tradeoffs, so record them precisely (do NOT editorialize here; this file is facts).
|
|
136
136
|
- `frontend-routes.json` is the page tree: each route's `purpose` in product terms, `auth`, the `dataFrom` (query/command names it reads — reuse the backend concept names so the UI ties back to the domain), the `usesComponents`, and the `userFlows` it belongs to.
|
|
137
137
|
- `frontend.json.designFindings` captures **broken/inconsistent design patterns** as concrete, cited observations (not taste). Actively hunt for: _interaction inconsistency_ (the same job done as a modal in one place and a drawer in another; inconsistent confirm dialogs); _theming not tokenized_ (hardcoded hex colors, magic spacing/font sizes, inline styles instead of theme tokens/variables — grep for `#[0-9a-f]{3,6}`, `style={{`, raw `px` values); _cross-page inconsistency_ (the same element — button, page header, card — styled differently across routes); _component duplication_ (three near-identical cards/tables for one purpose); _design-system bypass_ (raw HTML/CSS where a library component exists). Each finding gets an example, its impact (feels unpolished / a color change means hunting every file), and a fix (standardize on one pattern / move to tokens / extract one shared component). These are almost always cheap cleanups, and they are exactly what a non-technical owner perceives as "the app looks off" without being able to say why.
|
|
138
138
|
- `frontend-components.json` is where the frontend's real migration cost lives, in the **`rebuild`** field: `mantine-standard` (maps 1:1 to a Mantine component — trivial), `mantine-composition` (built from Mantine primitives — straightforward), `custom-style` (diverges only visually — normalize to Mantine), or **`custom-logic`** (bespoke behavior — a custom chart, a virtualized/complex table, a canvas, drag-and-drop, a rich editor — that must be **ported**, not re-skinned). A `custom-logic` component MUST fill `customLogic` explaining the behavior, and should list the `dependencies` (charting/table/editor libs) that make it a real port. This split — "trivially re-Mantine-able" vs "carries logic that must survive the port" — is the single most useful thing the frontend extraction produces.
|
|
@@ -187,3 +187,16 @@ node <skill-dir>/scripts/validate.mjs <repo>/.knowledge
|
|
|
187
187
|
|
|
188
188
|
- Schema/contract: `references/blueprint.schema.json`
|
|
189
189
|
- How Pikku consumes the blueprint: `references/pikku-mapping.md`
|
|
190
|
+
|
|
191
|
+
## The second phase — a report the owner can act on
|
|
192
|
+
|
|
193
|
+
Extraction produces a blueprint for a machine to rebuild from. The same
|
|
194
|
+
`.knowledge/` directory is also the input to a second opinion for the person who
|
|
195
|
+
holds the app: what works, what is holding them back, and what you would build
|
|
196
|
+
instead, argued in business outcomes rather than architecture.
|
|
197
|
+
|
|
198
|
+
That phase has its own voice rules, structure and red flags — read
|
|
199
|
+
`references/second-opinion.md`, fill in
|
|
200
|
+
`references/second-opinion-report-template.md`, and match the tone of
|
|
201
|
+
`example/second-opinion-sample-report.md`. It consumes the blueprint; it never
|
|
202
|
+
re-derives facts from the code, so run the extraction first.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
_A second opinion on the competitor-tracking system_
|
|
4
4
|
|
|
5
|
-
> Worked example for the pikku-
|
|
5
|
+
> Worked example for the pikku-software-archaeology skill. Shows the voice and the
|
|
6
6
|
> layered structure on one real area (competitor tracking), drawn from a
|
|
7
7
|
> pikku-software-archaeology blueprint + parity report. A full report would repeat
|
|
8
8
|
> Part 2 for each major area, and cover every significant technology bet — not just
|
|
@@ -961,7 +961,7 @@
|
|
|
961
961
|
"stateManagement": { "type": "string" },
|
|
962
962
|
"dataLayer": {
|
|
963
963
|
"type": "string",
|
|
964
|
-
"description": "how the UI talks to the backend: e.g. pikku-react
|
|
964
|
+
"description": "how the UI talks to the backend: e.g. pikku-react hooks, REST fetch helpers, tRPC, GraphQL client"
|
|
965
965
|
},
|
|
966
966
|
"auth": {
|
|
967
967
|
"type": "string",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# How Pikku Consumes a Product Blueprint
|
|
2
2
|
|
|
3
|
-
The `.knowledge/` blueprint is designed so each concept maps onto exactly one Pikku primitive. A generator (or an agent following `pikku-
|
|
3
|
+
The `.knowledge/` blueprint is designed so each concept maps onto exactly one Pikku primitive. A generator (or an agent following `pikku-build`) walks the JSON files in this order:
|
|
4
4
|
|
|
5
5
|
| Blueprint source | Pikku target |
|
|
6
6
|
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -26,8 +26,8 @@ The `.knowledge/` blueprint is designed so each concept maps onto exactly one Pi
|
|
|
26
26
|
| `interfaces.json` kind=cli | `wireCLI` entrypoints — the CLI commands are the same funcs the routes expose |
|
|
27
27
|
| `interfaces.json` kind=mcp | `wireMCP` — each MCP tool IS a `pikkuFunc` (reuse the command/query funcs; don't author tool duplicates) |
|
|
28
28
|
| `interfaces.json` kind=openapi-rest / sdk | generated, not hand-written: the OpenAPI spec + typed client SDK fall out of the `wireHTTP` routes + codegen |
|
|
29
|
-
| `interfaces.json` kind=websocket-realtime | `pikku-
|
|
30
|
-
| `frontend.json` | `apps/app` (TanStack Start) shell: router, `@pikku/mantine` theme, `pikku-react
|
|
29
|
+
| `interfaces.json` kind=websocket-realtime | `pikku-wiring` EventHub topics / channels |
|
|
30
|
+
| `frontend.json` | `apps/app` (TanStack Start) shell: router, `@pikku/mantine` theme, `pikku-react` data layer, `better-auth` client — the target stack the legacy UI is rebuilt onto |
|
|
31
31
|
| `frontend-routes.json` | TanStack Router routes under `apps/app/src/routes/**` (thin data containers calling `usePikkuQuery`); `dataFrom` names become the generated hooks; subpath routes for rich detail views |
|
|
32
32
|
| `frontend-components.json` rebuild=`mantine-standard`/`mantine-composition` | components in `packages/components` composed from `@pikku/mantine` — the trivial/straightforward bulk |
|
|
33
33
|
| `frontend-components.json` rebuild=`custom-logic` | the PORT list — each becomes a `packages/components` component that reimplements the bespoke behavior (chart/table/editor); its `dependencies` inform whether the lib is kept or replaced. These are the frontend's real work items |
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-product-second-opinion
|
|
3
|
-
description: 'Use when a non-technical owner (founder, PM, operator) wants a plain-language report on an app they hold but did not build — explaining how it works and how it could be better. Reads the .knowledge/ blueprint from pikku-software-archaeology and produces a layered, jargon-free report that credits what works, names what does not (with business impact + effort), and argues an opinionated better design. TRIGGER: "explain how my app works", "what would you do differently", "review my app for a non-technical audience", "I inherited/am stuck with an agency-built app", "is this built well?". DO NOT TRIGGER for: extracting the machine-readable blueprint itself (use pikku-software-archaeology), or an engineer-facing technical code review.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
1
|
# Product Second Opinion
|
|
7
2
|
|
|
8
3
|
## Overview
|
|
@@ -11,7 +6,7 @@ Turn an extracted product blueprint into a **report a non-technical owner can ac
|
|
|
11
6
|
|
|
12
7
|
The reader is a founder/PM/operator, not an engineer. If they finish a section and don't know what it means for their business or what to do about it, the report failed — no matter how correct it is.
|
|
13
8
|
|
|
14
|
-
**REQUIRED INPUT:** the `.knowledge/` blueprint produced by
|
|
9
|
+
**REQUIRED INPUT:** the `.knowledge/` blueprint produced by the extraction phase (see SKILL.md). If none exists, run that first — this one consumes its output (`product.json`, `domains.json`, `workflows.json`, `gaps.json`, `invariants.json`, `migration.json`, and any `parity-*.md`), it does not re-derive facts from the code. When the optional consumer-surface files are present (`interfaces.json`, `frontend.json`, `frontend-routes.json`, `frontend-components.json`), cover them too — see "The frontend and the other ways your app is used" and "Technology choices" below.
|
|
15
10
|
|
|
16
11
|
## The cardinal rule: translate, don't dump
|
|
17
12
|
|
|
@@ -52,7 +47,7 @@ Write these three parts in order. A reader can stop after Part 1.
|
|
|
52
47
|
- _The headline_: the 3–5 biggest risks/opportunities, one plain line each.
|
|
53
48
|
- _Recommended order_: a table (Fix | Why it matters | Effort | Payoff). This is the part they act on.
|
|
54
49
|
|
|
55
|
-
**Part 2 — One section per major area** (drive the areas from `domains.json`; skip domains with nothing worth saying). Each section follows this shape (see `example/sample-report.md`):
|
|
50
|
+
**Part 2 — One section per major area** (drive the areas from `domains.json`; skip domains with nothing worth saying). Each section follows this shape (see `example/second-opinion-sample-report.md`):
|
|
56
51
|
|
|
57
52
|
- _What this does_ — the capability in business terms.
|
|
58
53
|
- _How it works today_ — a plain walkthrough, ideally as a small story ("on a timer, the app re-reads each site, compares…").
|
|
@@ -162,6 +157,6 @@ Produce **both**:
|
|
|
162
157
|
| A design point with no "why it matters" | Taste, not advice. Tie every design finding to user perception (polish/trust) or maintenance cost (change-once vs hunt-everywhere), plus effort. |
|
|
163
158
|
| Reads like a code review | Wrong audience. Would a founder know what to _do_ after this paragraph? |
|
|
164
159
|
|
|
165
|
-
## Relationship to
|
|
160
|
+
## Relationship to the extraction phase
|
|
166
161
|
|
|
167
|
-
|
|
162
|
+
Extraction = facts → `.knowledge/` blueprint, for a machine to rebuild from. This report = blueprint → opinionated argument, for a human to decide from. One extracts; one advises. Run the extraction first (or point this at an existing `.knowledge/`), then translate its `gaps.json` + `invariants.json` + `migration.json` into the business-language report above.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pikku-webhook
|
|
3
|
+
description: >-
|
|
4
|
+
Use when an application needs to SEND outgoing webhooks — notifying a customer's endpoint that
|
|
5
|
+
something happened, with signing, retries and a delivery log. Covers WebhookService,
|
|
6
|
+
QueueWebhookService, the `pikku-outgoing-webhooks` queue worker, `scaffold.webhook`,
|
|
7
|
+
`config.webhook`, signature verification on the receiving side, and KyselyWebhookService's
|
|
8
|
+
delivery history. TRIGGER when: code uses webhookService, QueueWebhookService,
|
|
9
|
+
KyselyWebhookService, pikkuWebhookWorkerFunc, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME,
|
|
10
|
+
SendWebhookInput or X-Pikku-Signature. TRIGGER when: the user asks to emit events to a
|
|
11
|
+
customer URL, build a webhook endpoint settings screen, add a signing secret, or show
|
|
12
|
+
delivery attempts. DO NOT TRIGGER when: the user is RECEIVING webhooks from a third party
|
|
13
|
+
into a route — that is an ordinary wireHTTP function (use pikku-wiring).
|
|
14
|
+
installGroups: [core]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Pikku Outgoing Webhooks
|
|
18
|
+
|
|
19
|
+
Pikku ships an outgoing webhook primitive, so an application never hand-rolls
|
|
20
|
+
`fetch` + HMAC + retries. `WebhookService.send()` signs the body, enqueues a
|
|
21
|
+
delivery job, and the generated `pikku-outgoing-webhooks` queue worker POSTs it;
|
|
22
|
+
a non-2xx throws, so the queue retries with backoff. Swapping the queue-only
|
|
23
|
+
default for `KyselyWebhookService` adds a durable delivery + attempt history
|
|
24
|
+
with no change to call sites.
|
|
25
|
+
|
|
26
|
+
**Do not write a bespoke `fetch(url, { headers: { 'x-my-signature': … } })` for
|
|
27
|
+
an outgoing event.** If the shipped signing scheme or delivery model genuinely
|
|
28
|
+
does not fit, subclass `WebhookService` — it is an abstract class precisely so
|
|
29
|
+
an app can substitute its own transport (direct send, Svix) and keep the same
|
|
30
|
+
call sites and delivery history.
|
|
31
|
+
|
|
32
|
+
## Agent Operating Procedure
|
|
33
|
+
|
|
34
|
+
1. Turn the feature on: `"scaffold": { "webhook": true }` in `pikku.config.json`.
|
|
35
|
+
2. Run `pikku all`. It writes `<scaffold.pikkuDir>/webhook/webhook.gen.ts` (the
|
|
36
|
+
queue worker) and `webhook.schemas.gen.ts`. Never hand-edit either.
|
|
37
|
+
3. Register a `webhookService` singleton in `createSingletonServices`. Without
|
|
38
|
+
it `services.webhookService` is `undefined` and nothing sends.
|
|
39
|
+
4. Make sure a queue backend is wired. The worker is an ordinary
|
|
40
|
+
`wireQueueWorker` — with no `queueService`, `send()` throws.
|
|
41
|
+
5. Call `webhookService.send(...)` from a function body. Never call `fetch`
|
|
42
|
+
directly for an outgoing event.
|
|
43
|
+
6. Validate with `pikku all` and the project's typecheck.
|
|
44
|
+
|
|
45
|
+
## Config
|
|
46
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
// pikku.config.json
|
|
49
|
+
{
|
|
50
|
+
"scaffold": {
|
|
51
|
+
"pikkuDir": "src/pikku",
|
|
52
|
+
"webhook": true, // on or off — it exposes no endpoint and has no path override
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`scaffold.webhook` is a plain boolean, unlike the other scaffold flags which
|
|
58
|
+
accept `{ path }`. The two generated paths are derived
|
|
59
|
+
(`webhookWorkersFile`, `webhookSchemasFile`) and can be set explicitly in
|
|
60
|
+
`pikku.config.json` if a project needs them somewhere else.
|
|
61
|
+
|
|
62
|
+
Runtime defaults live on `CoreConfig.webhook`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export interface Config extends CoreConfig {}
|
|
66
|
+
|
|
67
|
+
const config: Config = {
|
|
68
|
+
webhook: {
|
|
69
|
+
secret: 'WEBHOOK_SIGNING_KEY', // a secret NAME, resolved via services.secrets
|
|
70
|
+
signatureHeader: 'X-Pikku-Signature', // default
|
|
71
|
+
retries: 3, // default; attempts = retries + 1
|
|
72
|
+
retryDelay: '30s', // omit for exponential backoff
|
|
73
|
+
allowedHosts: ['hooks.example.com'], // SSRF allowlist
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`allowedHosts` set means _only_ those hostnames are deliverable. Omitted, every
|
|
79
|
+
public host is allowed and private/internal ranges are blocked — loopback,
|
|
80
|
+
RFC1918, link-local `169.254.0.0/16` (cloud metadata), CGNAT `100.64.0.0/10`
|
|
81
|
+
(Alibaba's metadata endpoint), multicast and the TEST-NETs. A URL is
|
|
82
|
+
user-supplied data; do not bypass `safeFetch` by delivering yourself.
|
|
83
|
+
|
|
84
|
+
## Sending
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { pikkuFunc } from '#pikku/function'
|
|
88
|
+
|
|
89
|
+
export const notifyOrderShipped = pikkuFunc({
|
|
90
|
+
func: async ({ webhookService }, { endpointUrl, orderId, secret }) => {
|
|
91
|
+
const { jobId } = await webhookService.send({
|
|
92
|
+
url: endpointUrl,
|
|
93
|
+
event: 'order.shipped',
|
|
94
|
+
data: { orderId, shippedAt: new Date().toISOString() },
|
|
95
|
+
secret, // per-endpoint raw key; overrides config.webhook.secret
|
|
96
|
+
organizationId: orgId, // persisted by store-backed services only
|
|
97
|
+
})
|
|
98
|
+
return { jobId }
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`send()` returns as soon as the job is enqueued — it is **not** a delivery
|
|
104
|
+
receipt. `jobId` is the queue job; with `KyselyWebhookService` it is also the
|
|
105
|
+
`deliveryId`, so it is stable across retries and is what a UI polls.
|
|
106
|
+
|
|
107
|
+
The two `secret` fields are deliberately different and are the most common
|
|
108
|
+
mistake:
|
|
109
|
+
|
|
110
|
+
| Where | Meaning |
|
|
111
|
+
| ------------------------- | ------------------------------------------------------------------- |
|
|
112
|
+
| `config.webhook.secret` | a secret **name**, read through `services.secrets` at enqueue time |
|
|
113
|
+
| `SendWebhookInput.secret` | a **raw HMAC key**, for per-endpoint secrets held in your own table |
|
|
114
|
+
|
|
115
|
+
The raw key never enters the queue payload: the body is signed at enqueue time
|
|
116
|
+
and only the resulting header travels with the job. That is also why the body
|
|
117
|
+
is serialized once — a retry re-POSTs identical bytes, so the signature stays
|
|
118
|
+
valid.
|
|
119
|
+
|
|
120
|
+
With neither secret set, deliveries go **unsigned**. A missing named secret is
|
|
121
|
+
logged as an error and still sends unsigned; treat that log line as a
|
|
122
|
+
misconfiguration, not noise.
|
|
123
|
+
|
|
124
|
+
## Verifying on the receiving side
|
|
125
|
+
|
|
126
|
+
`sign()` produces `sha256=<hex>` (GitHub style, body only, no timestamp) into
|
|
127
|
+
`X-Pikku-Signature`, and `X-Pikku-Event` carries the event name. `verify()` is
|
|
128
|
+
public because receivers share the scheme:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const raw = await request.text()
|
|
132
|
+
if (
|
|
133
|
+
!webhookService.verify(secret, request.headers.get('x-pikku-signature')!, raw)
|
|
134
|
+
) {
|
|
135
|
+
throw new UnauthorizedError()
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
It compares in constant time via `timingSafeStringEqual`. Never compare
|
|
140
|
+
signatures with `===`, and verify against the **raw body text**, not a
|
|
141
|
+
re-serialized parsed object.
|
|
142
|
+
|
|
143
|
+
## Delivery history
|
|
144
|
+
|
|
145
|
+
The default `QueueWebhookService` keeps no history: `listDeliveries`,
|
|
146
|
+
`getDelivery` and `recordAttempt` throw `NotImplementedError`. Register
|
|
147
|
+
`KyselyWebhookService` from `@pikku/kysely` to get them.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { KyselyWebhookService } from '@pikku/kysely'
|
|
151
|
+
|
|
152
|
+
const webhookService = new KyselyWebhookService(queueService, kysely)
|
|
153
|
+
await webhookService.init() // creates webhookDelivery + webhookDeliveryAttempt
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`init()` is idempotent and creates the tables through the pikku schema
|
|
157
|
+
bootstrap. Do not write your own migration for these tables.
|
|
158
|
+
|
|
159
|
+
- `webhookDelivery` — one row per `send()`: `deliveryId`, `organizationId`,
|
|
160
|
+
`url`, `event`, `status` (`pending` | `delivered` | `failed`), `attempts`,
|
|
161
|
+
`createdAt`, `updatedAt`, `deliveredAt`.
|
|
162
|
+
- `webhookDeliveryAttempt` — one row per try: `attemptNumber`, `statusCode`,
|
|
163
|
+
`responseBody` (failures only, truncated to 2000 chars), `error`.
|
|
164
|
+
|
|
165
|
+
Read them through the service, not with your own query:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const deliveries = await webhookService.listDeliveries({
|
|
169
|
+
organizationId,
|
|
170
|
+
limit: 25,
|
|
171
|
+
})
|
|
172
|
+
const detail = await webhookService.getDelivery(deliveryId) // { delivery, attempts }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Building a console screen is `listDeliveries` for the list and `getDelivery` for
|
|
176
|
+
the drill-in. A hand-written select over `webhookDelivery` is a sign the wrong
|
|
177
|
+
service is registered.
|
|
178
|
+
|
|
179
|
+
## What the worker does
|
|
180
|
+
|
|
181
|
+
The generated worker is a thin wrapper over `pikkuWebhookWorkerFunc`. It POSTs
|
|
182
|
+
through `safeFetch` with a 30s timeout, treats 2xx as delivered, captures the
|
|
183
|
+
response body on failure, records the attempt when a `deliveryId` is present,
|
|
184
|
+
and **throws** on failure so the queue retries. Attempt recording is
|
|
185
|
+
best-effort: a store error is logged and does not fail the delivery. Retry
|
|
186
|
+
exhaustion is logged by the queue runner — there is no `onFailure` hook.
|
|
187
|
+
|
|
188
|
+
## Gotchas
|
|
189
|
+
|
|
190
|
+
- `webhookService` is optional on `CoreSingletonServices`, but do **not** guard
|
|
191
|
+
it in a function body — destructuring it marks it required (see
|
|
192
|
+
`pikku-services`). Register it in `services.ts` or fail fast at startup.
|
|
193
|
+
- The queue name is `pikku-outgoing-webhooks`, not `pikku-webhooks`.
|
|
194
|
+
- `retries: 0` means one attempt and no backoff, not "retry forever".
|
|
195
|
+
- The gateway's inbound `webhook` transport type is a different feature. This
|
|
196
|
+
skill is outbound only.
|
|
197
|
+
- Signing is body-only with no timestamp, so it does not defend against replay
|
|
198
|
+
on its own. If a receiver needs replay protection, put a nonce or timestamp
|
|
199
|
+
**inside** the payload, where it is covered by the signature.
|