@pikku/inspector 0.12.44 → 0.12.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,129 @@
1
+ ## 0.12.46
2
+
3
+ ### Patch Changes
4
+
5
+ - a8f9a7d: Distinguish an unresolvable schema type from an unsupported schema library.
6
+
7
+ Both failures shared one message, so a plain `z.object({...})` whose type TypeScript
8
+ could not resolve — a file outside tsconfig `include`, or a generated file such as
9
+ `.pikku/db/zod.gen.ts` that had not been written yet — was reported as
10
+ "Ensure your schema is imported from a supported validation library". The schema is
11
+ dropped either way (`inputSchemaName: null`, no generated schema file), so the message
12
+ is the only signal that a function has silently lost its input contract, and that
13
+ advice is unactionable when the schema already is zod.
14
+
15
+ - eaabcbf: fix(inspector): don't reject plain destructuring in DSL workflows
16
+
17
+ `extractDestructuredDeclaration` reported "Destructuring a step result is not
18
+ supported in DSL workflows" for EVERY destructuring statement whose initializer
19
+ wasn't `await Promise.all([...])` — including ordinary local bindings that
20
+ involve no step at all, such as `const { runId } = input`.
21
+
22
+ Destructuring the workflow's own input is the single most idiomatic line in a
23
+ DSL workflow, so this rejected working workflows wholesale under a message
24
+ about step results that named nothing the author had written.
25
+
26
+ The diagnostic now fires only when the destructured initializer really is a
27
+ step (`await workflow.do(...)`, a parallel group, or a parallel fanout).
28
+ Anything else passes through as a non-step, exactly as the identifier path
29
+ already does for `const x = someLocal`.
30
+
31
+ - Updated dependencies [f11675f]
32
+ - @pikku/core@0.12.68
33
+
34
+ ## 0.12.45
35
+
36
+ ### Patch Changes
37
+
38
+ - ae4f59a: Gate admin capabilities on scopes, and scaffold user management
39
+
40
+ Admin capabilities were gated on `user.role === 'admin'` — a single text column
41
+ meaning "can do everything". Impersonating a user, rebinding a shared
42
+ credential and reading the user directory are distinct capabilities that one
43
+ user can hold independently, so they are now scopes on an `admin` tree:
44
+
45
+ | Gate | Scope |
46
+ | -------------------------------------- | ------------------------ |
47
+ | impersonation | `admin:impersonate` |
48
+ | `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
49
+ | reading the user directory | `admin:users:list` |
50
+ | creating a user out of band | `admin:users:create` |
51
+ | ban / unban | `admin:users:ban` |
52
+ | delete a user | `admin:users:remove` |
53
+ | revoke a user's sessions | `admin:users:sessions` |
54
+ | set a user's password | `admin:users:password` |
55
+
56
+ Holding the bare `admin` scope satisfies all of them via pikku's existing
57
+ parent-grant rule, so it is a one-for-one replacement for the old role.
58
+
59
+ better-auth's `admin()` plugin is still what implements ban, delete,
60
+ session-revocation and set-password, so it stays. Its `user.role` column is no
61
+ longer something pikku grants: it is _projected_ from the scope store when a
62
+ session is built, and only from the scopes whose capability better-auth's own
63
+ endpoints gate on the caller's role. Someone granted `admin:users:list` can read
64
+ the directory — which goes straight to the auth adapter — without gaining the
65
+ power to ban, and revoking a scope demotes the role on the next sign-in. Scopes
66
+ remain the single source of truth.
67
+
68
+ New `scaffold.userAdmin` in `pikku.config.json` generates the whole set —
69
+ `pikkuAdminListUsers`, `pikkuAdminCreateUser`, `pikkuAdminSetUserBanned`,
70
+ `pikkuAdminRemoveUser`, `pikkuAdminRevokeUserSessions` and
71
+ `pikkuAdminSetUserPassword` — into your project. Listing or banning a user is
72
+ ordinary application behaviour and must not require installing the console.
73
+ Codegen fails with an actionable error if better-auth is wired without
74
+ `admin()`. The console's Users page calls these same functions, showing each
75
+ action only where the caller holds its scope.
76
+
77
+ Every scaffold now emits a directory named for its domain — `scaffold/admin/`,
78
+ `scaffold/rpc/`, `scaffold/agent/`, `scaffold/auth/`, `scaffold/console/`,
79
+ `scaffold/graph/`, `scaffold/realtime/`, `scaffold/scenarios/`,
80
+ `scaffold/webhook/`, `scaffold/workflow/` — holding its wiring file beside a
81
+ `*.schemas.gen.ts` sibling, and every generated payload is a zod schema instead
82
+ of a TypeScript generic. The schemas have to stand alone: the inspector reads a
83
+ zod schema by importing the module that declares it, which it cannot do for a
84
+ wiring file whose relative pikku-types import per-unit deploy codegen rewrites.
85
+
86
+ Resolving a schema by reference rather than by name also fixes the agent HTTP
87
+ surface. `agentCaller` and `agentStreamCaller` take the same payload but had to
88
+ repeat the type literal verbatim in each generic position, because the extractor
89
+ synthesised the schema name from the _function_ name and so recorded an
90
+ `inputSchemaName` with no schema behind it whenever the two shared a named
91
+ alias — every agent call through that alias failed with `MissingSchemaError`.
92
+ One `AgentCall` schema now backs both.
93
+
94
+ Where a payload's shape belongs to `@pikku/core` (`WorkflowRunStatus`,
95
+ `FunctionCoverageReport`, `StubCall[]`) the generated function carries no
96
+ `output` schema and the inspector infers it from the handler's return type;
97
+ re-declaring a core type in zod would be a second definition free to drift.
98
+
99
+ Upgrading rewrites the layout in place: codegen prunes the pre-directory copy of
100
+ each scaffold file before it inspects the source tree, since the old flat file
101
+ still wires the same routes and leaving it behind would wire everything twice.
102
+
103
+ `@pikku/core` gains `hasScopes(required, held)`, the non-throwing counterpart to
104
+ `verifyScopes`, and declares `auth` on `CoreSingletonServices` — the auth
105
+ instance the generated `pikkuServices` wrapper already injected but never typed.
106
+ A scope root declared twice (an addon and its host both contributing the same
107
+ `admin` tree) now flattens to one entry per id instead of emitting it twice.
108
+
109
+ BREAKING: there is no role fallback for the scope-gated capabilities. An app
110
+ that relied on the old default must register a `ScopeService` and grant `admin`
111
+ (or a narrower `admin:*` scope). Every gate fails closed and warns when no
112
+ `ScopeService` is registered. `delegatedAuth`'s `defaultRole`/`mapRole` now
113
+ grant a pikku role through the `ScopeService` instead of writing better-auth's
114
+ `role` column, and the `credentialOAuth` platform user no longer sets `banned`.
115
+
116
+ BREAKING: the console reads its user directory over the scaffolded
117
+ `pikkuAdminListUsers` RPC (gated on `admin:users:list`, backed by better-auth's
118
+ `$context.adapter`) instead of `client.admin.listUsers`, and
119
+ `UsersTableUser`/`UsersTableLabels` no longer carry `role` — there is no role
120
+ column to render. `@pikku/addon-console` no longer ships a `console:listUsers`
121
+ function: user management is not the console's job, so a host that wants the
122
+ Users page must enable `scaffold.userAdmin`.
123
+
124
+ - Updated dependencies [ae4f59a]
125
+ - @pikku/core@0.12.67
126
+
1
127
  ## 0.12.44
2
128
 
3
129
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- import type * as ts from 'typescript';
1
+ import * as ts from 'typescript';
2
2
  import type { SchemaVendor, InspectorLogger } from '../types.js';
3
3
  /**
4
4
  * Detect the schema vendor by tracing the type back to its library origin.
@@ -1,4 +1,16 @@
1
+ import * as ts from 'typescript';
1
2
  import { ErrorCode } from '../error-codes.js';
3
+ /**
4
+ * Whether the checker could type the identifier at all.
5
+ *
6
+ * `any` here is not a schema written in an exotic library — it is TypeScript telling us
7
+ * it could not resolve the declaration, which happens when the schema comes from a file
8
+ * outside the program (not matched by tsconfig `include`) or from a generated file that
9
+ * has not been written yet (`.pikku/db/zod.gen.ts` before `pikku db generate` runs).
10
+ * The distinction matters because the two have opposite fixes, and conflating them told
11
+ * users to "import from a supported validation library" about a plain `z.object`.
12
+ */
13
+ const isUnresolved = (type) => (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
2
14
  /**
3
15
  * Detect the schema vendor by tracing the type back to its library origin.
4
16
  * This handles locally-defined schemas like `export const MySchema = z.object({...})`
@@ -67,6 +79,20 @@ export const detectSchemaVendor = (identifier, checker) => {
67
79
  export const detectSchemaVendorOrError = (identifier, checker, logger, context, sourceFile) => {
68
80
  const vendor = detectSchemaVendor(identifier, checker);
69
81
  if (vendor === 'unknown') {
82
+ // Two different failures used to share one message. Reporting the wrong one is not a
83
+ // cosmetic problem: the schema is dropped either way (`inputSchemaName: null`, no
84
+ // generated schema file), so the message is the only thing standing between the user
85
+ // and a function that silently validates nothing — and "use a supported validation
86
+ // library" is unactionable when the schema already IS `z.object({...})`.
87
+ if (isUnresolved(checker.getTypeAtLocation(identifier))) {
88
+ logger.critical(ErrorCode.INLINE_SCHEMA, `${context} schema '${identifier.text}' could not be TYPED from '${sourceFile}' — ` +
89
+ `TypeScript resolved it to 'any', so its vendor cannot be traced. This is a ` +
90
+ `resolution problem, not a schema problem: the file it is imported from is either ` +
91
+ `outside your tsconfig 'include', or has not been generated yet (a schema built ` +
92
+ `from '.pikku/db/zod.gen.ts' needs 'pikku db generate' to have run first). ` +
93
+ `Fix the import so the type resolves, then re-run.`);
94
+ return undefined;
95
+ }
70
96
  logger.critical(ErrorCode.INLINE_SCHEMA, `${context} schema vendor could not be determined from '${sourceFile}'. ` +
71
97
  `Supported vendors: zod, valibot, arktype, @effect/schema. ` +
72
98
  `Ensure your schema is imported from a supported validation library.`);
@@ -304,8 +304,15 @@ function extractVariableDeclaration(statement, context) {
304
304
  *
305
305
  * `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
