@pikku/inspector 0.12.20 → 0.12.22
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 +31 -0
- package/dist/add/add-auth.js +15 -0
- package/dist/add/add-cli.js +8 -22
- package/dist/add/add-functions.js +16 -8
- package/dist/add/add-workflow.js +16 -2
- package/dist/error-codes.d.ts +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/inspector.js +5 -1
- package/dist/types.d.ts +13 -1
- package/dist/utils/extract-node-value.js +19 -2
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +15 -0
- package/package.json +3 -2
- package/src/add/add-auth.test.ts +86 -0
- package/src/add/add-auth.ts +19 -0
- package/src/add/add-cli-renderers.test.ts +74 -0
- package/src/add/add-cli.ts +9 -25
- package/src/add/add-functions.test.ts +13 -0
- package/src/add/add-functions.ts +14 -10
- package/src/add/add-workflow-fanout.test.ts +106 -0
- package/src/add/add-workflow.test.ts +3 -0
- package/src/add/add-workflow.ts +16 -2
- package/src/add/pii-check.test.ts +4 -0
- package/src/add/wire-name-literal.test.ts +3 -0
- package/src/error-codes.ts +14 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +8 -1
- package/src/types.ts +13 -1
- package/src/utils/extract-node-value.test.ts +49 -1
- package/src/utils/extract-node-value.ts +19 -2
- package/src/utils/filter-inspector-state.test.ts +1 -0
- package/src/utils/filter-utils.test.ts +1 -0
- package/src/utils/resolve-versions.test.ts +1 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +16 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## 0.12.22
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 06234a9: Fix DSL `Promise.all` fanout silently failing to register its child RPC (causing a runtime "Function not found").
|
|
6
|
+
|
|
7
|
+
Two distinct causes are addressed:
|
|
8
|
+
- A fanout/group captured into a variable (`const results = await Promise.all(array.map(e => workflow.do(...)))`) was dropped entirely, because the `const`-declaration path had no `Promise.all` branch — fanout handling only ran on the bare/assignment path. The declaration path now extracts fanout and parallel groups too.
|
|
9
|
+
- `extractStringLiteral` threw on a `+` concatenation with a non-static operand (e.g. `'Enrich ' + (e.id ?? e.name)`), unlike a template literal (`` `Enrich ${e.id ?? e.name}` ``) which never threw. The throw was uncaught while scanning workflow invocations and aborted the run. The `+` branch now falls back to `${...}` placeholders to match template literals, and a step's cosmetic display name can no longer block RPC registration.
|
|
10
|
+
|
|
11
|
+
- 8e72c93: Exclude `node_modules` from inspector source scanning. A locally-installed addon (under the project's `node_modules`) is a dependency, not project source — scanning it double-counted the addon's own application types (`CoreConfig`/`CoreServices`/`CoreSingletonServices`) and failed `pikku all` with "More than one … found". Addons still contribute via their generated metadata, not by being re-scanned as source.
|
|
12
|
+
- 6645e7a: Add a severity model for coded diagnostics so security findings can surface without blocking the dev server.
|
|
13
|
+
- `InspectorLogger` gains `diagnostic({ severity, code, message })` (`severity: 'warn' | 'error' | 'critical'`). `critical(code, message)` is now sugar for `diagnostic({ severity: 'critical', ... })`.
|
|
14
|
+
- The CLI fails the build only on `critical` diagnostics by default. New global flags `--fail-on-error` and `--fail-on-warn` (implies `--fail-on-error`) opt into stricter gating; `--fail-on-critical` is always on.
|
|
15
|
+
- Data-classification leaks (`PKU910`) are now emitted at `error` severity instead of `critical`. They are still printed, but no longer abort `pikku all` / the dev server — pass `--fail-on-error` (e.g. at deploy) to make them blocking and recommend a fix.
|
|
16
|
+
- Contract-immutability drift (`PKU861`) during `pikku versions update` (run inside `pikku all`) no longer calls `process.exit(1)`. It is surfaced as an `error` diagnostic and skips saving the manifest, so a stale baseline can't crash-loop the dev server. `pikku versions check` remains the hard gate, and `--fail-on-error` makes `pikku all` block on it at deploy.
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [6bca38f]
|
|
19
|
+
- @pikku/core@0.12.35
|
|
20
|
+
|
|
21
|
+
## 0.12.21
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- ef50347: Tree-shake the better-auth server out of non-auth units.
|
|
26
|
+
- `@pikku/better-auth`: add `betterAuthStatelessSession()` — a session middleware that verifies the signed better-auth cookie cache via `better-auth/cookies` (`getCookieCache`) using only `BETTER_AUTH_SECRET`, with no `services.auth()`, DB round-trip, or full server import. Mark the package `sideEffects: false` so unused barrel re-exports drop.
|
|
27
|
+
- `@pikku/cli`: when `session.cookieCache` is enabled in the better-auth config, generate the stateless session middleware into a separate `auth-middleware.gen.ts` and wire it globally, keeping the full `/api/auth/**` server only in the auth unit. Deploy artifacts (esbuild metafile + sourcemap) are now off by default; `--debug-artifacts` re-enables them.
|
|
28
|
+
- `@pikku/inspector`: ensure the orphan `auth-middleware.gen.ts` (imported by nothing) is still inspected so its global `addHTTPMiddleware('*')` registration is not dropped.
|
|
29
|
+
|
|
30
|
+
Net effect: a non-auth unit carries ~22KB (cookie-verify floor) instead of the full ~1.25MB better-auth backend.
|
|
31
|
+
|
|
1
32
|
## 0.12.20
|
|
2
33
|
|
|
3
34
|
### Patch Changes
|
package/dist/add/add-auth.js
CHANGED
|
@@ -136,10 +136,24 @@ export const addAuth = (logger, node, _checker, state) => {
|
|
|
136
136
|
// Find the inner betterAuth({...}) call to read providers/basePath/credentials.
|
|
137
137
|
let basePath = DEFAULT_BASE_PATH;
|
|
138
138
|
let hasCredentials = false;
|
|
139
|
+
let cookieCache = false;
|
|
139
140
|
const betterAuthCall = findBetterAuthCall(factory);
|
|
140
141
|
const config = betterAuthCall?.arguments[0];
|
|
141
142
|
if (config && ts.isObjectLiteralExpression(config)) {
|
|
142
143
|
basePath = readStringProp(config, 'basePath') ?? DEFAULT_BASE_PATH;
|
|
144
|
+
// Detect `session.cookieCache.enabled` → drives the stateless middleware split.
|
|
145
|
+
const session = readObjectProp(config, 'session');
|
|
146
|
+
if (session) {
|
|
147
|
+
const cookieCacheBlock = readObjectProp(session, 'cookieCache');
|
|
148
|
+
if (cookieCacheBlock) {
|
|
149
|
+
const enabledProp = cookieCacheBlock.properties.find((p) => ts.isPropertyAssignment(p) &&
|
|
150
|
+
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
151
|
+
p.name.text === 'enabled');
|
|
152
|
+
cookieCache =
|
|
153
|
+
!enabledProp ||
|
|
154
|
+
enabledProp.initializer.kind !== ts.SyntaxKind.FalseKeyword;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
143
157
|
const emailAndPassword = readObjectProp(config, 'emailAndPassword');
|
|
144
158
|
if (emailAndPassword) {
|
|
145
159
|
// `emailAndPassword: { enabled: true }` — treat a present block without an
|
|
@@ -182,6 +196,7 @@ export const addAuth = (logger, node, _checker, state) => {
|
|
|
182
196
|
sourceFile,
|
|
183
197
|
basePath,
|
|
184
198
|
hasCredentials,
|
|
199
|
+
cookieCache,
|
|
185
200
|
plugins: [...state.auth.plugins],
|
|
186
201
|
services,
|
|
187
202
|
};
|
package/dist/add/add-cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getPropertyValue } from '../utils/get-property-value.js';
|
|
|
7
7
|
import { resolveIdentifier } from '../utils/resolve-identifier.js';
|
|
8
8
|
import { resolveAddonName } from '../utils/resolve-addon-package.js';
|
|
9
9
|
import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js';
|
|
10
|
+
import { extractServicesFromFunction } from '../utils/extract-services.js';
|
|
10
11
|
// Track if we've warned about missing Config type to avoid duplicate warnings
|
|
11
12
|
const configTypeWarningShown = new Set();
|
|
12
13
|
/**
|
|
@@ -628,7 +629,7 @@ function parseCommandPattern(pattern) {
|
|
|
628
629
|
export const addCLIRenderers = (logger, node, typeChecker, inspectorState, options) => {
|
|
629
630
|
if (!ts.isCallExpression(node))
|
|
630
631
|
return;
|
|
631
|
-
const { expression, arguments: args
|
|
632
|
+
const { expression, arguments: args } = node;
|
|
632
633
|
// Only handle pikkuCLIRender calls
|
|
633
634
|
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuCLIRender') {
|
|
634
635
|
return;
|
|
@@ -640,27 +641,12 @@ export const addCLIRenderers = (logger, node, typeChecker, inspectorState, optio
|
|
|
640
641
|
// Get the source file path
|
|
641
642
|
const sourceFile = node.getSourceFile();
|
|
642
643
|
const filePath = sourceFile.fileName;
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
// Second type parameter is the Services type
|
|
650
|
-
const servicesTypeNode = typeArguments[1];
|
|
651
|
-
if (servicesTypeNode) {
|
|
652
|
-
const servicesType = typeChecker.getTypeFromTypeNode(servicesTypeNode);
|
|
653
|
-
// Extract property names from the Services type
|
|
654
|
-
const properties = servicesType.getProperties();
|
|
655
|
-
for (const prop of properties) {
|
|
656
|
-
services.services.push(prop.getName());
|
|
657
|
-
}
|
|
658
|
-
// If no specific services found, it might be using the full services object
|
|
659
|
-
if (properties.length === 0) {
|
|
660
|
-
services.optimized = false;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
644
|
+
// Renderer service usage is defined by the callback's first parameter shape,
|
|
645
|
+
// the same way function/auth service usage is tracked elsewhere in the inspector.
|
|
646
|
+
const renderFunc = args[0];
|
|
647
|
+
const services = ts.isArrowFunction(renderFunc) || ts.isFunctionExpression(renderFunc)
|
|
648
|
+
? extractServicesFromFunction(renderFunc)
|
|
649
|
+
: { optimized: true, services: [] };
|
|
664
650
|
// Store renderer metadata
|
|
665
651
|
inspectorState.cli.meta.renderers[pikkuFuncId] = {
|
|
666
652
|
name: pikkuFuncId,
|
|
@@ -660,16 +660,24 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
660
660
|
.filter((f) => f.classification === 'private' || f.classification === 'pii')
|
|
661
661
|
.map((f) => f.path);
|
|
662
662
|
if (secretPaths.length > 0) {
|
|
663
|
-
logger.
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
`
|
|
663
|
+
logger.diagnostic({
|
|
664
|
+
severity: 'error',
|
|
665
|
+
code: ErrorCode.PII_IN_OUTPUT,
|
|
666
|
+
message: `Function '${name}' exposes secret-classified field(s) in its return type: ` +
|
|
667
|
+
secretPaths.map((p) => `'${p}'`).join(', ') +
|
|
668
|
+
`.\n Secret fields must never appear in function output. ` +
|
|
669
|
+
`Strip these fields before returning or change the column classification.`,
|
|
670
|
+
});
|
|
667
671
|
}
|
|
668
672
|
if (sessionless && privatePaths.length > 0) {
|
|
669
|
-
logger.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
`
|
|
673
|
+
logger.diagnostic({
|
|
674
|
+
severity: 'error',
|
|
675
|
+
code: ErrorCode.PII_IN_OUTPUT,
|
|
676
|
+
message: `Sessionless function '${name}' exposes private-classified field(s) in its return type: ` +
|
|
677
|
+
privatePaths.map((p) => `'${p}'`).join(', ') +
|
|
678
|
+
`.\n Private fields are only safe to return from authenticated (sessioned) functions. ` +
|
|
679
|
+
`Either require a session (use pikkuFunc) or mark the column @public if it is safe to expose publicly.`,
|
|
680
|
+
});
|
|
673
681
|
}
|
|
674
682
|
}
|
|
675
683
|
}
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -5,6 +5,20 @@ import { ErrorCode } from '../error-codes.js';
|
|
|
5
5
|
import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
|
|
6
6
|
import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
|
|
7
7
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
|
|
8
|
+
import { getSourceText } from '../utils/workflow/dsl/patterns.js';
|
|
9
|
+
/**
|
|
10
|
+
* Extract a workflow step's display name without letting a non-static name
|
|
11
|
+
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
12
|
+
* resolution failure must never prevent the RPC from being registered.
|
|
13
|
+
*/
|
|
14
|
+
function extractStepName(node, checker) {
|
|
15
|
+
try {
|
|
16
|
+
return extractStringLiteral(node, checker);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return getSourceText(node);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
23
|
* Recursively check if any step has inline type (non-serializable)
|
|
10
24
|
*/
|
|
@@ -89,7 +103,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
89
103
|
const stepNameArg = args[0];
|
|
90
104
|
const secondArg = args[1];
|
|
91
105
|
const optionsArg = args.length >= 4 ? args[args.length - 1] : undefined;
|
|
92
|
-
const stepName =
|
|
106
|
+
const stepName = extractStepName(stepNameArg, checker);
|
|
93
107
|
const description = extractDescription(optionsArg, checker) ?? undefined;
|
|
94
108
|
// Determine form by checking 2nd argument type
|
|
95
109
|
if (isStringLike(secondArg, checker)) {
|
|
@@ -115,7 +129,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
115
129
|
// workflow.sleep(stepName, duration)
|
|
116
130
|
const stepNameArg = args[0];
|
|
117
131
|
const durationArg = args[1];
|
|
118
|
-
const stepName =
|
|
132
|
+
const stepName = extractStepName(stepNameArg, checker);
|
|
119
133
|
const duration = extractDuration(durationArg, checker);
|
|
120
134
|
steps.push({
|
|
121
135
|
type: 'sleep',
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -59,3 +59,15 @@ export declare enum ErrorCode {
|
|
|
59
59
|
WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901",
|
|
60
60
|
PII_IN_OUTPUT = "PKU910"
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Severity of a tracked, coded diagnostic. `critical` always blocks the build;
|
|
64
|
+
* `error`/`warn` only block when the CLI is told to via `--fail-on-error` /
|
|
65
|
+
* `--fail-on-warn` (default: critical only). All severities are still printed.
|
|
66
|
+
*/
|
|
67
|
+
export type DiagnosticSeverity = 'warn' | 'error' | 'critical';
|
|
68
|
+
/** A coded diagnostic emitted via `logger.diagnostic(...)`. */
|
|
69
|
+
export interface CodedDiagnostic {
|
|
70
|
+
severity: DiagnosticSeverity;
|
|
71
|
+
code: ErrorCode;
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { TypesMap } from './types-map.js';
|
|
|
3
3
|
export type * from './types.js';
|
|
4
4
|
export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
|
|
5
5
|
export { ErrorCode } from './error-codes.js';
|
|
6
|
+
export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
|
|
6
7
|
export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
|
|
7
8
|
export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
|
|
8
9
|
export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
|
package/dist/inspector.js
CHANGED
|
@@ -209,9 +209,13 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
209
209
|
// Use provided rootDir or infer from source files
|
|
210
210
|
const rootDir = options.rootDir || findCommonAncestor(routeFiles);
|
|
211
211
|
const startSourceFiles = performance.now();
|
|
212
|
+
// node_modules under rootDir (e.g. a locally-installed addon) is a
|
|
213
|
+
// dependency, not project source — scanning it double-counts the addon's
|
|
214
|
+
// own application types (CoreConfig/Services/SingletonServices).
|
|
212
215
|
const sourceFiles = program
|
|
213
216
|
.getSourceFiles()
|
|
214
|
-
.filter((sf) => sf.fileName.startsWith(rootDir)
|
|
217
|
+
.filter((sf) => sf.fileName.startsWith(rootDir) &&
|
|
218
|
+
!sf.fileName.includes('/node_modules/'));
|
|
215
219
|
logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
|
|
216
220
|
const state = getInitialInspectorState(rootDir);
|
|
217
221
|
// First sweep: add all functions
|
package/dist/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { VariableDefinitions } from '@pikku/core/variable';
|
|
|
16
16
|
import type { TypesMap } from './types-map.js';
|
|
17
17
|
import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
|
|
18
18
|
import type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
|
|
19
|
-
import type { ErrorCode } from './error-codes.js';
|
|
19
|
+
import type { ErrorCode, CodedDiagnostic } from './error-codes.js';
|
|
20
20
|
import type { VersionManifest, VersionValidateError } from './utils/contract-hashes.js';
|
|
21
21
|
import type { SerializedWorkflowGraphs } from './utils/workflow/graph/workflow-graph.types.js';
|
|
22
22
|
export type PathToNameAndType = Map<string, {
|
|
@@ -192,6 +192,15 @@ export interface InspectorLogger {
|
|
|
192
192
|
error: (message: string) => void;
|
|
193
193
|
warn: (message: string) => void;
|
|
194
194
|
debug: (message: string) => void;
|
|
195
|
+
/**
|
|
196
|
+
* Emit a tracked, coded diagnostic. It is recorded and printed; `error`/`warn`
|
|
197
|
+
* only block the build when the CLI is run with `--fail-on-error` /
|
|
198
|
+
* `--fail-on-warn` (default: critical only). Use this for issues worth
|
|
199
|
+
* surfacing (e.g. data-classification leaks) that should not stop the dev
|
|
200
|
+
* server from starting.
|
|
201
|
+
*/
|
|
202
|
+
diagnostic: (diagnostic: CodedDiagnostic) => void;
|
|
203
|
+
/** Sugar for `diagnostic({ severity: 'critical', code, message })`. */
|
|
195
204
|
critical: (code: ErrorCode, message: string) => void;
|
|
196
205
|
hasCriticalErrors: () => boolean;
|
|
197
206
|
}
|
|
@@ -263,6 +272,9 @@ export interface AuthDefinition {
|
|
|
263
272
|
* `auth-meta.gen.json` so the console SSO page can show which plugins are
|
|
264
273
|
* enabled. */
|
|
265
274
|
plugins: string[];
|
|
275
|
+
/** Whether `session.cookieCache` is enabled — drives the stateless session
|
|
276
|
+
* middleware split in the auth codegen. Absent/false ⇒ stateful middleware. */
|
|
277
|
+
cookieCache?: boolean;
|
|
266
278
|
/**
|
|
267
279
|
* Singleton services the generated auth handler must have available at
|
|
268
280
|
* runtime — the services the `pikkuBetterAuth` factory reaches for (either
|
|
@@ -26,8 +26,8 @@ export function extractStringLiteral(node, checker) {
|
|
|
26
26
|
}
|
|
27
27
|
if (ts.isBinaryExpression(node) &&
|
|
28
28
|
node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
29
|
-
return (
|
|
30
|
-
|
|
29
|
+
return (extractConcatOperand(node.left, checker) +
|
|
30
|
+
extractConcatOperand(node.right, checker));
|
|
31
31
|
}
|
|
32
32
|
// Try to evaluate constant identifiers
|
|
33
33
|
if (ts.isIdentifier(node)) {
|
|
@@ -42,6 +42,23 @@ export function extractStringLiteral(node, checker) {
|
|
|
42
42
|
}
|
|
43
43
|
throw new Error('Unable to extract string literal from node');
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve one operand of a `+` string concatenation.
|
|
47
|
+
*
|
|
48
|
+
* An operand that can't be statically resolved (e.g. `a ?? b`) becomes a
|
|
49
|
+
* `${...}` placeholder rather than throwing — mirroring the TemplateExpression
|
|
50
|
+
* branch above, so `'x ' + expr` and `` `x ${expr}` `` produce the same string.
|
|
51
|
+
* This keeps an unresolvable display name from aborting the whole extraction.
|
|
52
|
+
*/
|
|
53
|
+
function extractConcatOperand(node, checker) {
|
|
54
|
+
try {
|
|
55
|
+
return extractStringLiteral(node, checker);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
const inner = ts.isParenthesizedExpression(node) ? node.expression : node;
|
|
59
|
+
return '${' + inner.getText() + '}';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
45
62
|
/**
|
|
46
63
|
* Check if node is string-like (string literal or template expression)
|
|
47
64
|
*/
|
|
@@ -255,6 +255,21 @@ function extractVariableDeclaration(statement, context) {
|
|
|
255
255
|
return step;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
// Promise.all fanout/group captured into a variable
|
|
259
|
+
// (const results = await Promise.all(array.map(...)))
|
|
260
|
+
if (isParallelFanout(call) || isParallelGroup(call)) {
|
|
261
|
+
const step = isParallelFanout(call)
|
|
262
|
+
? extractParallelFanout(call, context)
|
|
263
|
+
: extractParallelGroup(call, context);
|
|
264
|
+
if (step) {
|
|
265
|
+
const type = context.checker.getTypeAtLocation(decl);
|
|
266
|
+
context.outputVars.set(varName, { type, node: decl });
|
|
267
|
+
if (isArrayType(type, context.checker)) {
|
|
268
|
+
context.arrayVars.add(varName);
|
|
269
|
+
}
|
|
270
|
+
return step;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
258
273
|
}
|
|
259
274
|
// Check for array.filter(...)
|
|
260
275
|
if (ts.isCallExpression(init)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.22",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.35",
|
|
39
|
+
"openapi-types": "^12.1.3",
|
|
39
40
|
"path-to-regexp": "^8.3.0",
|
|
40
41
|
"ts-json-schema-generator": "^2.5.0",
|
|
41
42
|
"tsx": "^4.21.0",
|
package/src/add/add-auth.test.ts
CHANGED
|
@@ -13,6 +13,9 @@ const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
|
13
13
|
info: () => {},
|
|
14
14
|
warn: () => {},
|
|
15
15
|
error: () => {},
|
|
16
|
+
diagnostic: ({ code, message }) => {
|
|
17
|
+
criticals.push({ code, message })
|
|
18
|
+
},
|
|
16
19
|
critical: (code: ErrorCode, message: string) => {
|
|
17
20
|
criticals.push({ code, message })
|
|
18
21
|
},
|
|
@@ -50,6 +53,7 @@ describe('addAuth inspector', () => {
|
|
|
50
53
|
sourceFile: file,
|
|
51
54
|
basePath: '/api/auth',
|
|
52
55
|
hasCredentials: false,
|
|
56
|
+
cookieCache: false,
|
|
53
57
|
plugins: [],
|
|
54
58
|
services: { optimized: true, services: [] },
|
|
55
59
|
})
|
|
@@ -95,6 +99,88 @@ describe('addAuth inspector', () => {
|
|
|
95
99
|
}
|
|
96
100
|
})
|
|
97
101
|
|
|
102
|
+
test('detects session.cookieCache for the stateless middleware split', async () => {
|
|
103
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-cookiecache-'))
|
|
104
|
+
const file = join(rootDir, 'auth.ts')
|
|
105
|
+
|
|
106
|
+
await writeFile(
|
|
107
|
+
file,
|
|
108
|
+
[
|
|
109
|
+
"import { pikkuBetterAuth } from '@pikku/better-auth'",
|
|
110
|
+
"import { betterAuth } from 'better-auth'",
|
|
111
|
+
'export const auth = pikkuBetterAuth(() =>',
|
|
112
|
+
' betterAuth({',
|
|
113
|
+
' session: { cookieCache: { enabled: true } },',
|
|
114
|
+
" emailAndPassword: { enabled: true },",
|
|
115
|
+
' })',
|
|
116
|
+
')',
|
|
117
|
+
].join('\n')
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
121
|
+
try {
|
|
122
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
123
|
+
assert.equal(criticals.length, 0)
|
|
124
|
+
assert.equal(state.auth.definition?.cookieCache, true)
|
|
125
|
+
} finally {
|
|
126
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('cookieCache: { enabled: false } does not enable the stateless split', async () => {
|
|
131
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-nocache-'))
|
|
132
|
+
const file = join(rootDir, 'auth.ts')
|
|
133
|
+
|
|
134
|
+
await writeFile(
|
|
135
|
+
file,
|
|
136
|
+
[
|
|
137
|
+
"import { pikkuBetterAuth } from '@pikku/better-auth'",
|
|
138
|
+
"import { betterAuth } from 'better-auth'",
|
|
139
|
+
'export const auth = pikkuBetterAuth(() =>',
|
|
140
|
+
' betterAuth({',
|
|
141
|
+
' session: { cookieCache: { enabled: false } },',
|
|
142
|
+
' })',
|
|
143
|
+
')',
|
|
144
|
+
].join('\n')
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
148
|
+
try {
|
|
149
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
150
|
+
assert.equal(criticals.length, 0)
|
|
151
|
+
assert.equal(state.auth.definition?.cookieCache, false)
|
|
152
|
+
} finally {
|
|
153
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('cookieCache: { "enabled": false } (string-literal key) is honoured', async () => {
|
|
158
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-strkey-'))
|
|
159
|
+
const file = join(rootDir, 'auth.ts')
|
|
160
|
+
|
|
161
|
+
await writeFile(
|
|
162
|
+
file,
|
|
163
|
+
[
|
|
164
|
+
"import { pikkuBetterAuth } from '@pikku/better-auth'",
|
|
165
|
+
"import { betterAuth } from 'better-auth'",
|
|
166
|
+
'export const auth = pikkuBetterAuth(() =>',
|
|
167
|
+
' betterAuth({',
|
|
168
|
+
' session: { cookieCache: { "enabled": false } },',
|
|
169
|
+
' })',
|
|
170
|
+
')',
|
|
171
|
+
].join('\n')
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
175
|
+
try {
|
|
176
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
177
|
+
assert.equal(criticals.length, 0)
|
|
178
|
+
assert.equal(state.auth.definition?.cookieCache, false)
|
|
179
|
+
} finally {
|
|
180
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
98
184
|
test('extracts plugin ids from the betterAuth plugins array', async () => {
|
|
99
185
|
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-plugins-'))
|
|
100
186
|
const file = join(rootDir, 'auth.ts')
|
package/src/add/add-auth.ts
CHANGED
|
@@ -188,12 +188,30 @@ export const addAuth: AddWiring = (logger, node, _checker, state) => {
|
|
|
188
188
|
// Find the inner betterAuth({...}) call to read providers/basePath/credentials.
|
|
189
189
|
let basePath = DEFAULT_BASE_PATH
|
|
190
190
|
let hasCredentials = false
|
|
191
|
+
let cookieCache = false
|
|
191
192
|
const betterAuthCall = findBetterAuthCall(factory)
|
|
192
193
|
const config = betterAuthCall?.arguments[0]
|
|
193
194
|
|
|
194
195
|
if (config && ts.isObjectLiteralExpression(config)) {
|
|
195
196
|
basePath = readStringProp(config, 'basePath') ?? DEFAULT_BASE_PATH
|
|
196
197
|
|
|
198
|
+
// Detect `session.cookieCache.enabled` → drives the stateless middleware split.
|
|
199
|
+
const session = readObjectProp(config, 'session')
|
|
200
|
+
if (session) {
|
|
201
|
+
const cookieCacheBlock = readObjectProp(session, 'cookieCache')
|
|
202
|
+
if (cookieCacheBlock) {
|
|
203
|
+
const enabledProp = cookieCacheBlock.properties.find(
|
|
204
|
+
(p) =>
|
|
205
|
+
ts.isPropertyAssignment(p) &&
|
|
206
|
+
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
207
|
+
p.name.text === 'enabled'
|
|
208
|
+
) as ts.PropertyAssignment | undefined
|
|
209
|
+
cookieCache =
|
|
210
|
+
!enabledProp ||
|
|
211
|
+
enabledProp.initializer.kind !== ts.SyntaxKind.FalseKeyword
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
197
215
|
const emailAndPassword = readObjectProp(config, 'emailAndPassword')
|
|
198
216
|
if (emailAndPassword) {
|
|
199
217
|
// `emailAndPassword: { enabled: true }` — treat a present block without an
|
|
@@ -244,6 +262,7 @@ export const addAuth: AddWiring = (logger, node, _checker, state) => {
|
|
|
244
262
|
sourceFile,
|
|
245
263
|
basePath,
|
|
246
264
|
hasCredentials,
|
|
265
|
+
cookieCache,
|
|
247
266
|
plugins: [...state.auth.plugins],
|
|
248
267
|
services,
|
|
249
268
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { inspect } from '../inspector.js'
|
|
7
|
+
import type { InspectorLogger } from '../types.js'
|
|
8
|
+
|
|
9
|
+
const makeLogger = () =>
|
|
10
|
+
({
|
|
11
|
+
debug: () => {},
|
|
12
|
+
info: () => {},
|
|
13
|
+
warn: () => {},
|
|
14
|
+
error: () => {},
|
|
15
|
+
diagnostic: () => {},
|
|
16
|
+
critical: () => {},
|
|
17
|
+
hasCriticalErrors: () => false,
|
|
18
|
+
}) satisfies InspectorLogger
|
|
19
|
+
|
|
20
|
+
describe('addCLIRenderers inspector', () => {
|
|
21
|
+
test('extracts destructured renderer services from the callback param', async () => {
|
|
22
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-cli-renderer-'))
|
|
23
|
+
const file = join(rootDir, 'cli.ts')
|
|
24
|
+
|
|
25
|
+
await writeFile(
|
|
26
|
+
file,
|
|
27
|
+
[
|
|
28
|
+
"import { wireCLI, pikkuCLIRender } from '@pikku/core/cli'",
|
|
29
|
+
'export const render = pikkuCLIRender(({ logger }, data) => {',
|
|
30
|
+
' logger.info(data.message)',
|
|
31
|
+
'})',
|
|
32
|
+
"wireCLI({ program: 'pikku', render, commands: {} })",
|
|
33
|
+
].join('\n')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const state = await inspect(makeLogger(), [file], { rootDir })
|
|
38
|
+
assert.deepEqual(state.cli.meta.renderers['render'], {
|
|
39
|
+
name: 'render',
|
|
40
|
+
exportedName: 'render',
|
|
41
|
+
services: { optimized: true, services: ['logger'] },
|
|
42
|
+
filePath: file,
|
|
43
|
+
})
|
|
44
|
+
} finally {
|
|
45
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('marks non-destructured renderer services as unoptimized', async () => {
|
|
50
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-cli-renderer-all-'))
|
|
51
|
+
const file = join(rootDir, 'cli.ts')
|
|
52
|
+
|
|
53
|
+
await writeFile(
|
|
54
|
+
file,
|
|
55
|
+
[
|
|
56
|
+
"import { wireCLI, pikkuCLIRender } from '@pikku/core/cli'",
|
|
57
|
+
'export const render = pikkuCLIRender((services, data) => {',
|
|
58
|
+
' services.logger.info(data.message)',
|
|
59
|
+
'})',
|
|
60
|
+
"wireCLI({ program: 'pikku', render, commands: {} })",
|
|
61
|
+
].join('\n')
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const state = await inspect(makeLogger(), [file], { rootDir })
|
|
66
|
+
assert.deepEqual(state.cli.meta.renderers['render']?.services, {
|
|
67
|
+
optimized: false,
|
|
68
|
+
services: [],
|
|
69
|
+
})
|
|
70
|
+
} finally {
|
|
71
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})
|
package/src/add/add-cli.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getPropertyValue } from '../utils/get-property-value.js'
|
|
|
18
18
|
import { resolveIdentifier } from '../utils/resolve-identifier.js'
|
|
19
19
|
import { resolveAddonName } from '../utils/resolve-addon-package.js'
|
|
20
20
|
import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
|
|
21
|
+
import { extractServicesFromFunction } from '../utils/extract-services.js'
|
|
21
22
|
|
|
22
23
|
// Track if we've warned about missing Config type to avoid duplicate warnings
|
|
23
24
|
const configTypeWarningShown = new Set<string>()
|
|
@@ -924,7 +925,7 @@ export const addCLIRenderers: AddWiring = (
|
|
|
924
925
|
) => {
|
|
925
926
|
if (!ts.isCallExpression(node)) return
|
|
926
927
|
|
|
927
|
-
const { expression, arguments: args
|
|
928
|
+
const { expression, arguments: args } = node
|
|
928
929
|
|
|
929
930
|
// Only handle pikkuCLIRender calls
|
|
930
931
|
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuCLIRender') {
|
|
@@ -944,30 +945,13 @@ export const addCLIRenderers: AddWiring = (
|
|
|
944
945
|
const sourceFile = node.getSourceFile()
|
|
945
946
|
const filePath = sourceFile.fileName
|
|
946
947
|
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
// Second type parameter is the Services type
|
|
955
|
-
const servicesTypeNode = typeArguments[1]
|
|
956
|
-
if (servicesTypeNode) {
|
|
957
|
-
const servicesType = typeChecker.getTypeFromTypeNode(servicesTypeNode)
|
|
958
|
-
|
|
959
|
-
// Extract property names from the Services type
|
|
960
|
-
const properties = servicesType.getProperties()
|
|
961
|
-
for (const prop of properties) {
|
|
962
|
-
services.services.push(prop.getName())
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// If no specific services found, it might be using the full services object
|
|
966
|
-
if (properties.length === 0) {
|
|
967
|
-
services.optimized = false
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
948
|
+
// Renderer service usage is defined by the callback's first parameter shape,
|
|
949
|
+
// the same way function/auth service usage is tracked elsewhere in the inspector.
|
|
950
|
+
const renderFunc = args[0]
|
|
951
|
+
const services =
|
|
952
|
+
ts.isArrowFunction(renderFunc) || ts.isFunctionExpression(renderFunc)
|
|
953
|
+
? extractServicesFromFunction(renderFunc)
|
|
954
|
+
: { optimized: true, services: [] }
|
|
971
955
|
|
|
972
956
|
// Store renderer metadata
|
|
973
957
|
inspectorState.cli.meta.renderers[pikkuFuncId] = {
|