@pikku/inspector 0.12.22 → 0.12.24
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 +40 -0
- package/dist/add/add-addon-bans.d.ts +7 -0
- package/dist/add/add-addon-bans.js +65 -0
- package/dist/add/add-auth.js +43 -0
- package/dist/add/add-channel.js +47 -6
- package/dist/add/add-cli.js +17 -0
- package/dist/add/add-http-route.d.ts +11 -1
- package/dist/add/add-http-route.js +37 -0
- package/dist/add/add-http-routes.d.ts +0 -3
- package/dist/add/add-http-routes.js +179 -36
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -0
- package/dist/inspector.js +17 -5
- package/dist/types.d.ts +48 -1
- package/dist/utils/get-exported-variable-name.d.ts +2 -0
- package/dist/utils/get-exported-variable-name.js +34 -0
- package/dist/utils/load-addon-functions-meta.js +98 -0
- package/dist/utils/post-process.js +16 -3
- package/dist/utils/resolve-addon-package.js +3 -1
- package/dist/utils/resolve-ref-contract.d.ts +21 -0
- package/dist/utils/resolve-ref-contract.js +46 -0
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +9 -0
- package/dist/visit.js +24 -19
- package/package.json +1 -1
- package/src/add/add-addon-bans.ts +84 -0
- package/src/add/add-auth.test.ts +94 -0
- package/src/add/add-auth.ts +46 -0
- package/src/add/add-channel.ts +66 -7
- package/src/add/add-cli.ts +30 -0
- package/src/add/add-http-route.ts +75 -1
- package/src/add/add-http-routes.ts +283 -41
- package/src/add/addon-bans.test.ts +121 -0
- package/src/add/addon-contracts.test.ts +221 -0
- package/src/error-codes.ts +4 -0
- package/src/inspector.ts +17 -5
- package/src/types.ts +70 -1
- package/src/utils/get-exported-variable-name.ts +48 -0
- package/src/utils/load-addon-functions-meta.ts +164 -0
- package/src/utils/post-process.ts +17 -3
- package/src/utils/resolve-addon-package.ts +6 -1
- package/src/utils/resolve-ref-contract.ts +71 -0
- package/src/utils/serialize-inspector-state.ts +10 -0
- package/src/visit.ts +26 -19
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
export interface RefContractResolution<T> {
|
|
3
|
+
contract: T;
|
|
4
|
+
/**
|
|
5
|
+
* Optional basePath override supplied by the consumer via the second
|
|
6
|
+
* argument, e.g. refHTTP('ns:routes', { basePath: '/ext' }). When undefined
|
|
7
|
+
* the addon contract's own basePath is preserved.
|
|
8
|
+
*/
|
|
9
|
+
basePath?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a refHTTP / refChannel / refCLI call expression against the addon
|
|
13
|
+
* contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
|
|
14
|
+
*
|
|
15
|
+
* The first string argument has the form 'namespace:contractName', mirroring
|
|
16
|
+
* how ref('namespace:fn') references an addon function. Detection is purely
|
|
17
|
+
* syntactic — no import resolution is required because the namespace and
|
|
18
|
+
* contract name are carried in the string literal. An optional second object
|
|
19
|
+
* argument may override mount details such as basePath.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolveRefContract: <T>(node: ts.Node, helperName: "refHTTP" | "refChannel" | "refCLI", addonContracts: Record<string, Record<string, T>>) => RefContractResolution<T> | null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
const getStringProperty = (obj, name) => {
|
|
3
|
+
for (const prop of obj.properties) {
|
|
4
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
5
|
+
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
|
|
6
|
+
prop.name.text === name &&
|
|
7
|
+
ts.isStringLiteral(prop.initializer)) {
|
|
8
|
+
return prop.initializer.text;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a refHTTP / refChannel / refCLI call expression against the addon
|
|
15
|
+
* contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
|
|
16
|
+
*
|
|
17
|
+
* The first string argument has the form 'namespace:contractName', mirroring
|
|
18
|
+
* how ref('namespace:fn') references an addon function. Detection is purely
|
|
19
|
+
* syntactic — no import resolution is required because the namespace and
|
|
20
|
+
* contract name are carried in the string literal. An optional second object
|
|
21
|
+
* argument may override mount details such as basePath.
|
|
22
|
+
*/
|
|
23
|
+
export const resolveRefContract = (node, helperName, addonContracts) => {
|
|
24
|
+
if (!ts.isCallExpression(node))
|
|
25
|
+
return null;
|
|
26
|
+
if (!ts.isIdentifier(node.expression) ||
|
|
27
|
+
node.expression.text !== helperName) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const [arg, optionsArg] = node.arguments;
|
|
31
|
+
if (!arg || !ts.isStringLiteral(arg))
|
|
32
|
+
return null;
|
|
33
|
+
const separator = arg.text.indexOf(':');
|
|
34
|
+
if (separator === -1)
|
|
35
|
+
return null;
|
|
36
|
+
const namespace = arg.text.slice(0, separator);
|
|
37
|
+
const contractName = arg.text.slice(separator + 1);
|
|
38
|
+
const contract = addonContracts[namespace]?.[contractName];
|
|
39
|
+
if (contract === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
let basePath;
|
|
42
|
+
if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) {
|
|
43
|
+
basePath = getStringProperty(optionsArg, 'basePath');
|
|
44
|
+
}
|
|
45
|
+
return { contract, basePath };
|
|
46
|
+
};
|
|
@@ -267,6 +267,7 @@ export interface SerializableInspectorState {
|
|
|
267
267
|
openAPISpec: Record<string, any> | null;
|
|
268
268
|
diagnostics: InspectorDiagnostic[];
|
|
269
269
|
addonFunctions: InspectorState['addonFunctions'];
|
|
270
|
+
exportedContracts: InspectorState['exportedContracts'];
|
|
270
271
|
}
|
|
271
272
|
/**
|
|
272
273
|
* Serializes InspectorState to a JSON-compatible format
|
|
@@ -150,6 +150,7 @@ export function serializeInspectorState(state) {
|
|
|
150
150
|
openAPISpec: state.openAPISpec,
|
|
151
151
|
diagnostics: state.diagnostics,
|
|
152
152
|
addonFunctions: state.addonFunctions,
|
|
153
|
+
exportedContracts: state.exportedContracts,
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
@@ -314,6 +315,14 @@ export function deserializeInspectorState(data) {
|
|
|
314
315
|
openAPISpec: data.openAPISpec || null,
|
|
315
316
|
diagnostics: data.diagnostics || [],
|
|
316
317
|
addonFunctions: data.addonFunctions || {},
|
|
318
|
+
exportedContracts: data.exportedContracts || {
|
|
319
|
+
http: {},
|
|
320
|
+
cli: {},
|
|
321
|
+
channel: {},
|
|
322
|
+
addonHttp: {},
|
|
323
|
+
addonCli: {},
|
|
324
|
+
addonChannel: {},
|
|
325
|
+
},
|
|
317
326
|
program: null,
|
|
318
327
|
};
|
|
319
328
|
}
|
package/dist/visit.js
CHANGED
|
@@ -3,6 +3,7 @@ import { addFileWithFactory } from './add/add-file-with-factory.js';
|
|
|
3
3
|
import { addFileExtendsCoreType } from './add/add-file-extends-core-type.js';
|
|
4
4
|
import { addHTTPRoute } from './add/add-http-route.js';
|
|
5
5
|
import { addHTTPRoutes } from './add/add-http-routes.js';
|
|
6
|
+
import { checkAddonBans } from './add/add-addon-bans.js';
|
|
6
7
|
import { addSchedule } from './add/add-schedule.js';
|
|
7
8
|
import { addTrigger } from './add/add-trigger.js';
|
|
8
9
|
import { addQueueWorker } from './add/add-queue-worker.js';
|
|
@@ -41,23 +42,27 @@ export const visitSetup = (logger, checker, node, state, options) => {
|
|
|
41
42
|
ts.forEachChild(node, (child) => visitSetup(logger, checker, child, state, options));
|
|
42
43
|
};
|
|
43
44
|
export const visitRoutes = (logger, checker, node, state, options) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
const nextOptions = ts.isSourceFile(node)
|
|
46
|
+
? { ...options, sourceFile: node }
|
|
47
|
+
: options;
|
|
48
|
+
checkAddonBans(logger, node, checker, state, nextOptions);
|
|
49
|
+
addFunctions(logger, node, checker, state, nextOptions);
|
|
50
|
+
addAuth(logger, node, checker, state, nextOptions);
|
|
51
|
+
addSecret(logger, node, checker, state, nextOptions);
|
|
52
|
+
addCredential(logger, node, checker, state, nextOptions);
|
|
53
|
+
addVariable(logger, node, checker, state, nextOptions);
|
|
54
|
+
addHTTPRoute(logger, node, checker, state, nextOptions);
|
|
55
|
+
addHTTPRoutes(logger, node, checker, state, nextOptions);
|
|
56
|
+
addSchedule(logger, node, checker, state, nextOptions);
|
|
57
|
+
addTrigger(logger, node, checker, state, nextOptions);
|
|
58
|
+
addQueueWorker(logger, node, checker, state, nextOptions);
|
|
59
|
+
addChannel(logger, node, checker, state, nextOptions);
|
|
60
|
+
addGateway(logger, node, checker, state, nextOptions);
|
|
61
|
+
addCLI(logger, node, checker, state, nextOptions);
|
|
62
|
+
addCLIRenderers(logger, node, checker, state, nextOptions);
|
|
63
|
+
addMCPResource(logger, node, checker, state, nextOptions);
|
|
64
|
+
addMCPPrompt(logger, node, checker, state, nextOptions);
|
|
65
|
+
addWorkflowGraph(logger, node, checker, state, nextOptions);
|
|
66
|
+
addAIAgent(logger, node, checker, state, nextOptions);
|
|
67
|
+
ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, nextOptions));
|
|
63
68
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import type { AddWiring } from '../types.js'
|
|
3
|
+
import { ErrorCode } from '../error-codes.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wiring helpers an addon must not call. Addons declare contracts with the
|
|
7
|
+
* define* helpers and export functions; the consuming app does the wiring via
|
|
8
|
+
* refHTTP / refChannel / refCLI. Service declarations remain allowed.
|
|
9
|
+
*/
|
|
10
|
+
const BANNED_WIRINGS = new Set([
|
|
11
|
+
'wireAddon',
|
|
12
|
+
'wireChannel',
|
|
13
|
+
'wireCLI',
|
|
14
|
+
'wireGateway',
|
|
15
|
+
'wireHTTP',
|
|
16
|
+
'wireHTTPRoutes',
|
|
17
|
+
'wireMCPPrompt',
|
|
18
|
+
'wireMCPResource',
|
|
19
|
+
'wireQueueWorker',
|
|
20
|
+
'wireScheduler',
|
|
21
|
+
'wireTrigger',
|
|
22
|
+
'wireTriggerSource',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const CONTRACT_DEFINERS = new Set([
|
|
26
|
+
'defineHTTPRoutes',
|
|
27
|
+
'defineChannelRoutes',
|
|
28
|
+
'defineCLICommands',
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const hasHandlerProperty = (node: ts.Node): boolean => {
|
|
32
|
+
let found = false
|
|
33
|
+
const visit = (current: ts.Node) => {
|
|
34
|
+
if (found) return
|
|
35
|
+
if (
|
|
36
|
+
ts.isPropertyAssignment(current) &&
|
|
37
|
+
(ts.isIdentifier(current.name) || ts.isStringLiteral(current.name)) &&
|
|
38
|
+
(current.name.text === 'middleware' ||
|
|
39
|
+
current.name.text === 'permissions')
|
|
40
|
+
) {
|
|
41
|
+
found = true
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
ts.forEachChild(current, visit)
|
|
45
|
+
}
|
|
46
|
+
visit(node)
|
|
47
|
+
return found
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Enforce addon authoring rules. Only runs when inspecting an addon package
|
|
52
|
+
* (options.isAddon). Addons cannot wire transports, and their contracts cannot
|
|
53
|
+
* carry middleware or permissions — those are the consuming app's concern.
|
|
54
|
+
*/
|
|
55
|
+
export const checkAddonBans: AddWiring = (
|
|
56
|
+
logger,
|
|
57
|
+
node,
|
|
58
|
+
_checker,
|
|
59
|
+
_state,
|
|
60
|
+
options
|
|
61
|
+
) => {
|
|
62
|
+
if (!options.isAddon) return
|
|
63
|
+
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) return
|
|
64
|
+
|
|
65
|
+
const name = node.expression.text
|
|
66
|
+
|
|
67
|
+
if (BANNED_WIRINGS.has(name)) {
|
|
68
|
+
logger.critical(
|
|
69
|
+
ErrorCode.ADDON_WIRING_NOT_ALLOWED,
|
|
70
|
+
`Addons must not call '${name}'. Declare contracts with define* and export functions; the consuming app wires them via refHTTP / refChannel / refCLI.`
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (CONTRACT_DEFINERS.has(name)) {
|
|
76
|
+
const [arg] = node.arguments
|
|
77
|
+
if (arg && hasHandlerProperty(arg)) {
|
|
78
|
+
logger.critical(
|
|
79
|
+
ErrorCode.ADDON_CONTRACT_HANDLERS_NOT_ALLOWED,
|
|
80
|
+
`Addon contract '${name}' must not declare middleware or permissions — these are applied by the consuming app, not the addon.`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
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-channel.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { resolveIdentifier } from '../utils/resolve-identifier.js'
|
|
|
21
21
|
import { resolveFunctionMeta } from '../utils/resolve-function-meta.js'
|
|
22
22
|
import { resolveAddonName } from '../utils/resolve-addon-package.js'
|
|
23
23
|
import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js'
|
|
24
|
+
import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
|
|
25
|
+
import { resolveRefContract } from '../utils/resolve-ref-contract.js'
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Safely get the "initializer" expression of a property-like AST node:
|
|
@@ -40,6 +42,16 @@ function getInitializerOf(
|
|
|
40
42
|
return undefined
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
function getObjectPropertyName(
|
|
46
|
+
name: ts.PropertyName | undefined
|
|
47
|
+
): string | null {
|
|
48
|
+
if (!name) return null
|
|
49
|
+
if (ts.isIdentifier(name)) return name.text
|
|
50
|
+
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text
|
|
51
|
+
if (ts.isComputedPropertyName(name)) return null
|
|
52
|
+
return name.getText()
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/**
|
|
44
56
|
* Resolve a handler expression (Identifier, CallExpression, or { func })
|
|
45
57
|
* into its underlying function name.
|
|
@@ -116,6 +128,27 @@ function getHandlerNameFromExpression(
|
|
|
116
128
|
return null
|
|
117
129
|
}
|
|
118
130
|
|
|
131
|
+
function extractExportedChannelRoutes(
|
|
132
|
+
logger: {
|
|
133
|
+
error: (msg: string) => void
|
|
134
|
+
critical: (code: ErrorCode, msg: string) => void
|
|
135
|
+
},
|
|
136
|
+
routes: ts.ObjectLiteralExpression,
|
|
137
|
+
state: InspectorState,
|
|
138
|
+
checker: ts.TypeChecker
|
|
139
|
+
): Record<string, ChannelMessageMeta> {
|
|
140
|
+
const wrapper = ts.factory.createObjectLiteralExpression([
|
|
141
|
+
ts.factory.createPropertyAssignment(
|
|
142
|
+
'onMessageWiring',
|
|
143
|
+
ts.factory.createObjectLiteralExpression([
|
|
144
|
+
ts.factory.createPropertyAssignment('contract', routes),
|
|
145
|
+
])
|
|
146
|
+
),
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
return addMessagesRoutes(logger, wrapper, state, checker).contract ?? {}
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
/**
|
|
120
153
|
* Build out the nested message-routes by looking up each handler
|
|
121
154
|
* in state.functions.meta instead of re-inferring it here.
|
|
@@ -155,9 +188,24 @@ export function addMessagesRoutes(
|
|
|
155
188
|
}
|
|
156
189
|
}
|
|
157
190
|
|
|
191
|
+
const refContract = resolveRefContract(
|
|
192
|
+
chanInit,
|
|
193
|
+
'refChannel',
|
|
194
|
+
state.exportedContracts.addonChannel
|
|
195
|
+
)
|
|
196
|
+
if (refContract) {
|
|
197
|
+
const refChannelKey = getObjectPropertyName(chanElem.name)
|
|
198
|
+
if (!refChannelKey) continue
|
|
199
|
+
result[refChannelKey] = {
|
|
200
|
+
...refContract.contract,
|
|
201
|
+
}
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
158
205
|
if (!ts.isObjectLiteralExpression(chanInit)) continue
|
|
159
206
|
|
|
160
|
-
const channelKey = chanElem.name
|
|
207
|
+
const channelKey = getObjectPropertyName(chanElem.name)
|
|
208
|
+
if (!channelKey) continue
|
|
161
209
|
result[channelKey] = {}
|
|
162
210
|
|
|
163
211
|
for (const routeElem of chanInit.properties) {
|
|
@@ -168,11 +216,8 @@ export function addMessagesRoutes(
|
|
|
168
216
|
const routeName = routeElem.name
|
|
169
217
|
if (!routeName) continue
|
|
170
218
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (ts.isStringLiteral(routeName)) {
|
|
174
|
-
routeKey = routeName.text
|
|
175
|
-
}
|
|
219
|
+
const routeKey = getObjectPropertyName(routeName)
|
|
220
|
+
if (!routeKey) continue
|
|
176
221
|
|
|
177
222
|
// For shorthand properties, we need to resolve the identifier to its declaration
|
|
178
223
|
if (ts.isShorthandPropertyAssignment(routeElem)) {
|
|
@@ -529,6 +574,18 @@ export const addChannel: AddWiring = (
|
|
|
529
574
|
options
|
|
530
575
|
) => {
|
|
531
576
|
if (!ts.isCallExpression(node)) return
|
|
577
|
+
if (
|
|
578
|
+
ts.isIdentifier(node.expression) &&
|
|
579
|
+
node.expression.text === 'defineChannelRoutes'
|
|
580
|
+
) {
|
|
581
|
+
const exportName = getExportedVariableName(node, options.sourceFile)
|
|
582
|
+
const [firstArg] = node.arguments
|
|
583
|
+
if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
584
|
+
state.exportedContracts.channel[exportName] =
|
|
585
|
+
extractExportedChannelRoutes(logger, firstArg, state, checker)
|
|
586
|
+
}
|
|
587
|
+
return
|
|
588
|
+
}
|
|
532
589
|
const { expression, arguments: args } = node
|
|
533
590
|
if (!ts.isIdentifier(expression) || expression.text !== 'wireChannel') return
|
|
534
591
|
const first = args[0]
|
|
@@ -664,7 +721,9 @@ export const addChannel: AddWiring = (
|
|
|
664
721
|
state.serviceAggregation.usedFunctions.add(message.pikkuFuncId)
|
|
665
722
|
}
|
|
666
723
|
|
|
667
|
-
for (const channelHandlers of Object.values(
|
|
724
|
+
for (const channelHandlers of Object.values(
|
|
725
|
+
messageWirings as Record<string, Record<string, ChannelMessageMeta>>
|
|
726
|
+
)) {
|
|
668
727
|
for (const handler of Object.values(channelHandlers)) {
|
|
669
728
|
state.serviceAggregation.usedFunctions.add(handler.pikkuFuncId)
|
|
670
729
|
}
|
package/src/add/add-cli.ts
CHANGED
|
@@ -19,6 +19,8 @@ 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
21
|
import { extractServicesFromFunction } from '../utils/extract-services.js'
|
|
22
|
+
import { getExportedVariableName } from '../utils/get-exported-variable-name.js'
|
|
23
|
+
import { resolveRefContract } from '../utils/resolve-ref-contract.js'
|
|
22
24
|
|
|
23
25
|
// Track if we've warned about missing Config type to avoid duplicate warnings
|
|
24
26
|
const configTypeWarningShown = new Set<string>()
|
|
@@ -34,6 +36,25 @@ export const addCLI: AddWiring = (
|
|
|
34
36
|
options
|
|
35
37
|
) => {
|
|
36
38
|
if (!ts.isCallExpression(node)) return
|
|
39
|
+
if (
|
|
40
|
+
ts.isIdentifier(node.expression) &&
|
|
41
|
+
node.expression.text === 'defineCLICommands'
|
|
42
|
+
) {
|
|
43
|
+
const exportName = getExportedVariableName(node, options.sourceFile)
|
|
44
|
+
const [firstArg] = node.arguments
|
|
45
|
+
if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
46
|
+
inspectorState.exportedContracts.cli[exportName] = processCommands(
|
|
47
|
+
logger,
|
|
48
|
+
firstArg,
|
|
49
|
+
node.getSourceFile(),
|
|
50
|
+
typeChecker,
|
|
51
|
+
exportName,
|
|
52
|
+
inspectorState,
|
|
53
|
+
options
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
}
|
|
37
58
|
// Check if this is a wireCLI call
|
|
38
59
|
if (!node || !node.expression) {
|
|
39
60
|
return
|
|
@@ -214,6 +235,15 @@ function processCommands(
|
|
|
214
235
|
programTags
|
|
215
236
|
)
|
|
216
237
|
Object.assign(commands, spreadCommands)
|
|
238
|
+
} else {
|
|
239
|
+
const refCommands = resolveRefContract(
|
|
240
|
+
prop.expression,
|
|
241
|
+
'refCLI',
|
|
242
|
+
inspectorState.exportedContracts.addonCli
|
|
243
|
+
)
|
|
244
|
+
if (refCommands) {
|
|
245
|
+
Object.assign(commands, refCommands.contract)
|
|
246
|
+
}
|
|
217
247
|
}
|
|
218
248
|
continue
|
|
219
249
|
}
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
getPropertyAssignmentInitializer,
|
|
14
14
|
extractTypeKeys,
|
|
15
15
|
} from '../utils/type-utils.js'
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
AddWiring,
|
|
18
|
+
ExportedHTTPRouteConfigMeta,
|
|
19
|
+
InspectorState,
|
|
20
|
+
} from '../types.js'
|
|
17
21
|
import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js'
|
|
18
22
|
import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js'
|
|
19
23
|
import { extractWireNames } from '../utils/post-process.js'
|
|
@@ -40,6 +44,16 @@ export interface RegisterHTTPRouteParams {
|
|
|
40
44
|
inheritedAuth?: boolean
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
export interface RegisterHTTPRouteMetaParams {
|
|
48
|
+
route: ExportedHTTPRouteConfigMeta
|
|
49
|
+
state: InspectorState
|
|
50
|
+
logger: InspectorLogger
|
|
51
|
+
sourceFile: ts.SourceFile
|
|
52
|
+
basePath?: string
|
|
53
|
+
inheritedTags?: string[]
|
|
54
|
+
inheritedAuth?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
/**
|
|
44
58
|
* Extract header schema reference from headers property
|
|
45
59
|
*/
|
|
@@ -421,6 +435,66 @@ export function registerHTTPRoute({
|
|
|
421
435
|
}
|
|
422
436
|
}
|
|
423
437
|
|
|
438
|
+
export function registerHTTPRouteMeta({
|
|
439
|
+
route,
|
|
440
|
+
state,
|
|
441
|
+
logger,
|
|
442
|
+
sourceFile,
|
|
443
|
+
basePath = '',
|
|
444
|
+
inheritedTags = [],
|
|
445
|
+
inheritedAuth,
|
|
446
|
+
}: RegisterHTTPRouteMetaParams): void {
|
|
447
|
+
const method = route.method.toLowerCase()
|
|
448
|
+
const fullRoute = basePath + route.route
|
|
449
|
+
const tags = [...inheritedTags, ...(route.tags || [])]
|
|
450
|
+
const funcName = route.func.pikkuFuncId
|
|
451
|
+
const fnMeta = resolveFunctionMeta(state, funcName)
|
|
452
|
+
|
|
453
|
+
if (!fnMeta) {
|
|
454
|
+
logger.critical(
|
|
455
|
+
ErrorCode.FUNCTION_METADATA_NOT_FOUND,
|
|
456
|
+
`No function metadata found for '${funcName}'.`
|
|
457
|
+
)
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let params: string[] = []
|
|
462
|
+
try {
|
|
463
|
+
const keys = pathToRegexp(fullRoute).keys
|
|
464
|
+
params = keys.filter((k) => k.type === 'param').map((k) => k.name)
|
|
465
|
+
} catch (error) {
|
|
466
|
+
logger.error(
|
|
467
|
+
`Failed to parse route '${fullRoute}': ${error instanceof Error ? error.message : error}`
|
|
468
|
+
)
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!route.func.packageName) {
|
|
473
|
+
computeInputTypes(
|
|
474
|
+
state.http.metaInputTypes,
|
|
475
|
+
method,
|
|
476
|
+
fnMeta.inputs?.[0] || null,
|
|
477
|
+
[],
|
|
478
|
+
params
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
state.serviceAggregation.usedFunctions.add(funcName)
|
|
483
|
+
state.http.files.add(sourceFile.fileName)
|
|
484
|
+
state.http.meta[method][fullRoute] = {
|
|
485
|
+
pikkuFuncId: funcName,
|
|
486
|
+
...(route.func.packageName && { packageName: route.func.packageName }),
|
|
487
|
+
route: fullRoute,
|
|
488
|
+
sourceFile: sourceFile.fileName,
|
|
489
|
+
method: method as HTTPMethod,
|
|
490
|
+
params: params.length > 0 ? params : undefined,
|
|
491
|
+
inputTypes: undefined,
|
|
492
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
493
|
+
sse: route.sse ? true : undefined,
|
|
494
|
+
groupBasePath: basePath || undefined,
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
424
498
|
/**
|
|
425
499
|
* Process wireHTTP calls
|
|
426
500
|
*/
|