306
306
  * in parallel and keep both results, so each element of the pattern is bound to
307
- * the matching child step's output. Every other destructuring shape reports a
307
+ * the matching child step's output. Every other destructuring OF A STEP reports a
308
308
  * diagnostic rather than silently dropping the step.
309
+ *
310
+ * A destructure whose initializer is NOT a step is just an ordinary local binding
311
+ * — `const { runId } = input`, `const { a } = someObject` — and has nothing to do
312
+ * with step results. Those pass through as non-steps, exactly as the identifier
313
+ * path already does for `const x = someLocal`. Reporting them was rejecting the
314
+ * single most idiomatic line in a DSL workflow (destructuring the workflow's own
315
+ * input), under a message about step results that named nothing the author wrote.
309
316
  */
310
317
  function extractDestructuredDeclaration(statement, decl, context) {
311
318
  const init = decl.initializer;
@@ -342,6 +349,17 @@ function extractDestructuredDeclaration(statement, decl, context) {
342
349
  }
343
350
  }
344
351
  }
352
+ // Only a destructure OF A STEP is an error. Anything else is a plain local
353
+ // binding the DSL simply doesn't model as a step.
354
+ const stepCall = init && ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)
355
+ ? init.expression
356
+ : null;
357
+ const destructuresAStep = stepCall !== null &&
358
+ (isWorkflowDoCall(stepCall, context.checker) ||
359
+ isParallelGroup(stepCall) ||
360
+ isParallelFanout(stepCall));
361
+ if (!destructuresAStep)
362
+ return null;
345
363
  context.errors.push({
346
364
  message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
347
365
  node: statement,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.44",
3
+ "version": "0.12.46",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -44,10 +44,10 @@
44
44
  "zod-to-ts": "^2.1.0"
45
45
  },
46
46
  "peerDependencies": {
47
- "@pikku/core": "^0.12.65"
47
+ "@pikku/core": "^0.12.68"
48
48
  },
49
49
  "devDependencies": {
50
- "@pikku/core": "^0.12.65",
50
+ "@pikku/core": "^0.12.68",
51
51
  "@types/node": "^24.11.0"
52
52
  },
53
53
  "engines": {
@@ -190,11 +190,11 @@ describe('addAuth inspector', () => {
190
190
  [
191
191
  "import { pikkuBetterAuth } from '@pikku/better-auth'",
192
192
  "import { betterAuth } from 'better-auth'",
193
- "import { bearer, admin, twoFactor } from 'better-auth/plugins'",
193
+ "import { bearer, apiKey, twoFactor } from 'better-auth/plugins'",
194
194
  'export const auth = pikkuBetterAuth(() =>',
195
195
  ' betterAuth({',
196
196
  " socialProviders: { github: { clientId: 'x', clientSecret: 'y' } },",
197
- ' plugins: [bearer(), admin(), twoFactor({ issuer: "pikku" })],',
197
+ ' plugins: [bearer(), apiKey(), twoFactor({ issuer: "pikku" })],',
198
198
  ' })',
199
199
  ')',
200
200
  ].join('\n')
@@ -204,10 +204,10 @@ describe('addAuth inspector', () => {
204
204
  try {
205
205
  const state = await inspect(makeLogger(criticals), [file], { rootDir })
206
206
  assert.equal(criticals.length, 0)
207
- assert.deepEqual(state.auth.plugins, ['bearer', 'admin', 'twoFactor'])
207
+ assert.deepEqual(state.auth.plugins, ['bearer', 'apiKey', 'twoFactor'])
208
208
  assert.deepEqual(state.auth.definition?.plugins, [
209
209
  'bearer',
210
- 'admin',
210
+ 'apiKey',
211
211
  'twoFactor',
212
212
  ])
213
213
  } finally {
@@ -0,0 +1,123 @@
1
+ import { test, describe, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
4
+ import { tmpdir } from 'os'
5
+ import { join } from 'path'
6
+ import * as ts from 'typescript'
7
+
8
+ import { detectSchemaVendorOrError } from './detect-schema-vendor.js'
9
+
10
+ let projectDir: string
11
+ let program: ts.Program
12
+ let checker: ts.TypeChecker
13
+ let sourceFile: ts.SourceFile
14
+
15
+ const criticals: string[] = []
16
+ const logger = {
17
+ debug() {},
18
+ info() {},
19
+ warn() {},
20
+ error() {},
21
+ critical(_code: unknown, message: string) {
22
+ criticals.push(message)
23
+ },
24
+ } as any
25
+
26
+ const initializerIdentifier = (name: string): ts.Identifier => {
27
+ let found: ts.Identifier | undefined
28
+ const visit = (node: ts.Node) => {
29
+ if (
30
+ ts.isVariableDeclaration(node) &&
31
+ ts.isIdentifier(node.name) &&
32
+ node.name.text === name &&
33
+ node.initializer &&
34
+ ts.isIdentifier(node.initializer)
35
+ ) {
36
+ found = node.initializer
37
+ }
38
+ ts.forEachChild(node, visit)
39
+ }
40
+ visit(sourceFile)
41
+ assert.ok(found, `no identifier initializer found for '${name}'`)
42
+ return found
43
+ }
44
+
45
+ before(() => {
46
+ projectDir = mkdtempSync(join(tmpdir(), 'pikku-schema-vendor-'))
47
+ mkdirSync(join(projectDir, 'src'))
48
+ writeFileSync(
49
+ join(projectDir, 'src', 'schemas.ts'),
50
+ [
51
+ // Imported from a file that does not exist — exactly what a schema built
52
+ // from an ungenerated '.pikku/db/zod.gen.ts' looks like to the checker.
53
+ `import { UngeneratedSchema } from './not-written-yet.js'`,
54
+ `export const unresolved = UngeneratedSchema`,
55
+ `const plainObject = { a: 1 }`,
56
+ `export const resolvedButNotASchema = plainObject`,
57
+ '',
58
+ ].join('\n')
59
+ )
60
+
61
+ program = ts.createProgram([join(projectDir, 'src', 'schemas.ts')], {
62
+ target: ts.ScriptTarget.ESNext,
63
+ module: ts.ModuleKind.NodeNext,
64
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
65
+ strict: true,
66
+ skipLibCheck: true,
67
+ noEmit: true,
68
+ })
69
+ checker = program.getTypeChecker()
70
+ sourceFile = program.getSourceFile(join(projectDir, 'src', 'schemas.ts'))!
71
+ })
72
+
73
+ after(() => {
74
+ rmSync(projectDir, { recursive: true, force: true })
75
+ })
76
+
77
+ describe('detectSchemaVendorOrError', () => {
78
+ test('reports a type the checker could not resolve as a resolution failure', () => {
79
+ criticals.length = 0
80
+
81
+ const vendor = detectSchemaVendorOrError(
82
+ initializerIdentifier('unresolved'),
83
+ checker,
84
+ logger,
85
+ `Function 'createTodo' input`,
86
+ 'src/schemas.ts'
87
+ )
88
+
89
+ assert.equal(vendor, undefined)
90
+ assert.equal(criticals.length, 1)
91
+ const [message] = criticals
92
+ assert.match(message, /could not be TYPED/)
93
+ assert.match(message, /UngeneratedSchema/)
94
+ assert.match(message, /pikku db generate/)
95
+ // The unactionable advice must NOT be given for a type that simply failed
96
+ // to resolve — the schema may well already be zod.
97
+ assert.doesNotMatch(
98
+ message,
99
+ /Ensure your schema is imported from a supported validation library/
100
+ )
101
+ })
102
+
103
+ test('still reports a resolvable non-vendor type as an unsupported library', () => {
104
+ criticals.length = 0
105
+
106
+ const vendor = detectSchemaVendorOrError(
107
+ initializerIdentifier('resolvedButNotASchema'),
108
+ checker,
109
+ logger,
110
+ `Function 'createTodo' input`,
111
+ 'src/schemas.ts'
112
+ )
113
+
114
+ assert.equal(vendor, undefined)
115
+ assert.equal(criticals.length, 1)
116
+ const [message] = criticals
117
+ assert.match(
118
+ message,
119
+ /Ensure your schema is imported from a supported validation library/
120
+ )
121
+ assert.doesNotMatch(message, /could not be TYPED/)
122
+ })
123
+ })
@@ -1,7 +1,20 @@
1
- import type * as ts from 'typescript'
1
+ import * as ts from 'typescript'
2
2
  import type { SchemaVendor, InspectorLogger } from '../types.js'
3
3
  import { ErrorCode } from '../error-codes.js'
4
4
 
5
+ /**
6
+ * Whether the checker could type the identifier at all.
7
+ *
8
+ * `any` here is not a schema written in an exotic library — it is TypeScript telling us
9
+ * it could not resolve the declaration, which happens when the schema comes from a file
10
+ * outside the program (not matched by tsconfig `include`) or from a generated file that
11
+ * has not been written yet (`.pikku/db/zod.gen.ts` before `pikku db generate` runs).
12
+ * The distinction matters because the two have opposite fixes, and conflating them told
13
+ * users to "import from a supported validation library" about a plain `z.object`.
14
+ */
15
+ const isUnresolved = (type: ts.Type): boolean =>
16
+ (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0
17
+
5
18
  /**
6
19
  * Detect the schema vendor by tracing the type back to its library origin.
7
20
  * This handles locally-defined schemas like `export const MySchema = z.object({...})`
@@ -78,6 +91,23 @@ export const detectSchemaVendorOrError = (
78
91
  ): Exclude<SchemaVendor, 'unknown'> | undefined => {
79
92
  const vendor = detectSchemaVendor(identifier, checker)
80
93
  if (vendor === 'unknown') {
94
+ // Two different failures used to share one message. Reporting the wrong one is not a
95
+ // cosmetic problem: the schema is dropped either way (`inputSchemaName: null`, no
96
+ // generated schema file), so the message is the only thing standing between the user
97
+ // and a function that silently validates nothing — and "use a supported validation
98
+ // library" is unactionable when the schema already IS `z.object({...})`.
99
+ if (isUnresolved(checker.getTypeAtLocation(identifier))) {
100
+ logger.critical(
101
+ ErrorCode.INLINE_SCHEMA,
102
+ `${context} schema '${identifier.text}' could not be TYPED from '${sourceFile}' — ` +
103
+ `TypeScript resolved it to 'any', so its vendor cannot be traced. This is a ` +
104
+ `resolution problem, not a schema problem: the file it is imported from is either ` +
105
+ `outside your tsconfig 'include', or has not been generated yet (a schema built ` +
106
+ `from '.pikku/db/zod.gen.ts' needs 'pikku db generate' to have run first). ` +
107
+ `Fix the import so the type resolves, then re-run.`
108
+ )
109
+ return undefined
110
+ }
81
111
  logger.critical(
82
112
  ErrorCode.INLINE_SCHEMA,
83
113
  `${context} schema vendor could not be determined from '${sourceFile}'. ` +
@@ -447,8 +447,15 @@ function extractVariableDeclaration(
447
447
  *
448
448
  * `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
449
449
  * in parallel and keep both results, so each element of the pattern is bound to
450
- * the matching child step's output. Every other destructuring shape reports a
450
+ * the matching child step's output. Every other destructuring OF A STEP reports a
451
451
  * diagnostic rather than silently dropping the step.
452
+ *
453
+ * A destructure whose initializer is NOT a step is just an ordinary local binding
454
+ * — `const { runId } = input`, `const { a } = someObject` — and has nothing to do
455
+ * with step results. Those pass through as non-steps, exactly as the identifier
456
+ * path already does for `const x = someLocal`. Reporting them was rejecting the
457
+ * single most idiomatic line in a DSL workflow (destructuring the workflow's own
458
+ * input), under a message about step results that named nothing the author wrote.
452
459
  */
453
460
  function extractDestructuredDeclaration(
454
461
  statement: ts.VariableStatement,
@@ -499,6 +506,19 @@ function extractDestructuredDeclaration(
499
506
  }
500
507
  }
501
508
 
509
+ // Only a destructure OF A STEP is an error. Anything else is a plain local
510
+ // binding the DSL simply doesn't model as a step.
511
+ const stepCall =
512
+ init && ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)
513
+ ? init.expression
514
+ : null
515
+ const destructuresAStep =
516
+ stepCall !== null &&
517
+ (isWorkflowDoCall(stepCall, context.checker) ||
518
+ isParallelGroup(stepCall) ||
519
+ isParallelFanout(stepCall))
520
+ if (!destructuresAStep) return null
521
+
502
522
  context.errors.push({
503
523
  message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
504
524
  node: statement,