@pikku/inspector 0.12.23 → 0.12.25
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 +57 -0
- package/dist/add/add-auth.js +43 -0
- package/dist/add/add-gateway.js +5 -1
- package/dist/add/add-permission.js +5 -1
- package/dist/types.d.ts +5 -0
- package/dist/utils/post-process.js +24 -3
- package/package.json +2 -2
- package/src/add/add-auth.test.ts +94 -0
- package/src/add/add-auth.ts +46 -0
- package/src/add/add-gateway.ts +6 -2
- package/src/add/add-permission-auth.test.ts +59 -0
- package/src/add/add-permission.ts +6 -1
- package/src/types.ts +5 -0
- package/src/utils/compute-diagnostics.test.ts +69 -0
- package/src/utils/post-process.ts +25 -3
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
## 0.12.25
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- b6ba601: fix(lint): don't flag pikkuAuth's session param as a non-destructured wire
|
|
6
|
+
|
|
7
|
+
`pikkuAuth`'s handler is `(services, session)` — the second parameter is the
|
|
8
|
+
resolved user session, not a wires bag. The inspector was extracting "wires"
|
|
9
|
+
from that parameter (`extractUsedWires(handler, 1)`), so a permission like
|
|
10
|
+
`pikkuAuth(async ({ logger }, session) => !!session)` tripped
|
|
11
|
+
`wiresNotDestructured` even though `session` cannot be destructured. pikkuAuth
|
|
12
|
+
exposes no user-facing wires parameter, so no wires meta is recorded for it.
|
|
13
|
+
|
|
14
|
+
- ae7fc5d: Include gateway platform and auth fields in inspected gateway metadata.
|
|
15
|
+
- decdad5: fix(lint): don't fail the build on framework-synthesized functions
|
|
16
|
+
|
|
17
|
+
The `servicesNotDestructured`/`wiresNotDestructured` defaults (`error`) were
|
|
18
|
+
tripping on functions the user can't edit: generated `.gen.ts` wrappers (the
|
|
19
|
+
opaque `authHandler`, the cli channel raw dispatcher) and synthetic route→addon
|
|
20
|
+
bridges (`http:<method>:<route>`, no source file). `computeDiagnostics` now skips
|
|
21
|
+
any function without a real, non-generated source file, so the lint only nudges
|
|
22
|
+
hand-written user code. Also destructures the CLI's own `all` command.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [ae7fc5d]
|
|
25
|
+
- Updated dependencies [fa7a09c]
|
|
26
|
+
- @pikku/core@0.12.37
|
|
27
|
+
|
|
28
|
+
## 0.12.24
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- 5fe3f47: fix(better-auth): skip the auto-generated stateless session middleware when the
|
|
33
|
+
project registers its own. Closes #754.
|
|
34
|
+
|
|
35
|
+
With `session.cookieCache` enabled the CLI generates a global
|
|
36
|
+
`betterAuthStatelessSession()` using the default `{ userId }` map. Because session
|
|
37
|
+
middleware short-circuits once a session is set (`if (session) next()`) and the
|
|
38
|
+
generated file is imported before user wirings, that default-map middleware ran
|
|
39
|
+
first and **pre-empted** a project's own `betterAuthStatelessSession({ mapSession })`
|
|
40
|
+
— silently dropping custom session fields (`role`, `locale`, …).
|
|
41
|
+
|
|
42
|
+
The inspector now detects a user-owned global registration (a
|
|
43
|
+
`betterAuthStatelessSession(...)` call inside `addGlobalMiddleware` or the global
|
|
44
|
+
form of `addHTTPMiddleware` — the array form or the `'*'` pattern, not a
|
|
45
|
+
route-scoped `addHTTPMiddleware('/path', …)`; ignoring `.gen.ts` files and bare
|
|
46
|
+
standalone calls) and
|
|
47
|
+
sets `state.auth.userStatelessSession`. When set, the CLI skips writing
|
|
48
|
+
`auth-middleware.gen.ts` (and removes a stale one) so the project's own middleware
|
|
49
|
+
— with its custom `mapSession` — is the only one registered. Projects without a
|
|
50
|
+
custom map are unaffected: the default middleware is still generated.
|
|
51
|
+
|
|
52
|
+
- 3ba12ca: Stop consumed-addon parent services from polluting every per-unit deploy bundle, and stub the AI SDKs out of non-agent units.
|
|
53
|
+
|
|
54
|
+
`aggregateRequiredServices` added `addonRequiredParentServices` (the services a consumed addon needs from its parent — e.g. `aiAgentRunner`, `deploymentService`, `metaService`) to **every** unit's `requiredServices` unconditionally. For any project that consumes an addon, this marked those services required on all units, so the per-unit service tree-shaking (and the gen-file/module stubs that key off the `false` flags) never fired — every unit shipped the full set. These parent services are now added only to units that actually deploy an addon function (its `pikkuFuncId` appears in `usedFunctions`); a unit that only calls the addon over RPC, or never touches it, no longer carries them.
|
|
55
|
+
|
|
56
|
+
On the back of the now-honest flags, the bundler stubs the AI SDK packages (`@pikku/ai-vercel`, `@ai-sdk/*`, `ai`) out of any unit where `aiAgentRunner` is not required, via a new service→module stub map alongside the existing gen-file stub map. The shared services factory must guard runner construction behind a defined-check on the dynamic import so a stubbed unit simply skips building the runner.
|
|
57
|
+
|
|
1
58
|
## 0.12.23
|
|
2
59
|
|
|
3
60
|
### Patch Changes
|
package/dist/add/add-auth.js
CHANGED
|
@@ -71,6 +71,36 @@ const readPluginId = (el) => {
|
|
|
71
71
|
return el.expression.text;
|
|
72
72
|
return undefined;
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* True when `node` sits inside a GLOBAL middleware registration — i.e. an actual
|
|
76
|
+
* global registration, not a bare standalone call or a route-scoped one.
|
|
77
|
+
*
|
|
78
|
+
* `addGlobalMiddleware(...)` is always global. `addHTTPMiddleware` is global only
|
|
79
|
+
* in its array form (`addHTTPMiddleware([...])`) or with the `'*'` wildcard
|
|
80
|
+
* pattern; a specific route pattern (`addHTTPMiddleware('/api/admin/*', [...])`)
|
|
81
|
+
* scopes the middleware to that route and must NOT count as a global stateless
|
|
82
|
+
* registration (#754).
|
|
83
|
+
*/
|
|
84
|
+
const isInsideGlobalMiddlewareRegistration = (node) => {
|
|
85
|
+
let parent = node.parent;
|
|
86
|
+
while (parent) {
|
|
87
|
+
if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression)) {
|
|
88
|
+
const fn = parent.expression.text;
|
|
89
|
+
if (fn === 'addGlobalMiddleware')
|
|
90
|
+
return true;
|
|
91
|
+
if (fn === 'addHTTPMiddleware') {
|
|
92
|
+
const first = parent.arguments[0];
|
|
93
|
+
if (!first)
|
|
94
|
+
return false;
|
|
95
|
+
// String first arg → route pattern (global only when '*'); otherwise
|
|
96
|
+
// the array form, which is global.
|
|
97
|
+
return ts.isStringLiteral(first) ? first.text === '*' : true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
parent = parent.parent;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
};
|
|
74
104
|
/**
|
|
75
105
|
* Detects `pikkuBetterAuth((services) => betterAuth({...}))` calls.
|
|
76
106
|
*
|
|
@@ -94,6 +124,19 @@ export const addAuth = (logger, node, _checker, state) => {
|
|
|
94
124
|
if (!ts.isCallExpression(node))
|
|
95
125
|
return;
|
|
96
126
|
const expression = node.expression;
|
|
127
|
+
// A user-registered stateless session middleware (custom mapSession) means the
|
|
128
|
+
// CLI must NOT auto-generate its own default-map one — the generated one runs
|
|
129
|
+
// first and pre-empts the user's via the `if (session) next()` short-circuit
|
|
130
|
+
// (pikkujs/pikku#754). Only a GLOBAL registration counts (inside
|
|
131
|
+
// addHTTPMiddleware/addGlobalMiddleware) — a bare betterAuthStatelessSession()
|
|
132
|
+
// call (e.g. a test harness) is not a registration. Ignore generated files so
|
|
133
|
+
// the emitted middleware can't self-trigger the skip.
|
|
134
|
+
if (ts.isIdentifier(expression) &&
|
|
135
|
+
expression.text === 'betterAuthStatelessSession' &&
|
|
136
|
+
!node.getSourceFile().fileName.endsWith('.gen.ts') &&
|
|
137
|
+
isInsideGlobalMiddlewareRegistration(node)) {
|
|
138
|
+
state.auth.userStatelessSession = true;
|
|
139
|
+
}
|
|
97
140
|
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
|
|
98
141
|
return;
|
|
99
142
|
const sourceFile = node.getSourceFile().fileName;
|
package/dist/add/add-gateway.js
CHANGED
|
@@ -23,6 +23,8 @@ export const addGateway = (logger, node, checker, state, _options) => {
|
|
|
23
23
|
const nameValue = getPropertyValue(obj, 'name');
|
|
24
24
|
const typeValue = getPropertyValue(obj, 'type');
|
|
25
25
|
const routeValue = getPropertyValue(obj, 'route');
|
|
26
|
+
const platformValue = getPropertyValue(obj, 'platform');
|
|
27
|
+
const authValue = getPropertyValue(obj, 'auth');
|
|
26
28
|
const { disabled, tags, summary, description, errors } = getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker);
|
|
27
29
|
if (disabled)
|
|
28
30
|
return;
|
|
@@ -51,7 +53,9 @@ export const addGateway = (logger, node, checker, state, _options) => {
|
|
|
51
53
|
...(packageName && { packageName }),
|
|
52
54
|
name: nameValue,
|
|
53
55
|
type: typeValue,
|
|
54
|
-
route: routeValue,
|
|
56
|
+
...(routeValue && { route: routeValue }),
|
|
57
|
+
...(platformValue && { platform: platformValue }),
|
|
58
|
+
...(typeof authValue === 'boolean' && { auth: authValue }),
|
|
55
59
|
gateway: true,
|
|
56
60
|
summary,
|
|
57
61
|
description,
|
|
@@ -141,7 +141,11 @@ export const addPermission = (logger, node, checker, state) => {
|
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
143
|
const services = extractServicesFromFunction(actualHandler);
|
|
144
|
-
|
|
144
|
+
// pikkuAuth's handler is (services, session) — its second parameter is the
|
|
145
|
+
// resolved user session, NOT a wires bag, so it must not be analyzed (or
|
|
146
|
+
// flagged) as a non-destructured wires parameter. pikkuAuth exposes no
|
|
147
|
+
// user-facing wires parameter.
|
|
148
|
+
const wires = { optimized: true, wires: [] };
|
|
145
149
|
let { pikkuFuncId, exportedName } = extractFunctionName(node, checker, state.rootDir);
|
|
146
150
|
if (pikkuFuncId.startsWith('__temp_')) {
|
|
147
151
|
if (ts.isVariableDeclaration(node.parent) &&
|
package/dist/types.d.ts
CHANGED
|
@@ -431,6 +431,11 @@ export interface InspectorState {
|
|
|
431
431
|
* codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
|
|
432
432
|
* More than one `pikkuBetterAuth` is a critical error. */
|
|
433
433
|
definition: AuthDefinition | null;
|
|
434
|
+
/** True when a user (non-generated) file already registers
|
|
435
|
+
* `betterAuthStatelessSession(...)`. The CLI then skips auto-generating its
|
|
436
|
+
* own default-map stateless middleware, which would otherwise pre-empt the
|
|
437
|
+
* user's custom mapSession (pikkujs/pikku#754). */
|
|
438
|
+
userStatelessSession?: boolean;
|
|
434
439
|
};
|
|
435
440
|
secrets: {
|
|
436
441
|
definitions: SecretDefinitions;
|
|
@@ -245,9 +245,22 @@ export function aggregateRequiredServices(state) {
|
|
|
245
245
|
if (Object.keys(state.channels.meta).length > 0) {
|
|
246
246
|
requiredServices.add('eventHub');
|
|
247
247
|
}
|
|
248
|
-
// 7. Services that addons need from the parent project
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
// 7. Services that consumed addons need from the parent project.
|
|
249
|
+
// These are required ONLY by units that actually deploy an addon function;
|
|
250
|
+
// a unit that merely calls the addon over RPC (or never touches it) must not
|
|
251
|
+
// carry them, or every per-unit bundle would over-include the addon's
|
|
252
|
+
// parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
|
|
253
|
+
// defeat per-unit tree-shaking.
|
|
254
|
+
const addonFuncIds = new Set();
|
|
255
|
+
for (const fns of Object.values(state.addonFunctions ?? {})) {
|
|
256
|
+
for (const id of Object.keys(fns))
|
|
257
|
+
addonFuncIds.add(id);
|
|
258
|
+
}
|
|
259
|
+
const unitDeploysAddonFn = [...usedFunctions].some((fn) => addonFuncIds.has(fn));
|
|
260
|
+
if (unitDeploysAddonFn) {
|
|
261
|
+
for (const service of state.addonRequiredParentServices ?? []) {
|
|
262
|
+
requiredServices.add(service);
|
|
263
|
+
}
|
|
251
264
|
}
|
|
252
265
|
}
|
|
253
266
|
export function validateSecretOverrides(logger, state) {
|
|
@@ -512,6 +525,14 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
512
525
|
export function computeDiagnostics(state) {
|
|
513
526
|
const diagnostics = [];
|
|
514
527
|
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
528
|
+
// Skip framework-synthesized functions: generated wrappers (auth.gen.ts's
|
|
529
|
+
// opaque authHandler, the cli channel's raw dispatcher) and synthetic route
|
|
530
|
+
// bridges that reference addon functions (id `http:<method>:<route>`, no
|
|
531
|
+
// source file). The user can't edit any of these, so a destructure lint
|
|
532
|
+
// meant to nudge them about their own code must not fail the build over them.
|
|
533
|
+
if (!meta.sourceFile || meta.sourceFile.endsWith('.gen.ts')) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
515
536
|
if (meta.services && !meta.services.optimized) {
|
|
516
537
|
diagnostics.push({
|
|
517
538
|
code: ErrorCode.SERVICES_NOT_DESTRUCTURED,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.25",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
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.37",
|
|
39
39
|
"openapi-types": "^12.1.3",
|
|
40
40
|
"path-to-regexp": "^8.3.0",
|
|
41
41
|
"ts-json-schema-generator": "^2.5.0",
|
package/src/add/add-auth.test.ts
CHANGED
|
@@ -522,4 +522,98 @@ describe('addAuth inspector', () => {
|
|
|
522
522
|
await rm(rootDir, { recursive: true, force: true })
|
|
523
523
|
}
|
|
524
524
|
})
|
|
525
|
+
|
|
526
|
+
test('user-registered betterAuthStatelessSession sets userStatelessSession', async () => {
|
|
527
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-stateless-'))
|
|
528
|
+
const file = join(rootDir, 'middleware.ts')
|
|
529
|
+
await writeFile(
|
|
530
|
+
file,
|
|
531
|
+
[
|
|
532
|
+
"import { addHTTPMiddleware } from '#pikku'",
|
|
533
|
+
"import { betterAuthStatelessSession } from '@pikku/better-auth'",
|
|
534
|
+
"addHTTPMiddleware('*', [",
|
|
535
|
+
' betterAuthStatelessSession({',
|
|
536
|
+
' mapSession: (r: any) => ({ userId: r.user.id, role: r.user.role }),',
|
|
537
|
+
' }),',
|
|
538
|
+
'])',
|
|
539
|
+
].join('\n')
|
|
540
|
+
)
|
|
541
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
542
|
+
try {
|
|
543
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
544
|
+
assert.equal(state.auth.userStatelessSession, true)
|
|
545
|
+
} finally {
|
|
546
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
547
|
+
}
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
test('a standalone betterAuthStatelessSession() call (not a registration) does NOT set the flag', async () => {
|
|
551
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-standalone-'))
|
|
552
|
+
const file = join(rootDir, 'start.ts')
|
|
553
|
+
await writeFile(
|
|
554
|
+
file,
|
|
555
|
+
[
|
|
556
|
+
"import { betterAuthStatelessSession } from '@pikku/better-auth'",
|
|
557
|
+
'// harness use, not a global registration',
|
|
558
|
+
'const mw = betterAuthStatelessSession()',
|
|
559
|
+
'void mw',
|
|
560
|
+
].join('\n')
|
|
561
|
+
)
|
|
562
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
563
|
+
try {
|
|
564
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
565
|
+
assert.ok(
|
|
566
|
+
!state.auth.userStatelessSession,
|
|
567
|
+
'a bare call must not count as a registration'
|
|
568
|
+
)
|
|
569
|
+
} finally {
|
|
570
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
test('a route-scoped addHTTPMiddleware registration does NOT set the flag', async () => {
|
|
575
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-scoped-'))
|
|
576
|
+
const file = join(rootDir, 'middleware.ts')
|
|
577
|
+
await writeFile(
|
|
578
|
+
file,
|
|
579
|
+
[
|
|
580
|
+
"import { addHTTPMiddleware } from '#pikku'",
|
|
581
|
+
"import { betterAuthStatelessSession } from '@pikku/better-auth'",
|
|
582
|
+
"addHTTPMiddleware('/api/admin/*', [betterAuthStatelessSession()])",
|
|
583
|
+
].join('\n')
|
|
584
|
+
)
|
|
585
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
586
|
+
try {
|
|
587
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
588
|
+
assert.ok(
|
|
589
|
+
!state.auth.userStatelessSession,
|
|
590
|
+
'a route-scoped registration must not suppress the global generated middleware'
|
|
591
|
+
)
|
|
592
|
+
} finally {
|
|
593
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
test('betterAuthStatelessSession in a .gen.ts file does NOT set the flag', async () => {
|
|
598
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-genonly-'))
|
|
599
|
+
const file = join(rootDir, 'auth-middleware.gen.ts')
|
|
600
|
+
await writeFile(
|
|
601
|
+
file,
|
|
602
|
+
[
|
|
603
|
+
"import { addHTTPMiddleware } from '#pikku'",
|
|
604
|
+
"import { betterAuthStatelessSession } from '@pikku/better-auth'",
|
|
605
|
+
"addHTTPMiddleware('*', [betterAuthStatelessSession()])",
|
|
606
|
+
].join('\n')
|
|
607
|
+
)
|
|
608
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
609
|
+
try {
|
|
610
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
611
|
+
assert.ok(
|
|
612
|
+
!state.auth.userStatelessSession,
|
|
613
|
+
'generated file must not self-trigger the skip'
|
|
614
|
+
)
|
|
615
|
+
} finally {
|
|
616
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
617
|
+
}
|
|
618
|
+
})
|
|
525
619
|
})
|
package/src/add/add-auth.ts
CHANGED
|
@@ -99,6 +99,35 @@ const readPluginId = (el: ts.Expression): string | undefined => {
|
|
|
99
99
|
return undefined
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* True when `node` sits inside a GLOBAL middleware registration — i.e. an actual
|
|
104
|
+
* global registration, not a bare standalone call or a route-scoped one.
|
|
105
|
+
*
|
|
106
|
+
* `addGlobalMiddleware(...)` is always global. `addHTTPMiddleware` is global only
|
|
107
|
+
* in its array form (`addHTTPMiddleware([...])`) or with the `'*'` wildcard
|
|
108
|
+
* pattern; a specific route pattern (`addHTTPMiddleware('/api/admin/*', [...])`)
|
|
109
|
+
* scopes the middleware to that route and must NOT count as a global stateless
|
|
110
|
+
* registration (#754).
|
|
111
|
+
*/
|
|
112
|
+
const isInsideGlobalMiddlewareRegistration = (node: ts.Node): boolean => {
|
|
113
|
+
let parent: ts.Node | undefined = node.parent
|
|
114
|
+
while (parent) {
|
|
115
|
+
if (ts.isCallExpression(parent) && ts.isIdentifier(parent.expression)) {
|
|
116
|
+
const fn = parent.expression.text
|
|
117
|
+
if (fn === 'addGlobalMiddleware') return true
|
|
118
|
+
if (fn === 'addHTTPMiddleware') {
|
|
119
|
+
const first = parent.arguments[0]
|
|
120
|
+
if (!first) return false
|
|
121
|
+
// String first arg → route pattern (global only when '*'); otherwise
|
|
122
|
+
// the array form, which is global.
|
|
123
|
+
return ts.isStringLiteral(first) ? first.text === '*' : true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
parent = parent.parent
|
|
127
|
+
}
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
/**
|
|
103
132
|
* Detects `pikkuBetterAuth((services) => betterAuth({...}))` calls.
|
|
104
133
|
*
|
|
@@ -122,6 +151,23 @@ export const addAuth: AddWiring = (logger, node, _checker, state) => {
|
|
|
122
151
|
if (!ts.isCallExpression(node)) return
|
|
123
152
|
|
|
124
153
|
const expression = node.expression
|
|
154
|
+
|
|
155
|
+
// A user-registered stateless session middleware (custom mapSession) means the
|
|
156
|
+
// CLI must NOT auto-generate its own default-map one — the generated one runs
|
|
157
|
+
// first and pre-empts the user's via the `if (session) next()` short-circuit
|
|
158
|
+
// (pikkujs/pikku#754). Only a GLOBAL registration counts (inside
|
|
159
|
+
// addHTTPMiddleware/addGlobalMiddleware) — a bare betterAuthStatelessSession()
|
|
160
|
+
// call (e.g. a test harness) is not a registration. Ignore generated files so
|
|
161
|
+
// the emitted middleware can't self-trigger the skip.
|
|
162
|
+
if (
|
|
163
|
+
ts.isIdentifier(expression) &&
|
|
164
|
+
expression.text === 'betterAuthStatelessSession' &&
|
|
165
|
+
!node.getSourceFile().fileName.endsWith('.gen.ts') &&
|
|
166
|
+
isInsideGlobalMiddlewareRegistration(node)
|
|
167
|
+
) {
|
|
168
|
+
state.auth.userStatelessSession = true
|
|
169
|
+
}
|
|
170
|
+
|
|
125
171
|
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
|
|
126
172
|
return
|
|
127
173
|
|
package/src/add/add-gateway.ts
CHANGED
|
@@ -43,7 +43,9 @@ export const addGateway: AddWiring = (
|
|
|
43
43
|
|
|
44
44
|
const nameValue = getPropertyValue(obj, 'name') as string | null
|
|
45
45
|
const typeValue = getPropertyValue(obj, 'type') as GatewayTransportType | null
|
|
46
|
-
const routeValue = getPropertyValue(obj, 'route') as string |
|
|
46
|
+
const routeValue = getPropertyValue(obj, 'route') as string | null
|
|
47
|
+
const platformValue = getPropertyValue(obj, 'platform') as string | null
|
|
48
|
+
const authValue = getPropertyValue(obj, 'auth')
|
|
47
49
|
const { disabled, tags, summary, description, errors } =
|
|
48
50
|
getCommonWireMetaData(obj, 'Gateway', nameValue, logger, checker)
|
|
49
51
|
|
|
@@ -94,7 +96,9 @@ export const addGateway: AddWiring = (
|
|
|
94
96
|
...(packageName && { packageName }),
|
|
95
97
|
name: nameValue,
|
|
96
98
|
type: typeValue,
|
|
97
|
-
route: routeValue,
|
|
99
|
+
...(routeValue && { route: routeValue }),
|
|
100
|
+
...(platformValue && { platform: platformValue }),
|
|
101
|
+
...(typeof authValue === 'boolean' && { auth: authValue }),
|
|
98
102
|
gateway: true,
|
|
99
103
|
summary,
|
|
100
104
|
description,
|
|
@@ -0,0 +1,59 @@
|
|
|
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 { ErrorCode } from '../error-codes.js'
|
|
8
|
+
import type { InspectorLogger } from '../types.js'
|
|
9
|
+
|
|
10
|
+
const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
|
|
11
|
+
({
|
|
12
|
+
debug: () => {},
|
|
13
|
+
info: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
error: () => {},
|
|
16
|
+
diagnostic: ({ code, message }) => {
|
|
17
|
+
criticals.push({ code, message })
|
|
18
|
+
},
|
|
19
|
+
critical: (code: ErrorCode, message: string) => {
|
|
20
|
+
criticals.push({ code, message })
|
|
21
|
+
},
|
|
22
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
23
|
+
}) satisfies InspectorLogger
|
|
24
|
+
|
|
25
|
+
describe('addPermission — pikkuAuth', () => {
|
|
26
|
+
test('does not record a wires meta for the session parameter', async () => {
|
|
27
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-auth-wires-'))
|
|
28
|
+
const file = join(rootDir, 'auth.ts')
|
|
29
|
+
|
|
30
|
+
await writeFile(
|
|
31
|
+
file,
|
|
32
|
+
[
|
|
33
|
+
'const pikkuAuth = (x: any) => x',
|
|
34
|
+
'export const isAuthenticated = pikkuAuth(async ({ logger }, session) => {',
|
|
35
|
+
' logger.info({ type: "auth-check" })',
|
|
36
|
+
' return !!session',
|
|
37
|
+
'})',
|
|
38
|
+
].join('\n')
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
42
|
+
try {
|
|
43
|
+
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
44
|
+
const def = state.permissions.definitions['isAuthenticated']
|
|
45
|
+
assert.ok(def, 'isAuthenticated permission should be recorded')
|
|
46
|
+
// The pikkuAuth handler is (services, session) — session is NOT a wires
|
|
47
|
+
// bag and must not be flagged as a non-destructured wires parameter.
|
|
48
|
+
assert.equal(def.wires, undefined)
|
|
49
|
+
const wireDiag = (state.diagnostics ?? []).find(
|
|
50
|
+
(d) =>
|
|
51
|
+
d.code === ErrorCode.WIRES_NOT_DESTRUCTURED &&
|
|
52
|
+
d.message.includes('isAuthenticated')
|
|
53
|
+
)
|
|
54
|
+
assert.equal(wireDiag, undefined)
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
+
import type { FunctionWiresMeta } from '@pikku/core'
|
|
2
3
|
import type { AddWiring, InspectorState } from '../types.js'
|
|
3
4
|
import {
|
|
4
5
|
extractFunctionName,
|
|
@@ -195,7 +196,11 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
const services = extractServicesFromFunction(actualHandler)
|
|
198
|
-
|
|
199
|
+
// pikkuAuth's handler is (services, session) — its second parameter is the
|
|
200
|
+
// resolved user session, NOT a wires bag, so it must not be analyzed (or
|
|
201
|
+
// flagged) as a non-destructured wires parameter. pikkuAuth exposes no
|
|
202
|
+
// user-facing wires parameter.
|
|
203
|
+
const wires: FunctionWiresMeta = { optimized: true, wires: [] }
|
|
199
204
|
let { pikkuFuncId, exportedName } = extractFunctionName(
|
|
200
205
|
node,
|
|
201
206
|
checker,
|
package/src/types.ts
CHANGED
|
@@ -497,6 +497,11 @@ export interface InspectorState {
|
|
|
497
497
|
* codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
|
|
498
498
|
* More than one `pikkuBetterAuth` is a critical error. */
|
|
499
499
|
definition: AuthDefinition | null
|
|
500
|
+
/** True when a user (non-generated) file already registers
|
|
501
|
+
* `betterAuthStatelessSession(...)`. The CLI then skips auto-generating its
|
|
502
|
+
* own default-map stateless middleware, which would otherwise pre-empt the
|
|
503
|
+
* user's custom mapSession (pikkujs/pikku#754). */
|
|
504
|
+
userStatelessSession?: boolean
|
|
500
505
|
}
|
|
501
506
|
secrets: {
|
|
502
507
|
definitions: SecretDefinitions
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import { computeDiagnostics } from './post-process.js'
|
|
4
|
+
import type { InspectorState } from '../types.js'
|
|
5
|
+
import { ErrorCode } from '../error-codes.js'
|
|
6
|
+
|
|
7
|
+
function stateWithFunctions(
|
|
8
|
+
meta: InspectorState['functions']['meta']
|
|
9
|
+
): InspectorState {
|
|
10
|
+
return {
|
|
11
|
+
functions: { meta },
|
|
12
|
+
middleware: { definitions: {} },
|
|
13
|
+
permissions: { definitions: {} },
|
|
14
|
+
} as unknown as InspectorState
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('computeDiagnostics', () => {
|
|
18
|
+
test('flags a user-authored function that does not destructure services', () => {
|
|
19
|
+
const state = stateWithFunctions({
|
|
20
|
+
myFunc: {
|
|
21
|
+
pikkuFuncId: 'myFunc',
|
|
22
|
+
inputSchemaName: null,
|
|
23
|
+
outputSchemaName: null,
|
|
24
|
+
sourceFile: '/project/src/my-func.ts',
|
|
25
|
+
services: { optimized: false, services: ['kysely'] },
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
computeDiagnostics(state)
|
|
29
|
+
assert.equal(state.diagnostics.length, 1)
|
|
30
|
+
assert.equal(
|
|
31
|
+
state.diagnostics[0].code,
|
|
32
|
+
ErrorCode.SERVICES_NOT_DESTRUCTURED
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('does NOT flag a generated .gen.ts function (user cannot edit it)', () => {
|
|
37
|
+
const state = stateWithFunctions({
|
|
38
|
+
cliRaw: {
|
|
39
|
+
pikkuFuncId: 'cliRaw',
|
|
40
|
+
inputSchemaName: null,
|
|
41
|
+
outputSchemaName: null,
|
|
42
|
+
sourceFile: '/project/src/wirings/cli-channel.gen.ts',
|
|
43
|
+
services: { optimized: false, services: ['kysely'] },
|
|
44
|
+
},
|
|
45
|
+
authHandler: {
|
|
46
|
+
pikkuFuncId: 'authHandler',
|
|
47
|
+
inputSchemaName: null,
|
|
48
|
+
outputSchemaName: null,
|
|
49
|
+
sourceFile: '/project/.pikku/auth.gen.ts',
|
|
50
|
+
wires: { optimized: false, wires: ['http'] },
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
computeDiagnostics(state)
|
|
54
|
+
assert.equal(state.diagnostics.length, 0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('does NOT flag a synthetic route bridge with no source file', () => {
|
|
58
|
+
const state = stateWithFunctions({
|
|
59
|
+
'http:get:/workflow-run/:runId/stream': {
|
|
60
|
+
pikkuFuncId: 'http:get:/workflow-run/:runId/stream',
|
|
61
|
+
inputSchemaName: null,
|
|
62
|
+
outputSchemaName: null,
|
|
63
|
+
services: { optimized: false, services: [] },
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
computeDiagnostics(state)
|
|
67
|
+
assert.equal(state.diagnostics.length, 0)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -317,9 +317,23 @@ export function aggregateRequiredServices(
|
|
|
317
317
|
requiredServices.add('eventHub')
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
// 7. Services that addons need from the parent project
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
// 7. Services that consumed addons need from the parent project.
|
|
321
|
+
// These are required ONLY by units that actually deploy an addon function;
|
|
322
|
+
// a unit that merely calls the addon over RPC (or never touches it) must not
|
|
323
|
+
// carry them, or every per-unit bundle would over-include the addon's
|
|
324
|
+
// parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
|
|
325
|
+
// defeat per-unit tree-shaking.
|
|
326
|
+
const addonFuncIds = new Set<string>()
|
|
327
|
+
for (const fns of Object.values(state.addonFunctions ?? {})) {
|
|
328
|
+
for (const id of Object.keys(fns)) addonFuncIds.add(id)
|
|
329
|
+
}
|
|
330
|
+
const unitDeploysAddonFn = [...usedFunctions].some((fn) =>
|
|
331
|
+
addonFuncIds.has(fn)
|
|
332
|
+
)
|
|
333
|
+
if (unitDeploysAddonFn) {
|
|
334
|
+
for (const service of state.addonRequiredParentServices ?? []) {
|
|
335
|
+
requiredServices.add(service)
|
|
336
|
+
}
|
|
323
337
|
}
|
|
324
338
|
}
|
|
325
339
|
|
|
@@ -647,6 +661,14 @@ export function computeDiagnostics(state: InspectorState): void {
|
|
|
647
661
|
const diagnostics: InspectorDiagnostic[] = []
|
|
648
662
|
|
|
649
663
|
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
664
|
+
// Skip framework-synthesized functions: generated wrappers (auth.gen.ts's
|
|
665
|
+
// opaque authHandler, the cli channel's raw dispatcher) and synthetic route
|
|
666
|
+
// bridges that reference addon functions (id `http:<method>:<route>`, no
|
|
667
|
+
// source file). The user can't edit any of these, so a destructure lint
|
|
668
|
+
// meant to nudge them about their own code must not fail the build over them.
|
|
669
|
+
if (!meta.sourceFile || meta.sourceFile.endsWith('.gen.ts')) {
|
|
670
|
+
continue
|
|
671
|
+
}
|
|
650
672
|
if (meta.services && !meta.services.optimized) {
|
|
651
673
|
diagnostics.push({
|
|
652
674
|
code: ErrorCode.SERVICES_NOT_DESTRUCTURED,
|