@pikku/cli 0.12.92 → 0.12.96

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.
Files changed (196) hide show
  1. package/console-app/assets/{index-DDpIMCpy.js → index-CeQodYfg.js} +151 -149
  2. package/console-app/assets/{index-D0HG8q0B.css → index-CyYHTGWZ.css} +1 -1
  3. package/console-app/index.html +2 -2
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.d.ts +13 -15
  8. package/dist/.pikku/cli/pikku-cli-channel.js +72 -3
  9. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +14 -4
  10. package/dist/.pikku/cli/pikku-cli-client.gen.js +20 -10
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  14. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +156 -2
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  20. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  21. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.d.ts +55 -20
  23. package/dist/.pikku/function/pikku-function-types.gen.js +27 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  25. package/dist/.pikku/function/pikku-functions-meta.gen.json +176 -13
  26. package/dist/.pikku/function/pikku-functions.gen.js +11 -1
  27. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  37. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +9 -0
  47. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
  48. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
  50. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
  51. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
  52. package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
  53. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  54. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  55. package/dist/.pikku/schemas/register.gen.js +21 -3
  56. package/dist/.pikku/schemas/schemas/KnowledgeIndexInput.schema.json +1 -0
  57. package/dist/.pikku/schemas/schemas/KnowledgeIndexOutput.schema.json +1 -0
  58. package/dist/.pikku/schemas/schemas/KnowledgeValidateInput.schema.json +1 -0
  59. package/dist/.pikku/schemas/schemas/KnowledgeValidateOutput.schema.json +1 -0
  60. package/dist/.pikku/schemas/schemas/PersonaRunInput.schema.json +1 -0
  61. package/dist/.pikku/schemas/schemas/PersonaSyncInput.schema.json +1 -0
  62. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  63. package/dist/.pikku/schemas/schemas/PikkuPersonasInput.schema.json +1 -0
  64. package/dist/.pikku/schemas/schemas/PikkuRolesInput.schema.json +1 -0
  65. package/dist/.pikku/schemas/schemas/RolesAuditInput.schema.json +1 -0
  66. package/dist/.pikku/schemas/schemas/RolesPruneInput.schema.json +1 -0
  67. package/dist/.pikku/scopes/pikku-personas-meta.gen.json +1 -0
  68. package/dist/.pikku/scopes/pikku-personas.gen.d.ts +36 -0
  69. package/dist/.pikku/scopes/pikku-personas.gen.js +17 -0
  70. package/dist/.pikku/scopes/pikku-roles-meta.gen.json +1 -0
  71. package/dist/.pikku/scopes/pikku-roles.gen.d.ts +30 -0
  72. package/dist/.pikku/scopes/pikku-roles.gen.js +18 -0
  73. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +4 -2
  74. package/dist/.pikku/scopes/pikku-scope-types.gen.js +3 -2
  75. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  76. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +2 -2
  77. package/dist/.pikku/secrets/pikku-secret-types.gen.js +2 -2
  78. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  79. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  80. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  81. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  82. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +2 -2
  83. package/dist/.pikku/variables/pikku-variable-types.gen.js +2 -2
  84. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  85. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  86. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +27 -3
  87. package/dist/.pikku/workflow/pikku-personas.gen.d.ts +15 -0
  88. package/dist/.pikku/workflow/pikku-personas.gen.js +17 -0
  89. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +96 -19
  90. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +22 -2
  91. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  92. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  93. package/dist/bin/pikku-bin.mjs +2 -2
  94. package/dist/bin/pikku.js +0 -0
  95. package/dist/src/cli.wiring.js +124 -2
  96. package/dist/src/deploy/analyzer/analyzer.js +14 -0
  97. package/dist/src/deploy/analyzer/manifest.d.ts +11 -0
  98. package/dist/src/fabric/functions/validate.function.js +20 -11
  99. package/dist/src/functions/commands/bootstrap.js +3 -0
  100. package/dist/src/functions/commands/dev-ai-runner.d.ts +16 -4
  101. package/dist/src/functions/commands/dev-ai-runner.js +63 -29
  102. package/dist/src/functions/commands/dev.js +9 -4
  103. package/dist/src/functions/commands/enable.js +15 -19
  104. package/dist/src/functions/commands/{scenario-environment.d.ts → environment.d.ts} +9 -6
  105. package/dist/src/functions/commands/{scenario-environment.js → environment.js} +5 -4
  106. package/dist/src/functions/commands/knowledge-index.d.ts +30 -0
  107. package/dist/src/functions/commands/knowledge-index.js +11 -0
  108. package/dist/src/functions/commands/knowledge-validate.d.ts +33 -0
  109. package/dist/src/functions/commands/knowledge-validate.js +11 -0
  110. package/dist/src/functions/commands/new-addon.d.ts +18 -0
  111. package/dist/src/functions/commands/new-addon.js +52 -36
  112. package/dist/src/functions/commands/persona-sync.d.ts +33 -0
  113. package/dist/src/functions/commands/persona-sync.js +151 -0
  114. package/dist/src/functions/commands/persona.d.ts +58 -0
  115. package/dist/src/functions/commands/persona.js +217 -0
  116. package/dist/src/functions/commands/roles-audit.d.ts +1 -0
  117. package/dist/src/functions/commands/roles-audit.js +37 -0
  118. package/dist/src/functions/commands/roles-prune.d.ts +16 -0
  119. package/dist/src/functions/commands/roles-prune.js +52 -0
  120. package/dist/src/functions/commands/roles-shared.d.ts +40 -0
  121. package/dist/src/functions/commands/roles-shared.js +77 -0
  122. package/dist/src/functions/commands/scenario-browser.d.ts +3 -3
  123. package/dist/src/functions/commands/scenario-browser.js +1 -1
  124. package/dist/src/functions/commands/scenario-ladder.d.ts +44 -2
  125. package/dist/src/functions/commands/scenario-ladder.js +51 -2
  126. package/dist/src/functions/commands/scenario.d.ts +7 -0
  127. package/dist/src/functions/commands/scenario.js +79 -17
  128. package/dist/src/functions/commands/scopes-shared.d.ts +8 -0
  129. package/dist/src/functions/commands/scopes-shared.js +1 -1
  130. package/dist/src/functions/commands/serve.js +7 -3
  131. package/dist/src/functions/commands/virtual-user-formatter.d.ts +30 -0
  132. package/dist/src/functions/commands/virtual-user-formatter.js +65 -0
  133. package/dist/src/functions/db/local-db.js +38 -1
  134. package/dist/src/functions/knowledge/render.d.ts +3 -0
  135. package/dist/src/functions/knowledge/render.js +66 -0
  136. package/dist/src/functions/knowledge/schemas.d.ts +51 -0
  137. package/dist/src/functions/knowledge/schemas.js +25 -0
  138. package/dist/src/functions/validate/workspace-validate.js +1 -1
  139. package/dist/src/functions/wirings/ai-agent/pikku-command-public-agent.js +2 -1
  140. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +1 -0
  141. package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +2 -2
  142. package/dist/src/functions/wirings/auth/serialize-auth-gen.js +8 -8
  143. package/dist/src/functions/wirings/cli/serialize-channel-cli-client.d.ts +7 -3
  144. package/dist/src/functions/wirings/cli/serialize-channel-cli-client.js +32 -18
  145. package/dist/src/functions/wirings/cli/serialize-channel-cli.js +63 -6
  146. package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -7
  147. package/dist/src/functions/wirings/credentials/serialize-credentials-types.js +8 -1
  148. package/dist/src/functions/wirings/functions/serialize-function-types.js +88 -28
  149. package/dist/src/functions/wirings/middleware/serialize-middleware-imports.js +25 -9
  150. package/dist/src/functions/wirings/package/serialize-package-types.js +5 -3
  151. package/dist/src/functions/wirings/personas/pikku-command-personas.d.ts +7 -0
  152. package/dist/src/functions/wirings/personas/pikku-command-personas.js +43 -0
  153. package/dist/src/functions/wirings/personas/serialize-personas-types.d.ts +18 -0
  154. package/dist/src/functions/wirings/personas/serialize-personas-types.js +76 -0
  155. package/dist/src/functions/wirings/realtime/pikku-command-events-scaffold.js +2 -1
  156. package/dist/src/functions/wirings/roles/pikku-command-roles.d.ts +7 -0
  157. package/dist/src/functions/wirings/roles/pikku-command-roles.js +33 -0
  158. package/dist/src/functions/wirings/roles/serialize-roles-types.d.ts +14 -0
  159. package/dist/src/functions/wirings/roles/serialize-roles-types.js +55 -0
  160. package/dist/src/functions/wirings/rpc/pikku-command-public-rpc.js +2 -1
  161. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +3 -3
  162. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +3 -3
  163. package/dist/src/functions/wirings/scopes/serialize-scopes-types.d.ts +1 -1
  164. package/dist/src/functions/wirings/scopes/serialize-scopes-types.js +1 -1
  165. package/dist/src/functions/wirings/secrets/serialize-secrets-types.js +10 -3
  166. package/dist/src/functions/wirings/user-admin/pikku-command-user-admin-functions.js +2 -1
  167. package/dist/src/functions/wirings/user-admin/serialize-user-admin-functions.js +2 -2
  168. package/dist/src/functions/wirings/variables/serialize-variables-types.js +8 -1
  169. package/dist/src/functions/wirings/workflow/pikku-command-workflow-routes.js +2 -1
  170. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +13 -13
  171. package/dist/src/functions/wirings/workflow/serialize-personas.d.ts +10 -0
  172. package/dist/src/functions/wirings/workflow/serialize-personas.js +46 -0
  173. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +11 -2
  174. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +165 -29
  175. package/dist/src/functions/workflows/all.workflow.js +7 -1
  176. package/dist/src/server/content-signing-jwt.d.ts +13 -0
  177. package/dist/src/server/content-signing-jwt.js +51 -0
  178. package/dist/src/server/dev-server-runner.interface.d.ts +10 -2
  179. package/dist/src/server/node-server-runner.d.ts +2 -2
  180. package/dist/src/server/node-server-runner.js +2 -1
  181. package/dist/src/services.d.ts +3 -1
  182. package/dist/src/services.js +2 -1
  183. package/dist/src/utils/pikku-cli-config.js +45 -2
  184. package/dist/src/utils/resolve-personas.d.ts +32 -0
  185. package/dist/src/utils/resolve-personas.js +46 -0
  186. package/dist/src/utils/resolve-scaffold-feature.d.ts +19 -0
  187. package/dist/src/utils/resolve-scaffold-feature.js +49 -0
  188. package/dist/tsconfig.tsbuildinfo +1 -1
  189. package/package.json +17 -13
  190. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +0 -1
  191. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +0 -19
  192. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +0 -17
  193. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +0 -2
  194. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +0 -40
  195. package/dist/src/utils/resolve-scenario-actors.d.ts +0 -23
  196. package/dist/src/utils/resolve-scenario-actors.js +0 -74
