@pikku/skills 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "The Pikku agent skills — the instruction set coding agents read to build, wire and deploy Pikku projects",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -54,7 +54,7 @@ Better Auth owns its own HTTP surface, database tables, and session cookie. The
54
54
  1. **`pikkuBetterAuth(factory)`** — you export ONE `pikkuBetterAuth` call whose factory returns a configured `betterAuth({...})` instance. The pikku CLI inspects this export and generates everything else.
55
55
  2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router. The enabled providers and plugins are written to `auth/pikku-auth-meta.gen.json` (read by the console SSO page via `getAuthProviders`).
56
56
  3. **Generated session middleware** — with `session.cookieCache` enabled (recommended), a separate `auth-middleware.gen.ts` adds the lean stateless `betterAuthStatelessSession()`; without it, `auth.gen.ts` adds the stateful `betterAuthSession()` that bundles the full server into every unit. See "Stateless session" below.
57
- 4. **Generated `auth-secrets.gen.ts`** — a `wireSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `wireVariable` for any non-secret provider config (e.g. `tenantId`).
57
+ 4. **Generated `auth-secrets.gen.ts`** — a `defineSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `defineVariable` for any non-secret provider config (e.g. `tenantId`).
58
58
 
59
59
  You do NOT hand-write routes, the session middleware, or the secret wiring — `pikkuBetterAuth` + the CLI generate all of it. Re-run `pikku all` to regenerate.
60
60
 
@@ -86,7 +86,12 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
86
86
  secret: BETTER_AUTH_SECRET,
87
87
  // memoryAdapter needs an array per model — `{}` throws "Model user not found"
88
88
  // at runtime. Swap for the Kysely adapter in production (see below).
89
- database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),
89
+ database: memoryAdapter({
90
+ user: [],
91
+ session: [],
92
+ account: [],
93
+ verification: [],
94
+ }),
90
95
  emailAndPassword: { enabled: true },
91
96
  // ALWAYS enable for deployed apps — see "Stateless session" below.
92
97
  session: { cookieCache: { enabled: true } },
