@pikku/skills 0.12.2 → 0.12.6
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 +56 -29
- 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 +80 -34
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +82 -10
- package/skills/pikku-concepts/references/concept-mapping.md +2 -2
- package/skills/pikku-config/SKILL.md +134 -52
- 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 +30 -5
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +12 -7
- 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 +3 -3
- 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 +285 -50
- 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-software-archaeology/README.md +16 -6
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
- 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 +35 -1
- 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
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
name: pikku-config
|
|
3
3
|
description: >-
|
|
4
4
|
Use when managing secrets, environment variables, config, or OAuth2 credentials in a Pikku app.
|
|
5
|
-
Covers
|
|
6
|
-
code uses
|
|
7
|
-
config, OAuth2, or "how do I access environment
|
|
8
|
-
API versioning/breaking changes (use
|
|
9
|
-
|
|
5
|
+
Covers defineSecret, defineVariable, defineCredential, and typed config access. TRIGGER when:
|
|
6
|
+
code uses defineSecret/defineVariable/defineCredential, user asks about env vars, secrets,
|
|
7
|
+
config, OAuth2, SecretValue/.reveal(), SecretCoercionError, or "how do I access environment
|
|
8
|
+
variables". DO NOT TRIGGER when: user asks about API versioning/breaking changes (use
|
|
9
|
+
pikku-versioning), service factories (use pikku-services), middleware (use pikku-middleware), or
|
|
10
|
+
auth strategies and sessions (use pikku-security).
|
|
10
11
|
installGroups: [core]
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -35,34 +36,60 @@ See `pikku-concepts` for the core mental model.
|
|
|
35
36
|
|
|
36
37
|
## Secrets & Variables
|
|
37
38
|
|
|
38
|
-
### `
|
|
39
|
+
### `defineSecret(config)`
|
|
39
40
|
|
|
40
41
|
Declare a secret with a Zod schema for type-safe access:
|
|
41
42
|
|
|
42
43
|
```typescript
|
|
43
|
-
|
|
44
|
+
defineSecret({
|
|
44
45
|
name: string, // Secret identifier
|
|
45
46
|
schema: ZodSchema, // Shape and validation
|
|
46
47
|
})
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
### `
|
|
50
|
+
### `defineVariable(config)`
|
|
50
51
|
|
|
51
52
|
Declare a variable (non-sensitive config) with a Zod schema:
|
|
52
53
|
|
|
53
54
|
```typescript
|
|
54
|
-
|
|
55
|
+
defineVariable({
|
|
55
56
|
name: string,
|
|
56
57
|
schema: ZodSchema,
|
|
57
58
|
})
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
### Accessing
|
|
61
|
+
### Accessing Secrets
|
|
62
|
+
|
|
63
|
+
`secrets` is **not available inside functions, AI agents, workflows, permissions
|
|
64
|
+
or any wire** — it is removed from their services type and throws at runtime if
|
|
65
|
+
reached through a cast. Read it where you wire the app and hand the value to a
|
|
66
|
+
service:
|
|
67
|
+
|
|
68
|
+
`getSecret` returns a `SecretValue<T>`, not the bare value. It is nominal — not
|
|
69
|
+
assignable to `string`, so every concretely-typed sink rejects it — it serializes
|
|
70
|
+
to `[secret]` in logs and audits, and coercing it to a string (a template
|
|
71
|
+
literal, a concatenation) throws `SecretCoercionError`, because that is always a
|
|
72
|
+
leak. `.reveal()` is the one way out, which makes every disclosure deliberate and
|
|
73
|
+
greppable. Call it at the point the value reaches the thing that needs it:
|
|
61
74
|
|
|
62
75
|
```typescript
|
|
63
|
-
//
|
|
64
|
-
const
|
|
76
|
+
// services.ts — allowed
|
|
77
|
+
const createSingletonServices = pikkuServices(async (config, { secrets }) => ({
|
|
78
|
+
stripe: new StripeService((await secrets.getSecret('STRIPE_CONFIG')).reveal()),
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
// functions/*.ts — ask the service, never the secret store
|
|
82
|
+
export const charge = pikkuFunc({
|
|
83
|
+
func: async ({ stripe }, data) => stripe.charge(data.amount),
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Allowed: `pikkuServices`, `pikkuWireServices`, addon service factories,
|
|
88
|
+
middleware. Everywhere else, the service you constructed is the interface.
|
|
65
89
|
|
|
90
|
+
### Accessing Variables in Functions
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
66
93
|
// Variables — plain-text configuration
|
|
67
94
|
const flags = await services.variables.getVariableJSON('VARIABLE_NAME')
|
|
68
95
|
|
|
@@ -85,7 +112,7 @@ const createSingletonServices = pikkuServices(async (config) => ({
|
|
|
85
112
|
|
|
86
113
|
```typescript
|
|
87
114
|
// Declare secrets with typed schemas
|
|
88
|
-
|
|
115
|
+
defineSecret({
|
|
89
116
|
name: 'STRIPE_CONFIG',
|
|
90
117
|
schema: z.object({
|
|
91
118
|
apiKey: z.string().startsWith('sk_'),
|
|
@@ -93,13 +120,13 @@ wireSecret({
|
|
|
93
120
|
}),
|
|
94
121
|
})
|
|
95
122
|
|
|
96
|
-
// In your
|
|
97
|
-
const config = await secrets.getSecret('STRIPE_CONFIG')
|
|
123
|
+
// In your services factory — fully typed
|
|
124
|
+
const config = (await secrets.getSecret('STRIPE_CONFIG')).reveal()
|
|
98
125
|
// config.apiKey → string (autocompleted)
|
|
99
126
|
// config.webhookSecret → string (autocompleted)
|
|
100
127
|
|
|
101
128
|
// Declare variables
|
|
102
|
-
|
|
129
|
+
defineVariable({
|
|
103
130
|
name: 'FEATURE_FLAGS',
|
|
104
131
|
schema: z.object({
|
|
105
132
|
darkMode: z.boolean(),
|
|
@@ -113,46 +140,80 @@ const flags = await variables.getVariableJSON('FEATURE_FLAGS')
|
|
|
113
140
|
// flags.maxUploadMB → number
|
|
114
141
|
```
|
|
115
142
|
|
|
116
|
-
##
|
|
143
|
+
## Credentials
|
|
117
144
|
|
|
118
|
-
### `
|
|
145
|
+
### `defineCredential(config)`
|
|
119
146
|
|
|
120
147
|
```typescript
|
|
121
|
-
|
|
122
|
-
name: string,
|
|
123
|
-
displayName: string,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
defineCredential({
|
|
149
|
+
name: string, // Credential identifier
|
|
150
|
+
displayName: string, // Human-readable name
|
|
151
|
+
type: 'wire' | 'singleton', // Per-user ('wire') or platform-level ('singleton')
|
|
152
|
+
schema: ZodSchema, // Shape of the stored credential
|
|
153
|
+
oauth2?: { // Omit entirely for a plain API key
|
|
154
|
+
appCredentialSecretId: string, // Secret holding { clientId, clientSecret }
|
|
155
|
+
tokenSecretId: string, // Secret for token storage (auto-refreshed)
|
|
156
|
+
authorizationUrl: string, // OAuth2 authorization endpoint
|
|
157
|
+
tokenUrl: string, // OAuth2 token endpoint
|
|
158
|
+
scopes: string[], // Required OAuth2 scopes
|
|
159
|
+
},
|
|
129
160
|
})
|
|
130
161
|
```
|
|
131
162
|
|
|
132
163
|
### Usage
|
|
133
164
|
|
|
134
165
|
```typescript
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
tokenUrl: 'https://slack.com/api/oauth.v2.access',
|
|
142
|
-
scopes: ['chat:write', 'channels:read'],
|
|
166
|
+
// Per-user API key — no oauth2 block
|
|
167
|
+
defineCredential({
|
|
168
|
+
name: 'stripe',
|
|
169
|
+
displayName: 'Stripe API Key',
|
|
170
|
+
type: 'wire',
|
|
171
|
+
schema: z.object({ apiKey: z.string() }),
|
|
143
172
|
})
|
|
144
173
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
'
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
174
|
+
// Platform-level OAuth (singleton)
|
|
175
|
+
defineCredential({
|
|
176
|
+
name: 'slack',
|
|
177
|
+
displayName: 'Slack',
|
|
178
|
+
type: 'singleton',
|
|
179
|
+
schema: z.object({ accessToken: z.string(), refreshToken: z.string() }),
|
|
180
|
+
oauth2: {
|
|
181
|
+
appCredentialSecretId: 'SLACK_OAUTH_APP',
|
|
182
|
+
tokenSecretId: 'SLACK_OAUTH_TOKENS',
|
|
183
|
+
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
|
|
184
|
+
tokenUrl: 'https://slack.com/api/oauth.v2.access',
|
|
185
|
+
scopes: ['chat:write', 'channels:read'],
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
### Reading a Credential
|
|
190
|
+
|
|
191
|
+
A declared credential is resolved per invocation through `wire.getCredential(name)`,
|
|
192
|
+
so the natural place to read it is a wire service factory: build the client there
|
|
193
|
+
once and let functions ask the client, the same way they ask a service for a
|
|
194
|
+
secret-derived value. Tokens refresh automatically, so what arrives is already
|
|
195
|
+
valid.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
export const createWireServices = pikkuWireServices(async (_services, wire) => {
|
|
199
|
+
const cred = await wire.getCredential?.<{ accessToken: string }>('slack')
|
|
200
|
+
if (!cred?.accessToken) {
|
|
201
|
+
// Tells the caller which credential to connect, and where.
|
|
202
|
+
throw new MissingCredentialError('slack', 'oauth2', '/credentials/slack/connect')
|
|
151
203
|
}
|
|
152
|
-
)
|
|
153
|
-
|
|
204
|
+
return { slack: new SlackClient(cred.accessToken) }
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// functions/*.ts — ask the client, never the credential store
|
|
208
|
+
export const postMessage = pikkuFunc({
|
|
209
|
+
func: async ({ slack }, { channel, text }) => slack.postMessage(channel, text),
|
|
210
|
+
})
|
|
154
211
|
```
|
|
155
212
|
|
|
213
|
+
A `wire` credential resolves per user, so an unconnected user hits
|
|
214
|
+
`MissingCredentialError` rather than silently acting as someone else; a
|
|
215
|
+
`singleton` credential is platform-level and identical for every caller.
|
|
216
|
+
|
|
156
217
|
## Key Rule
|
|
157
218
|
|
|
158
219
|
**Never use `process.env` inside Pikku functions.** Use the `variables` or `secrets` service:
|
|
@@ -165,13 +226,30 @@ const apiKey = process.env.API_KEY
|
|
|
165
226
|
const apiKey = services.variables.get('API_KEY')
|
|
166
227
|
```
|
|
167
228
|
|
|
168
|
-
`process.env` belongs only in server bootstrap code (`start.ts`).
|
|
229
|
+
`process.env` belongs only in server bootstrap code (`start.ts`). Under `pikku dev` / `pikku serve` there is no `start.ts` — startup work goes in a `pikkuServerLifecycle` export, and the hooks receive the singleton services, so read configuration through `variables` there too (see pikku-services).
|
|
230
|
+
|
|
231
|
+
### Lint rules
|
|
232
|
+
|
|
233
|
+
`pikku.config.json` can set the severity of individual checks:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"lint": {
|
|
238
|
+
"servicesNotDestructured": "error",
|
|
239
|
+
"wiresNotDestructured": "error",
|
|
240
|
+
"functionDynamicImport": "warn",
|
|
241
|
+
"customServerBootstrap": "warn"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`customServerBootstrap` is the one evaluated by `pikku workspace validate` rather than codegen: it warns when the root `start`/`dev` script boots a server without `pikku dev` / `pikku serve` and no runtime adapter is installed. Set it to `"off"` to keep a hand-rolled entrypoint, or `"error"` to enforce the hooks.
|
|
169
247
|
|
|
170
248
|
## Complete Example
|
|
171
249
|
|
|
172
250
|
```typescript
|
|
173
251
|
// schemas/config.ts
|
|
174
|
-
|
|
252
|
+
defineSecret({
|
|
175
253
|
name: 'DATABASE_CONFIG',
|
|
176
254
|
schema: z.object({
|
|
177
255
|
connectionString: z.string().url(),
|
|
@@ -179,7 +257,7 @@ wireSecret({
|
|
|
179
257
|
}),
|
|
180
258
|
})
|
|
181
259
|
|
|
182
|
-
|
|
260
|
+
defineVariable({
|
|
183
261
|
name: 'APP_CONFIG',
|
|
184
262
|
schema: z.object({
|
|
185
263
|
appName: z.string(),
|
|
@@ -188,20 +266,24 @@ wireVariable({
|
|
|
188
266
|
}),
|
|
189
267
|
})
|
|
190
268
|
|
|
191
|
-
|
|
269
|
+
defineCredential({
|
|
192
270
|
name: 'githubOAuth',
|
|
193
271
|
displayName: 'GitHub OAuth',
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
272
|
+
type: 'wire',
|
|
273
|
+
schema: z.object({ accessToken: z.string(), refreshToken: z.string() }),
|
|
274
|
+
oauth2: {
|
|
275
|
+
appCredentialSecretId: 'GITHUB_OAUTH_APP',
|
|
276
|
+
tokenSecretId: 'GITHUB_OAUTH_TOKENS',
|
|
277
|
+
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
278
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
279
|
+
scopes: ['read:user', 'repo'],
|
|
280
|
+
},
|
|
199
281
|
})
|
|
200
282
|
|
|
201
283
|
// functions/admin.functions.ts
|
|
202
284
|
export const getAppStatus = pikkuSessionlessFunc({
|
|
203
285
|
title: 'Get App Status',
|
|
204
|
-
func: async ({ variables
|
|
286
|
+
func: async ({ variables }) => {
|
|
205
287
|
const appConfig = await variables.getVariableJSON('APP_CONFIG')
|
|
206
288
|
return {
|
|
207
289
|
appName: appConfig.appName,
|
|
@@ -6,6 +6,7 @@ description: >-
|
|
|
6
6
|
when: code uses wireScheduler, user asks about cron, scheduled tasks, recurring jobs, or "run
|
|
7
7
|
every X minutes/hours". DO NOT TRIGGER when: user asks about background jobs with retries (use
|
|
8
8
|
pikku-queue) or event-driven triggers (use pikku-trigger).
|
|
9
|
+
installGroups: [core]
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# Pikku Cron/Scheduler Wiring
|
|
@@ -42,6 +43,7 @@ wireScheduler({
|
|
|
42
43
|
name: string, // Unique scheduler name
|
|
43
44
|
schedule: string, // Cron expression
|
|
44
45
|
func: PikkuVoidFunc, // Must be pikkuVoidFunc (no input/output)
|
|
46
|
+
tags?: string[], // Targets tag middleware — see pikku-middleware
|
|
45
47
|
middleware?: PikkuMiddleware[],
|
|
46
48
|
})
|
|
47
49
|
```
|
|
@@ -53,10 +55,17 @@ Inside scheduled functions:
|
|
|
53
55
|
```typescript
|
|
54
56
|
wire.scheduledTask.name // Scheduler name
|
|
55
57
|
wire.scheduledTask.schedule // Cron expression string
|
|
56
|
-
wire.scheduledTask.executionTime //
|
|
57
|
-
wire.scheduledTask.skip(reason) //
|
|
58
|
+
wire.scheduledTask.executionTime // Date this execution was triggered
|
|
59
|
+
wire.scheduledTask.skip(reason?) // Abort this execution — THROWS, never returns
|
|
58
60
|
```
|
|
59
61
|
|
|
62
|
+
**`skip()` aborts by throwing.** It reads like an early return but it is not:
|
|
63
|
+
nothing after the call runs, so there is no need to `return` afterwards. The
|
|
64
|
+
consequence that bites is in middleware — a `try/catch` around `await next()`
|
|
65
|
+
will catch a skip and report it as a failure. If your middleware distinguishes
|
|
66
|
+
success from failure, let the skip pass through rather than logging it as an
|
|
67
|
+
error.
|
|
68
|
+
|
|
60
69
|
### Cron Expression Reference
|
|
61
70
|
|
|
62
71
|
```
|
|
@@ -113,8 +122,7 @@ const weeklyCleanup = pikkuVoidFunc({
|
|
|
113
122
|
|
|
114
123
|
const staleCount = await db.countStaleTodos()
|
|
115
124
|
if (staleCount === 0) {
|
|
116
|
-
wire.scheduledTask.skip('No stale todos found')
|
|
117
|
-
return
|
|
125
|
+
wire.scheduledTask.skip('No stale todos found') // throws — nothing below runs
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
await db.deleteCompletedTodos({ olderThan: '30d' })
|
|
@@ -178,8 +186,7 @@ export const cleanupExpired = pikkuVoidFunc({
|
|
|
178
186
|
func: async ({ db, logger }, _input, wire) => {
|
|
179
187
|
const count = await db.countExpiredSessions()
|
|
180
188
|
if (count === 0) {
|
|
181
|
-
wire.scheduledTask.skip('No expired sessions')
|
|
182
|
-
return
|
|
189
|
+
wire.scheduledTask.skip('No expired sessions') // throws — nothing below runs
|
|
183
190
|
}
|
|
184
191
|
await db.deleteExpiredSessions()
|
|
185
192
|
logger.info(`Cleaned ${count} expired sessions`)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-deploy-azure
|
|
3
3
|
description: >-
|
|
4
|
-
Use when deploying a Pikku app to Azure Functions. Covers
|
|
5
|
-
|
|
6
|
-
Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when:
|
|
7
|
-
(use pikku-deploy-lambda) or Cloudflare Workers (use
|
|
4
|
+
Use when deploying a Pikku app to Azure Functions. Covers createAzureHandler for HTTP, storage
|
|
5
|
+
queue and timer triggers, plus AzInvocationLogger and PikkuAZTimerRequest. TRIGGER when: user
|
|
6
|
+
asks about Azure Functions, Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when:
|
|
7
|
+
user asks about AWS Lambda (use pikku-deploy-lambda) or Cloudflare Workers (use
|
|
8
|
+
pikku-deploy-cloudflare).
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# Pikku Azure Functions Deployment
|
|
@@ -29,43 +30,97 @@ yarn add @pikku/azure-functions @azure/functions
|
|
|
29
30
|
|
|
30
31
|
## API Reference
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
Exported from `@pikku/azure-functions`:
|
|
34
|
+
|
|
35
|
+
- `createAzureHandler(factories, handlerTypes)` — the entry point. Returns
|
|
36
|
+
`{ http?, queue?, timer? }` for the handler types you ask for.
|
|
37
|
+
- `createAzureWorkerHandler(factories)` — `createAzureHandler(factories, ['fetch'])`.
|
|
38
|
+
- `createAzureWebSocketHandler(factories)` — **a stub**: its `negotiate` always
|
|
39
|
+
answers `501 WebSocket via Azure Web PubSub not yet implemented`. Channels do
|
|
40
|
+
not work on Azure yet; do not plan a deployment around it.
|
|
41
|
+
- `AzInvocationLogger` — the logger. Note the name: there is no
|
|
42
|
+
`PikkuAzFunctionsLogger`.
|
|
43
|
+
- `PikkuAZTimerRequest` — `new PikkuAZTimerRequest(context, data)`, a
|
|
44
|
+
`PikkuRequest` carrying the data. The context argument is accepted and
|
|
45
|
+
ignored.
|
|
46
|
+
- `AzureQueueService`, `AzureDeploymentService`.
|
|
47
|
+
|
|
48
|
+
`factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
|
|
49
|
+
Services are built from `process.env` and cached in module scope across
|
|
50
|
+
invocations of the same instance.
|
|
39
51
|
|
|
40
52
|
## Usage Patterns
|
|
41
53
|
|
|
42
|
-
###
|
|
54
|
+
### Registering handlers
|
|
43
55
|
|
|
44
56
|
```typescript
|
|
45
57
|
import { app } from '@azure/functions'
|
|
46
|
-
import {
|
|
58
|
+
import { createAzureHandler } from '@pikku/azure-functions'
|
|
59
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
60
|
+
import './.pikku/pikku-bootstrap.gen.js'
|
|
61
|
+
|
|
62
|
+
const handlers = createAzureHandler(
|
|
63
|
+
{ createConfig, createSingletonServices },
|
|
64
|
+
['fetch', 'queue', 'scheduled']
|
|
65
|
+
)
|
|
47
66
|
|
|
48
67
|
app.http('api', {
|
|
49
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
68
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
50
69
|
route: '{*path}',
|
|
51
|
-
handler:
|
|
52
|
-
const logger = new PikkuAzFunctionsLogger(context)
|
|
53
|
-
// Wire Pikku HTTP runner with Azure request/response
|
|
54
|
-
},
|
|
70
|
+
handler: handlers.http as any,
|
|
55
71
|
})
|
|
56
|
-
```
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
app.storageQueue('queue', {
|
|
74
|
+
queueName: 'my-queue',
|
|
75
|
+
connection: 'AzureWebJobsStorage',
|
|
76
|
+
handler: handlers.queue as any,
|
|
77
|
+
})
|
|
63
78
|
|
|
64
79
|
app.timer('scheduler', {
|
|
65
80
|
schedule: '0 */5 * * * *',
|
|
66
|
-
handler:
|
|
67
|
-
const request = new PikkuAzTimerRequest(timer)
|
|
68
|
-
// Process scheduled Pikku functions
|
|
69
|
-
},
|
|
81
|
+
handler: handlers.timer as any,
|
|
70
82
|
})
|
|
71
83
|
```
|
|
84
|
+
|
|
85
|
+
Note the key names: `handlerTypes` uses **`scheduled`**, but the handler it
|
|
86
|
+
returns is **`timer`**.
|
|
87
|
+
|
|
88
|
+
### HTTP
|
|
89
|
+
|
|
90
|
+
The handler buffers the whole body, converts to a standard `Request`, and
|
|
91
|
+
returns the response body as **text** — a streaming or binary response is
|
|
92
|
+
flattened. It takes no `RunHTTPWiringOptions`, so there is no `maxBodySize` or
|
|
93
|
+
`respondWith404` here; Azure's own request limits are the bound. A thrown error
|
|
94
|
+
is logged to `console.error` and whatever the response already holds is
|
|
95
|
+
returned.
|
|
96
|
+
|
|
97
|
+
### Queue
|
|
98
|
+
|
|
99
|
+
The queue name comes from the message's own `queueName`, falling back to
|
|
100
|
+
`context.triggerMetadata.queueTrigger` and then `'unknown'` — a name that does
|
|
101
|
+
not match a wired queue means the job has no handler. `attemptsMade` is read
|
|
102
|
+
from `dequeueCount`, and `waitForCompletion` throws: Azure Storage Queues are
|
|
103
|
+
fire-and-forget. A failing job throws out of the handler, so retries and the
|
|
104
|
+
poison queue are governed by `host.json`, not by Pikku.
|
|
105
|
+
|
|
106
|
+
Producer side, `AzureQueueService(connectionString?)` falls back to
|
|
107
|
+
`AzureWebJobsStorage` and throws at construction if neither is set. Messages are
|
|
108
|
+
base64-encoded (Azure requires it), `delay` is milliseconds mapped to
|
|
109
|
+
`visibilityTimeout` in whole seconds capped at 7 days, `supportsResults` is
|
|
110
|
+
`false` and `getJob()` always throws. The queue name is remapped through
|
|
111
|
+
`AZURE_QUEUE_NAME_<SCREAMING_SNAKE>` when that variable exists, otherwise used
|
|
112
|
+
as-is.
|
|
113
|
+
|
|
114
|
+
### Timer
|
|
115
|
+
|
|
116
|
+
The timer handler runs **every** scheduled task registered in the bundle,
|
|
117
|
+
ignoring both the `Timer` argument and each task's own cron expression. Unlike
|
|
118
|
+
the Lambda equivalent it does not catch per-task failures, so the first task
|
|
119
|
+
that throws aborts the ones after it — keep one schedule per function app, or
|
|
120
|
+
guard the task bodies yourself.
|
|
121
|
+
|
|
122
|
+
### Logging
|
|
123
|
+
|
|
124
|
+
`new AzInvocationLogger(context)` forwards to the invocation context's
|
|
125
|
+
`info`/`warn`/`error`/`debug`/`trace`. `setLevel()` is a **no-op**: every level
|
|
126
|
+
is emitted and filtering has to be done in Azure's own logging configuration.
|
|
@@ -26,57 +26,99 @@ yarn add @pikku/cloudflare
|
|
|
26
26
|
|
|
27
27
|
## Worker Entry
|
|
28
28
|
|
|
29
|
+
`@pikku/cloudflare` ships the handler factories the deploy codegen emits — use
|
|
30
|
+
them rather than hand-rolling an `ExportedHandler`. Each returns a
|
|
31
|
+
`WorkerEntrypoint` class that sets services up on every invocation (cached after
|
|
32
|
+
the first) and adds an RPC-callable `runRpc(name, args)`:
|
|
33
|
+
|
|
29
34
|
```typescript
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
35
|
+
import { createCloudflareHandler } from '@pikku/cloudflare'
|
|
36
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
32
37
|
import './.pikku/pikku-bootstrap.gen.js'
|
|
33
38
|
|
|
34
|
-
export default
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
async fetch(request, env): Promise<Response> {
|
|
41
|
-
await setupServices(env)
|
|
42
|
-
return await runFetch(request as unknown as Request)
|
|
43
|
-
},
|
|
44
|
-
} satisfies ExportedHandler<Record<string, string>>
|
|
39
|
+
export default createCloudflareHandler(
|
|
40
|
+
{ createConfig, createSingletonServices },
|
|
41
|
+
['fetch', 'scheduled']
|
|
42
|
+
)
|
|
45
43
|
```
|
|
46
44
|
|
|
45
|
+
| Factory | For |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `createCloudflareHandler(factories, handlerTypes)` | combined `fetch`/`queue`/`scheduled` |
|
|
48
|
+
| `createCloudflareWorkerHandler(factories)` | HTTP / agent / RPC / workflow-orchestrator units |
|
|
49
|
+
| `createCloudflareCronHandler(factories)` | cron units |
|
|
50
|
+
| `createCloudflareQueueHandler(factories)` | queue-consumer and workflow-step units |
|
|
51
|
+
| `createCloudflareMCPHandler(factories)` | MCP units (same HTTP transport) |
|
|
52
|
+
| `createCloudflareWebSocketHandler(factories)` | channel units, delegating to the DO |
|
|
53
|
+
|
|
54
|
+
`factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
|
|
55
|
+
|
|
47
56
|
## Service Setup
|
|
48
57
|
|
|
49
|
-
Cloudflare passes env
|
|
58
|
+
Cloudflare passes env bindings per-request, so services are built from `env`
|
|
59
|
+
rather than at module load. `setupServices(env, factories)` is exported from
|
|
60
|
+
`@pikku/cloudflare` and is what the factories call:
|
|
50
61
|
|
|
51
62
|
```typescript
|
|
52
|
-
|
|
53
|
-
import { LocalVariablesService, LocalSecretService } from '@pikku/core/services'
|
|
54
|
-
import { createConfig, createSingletonServices } from './services.js'
|
|
63
|
+
import { setupServices } from '@pikku/cloudflare'
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const config = await createConfig(localVariables)
|
|
61
|
-
const localSecrets = new LocalSecretService(localVariables)
|
|
62
|
-
return await createSingletonServices(config, {
|
|
63
|
-
variables: localVariables,
|
|
64
|
-
secrets: localSecrets,
|
|
65
|
-
})
|
|
66
|
-
}
|
|
65
|
+
const services = await setupServices(env, {
|
|
66
|
+
createConfig,
|
|
67
|
+
createSingletonServices,
|
|
68
|
+
})
|
|
67
69
|
```
|
|
68
70
|
|
|
71
|
+
**Do not hand-roll this.** Beyond building `LocalVariablesService` /
|
|
72
|
+
`LocalSecretService` and caching the result, it calls `setSingletonServices()` —
|
|
73
|
+
and the core runners (`fetchData`, `runQueueJob`, `runScheduled`) resolve
|
|
74
|
+
services through that global slot, *not* through the value you were returned. A
|
|
75
|
+
setup function that only returns the services leaves every request throwing
|
|
76
|
+
"Singleton services not initialized" as a CF `1101`. It also stashes the env via
|
|
77
|
+
`setCloudflareEnv`, which `getCloudflareEnv()` reads for bindings.
|
|
78
|
+
|
|
79
|
+
## HTTP
|
|
80
|
+
|
|
81
|
+
`runFetch(request, websocketHibernationServer?, options?)`:
|
|
82
|
+
|
|
83
|
+
- A `GET` with `Upgrade: websocket` is routed to the hibernation server. Without
|
|
84
|
+
one passed in it answers **426**, so a channel worker that forgets the second
|
|
85
|
+
argument fails every upgrade while plain HTTP keeps working.
|
|
86
|
+
- `CF-Ray` becomes the traceId when present, so a Cloudflare trace and a Pikku
|
|
87
|
+
trace line up without extra wiring.
|
|
88
|
+
- `options.exposeErrors` defaults to **`false`** — error detail is withheld from
|
|
89
|
+
responses unless you opt in.
|
|
90
|
+
|
|
91
|
+
## Scheduled Tasks
|
|
92
|
+
|
|
93
|
+
`runScheduled(controller)` matches registered tasks against
|
|
94
|
+
`controller.cron` and **returns after the first match**. Two tasks sharing one
|
|
95
|
+
cron expression means only one of them ever runs — give each its own expression,
|
|
96
|
+
or invoke `runScheduledTask({ name })` per task yourself.
|
|
97
|
+
|
|
69
98
|
## WebSocket (Durable Objects)
|
|
70
99
|
|
|
100
|
+
The ready-made DO class is exported; re-export it under the binding name and
|
|
101
|
+
point the worker at it:
|
|
102
|
+
|
|
71
103
|
```typescript
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return { singletonServices }
|
|
78
|
-
}
|
|
79
|
-
}
|
|
104
|
+
export { PikkuWebSocketHibernationServer as WebSocketHibernationServer } from '@pikku/cloudflare'
|
|
105
|
+
export default createCloudflareWebSocketHandler({
|
|
106
|
+
createConfig,
|
|
107
|
+
createSingletonServices,
|
|
108
|
+
})
|
|
80
109
|
```
|
|
81
110
|
|
|
82
|
-
|
|
111
|
+
Subclass `CloudflareWebSocketHibernationServer` only when you need something
|
|
112
|
+
`getParams()` cannot express — it is abstract with one method returning
|
|
113
|
+
`{ singletonServices, createWireServices? }`. The channel store
|
|
114
|
+
(`CloudflareWebsocketStore` over the DO's own storage), the event hub and the
|
|
115
|
+
channel handler factory are all built by the base class; do not supply them.
|
|
116
|
+
|
|
117
|
+
The router looks up the DO through the **`WEBSOCKET_HIBERNATION_SERVER`**
|
|
118
|
+
binding and answers `503` naming it if the binding is missing, so declare it in
|
|
119
|
+
`wrangler.toml` under exactly that name.
|
|
120
|
+
|
|
121
|
+
A throw during `onConnect` closes the socket with `1008` and answers `403
|
|
122
|
+
Forbidden` with a deliberately generic body — an auth denial and a genuine fault
|
|
123
|
+
look identical to the client. The real reason is on the logger, so read the
|
|
124
|
+
worker logs rather than the status code.
|