@@ -1,4 +1,4 @@
1
- export const serializeWorkflowTypes = (functionTypesImportPath, rpcMapImportPath, workflowMapImportPath, agentMapImportPath, scopesImportPath, scenarioStepMapImportPath = './pikku-scenario-step-map.gen.js', scenarioActorsImportPath = './pikku-scenario-actors.gen.js') => {
1
+ export const serializeWorkflowTypes = (functionTypesImportPath, rpcMapImportPath, workflowMapImportPath, agentMapImportPath, scopesImportPath, scenarioStepMapImportPath = './pikku-scenario-step-map.gen.js', scenarioActorsImportPath = './pikku-personas.gen.js') => {
2
2
  return `import { WorkflowCancelledException } from '@pikku/core/workflow'
3
3
  import { template } from '@pikku/core/workflow'
4
4
  import {
@@ -14,9 +14,9 @@ import type { FlattenedRPCMap } from '${rpcMapImportPath}'
14
14
  import type { FlattenedWorkflowMap } from '${workflowMapImportPath}'
15
15
  import type { AgentMap as FlattenedAgentMap } from '${agentMapImportPath}'
16
16
  import type { FlattenedScenarioStepMap } from '${scenarioStepMapImportPath}'
17
- import type { TypedScenarioActors } from '${scenarioActorsImportPath}'
17
+ import type { TypedPersonas } from '${scenarioActorsImportPath}'
18
18
 
19
- export type { TypedScenarioActors }
19
+ export type { TypedPersonas }
20
20
 
21
21
  export { template }
22
22
 
@@ -308,16 +308,26 @@ export function pikkuFeature<const Scenarios extends readonly unknown[]>(
308
308
  return config
309
309
  }
310
310
 
311
+ /**
312
+ * One surface's implementation of a step.
313
+ *
314
+ * Each binding is typed independently, so a browser binding gets a non-optional
315
+ * \`wire.browser\` and a cli binding a non-optional \`wire.cli\` without either
316
+ * leaking into the other.
317
+ */
311
318
  export type PikkuFunctionScenarioStep<
312
319
  In = unknown,
313
320
  Out = never,
314
- B extends boolean = false
315
- > = PikkuFunctionSessionless<In, Out, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep'>
321
+ Surface extends 'browser' | 'cli' | 'default' = 'default'
322
+ > = PikkuFunctionSessionless<
323
+ In,
324
+ Out,
325
+ Surface extends 'default' ? 'scenarioStep' : 'scenarioStep' | Surface
326
+ >
316
327
 
317
328
  export type PikkuScenarioStepConfigWithSchema<
318
329
  InputSchema extends StandardSchemaV1 | undefined = undefined,
319
- OutputSchema extends StandardSchemaV1 | undefined = undefined,
320
- B extends boolean = false
330
+ OutputSchema extends StandardSchemaV1 | undefined = undefined
321
331
  > = {
322
332
  /** Registered name — this is the string \`scenario.step()\` references. */
323
333
  name: string
@@ -336,49 +346,175 @@ export type PikkuScenarioStepConfigWithSchema<
336
346
  * sentence per call site.
337
347
  */
338
348
  template?: string
339
- /**
340
- * This step drives a browser. The runner provisions one for the step's actor
341
- * before calling it — which also makes an actor mandatory — and \`wire.browser\`
342
- * is non-optional inside \`func\`.
343
- */
344
- browser?: B
345
349
  title?: string
346
350
  tags?: string[]
347
351
  input?: InputSchema
348
352
  output?: OutputSchema
349
353
  errors?: Array<typeof PikkuError>
350
- func: PikkuFunctionScenarioStep<
354
+ /**
355
+ * Drive this step through a real browser, as a human would. The runner
356
+ * provisions a window for the step's actor first — which makes an actor
357
+ * mandatory — and \`wire.browser\` is non-optional inside it.
358
+ *
359
+ * Compose shared utilities here rather than writing raw clicks: the step says
360
+ * what the actor is doing, the utilities say how.
361
+ */
362
+ browser?: PikkuFunctionScenarioStep<
363
+ InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown,
364
+ OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown,
365
+ 'browser'
366
+ >
367
+ /** Drive this step remotely, over the websocket. */
368
+ cli?: PikkuFunctionScenarioStep<
351
369
  InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown,
352
370
  OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown,
353
- B
371
+ 'cli'
372
+ >
373
+ /**
374
+ * The server-side path — the fast suite, and the floor every other surface
375
+ * falls back to.
376
+ *
377
+ * On a \`then\`, this is not an alternative to the other bindings but a second
378
+ * **witness**: the run executes every binding it has and fails if they
379
+ * disagree, because "the row says paid" and "the page says paid" are different
380
+ * claims and the gap between them is the bug nobody catches.
381
+ */
382
+ default?: PikkuFunctionScenarioStep<
383
+ InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown,
384
+ OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown,
385
+ 'default'
354
386
  >
355
387
  }
356
388
 
357
- export type PikkuScenarioStepConfig<In, Out, B extends boolean = false> =
358
- Omit<PikkuScenarioStepConfigWithSchema<undefined, undefined, B>, 'func' | 'input' | 'output'> & {
359
- func: PikkuFunctionScenarioStep<In, Out, B>
389
+ export type PikkuScenarioStepConfig<In, Out> =
390
+ Omit<PikkuScenarioStepConfigWithSchema<undefined, undefined>, 'browser' | 'cli' | 'default' | 'input' | 'output'> & {
391
+ browser?: PikkuFunctionScenarioStep<In, Out, 'browser'>
392
+ cli?: PikkuFunctionScenarioStep<In, Out, 'cli'>
393
+ default?: PikkuFunctionScenarioStep<In, Out, 'default'>
360
394
  }
361
395
 
362
396
  /**
363
- * A named, reusable scenario step. Unlike \`scenario.do\`, which can only name an
364
- * RPC, a step's body is an ordinary pikku function — so it may drive a browser,
365
- * invoke RPCs as its actor, or run a workflow.
397
+ * A named, reusable scenario step, declaring one implementation per surface an
398
+ * actor can drive it through.
399
+ *
400
+ * The step's identity is what the actor is trying to do; which surface carries
401
+ * it out is a binding. That is what lets one ladder run through a real browser,
402
+ * over the websocket, or entirely server-side — and what makes "how much of this
403
+ * flow can a human actually reach" a number rather than a guess.
366
404
  *
367
405
  * Steps are deliberately NOT registered as RPCs: a browser-driving step must
368
406
  * never be network-callable.
369
407
  */
370
408
  export function pikkuScenarioStep<
371
409
  InputSchema extends StandardSchemaV1 | undefined = undefined,
372
- OutputSchema extends StandardSchemaV1 | undefined = undefined,
373
- B extends boolean = false
410
+ OutputSchema extends StandardSchemaV1 | undefined = undefined
374
411
  >(
375
- config: PikkuScenarioStepConfigWithSchema<InputSchema, OutputSchema, B>
376
- ): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep', PikkuFunctionScenarioStep<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, B>, InputSchema, OutputSchema>
377
- export function pikkuScenarioStep<In, Out = unknown, B extends boolean = false>(
378
- config: PikkuScenarioStepConfig<In, Out, B>
379
- ): PikkuFunctionConfig<In, Out, B extends true ? 'scenarioStep' | 'browser' : 'scenarioStep'>
412
+ config: PikkuScenarioStepConfigWithSchema<InputSchema, OutputSchema>
413
+ ): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'scenarioStep' | 'browser' | 'cli', PikkuFunctionScenarioStep<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'browser' | 'cli' | 'default'>, InputSchema, OutputSchema>
414
+ export function pikkuScenarioStep<In, Out = unknown>(
415
+ config: PikkuScenarioStepConfig<In, Out>
416
+ ): PikkuFunctionConfig<In, Out, 'scenarioStep' | 'browser' | 'cli'>
380
417
  export function pikkuScenarioStep(config: any) {
381
- return config
418
+ const surfaces = (['browser', 'cli', 'default'] as const).filter(
419
+ (surface) => typeof config[surface] === 'function'
420
+ )
421
+ if (surfaces.length === 0) {
422
+ throw new Error(
423
+ \`[scenario] step '\${config.name}' declares no surface bindings. Add at least a \\\`default\\\` (server-side) implementation.\`
424
+ )
425
+ }
426
+ return {
427
+ ...config,
428
+ surfaces,
429
+ // The runner sets the surface on the wire and calls this once per binding it
430
+ // resolved — every witness of a \`then\`, exactly one for an action step.
431
+ func: (services: any, data: any, wire: any) => {
432
+ const surface = wire?.scenarioStep?.surface ?? 'default'
433
+ const binding = config[surface] ?? config.default
434
+ return binding(services, data, wire)
435
+ },
436
+ }
437
+ }
438
+
439
+ /**
440
+ * What a step declares when nobody drives it: one implementation, and no
441
+ * surfaces to choose between.
442
+ *
443
+ * \`func\` rather than \`default\` is the point. \`default\` means *the fallback when
444
+ * no other surface applies*, which implies others could exist; \`func\` says
445
+ * structurally that there is one way this happens. That also keeps the phase
446
+ * rule coherent — an assertion runs every witness it has and fails if they
447
+ * disagree, and this has exactly one by construction.
448
+ */
449
+ export type PikkuSubjectScenarioStepConfigWithSchema<
450
+ InputSchema extends StandardSchemaV1 | undefined = undefined,
451
+ OutputSchema extends StandardSchemaV1 | undefined = undefined
452
+ > = Omit<
453
+ PikkuScenarioStepConfigWithSchema<InputSchema, OutputSchema>,
454
+ 'browser' | 'cli' | 'default'
455
+ > & {
456
+ func: PikkuFunctionScenarioStep<
457
+ InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown,
458
+ OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown,
459
+ 'default'
460
+ >
461
+ }
462
+
463
+ export type PikkuSubjectScenarioStepConfig<In, Out> =
464
+ Omit<PikkuScenarioStepConfig<In, Out>, 'browser' | 'cli' | 'default'> & {
465
+ func: PikkuFunctionScenarioStep<In, Out, 'default'>
466
+ }
467
+
468
+ /**
469
+ * A step in which the app acts on itself — "Given the platform has expired the
470
+ * trial".
471
+ *
472
+ * The grammatical subject of that sentence is not a user of your app; it **is**
473
+ * your app, which is why it is its own declaration rather than a persona with an
474
+ * asterisk. A persona is a person.
475
+ *
476
+ * Local-test-only, and never in a virtual user's catalogue: a virtual user that
477
+ * could expire its own trial is manufacturing the outcome it exists to discover.
478
+ */
479
+ export function pikkuPlatformScenarioStep<
480
+ InputSchema extends StandardSchemaV1 | undefined = undefined,
481
+ OutputSchema extends StandardSchemaV1 | undefined = undefined
482
+ >(
483
+ config: PikkuSubjectScenarioStepConfigWithSchema<InputSchema, OutputSchema>
484
+ ): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'scenarioStep', PikkuFunctionScenarioStep<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'default'>, InputSchema, OutputSchema>
485
+ export function pikkuPlatformScenarioStep<In, Out = unknown>(
486
+ config: PikkuSubjectScenarioStepConfig<In, Out>
487
+ ): PikkuFunctionConfig<In, Out, 'scenarioStep'>
488
+ export function pikkuPlatformScenarioStep(config: any) {
489
+ return { ...config, surfaces: ['default'] }
490
+ }
491
+
492
+ /**
493
+ * A step in which a third-party system acts — "Given Stripe's webhook arrives",
494
+ * "When Mailgun bounces it".
495
+ *
496
+ * \`addon\` names the addon that wraps that service, the same name its
497
+ * \`wireAddon\` declares. These steps *are* the mock its consumers currently
498
+ * hand-write: shipped by the addon author, maintained with the addon, and the
499
+ * same artifact that appears in the prose.
500
+ *
501
+ * Note that arranging and asserting are different — "Stripe's webhook arrives"
502
+ * stubs, "Then Stripe was charged" asserts, and only the first is a stub.
503
+ *
504
+ * Local-test-only, and never in a virtual user's catalogue: one that could
505
+ * invoke this would forge its own payment success.
506
+ */
507
+ export function pikkuAddonScenarioStep<
508
+ InputSchema extends StandardSchemaV1 | undefined = undefined,
509
+ OutputSchema extends StandardSchemaV1 | undefined = undefined
510
+ >(
511
+ config: PikkuSubjectScenarioStepConfigWithSchema<InputSchema, OutputSchema> & { addon: string }
512
+ ): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'scenarioStep', PikkuFunctionScenarioStep<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'default'>, InputSchema, OutputSchema>
513
+ export function pikkuAddonScenarioStep<In, Out = unknown>(
514
+ config: PikkuSubjectScenarioStepConfig<In, Out> & { addon: string }
515
+ ): PikkuFunctionConfig<In, Out, 'scenarioStep'>
516
+ export function pikkuAddonScenarioStep(config: any) {
517
+ return { ...config, surfaces: ['default'] }
382
518
  }
383
519
 
384
520
  type TypedRef<T> = { $ref: string; path?: string } & { __phantomType?: T }
@@ -53,6 +53,10 @@ export const allWorkflow = pikkuWorkflowComplexFunc({
53
53
  // schemas, for one — fails until these two files exist.
54
54
  await workflow.do('Bootstrap scope definition types', 'pikkuScopeDefinitionTypes', null);
55
55
  await workflow.do('Bootstrap scopes', 'pikkuScopes', { bootstrap: true });
56
+ await workflow.do('Bootstrap roles', 'pikkuRoles', { bootstrap: true });
57
+ await workflow.do('Bootstrap personas', 'pikkuPersonas', {
58
+ bootstrap: true,
59
+ });
56
60
  await workflow.do('Bootstrap function types split', 'pikkuFunctionTypesSplit', { bootstrap: true });
57
61
  // Pre-write a stub auth.types.ts (if the project uses better-auth) so the
58
62
  // pikkuBetterAuth re-export resolves before any user file is imported.
@@ -155,6 +159,8 @@ export const allWorkflow = pikkuWorkflowComplexFunc({
155
159
  workflow.do('Credentials', 'pikkuCredentials', null),
156
160
  workflow.do('Scope definition types', 'pikkuScopeDefinitionTypes', null),
157
161
  workflow.do('Scopes', 'pikkuScopes', {}),
162
+ workflow.do('Roles', 'pikkuRoles', {}),
163
+ workflow.do('Personas', 'pikkuPersonas', {}),
158
164
  workflow.do('Variable definition types', 'pikkuVariableDefinitionTypes', null),
159
165
  workflow.do('Variables', 'pikkuVariables', null),
160
166
  workflow.do('Addon types', 'pikkuAddonTypes', null),
@@ -165,7 +171,7 @@ export const allWorkflow = pikkuWorkflowComplexFunc({
165
171
  if (agents || !config.addon) {
166
172
  await workflow.do('Re-inspect after agents', async () => getInspectorState(true));
167
173
  }
168
- // pikkuAuth generates auth-secrets.gen.ts (with wireSecret calls) inside the
174
+ // pikkuAuth generates auth-secrets.gen.ts (with defineSecret calls) inside the
169
175
  // Promise.all above, but pikkuSecrets runs concurrently from the pre-auth
170
176
  // inspector state. Re-run secret/credential/variable codegen now so that the
171
177
  // freshly-generated auth-secrets.gen.ts (just picked up by Re-inspect above)
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `pikku dev` and `pikku serve` construct the local content service themselves,
3
+ * so they must supply the key its signed asset URLs are signed and verified
4
+ * with — a project is not required to wire a JWTService just to serve uploads
5
+ * in development.
6
+ *
7
+ * The key is random and per-process: URLs signed by one dev server mean
8
+ * nothing to the next. That is the right lifetime for a development secret,
9
+ * and it lets the runtime reject any signature it cannot verify instead of
10
+ * waving through requests when no key is configured.
11
+ */
12
+ import type { JWTService } from '@pikku/core/services';
13
+ export declare const createEphemeralContentSigningJWT: () => JWTService;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `pikku dev` and `pikku serve` construct the local content service themselves,
3
+ * so they must supply the key its signed asset URLs are signed and verified
4
+ * with — a project is not required to wire a JWTService just to serve uploads
5
+ * in development.
6
+ *
7
+ * The key is random and per-process: URLs signed by one dev server mean
8
+ * nothing to the next. That is the right lifetime for a development secret,
9
+ * and it lets the runtime reject any signature it cannot verify instead of
10
+ * waving through requests when no key is configured.
11
+ */
12
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
13
+ import { getRelativeTimeOffsetFromNow } from '@pikku/core';
14
+ export const createEphemeralContentSigningJWT = () => {
15
+ const key = randomBytes(32);
16
+ const sign = (body) => createHmac('sha256', key).update(body).digest('base64url');
17
+ return {
18
+ encode: async (expiresIn, payload) => {
19
+ const envelope = {
20
+ exp: getRelativeTimeOffsetFromNow(expiresIn).getTime(),
21
+ payload,
22
+ };
23
+ const body = Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64url');
24
+ return `${body}.${sign(body)}`;
25
+ },
26
+ decode: async (hash, invalidHashError) => {
27
+ const invalid = invalidHashError ?? new Error('Invalid content signature');
28
+ const [body, signature] = hash.split('.');
29
+ if (!body || !signature) {
30
+ throw invalid;
31
+ }
32
+ const expected = Buffer.from(sign(body), 'utf8');
33
+ const actual = Buffer.from(signature, 'utf8');
34
+ if (expected.length !== actual.length ||
35
+ !timingSafeEqual(expected, actual)) {
36
+ throw invalid;
37
+ }
38
+ let envelope;
39
+ try {
40
+ envelope = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
41
+ }
42
+ catch {
43
+ throw invalid;
44
+ }
45
+ if (typeof envelope.exp !== 'number' || Date.now() > envelope.exp) {
46
+ throw invalid;
47
+ }
48
+ return envelope.payload;
49
+ },
50
+ };
51
+ };
@@ -9,7 +9,7 @@
9
9
  * `services.ts` and injected — no `typeof Bun` checks in the dev command.
10
10
  */
11
11
  import type { EventHubService } from '@pikku/core';
12
- import type { Logger } from '@pikku/core/services';
12
+ import type { JWTService, Logger } from '@pikku/core/services';
13
13
  import type { NodeHTTPServerConfig } from '@pikku/node-http-server';
14
14
  export interface DevServerInstance {
15
15
  init(): Promise<void>;
@@ -19,6 +19,14 @@ export interface DevServerInstance {
19
19
  }
20
20
  /** Config shape both runtimes accept (Core config + host/port/content). */
21
21
  export type DevServerConfig = NodeHTTPServerConfig;
22
+ export type DevServerOptions = {
23
+ /**
24
+ * The JWTService the dev server's content service signs asset URLs with.
25
+ * The transport verifies signed asset reads with it, so it must be the same
26
+ * instance — otherwise every signed URL is rejected.
27
+ */
28
+ contentSigningJWT?: JWTService;
29
+ };
22
30
  export interface DevServerRunner {
23
31
  /**
24
32
  * Create the EventHub for this runtime. Shared into singleton services so
@@ -27,5 +35,5 @@ export interface DevServerRunner {
27
35
  */
28
36
  createEventHub(): Promise<EventHubService<any>>;
29
37
  /** Create the dev HTTP/WS server. Must be called after `createEventHub`. */
30
- createServer(config: DevServerConfig, logger: Logger): DevServerInstance;
38
+ createServer(config: DevServerConfig, logger: Logger, options?: DevServerOptions): DevServerInstance;
31
39
  }
@@ -5,8 +5,8 @@
5
5
  */
6
6
  import type { EventHubService } from '@pikku/core';
7
7
  import type { Logger } from '@pikku/core/services';
8
- import type { DevServerRunner, DevServerInstance, DevServerConfig } from './dev-server-runner.interface.js';
8
+ import type { DevServerRunner, DevServerInstance, DevServerConfig, DevServerOptions } from './dev-server-runner.interface.js';
9
9
  export declare class NodeServerRunner implements DevServerRunner {
10
10
  createEventHub(): Promise<EventHubService<any>>;
11
- createServer(config: DevServerConfig, logger: Logger): DevServerInstance;
11
+ createServer(config: DevServerConfig, logger: Logger, options?: DevServerOptions): DevServerInstance;
12
12
  }
@@ -11,12 +11,13 @@ export class NodeServerRunner {
11
11
  async createEventHub() {
12
12
  return new LocalEventHubService();
13
13
  }
14
- createServer(config, logger) {
14
+ createServer(config, logger, options = {}) {
15
15
  const wss = new WebSocketServer({ noServer: true });
16
16
  const server = new PikkuNodeHTTPServer(config, logger, {
17
17
  configureServer: (httpServer) => {
18
18
  pikkuWebsocketHandler({ server: httpServer, wss, logger });
19
19
  },
20
+ contentSigningJWT: options.contentSigningJWT,
20
21
  });
21
22
  return {
22
23
  init: () => server.init(),
@@ -12,7 +12,9 @@ export declare function processDiagnostics(diagnostics: InspectorDiagnostic[], l
12
12
  */
13
13
  export declare const defaultCLIRenderer: import("@pikku/core/cli").CorePikkuCLIRender<ForwardedLogMessage, import("@pikku/core").CoreSingletonServices<{
14
14
  logLevel?: LogLevel;
15
- secrets?: {};
15
+ secrets?: {
16
+ requireAllowedHosts?: boolean;
17
+ };
16
18
  workflow?: import("@pikku/core/workflow").WorkflowServiceConfig;
17
19
  webhook?: import("@pikku/core/services").WebhookServiceConfig;
18
20
  postgres?: import("@pikku/core").PostgresConfig;
@@ -216,7 +216,7 @@ export const createSingletonServices = async (config) => {
216
216
  config.publicRpcFile,
217
217
  config.publicAgentFile,
218
218
  // The auth scaffold (catch-all routes + session middleware) and its
219
- // sibling secrets file (wireSecret per provider) are generated into the
219
+ // sibling secrets file (defineSecret per provider) are generated into the
220
220
  // scaffold dir, which may live outside srcDirectories (e.g. a project's
221
221
  // `pikku/` dir). Add them explicitly so their wirings are inspected.
222
222
  config.authFile,
@@ -259,6 +259,7 @@ export const createSingletonServices = async (config) => {
259
259
  // data-classification leaks. Off by default — it's the dominant codegen
260
260
  // cost. Never runs during a plain `pikku all`.
261
261
  classificationCheck: !setupOnly && !!config.security,
262
+ strictMeta: !!config.strictMeta,
262
263
  schemaConfig: !setupOnly
263
264
  ? {
264
265
  tsconfig: config.tsconfig,
@@ -1,5 +1,6 @@
1
1
  import { join, dirname, resolve, isAbsolute, parse as parsePath } from 'path';
2
2
  import { readdir, readFile } from 'fs/promises';
3
+ import { resolveScaffoldFeature } from './resolve-scaffold-feature.js';
3
4
  const CLIENT_FILE_KEYS = [
4
5
  'fetchFile',
5
6
  'websocketFile',
@@ -231,6 +232,32 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
231
232
  const resolvedScaffoldDir = isAbsolute(scaffoldDir)
232
233
  ? scaffoldDir
233
234
  : join(result.rootDir, scaffoldDir);
235
+ // Read every scaffold feature once, here, so a legacy 'auth'/'no-auth'
236
+ // fails at load with the migration named, rather than downstream where the
237
+ // value has already been coerced into something plausible.
238
+ //
239
+ // An explicit `path` is written onto the feature's output field before the
240
+ // derivations below run. Each of those is guarded by `!result.<field>`, so
241
+ // setting it here wins and the pikkuDir-derived default is skipped.
242
+ const SCAFFOLD_OUTPUT_FIELDS = {
243
+ rpc: 'publicRpcFile',
244
+ agent: 'publicAgentFile',
245
+ console: 'consoleFunctionsFile',
246
+ scenarios: 'scenariosFunctionsFile',
247
+ userAdmin: 'userAdminFunctionsFile',
248
+ workflow: 'workflowRoutesFile',
249
+ events: 'eventsChannelFile',
250
+ remoteRpc: 'remoteRpcWorkersFile',
251
+ };
252
+ const scaffoldBlock = result.scaffold;
253
+ for (const [feature, outputField] of Object.entries(SCAFFOLD_OUTPUT_FIELDS)) {
254
+ const resolved = resolveScaffoldFeature(feature, scaffoldBlock?.[feature]);
255
+ if (!resolved.enabled || !resolved.path)
256
+ continue;
257
+ result[outputField] = isAbsolute(resolved.path)
258
+ ? resolved.path
259
+ : join(result.rootDir, resolved.path);
260
+ }
234
261
  if (result.scaffold?.remoteRpc && !result.remoteRpcWorkersFile) {
235
262
  result.remoteRpcWorkersFile = join(resolvedScaffoldDir, 'rpc', 'rpc-remote.gen.ts');
236
263
  }
@@ -373,8 +400,8 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
373
400
  if (!result.workflowMetaDir) {
374
401
  result.workflowMetaDir = join(workflowDir, 'meta');
375
402
  }
376
- if (!result.scenarioActorsFile) {
377
- result.scenarioActorsFile = join(workflowDir, 'pikku-scenario-actors.gen.ts');
403
+ if (!result.personasWiringFile) {
404
+ result.personasWiringFile = join(workflowDir, 'pikku-personas.gen.ts');
378
405
  }
379
406
  // Scenarios
380
407
  const scenarioDir = join(result.outDir, 'scenarios');
@@ -527,6 +554,22 @@ const _getPikkuCLIConfig = async (logger, configFile = undefined, requiredFields
527
554
  if (!result.scopesMetaJsonFile) {
528
555
  result.scopesMetaJsonFile = join(scopesDir, 'pikku-scopes-meta.gen.json');
529
556
  }
557
+ // System roles (SystemRoleName union + declared role set). Kept beside the
558
+ // scopes they are composed from — a role is unreadable without them.
559
+ if (!result.rolesFile) {
560
+ result.rolesFile = join(scopesDir, 'pikku-roles.gen.ts');
561
+ }
562
+ if (!result.rolesMetaJsonFile) {
563
+ result.rolesMetaJsonFile = join(scopesDir, 'pikku-roles-meta.gen.json');
564
+ }
565
+ // Personas. Beside the roles they are checked against, for the same reason
566
+ // roles sit beside their scopes.
567
+ if (!result.personasFile) {
568
+ result.personasFile = join(scopesDir, 'pikku-personas.gen.ts');
569
+ }
570
+ if (!result.personasMetaJsonFile) {
571
+ result.personasMetaJsonFile = join(scopesDir, 'pikku-personas-meta.gen.json');
572
+ }
530
573
  // Variables (typed wrapper for VariablesService)
531
574
  const variablesDir = join(result.outDir, 'variables');
532
575
  if (!result.variableTypesFile) {
@@ -0,0 +1,32 @@
1
+ import type { PersonaDefinitions } from '@pikku/core/persona';
2
+ import type { ResolvedPersona } from '@pikku/core/services';
3
+ /**
4
+ * Where a persona's mail lands when the project has not said.
5
+ *
6
+ * Deliberately a domain nobody can own rather than something plausible: an app
7
+ * that never sends mail runs fine against it, and one that does fails loudly at
8
+ * the first send instead of quietly delivering a magic link to a stranger.
9
+ */
10
+ export declare const DEFAULT_PERSONA_EMAIL_DOMAIN = "personas.invalid";
11
+ /**
12
+ * The personas a run actually sees: every declaration, with its address filled
13
+ * in.
14
+ *
15
+ * Every consumer resolves through here rather than reading the definitions
16
+ * directly, because the address is the part nobody declares and everybody
17
+ * needs — codegen writes it into `personaConfigs`, the seed creates the user
18
+ * rows from it, and both `scenario run` and `persona run` sign in with it.
19
+ * Computing it in three places is how two of them end up disagreeing.
20
+ */
21
+ export declare const resolvePersonas: (definitions: PersonaDefinitions, domain?: string, runId?: string) => Record<string, ResolvedPersona>;
22
+ /**
23
+ * The scopes a persona holds, resolved through its roles.
24
+ *
25
+ * Roles are the only thing a persona declares; scopes are what a function
26
+ * checks. Narrowing a virtual user's catalogue needs the second, so the
27
+ * expansion happens once, here, against the same `defineSystemRole` definitions
28
+ * the seed grants from.
29
+ */
30
+ export declare const personaScopes: (persona: {
31
+ roles?: readonly string[];
32
+ }, roleScopes: Record<string, readonly string[]>) => string[];
@@ -0,0 +1,46 @@
1
+ import { personaEmails, validateAndBuildPersonasMeta, } from '@pikku/core/persona';
2
+ /**
3
+ * Where a persona's mail lands when the project has not said.
4
+ *
5
+ * Deliberately a domain nobody can own rather than something plausible: an app
6
+ * that never sends mail runs fine against it, and one that does fails loudly at
7
+ * the first send instead of quietly delivering a magic link to a stranger.
8
+ */
9
+ export const DEFAULT_PERSONA_EMAIL_DOMAIN = 'personas.invalid';
10
+ /**
11
+ * The personas a run actually sees: every declaration, with its address filled
12
+ * in.
13
+ *
14
+ * Every consumer resolves through here rather than reading the definitions
15
+ * directly, because the address is the part nobody declares and everybody
16
+ * needs — codegen writes it into `personaConfigs`, the seed creates the user
17
+ * rows from it, and both `scenario run` and `persona run` sign in with it.
18
+ * Computing it in three places is how two of them end up disagreeing.
19
+ */
20
+ export const resolvePersonas = (definitions, domain = DEFAULT_PERSONA_EMAIL_DOMAIN, runId) => {
21
+ const meta = validateAndBuildPersonasMeta(definitions);
22
+ const ids = Object.keys(meta);
23
+ const emails = personaEmails(ids, domain, runId);
24
+ const resolved = {};
25
+ for (const id of ids) {
26
+ resolved[id] = { ...meta[id], email: emails[id] };
27
+ }
28
+ return resolved;
29
+ };
30
+ /**
31
+ * The scopes a persona holds, resolved through its roles.
32
+ *
33
+ * Roles are the only thing a persona declares; scopes are what a function
34
+ * checks. Narrowing a virtual user's catalogue needs the second, so the
35
+ * expansion happens once, here, against the same `defineSystemRole` definitions
36
+ * the seed grants from.
37
+ */
38
+ export const personaScopes = (persona, roleScopes) => {
39
+ const scopes = new Set();
40
+ for (const role of persona.roles ?? []) {
41
+ for (const scope of roleScopes[role] ?? []) {
42
+ scopes.add(scope);
43
+ }
44
+ }
45
+ return [...scopes].sort();
46
+ };
@@ -0,0 +1,19 @@
1
+ import type { PikkuScaffoldFeature } from '../../types/config.js';
2
+ export type ResolvedScaffoldFeature = {
3
+ /** Whether to generate this surface at all. */
4
+ enabled: boolean;
5
+ /** Whether the generated surface requires a session. */
6
+ auth: boolean;
7
+ /** An explicit output path, or undefined to derive one from `pikkuDir`. */
8
+ path?: string;
9
+ };
10
+ /**
11
+ * Reads one `scaffold.<feature>` value.
12
+ *
13
+ * `true` means enabled **and authenticated**. A surface only becomes public by
14
+ * writing `{ auth: false }`, so omitting the field can never open anything —
15
+ * the failure mode of a forgotten flag is a locked door, not an open one.
16
+ *
17
+ * knowledge: decisions/security/scaffold-features-are-authenticated-unless-opted-out.md
18
+ */
19
+ export declare const resolveScaffoldFeature: (feature: string, value: PikkuScaffoldFeature | undefined) => ResolvedScaffoldFeature;
@@ -0,0 +1,49 @@
1
+ const LEGACY_VALUES = new Set(['auth', 'no-auth']);
2
+ /**
3
+ * Reads one `scaffold.<feature>` value.
4
+ *
5
+ * `true` means enabled **and authenticated**. A surface only becomes public by
6
+ * writing `{ auth: false }`, so omitting the field can never open anything —
7
+ * the failure mode of a forgotten flag is a locked door, not an open one.
8
+ *
9
+ * knowledge: decisions/security/scaffold-features-are-authenticated-unless-opted-out.md
10
+ */
11
+ export const resolveScaffoldFeature = (feature, value) => {
12
+ if (value === undefined || value === false) {
13
+ return { enabled: false, auth: true };
14
+ }
15
+ if (typeof value === 'string') {
16
+ throw new Error(legacyValueMessage(feature, value));
17
+ }
18
+ if (value === true) {
19
+ return { enabled: true, auth: true };
20
+ }
21
+ if (typeof value !== 'object') {
22
+ throw new Error(`pikku.config.json: scaffold.${feature} must be true, false, or an object ` +
23
+ `like { "auth": false } — received ${JSON.stringify(value)}.`);
24
+ }
25
+ return {
26
+ enabled: true,
27
+ auth: value.auth !== false,
28
+ path: value.path,
29
+ };
30
+ };
31
+ /**
32
+ * A legacy `'auth'`/`'no-auth'` is refused by name rather than coerced. The
33
+ * value it would coerce to is exactly the one that caused the incident this
34
+ * change came out of, and under the new shape a bare string is a plausible
35
+ * path — so guessing would be wrong twice over.
36
+ */
37
+ const legacyValueMessage = (feature, value) => {
38
+ const migrated = value === 'no-auth' ? '{ "auth": false }' : LEGACY_VALUES.has(value) ? 'true' : null;
39
+ if (!migrated) {
40
+ return (`pikku.config.json: scaffold.${feature} must be true, false, or an object ` +
41
+ `like { "auth": false } — received the string ${JSON.stringify(value)}.`);
42
+ }
43
+ return (`pikku.config.json: scaffold.${feature} is ${JSON.stringify(value)}, which is no longer a ` +
44
+ `mode. Authentication is declared on the function or on the addon, and the ` +
45
+ `scaffold flag only says whether the surface exists.\n` +
46
+ ` Replace it with: "${feature}": ${migrated}\n` +
47
+ ` \`true\` enables the surface with a session required; ` +
48
+ `\`{ "auth": false }\` makes it public.`);
49
+ };