@@ -98,7 +103,8 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
98
103
  ```
99
104
 
100
105
  **Key points:**
101
- - `socialProviders` keys must be string literals — the CLI reads them statically to emit a `wireSecret` per provider. Provider keys mirror better-auth's built-in ids exactly (e.g. `microsoft`, NOT `microsoft-entra-id`; `cognito`; `github`).
106
+
107
+ - `socialProviders` keys must be string literals — the CLI reads them statically to emit a `defineSecret` per provider. Provider keys mirror better-auth's built-in ids exactly (e.g. `microsoft`, NOT `microsoft-entra-id`; `cognito`; `github`).
102
108
  - The factory runs lazily on the first auth request, so it pulls secrets/DB off the injected `services`.
103
109
  - The default `basePath` is `/api/auth`. Override it by passing `basePath` to `betterAuth`.
104
110
  - **Enable `session: { cookieCache: { enabled: true } }`** so non-auth units tree-shake the better-auth server out (see below).
@@ -115,7 +121,7 @@ Enabling `session: { cookieCache: { enabled: true } }` makes the CLI split out a
115
121
 
116
122
  **Customizing the session bridge (`mapSession`, `impersonation`, `apiKey`, …):** you do NOT chain a second middleware on top of the generated one — register your OWN global session middleware and the CLI steps aside (it stops generating its default). This works on both paths and is detected the same way:
117
123
 
118
- - **Stateless (cookieCache on):** register `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI sees the global registration and skips emitting `auth-middleware.gen.ts` (pikkujs/pikku#754), so you keep cookieCache's lean bundles *and* your custom fields.
124
+ - **Stateless (cookieCache on):** register `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI sees the global registration and skips emitting `auth-middleware.gen.ts` (pikkujs/pikku#754), so you keep cookieCache's lean bundles _and_ your custom fields.
119
125
  - **Stateful (cookieCache off):** register `betterAuthSession({ mapSession, impersonation })` **globally**. The CLI detects it (`hasUserSessionMiddleware`) and omits its own `addHTTPMiddleware('*', [betterAuthSession()])` from `auth.gen.ts` — so there's exactly one session bridge in the chain, yours.
120
126
 
121
127
  In both cases a **route-scoped** registration (`addHTTPMiddleware('/some/path', [...])`) does NOT count — only a global one suppresses the generated default. The generated middleware in a `.gen.ts` file is also ignored by the detector, so regeneration never self-suppresses.
@@ -129,22 +135,22 @@ hold independently, which a single `role` string cannot express. Every gate the
129
135
  package owns therefore resolves the caller's scopes through the registered
130
136
  `ScopeService` and checks the `admin:*` tree:
131
137
 
132
- | Gate | Scope required |
133
- | --- | --- |
134
- | `impersonation` (`betterAuthSession` / `betterAuthStatelessSession`) | `admin:impersonate` |
135
- | `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
136
- | the console's user directory | `admin:users:list` |
138
+ | Gate | Scope required |
139
+ | -------------------------------------------------------------------- | ------------------------ |
140
+ | `impersonation` (`betterAuthSession` / `betterAuthStatelessSession`) | `admin:impersonate` |
141
+ | `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
142
+ | the console's user directory | `admin:users:list` |
137
143
 
138
144
  Holding the bare `admin` scope satisfies all of them — a parent grant covers
139
145
  everything nested beneath it — so `admin` is the direct replacement for the old
140
146
  `role === 'admin'`.
141
147
 
142
- Declare the tree in your own `wireScope` (the CLI extracts it by AST, so it must
148
+ Declare the tree in your own `defineScope` (the CLI extracts it by AST, so it must
143
149
  be an inline literal; `ADMIN_SCOPE_TREE` is exported from `@pikku/better-auth`
144
150
  as the reference shape). Apps wiring `@pikku/addon-console` inherit it already.
145
151
 
146
152
  ```typescript
147
- wireScope({
153
+ defineScope({
148
154
  admin: {
149
155
  displayName: 'Administration',
150
156
  description: 'Capabilities that act on the application as a whole',
@@ -173,7 +179,7 @@ that is a configuration bug rather than a permissions decision. Pass your own
173
179
  `canImpersonate` / `canLinkSingleton` to override the default entirely.
174
180
 
175
181
  Sibling concerns — banning a user, listing users from your own screens — are
176
- actions your app *invokes*, not things pikku gates. Put them on your own
182
+ actions your app _invokes_, not things pikku gates. Put them on your own
177
183
  functions with `scopes: ['admin:users:ban']` and friends.
178
184
 
179
185
  ### 2. Production database adapter
@@ -184,9 +190,9 @@ For real deployments swap `memoryAdapter` for the Kysely adapter backed by an in
184
190
  import { kyselyAdapter } from 'better-auth/adapters/kysely'
185
191
 
186
192
  export const auth = pikkuBetterAuth(async ({ secrets, kysely }) => {
187
- const { BETTER_AUTH_SECRET } = await secrets.getSecrets<{ BETTER_AUTH_SECRET: string }>([
188
- 'BETTER_AUTH_SECRET',
189
- ])
193
+ const { BETTER_AUTH_SECRET } = await secrets.getSecrets<{
194
+ BETTER_AUTH_SECRET: string
195
+ }>(['BETTER_AUTH_SECRET'])
190
196
  return betterAuth({
191
197
  secret: BETTER_AUTH_SECRET,
192
198
  database: kyselyAdapter(kysely, { type: 'postgres' }),
@@ -204,7 +210,7 @@ If you place `auth.ts` under `srcDirectories` it is inspected automatically. The
204
210
 
205
211
  ## Social Providers needing extra config
206
212
 
207
- Some providers require non-secret config alongside the OAuth secret — the CLI emits a `wireVariable` for these:
213
+ Some providers require non-secret config alongside the OAuth secret — the CLI emits a `defineVariable` for these:
208
214
 
209
215
  - `microsoft` → `MICROSOFT_TENANT_ID` (or `"common"`)
210
216
  - `cognito` → `COGNITO_DOMAIN`, `COGNITO_REGION`, `COGNITO_USER_POOL_ID`
@@ -221,7 +227,12 @@ export const auth = pikkuBetterAuth(async ({ secrets, variables }) => {
221
227
 
222
228
  return betterAuth({
223
229
  secret: BETTER_AUTH_SECRET,
224
- database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),
230
+ database: memoryAdapter({
231
+ user: [],
232
+ session: [],
233
+ account: [],
234
+ verification: [],
235
+ }),
225
236
  socialProviders: {
226
237
  microsoft: { ...MICROSOFT_OAUTH, tenantId: MICROSOFT_TENANT_ID },
227
238
  },
@@ -258,13 +269,13 @@ For public endpoints that optionally vary by viewer, use `pikkuSessionlessFunc`
258
269
 
259
270
  Better Auth serves everything under `basePath` (default `/api/auth`). Call these directly — the Pikku SDK does not wrap them.
260
271
 
261
- | Action | Request | Result |
262
- |---|---|---|
263
- | Sign up | `POST /api/auth/sign-up/email` `{ name, email, password }` | 200 + `better-auth.session_token` cookie |
264
- | Log in | `POST /api/auth/sign-in/email` `{ email, password }` | 200 + cookie; wrong creds → 401 `{ code: "INVALID_EMAIL_OR_PASSWORD" }` |
265
- | Session | `GET /api/auth/get-session` | `{ session, user }` or `null` |
266
- | Social sign-in | `POST /api/auth/sign-in/social` `{ provider, callbackURL }` | 200 `{ url, redirect }` (authorize URL) |
267
- | Sign out | `POST /api/auth/sign-out` | 200, clears cookie |
272
+ | Action | Request | Result |
273
+ | -------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
274
+ | Sign up | `POST /api/auth/sign-up/email` `{ name, email, password }` | 200 + `better-auth.session_token` cookie |
275
+ | Log in | `POST /api/auth/sign-in/email` `{ email, password }` | 200 + cookie; wrong creds → 401 `{ code: "INVALID_EMAIL_OR_PASSWORD" }` |
276
+ | Session | `GET /api/auth/get-session` | `{ session, user }` or `null` |
277
+ | Social sign-in | `POST /api/auth/sign-in/social` `{ provider, callbackURL }` | 200 `{ url, redirect }` (authorize URL) |
278
+ | Sign out | `POST /api/auth/sign-out` | 200, clears cookie |
268
279
 
269
280
  **`Origin` header on state-changing POSTs:** better-auth enforces an `Origin` header matching `baseURL` on POSTs such as sign-out — omit it and you get `403`. Browsers send it automatically; server-to-server callers must set it.
270
281
 
@@ -221,6 +221,13 @@ const apiKey = services.variables.get('API_KEY')
221
221
 
222
222
  `process.env` belongs in server bootstrap code (`start.ts`) only.
223
223
 
224
+ ## Secrets
225
+
226
+ `secrets` is not part of a function's services. It is available only in
227
+ `pikkuServices`, `pikkuWireServices`, addon service factories and middleware —
228
+ read it there, give the value to a service, and have the function ask that
229
+ service. Reaching for it through a cast throws at runtime.
230
+
224
231
  ## Testing
225
232
 
226
233
  Functions are easily testable because they're pure:
@@ -18,8 +18,8 @@ Authoritative mapping table plus side-by-side code examples showing how common b
18
18
  | **Cron / Scheduled tasks** | `wireScheduler` | `pikku-cron` |
19
19
  | **Module / Feature grouping** | Tags + wiring files | `pikku-concepts` |
20
20
  | **Error handling** | Throw typed errors (`NotFoundError`, `ForbiddenError`) | `pikku-concepts` |
21
- | **Type-safe API client** | `npx pikku all` generates clients | `pikku-concepts` |
22
- | **Secrets / Config** | `wireSecret`, `wireVariable`, `services.variables` | `pikku-config` |
21
+ | **Type-safe API client** | `npx pikku all` generates clients | `pikku-concepts` |
22
+ | **Secrets / Config** | `defineSecret`, `defineVariable`, `services.variables` | `pikku-config` |
23
23
 
24
24
  ## Route Handler / Controller → pikkuFunc
25
25
 
@@ -2,8 +2,8 @@
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 wireSecret, wireVariable, wireOAuth2Credential, and typed config access. TRIGGER when:
6
- code uses wireSecret/wireVariable/wireOAuth2Credential, user asks about env vars, secrets,
5
+ Covers defineSecret, defineVariable, defineCredential, and typed config access. TRIGGER when:
6
+ code uses defineSecret/defineVariable/defineCredential, user asks about env vars, secrets,
7
7
  config, OAuth2, or "how do I access environment variables". DO NOT TRIGGER when: user asks about
8
8
  API versioning/breaking changes (use pikku-versioning), service factories (use pikku-services),
9
9
  or auth middleware (use pikku-security).
@@ -35,34 +35,53 @@ See `pikku-concepts` for the core mental model.
35
35
 
36
36
  ## Secrets & Variables
37
37
 
38
- ### `wireSecret(config)`
38
+ ### `defineSecret(config)`
39
39
 
40
40
  Declare a secret with a Zod schema for type-safe access:
41
41
 
42
42
  ```typescript
43
- wireSecret({
43
+ defineSecret({
44
44
  name: string, // Secret identifier
45
45
  schema: ZodSchema, // Shape and validation
46
46
  })
47
47
  ```
48
48
 
49
- ### `wireVariable(config)`
49
+ ### `defineVariable(config)`
50
50
 
51
51
  Declare a variable (non-sensitive config) with a Zod schema:
52
52
 
53
53
  ```typescript
54
- wireVariable({
54
+ defineVariable({
55
55
  name: string,
56
56
  schema: ZodSchema,
57
57
  })
58
58
  ```
59
59
 
60
- ### Accessing in Functions
60
+ ### Accessing Secrets
61
+
62
+ `secrets` is **not available inside functions, AI agents, workflows, permissions
63
+ or any wire** — it is removed from their services type and throws at runtime if
64
+ reached through a cast. Read it where you wire the app and hand the value to a
65
+ service:
61
66
 
62
67
  ```typescript
63
- // Secrets — encrypted, sensitive values
64
- const config = await services.secrets.getSecret('SECRET_NAME')
68
+ // services.ts — allowed
69
+ const createSingletonServices = pikkuServices(async (config, { secrets }) => ({
70
+ stripe: new StripeService(await secrets.getSecret('STRIPE_CONFIG')),
71
+ }))
72
+
73
+ // functions/*.ts — ask the service, never the secret store
74
+ export const charge = pikkuFunc({
75
+ func: async ({ stripe }, data) => stripe.charge(data.amount),
76
+ })
77
+ ```
78
+
79
+ Allowed: `pikkuServices`, `pikkuWireServices`, addon service factories,
80
+ middleware. Everywhere else, the service you constructed is the interface.
81
+
82
+ ### Accessing Variables in Functions
65
83
 
84
+ ```typescript
66
85
  // Variables — plain-text configuration
67
86
  const flags = await services.variables.getVariableJSON('VARIABLE_NAME')
68
87
 
@@ -85,7 +104,7 @@ const createSingletonServices = pikkuServices(async (config) => ({
85
104
 
86
105
  ```typescript
87
106
  // Declare secrets with typed schemas
88
- wireSecret({
107
+ defineSecret({
89
108
  name: 'STRIPE_CONFIG',
90
109
  schema: z.object({
91
110
  apiKey: z.string().startsWith('sk_'),
@@ -93,13 +112,13 @@ wireSecret({
93
112
  }),
94
113
  })
95
114
 
96
- // In your function — fully typed
115
+ // In your services factory — fully typed
97
116
  const config = await secrets.getSecret('STRIPE_CONFIG')
98
117
  // config.apiKey → string (autocompleted)
99
118
  // config.webhookSecret → string (autocompleted)
100
119
 
101
120
  // Declare variables
102
- wireVariable({
121
+ defineVariable({
103
122
  name: 'FEATURE_FLAGS',
104
123
  schema: z.object({
105
124
  darkMode: z.boolean(),
@@ -113,33 +132,50 @@ const flags = await variables.getVariableJSON('FEATURE_FLAGS')
113
132
  // flags.maxUploadMB → number
114
133
  ```
115
134
 
116
- ## OAuth2 Credentials
135
+ ## Credentials
117
136
 
118
- ### `wireOAuth2Credential(config)`
137
+ ### `defineCredential(config)`
119
138
 
120
139
  ```typescript
121
- wireOAuth2Credential({
122
- name: string, // Credential identifier
123
- displayName: string, // Human-readable name
124
- secretId: string, // Secret holding { clientId, clientSecret }
125
- tokenSecretId: string, // Secret for token storage (auto-refreshed)
126
- authorizationUrl: string, // OAuth2 authorization endpoint
127
- tokenUrl: string, // OAuth2 token endpoint
128
- scopes: string[], // Required OAuth2 scopes
140
+ defineCredential({
141
+ name: string, // Credential identifier
142
+ displayName: string, // Human-readable name
143
+ type: 'wire' | 'singleton', // Per-user ('wire') or platform-level ('singleton')
144
+ schema: ZodSchema, // Shape of the stored credential
145
+ oauth2?: { // Omit entirely for a plain API key
146
+ appCredentialSecretId: string, // Secret holding { clientId, clientSecret }
147
+ tokenSecretId: string, // Secret for token storage (auto-refreshed)
148
+ authorizationUrl: string, // OAuth2 authorization endpoint
149
+ tokenUrl: string, // OAuth2 token endpoint
150
+ scopes: string[], // Required OAuth2 scopes
151
+ },
129
152
  })
130
153
  ```
131
154
 
132
155
  ### Usage
133
156
 
134
157
  ```typescript
135
- wireOAuth2Credential({
136
- name: 'slackOAuth',
137
- displayName: 'Slack OAuth',
138
- secretId: 'SLACK_OAUTH_APP',
139
- tokenSecretId: 'SLACK_OAUTH_TOKENS',
140
- authorizationUrl: 'https://slack.com/oauth/v2/authorize',
141
- tokenUrl: 'https://slack.com/api/oauth.v2.access',
142
- scopes: ['chat:write', 'channels:read'],
158
+ // Per-user API key — no oauth2 block
159
+ defineCredential({
160
+ name: 'stripe',
161
+ displayName: 'Stripe API Key',
162
+ type: 'wire',
163
+ schema: z.object({ apiKey: z.string() }),
164
+ })
165
+
166
+ // Platform-level OAuth (singleton)
167
+ defineCredential({
168
+ name: 'slack',
169
+ displayName: 'Slack',
170
+ type: 'singleton',
171
+ schema: z.object({ accessToken: z.string(), refreshToken: z.string() }),
172
+ oauth2: {
173
+ appCredentialSecretId: 'SLACK_OAUTH_APP',
174
+ tokenSecretId: 'SLACK_OAUTH_TOKENS',
175
+ authorizationUrl: 'https://slack.com/oauth/v2/authorize',
176
+ tokenUrl: 'https://slack.com/api/oauth.v2.access',
177
+ scopes: ['chat:write', 'channels:read'],
178
+ },
143
179
  })
144
180
 
145
181
  // In your function — tokens refresh automatically
@@ -171,7 +207,7 @@ const apiKey = services.variables.get('API_KEY')
171
207
 
172
208
  ```typescript
173
209
  // schemas/config.ts
174
- wireSecret({
210
+ defineSecret({
175
211
  name: 'DATABASE_CONFIG',
176
212
  schema: z.object({
177
213
  connectionString: z.string().url(),
@@ -179,7 +215,7 @@ wireSecret({
179
215
  }),
180
216
  })
181
217
 
182
- wireVariable({
218
+ defineVariable({
183
219
  name: 'APP_CONFIG',
184
220
  schema: z.object({
185
221
  appName: z.string(),
@@ -188,20 +224,24 @@ wireVariable({
188
224
  }),
189
225
  })
190
226
 
191
- wireOAuth2Credential({
227
+ defineCredential({
192
228
  name: 'githubOAuth',
193
229
  displayName: 'GitHub OAuth',
194
- secretId: 'GITHUB_OAUTH_APP',
195
- tokenSecretId: 'GITHUB_OAUTH_TOKENS',
196
- authorizationUrl: 'https://github.com/login/oauth/authorize',
197
- tokenUrl: 'https://github.com/login/oauth/access_token',
198
- scopes: ['read:user', 'repo'],
230
+ type: 'wire',
231
+ schema: z.object({ accessToken: z.string(), refreshToken: z.string() }),
232
+ oauth2: {
233
+ appCredentialSecretId: 'GITHUB_OAUTH_APP',
234
+ tokenSecretId: 'GITHUB_OAUTH_TOKENS',
235
+ authorizationUrl: 'https://github.com/login/oauth/authorize',
236
+ tokenUrl: 'https://github.com/login/oauth/access_token',
237
+ scopes: ['read:user', 'repo'],
238
+ },
199
239
  })
200
240
 
201
241
  // functions/admin.functions.ts
202
242
  export const getAppStatus = pikkuSessionlessFunc({
203
243
  title: 'Get App Status',
204
- func: async ({ variables, secrets }) => {
244
+ func: async ({ variables }) => {
205
245
  const appConfig = await variables.getVariableJSON('APP_CONFIG')
206
246
  return {
207
247
  appName: appConfig.appName,
@@ -183,7 +183,7 @@ Links the repo to a Fabric project and declares its frontends:
183
183
  ```
184
184
 
185
185
  - `projectId`: written by `pikku fabric init` / `link`. Templates ship the
186
- `__PROJECT_ID__` placeholder — that is *not* a link, and the CLI treats it as
186
+ `__PROJECT_ID__` placeholder — that is _not_ a link, and the CLI treats it as
187
187
  unlinked.
188
188
  - `production.domain`: optional custom domain. Production always maps to `main`;
189
189
  without a domain it lives on the platform `*.pikkufabric.app` hostnames.
@@ -290,7 +290,7 @@ The output card shows whether any breaking changes were detected.
290
290
 
291
291
  These apply in every Fabric app:
292
292
 
293
- - **No `process.env`** — use `variables.get('NAME')` and `secrets.getSecret('NAME')`. Declare with `wireVariable` / `wireSecret`.
293
+ - **No `process.env`** — use `variables.get('NAME')` and `secrets.getSecret('NAME')`. Declare with `defineVariable` / `defineSecret`.
294
294
  - **No `as any`** — fix types properly.
295
295
  - **No generic `Error`** — throw `NotFoundError`, `ConflictError`, `BadRequestError`, `UnauthorizedError` from `@pikku/core/errors`.
296
296
  - **No auth checks in function bodies** — use `permissions:` field on the function config with a `pikkuPermission` factory.
@@ -311,7 +311,7 @@ Fix every `error` and `warn` in the output before continuing. Then:
311
311
  1. **Replace the database layer**: swap PostgreSQL/MySQL queries for Kysely + libSQL. Convert schema to SQLite-compatible SQL migrations in `db/sqlite/`.
312
312
  2. **Replace route handlers with pikkuFuncs**: extract business logic into `pikkuFunc`/`pikkuSessionlessFunc`, add `wireHTTP` or `expose: true` for transport.
313
313
  3. **Replace DI/IoC with pikkuServices**: move service construction to `createSingletonServices` in `services.ts`.
314
- 4. **Replace `process.env` calls** with `wireVariable`/`wireSecret` + `variables.get()`.
314
+ 4. **Replace `process.env` calls**: plain config becomes `defineVariable` + `variables.get()`, anything sensitive becomes `defineSecret` + `secrets.getSecret()`.
315
315
  5. **Add `pikku.config.json`** at project root with `srcDirectories`, `outDir`, and `clientFiles`.
316
316
  6. **Add `fabric.config.json`** at project root with `projectId`, `production.branch`, and `frontends`.
317
317
  7. **Run `pikku all`** — verify codegen succeeds and there are no type errors.
@@ -126,12 +126,12 @@ in-app features don't.
126
126
  - **Migrations are inline SQL files** in the project's migrations dir
127
127
  (typically `sql/`). Use a numbered prefix matching existing files.
128
128
  - **Secrets and env-vars: NEVER `process.env`.** Declare them with
129
- `wireSecret` (sensitive) or `wireVariable` (non-sensitive) — both with a
130
- zod schema for type-safe access. Read with
131
- `services.secrets.getSecret('NAME')` or `services.variables.get('NAME')`.
132
- See the **pikku-config** skill for the full pattern (including
133
- OAuth2 credentials). This applies even in `config.ts` and singleton
134
- service factories.
129
+ `defineSecret` (sensitive) or `defineVariable` (non-sensitive) — both with a
130
+ zod schema for type-safe access. Read variables with
131
+ `services.variables.get('NAME')`. Secrets are **not available in functions** —
132
+ read them in `services.ts` with `secrets.getSecret('NAME')` and pass the value
133
+ into the service the function uses. See the **pikku-config** skill for the full
134
+ pattern (including OAuth2 credentials). This applies even in `config.ts`.
135
135
 
136
136
  ### Conventions to copy from neighbours
137
137
 
@@ -161,8 +161,8 @@ And writing again replaces it rather than adding a second
161
161
  | `channel:` | a channel name | generated channel meta |
162
162
  | `table:` | a table name | the generated db schema |
163
163
  | `addon:` | `@pikku/addon-x` or bare `x` | the manifests that declare the dependency |
164
- | `scope:` | a scope name | the `scopes:` a function gates itself with, plus the grants in `scenarios.actors` |
165
- | `persona:` | a persona name | `scenarios.personas` in `pikku.config.json` |
164
+ | `scope:` | a scope name | the `scopes:` a function gates itself with, plus the scopes a `defineSystemRole()` confers |
165
+ | `persona:` | a persona name | `definePersonas()` |
166
166
 
167
167
  Ids are case-sensitive: `createEntry` is not `createentry`.
168
168
 
@@ -176,7 +176,7 @@ These are all things that exist somewhere better, so a note is always the copy t
176
176
 
177
177
  | Do not write | Because it lives in |
178
178
  | ----------------------------------- | ---------------------------------------------------------- |
179
- | a `personas/` section | `scenarios.personas` in `pikku.config.json` |
179
+ | a `personas/` section | `definePersonas()` in the project's own code |
180
180
  | a `scenarios/` section | the gherkin block inside the slice it belongs to |
181
181
  | a `permissions/` section | a decision note under `decisions/security/` |
182
182
  | a list of tables, columns or routes | `pikku meta` — the generated schema _is_ the schema |