@pikku/inspector 0.12.18 → 0.12.20
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 +100 -0
- package/dist/add/add-auth.d.ts +28 -0
- package/dist/add/add-auth.js +172 -18
- package/dist/error-codes.d.ts +2 -0
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +6 -1
- package/dist/types.d.ts +36 -0
- package/dist/utils/check-pii-output.js +26 -3
- package/dist/utils/post-process.d.ts +14 -0
- package/dist/utils/post-process.js +26 -0
- package/dist/utils/serialize-inspector-state.d.ts +2 -0
- package/dist/utils/serialize-inspector-state.js +4 -0
- package/package.json +2 -2
- package/src/add/add-auth.test.ts +308 -44
- package/src/add/add-auth.ts +223 -22
- package/src/add/pii-check.test.ts +8 -3
- package/src/error-codes.ts +4 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +6 -0
- package/src/types.ts +37 -0
- package/src/utils/check-pii-output.ts +27 -5
- package/src/utils/post-process.ts +27 -0
- package/src/utils/serialize-inspector-state.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/add/add-auth.ts
CHANGED
|
@@ -1,49 +1,250 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
+
import type { FunctionServicesMeta } from '@pikku/core'
|
|
2
3
|
import type { AddWiring } from '../types.js'
|
|
3
4
|
import { ErrorCode } from '../error-codes.js'
|
|
5
|
+
import { extractServicesFromFunction } from '../utils/extract-services.js'
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The pikku function id of the single shared auth handler the CLI generates
|
|
9
|
+
* (`export const authHandler = pikkuSessionlessFunc(...)` in auth.gen.ts). An
|
|
10
|
+
* exported top-level const collapses the catch-all `/api/auth/**` route onto one
|
|
11
|
+
* worker, and the export name becomes the function id. Shared with the CLI
|
|
12
|
+
* codegen and the post-process service stamp so all three agree on the same id
|
|
13
|
+
* without the inspector having to import `@pikku/better-auth`.
|
|
14
|
+
*/
|
|
15
|
+
export const AUTH_HANDLER_FUNC_ID = 'authHandler'
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
17
|
+
/** The default better-auth base path when `basePath` is not configured. */
|
|
18
|
+
const DEFAULT_BASE_PATH = '/api/auth'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find the first `betterAuth({...})` call anywhere inside the `pikkuBetterAuth`
|
|
22
|
+
* factory body. Supports both `(s) => betterAuth({...})` and
|
|
23
|
+
* `(s) => { ...; return betterAuth({...}) }`.
|
|
24
|
+
*/
|
|
25
|
+
const findBetterAuthCall = (node: ts.Node): ts.CallExpression | undefined => {
|
|
26
|
+
let found: ts.CallExpression | undefined
|
|
27
|
+
const visit = (n: ts.Node) => {
|
|
28
|
+
if (found) return
|
|
29
|
+
if (
|
|
30
|
+
ts.isCallExpression(n) &&
|
|
31
|
+
ts.isIdentifier(n.expression) &&
|
|
32
|
+
n.expression.text === 'betterAuth'
|
|
33
|
+
) {
|
|
34
|
+
found = n
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
ts.forEachChild(n, visit)
|
|
38
|
+
}
|
|
39
|
+
visit(node)
|
|
40
|
+
return found
|
|
41
|
+
}
|
|
10
42
|
|
|
11
|
-
|
|
12
|
-
|
|
43
|
+
/** Read a string-literal property off an object literal, if present. */
|
|
44
|
+
const readStringProp = (
|
|
45
|
+
obj: ts.ObjectLiteralExpression,
|
|
46
|
+
name: string
|
|
47
|
+
): string | undefined => {
|
|
48
|
+
const prop = obj.properties.find(
|
|
49
|
+
(p) =>
|
|
50
|
+
ts.isPropertyAssignment(p) &&
|
|
51
|
+
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
52
|
+
p.name.text === name
|
|
53
|
+
) as ts.PropertyAssignment | undefined
|
|
54
|
+
if (prop && ts.isStringLiteral(prop.initializer)) return prop.initializer.text
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
13
57
|
|
|
14
|
-
|
|
58
|
+
/** Find an object-literal-valued property off an object literal, if present. */
|
|
59
|
+
const readObjectProp = (
|
|
60
|
+
obj: ts.ObjectLiteralExpression,
|
|
61
|
+
name: string
|
|
62
|
+
): ts.ObjectLiteralExpression | undefined => {
|
|
63
|
+
const prop = obj.properties.find(
|
|
15
64
|
(p) =>
|
|
16
65
|
ts.isPropertyAssignment(p) &&
|
|
17
66
|
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
18
|
-
p.name.text ===
|
|
67
|
+
p.name.text === name
|
|
19
68
|
) as ts.PropertyAssignment | undefined
|
|
69
|
+
if (prop && ts.isObjectLiteralExpression(prop.initializer))
|
|
70
|
+
return prop.initializer
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Find an array-literal-valued property off an object literal, if present. */
|
|
75
|
+
const readArrayProp = (
|
|
76
|
+
obj: ts.ObjectLiteralExpression,
|
|
77
|
+
name: string
|
|
78
|
+
): ts.ArrayLiteralExpression | undefined => {
|
|
79
|
+
const prop = obj.properties.find(
|
|
80
|
+
(p) =>
|
|
81
|
+
ts.isPropertyAssignment(p) &&
|
|
82
|
+
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
83
|
+
p.name.text === name
|
|
84
|
+
) as ts.PropertyAssignment | undefined
|
|
85
|
+
if (prop && ts.isArrayLiteralExpression(prop.initializer))
|
|
86
|
+
return prop.initializer
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read the callee name of a `plugins: [...]` entry. better-auth plugins are
|
|
92
|
+
* factory calls (`bearer()`, `twoFactor({ ... })`, `admin()`); the entry's id
|
|
93
|
+
* is the called function's name. Member-expression callees (`foo.bar()`) and
|
|
94
|
+
* non-call entries are ignored.
|
|
95
|
+
*/
|
|
96
|
+
const readPluginId = (el: ts.Expression): string | undefined => {
|
|
97
|
+
if (ts.isCallExpression(el) && ts.isIdentifier(el.expression))
|
|
98
|
+
return el.expression.text
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Detects `pikkuBetterAuth((services) => betterAuth({...}))` calls.
|
|
104
|
+
*
|
|
105
|
+
* `pikkuBetterAuth` is pure: it wraps a factory that returns a configured better-auth
|
|
106
|
+
* instance and has NO side effects. The user assigns it to an exported binding,
|
|
107
|
+
* e.g.
|
|
108
|
+
*
|
|
109
|
+
* export const auth = pikkuBetterAuth(async (services) => betterAuth({ ... }))
|
|
110
|
+
*
|
|
111
|
+
* The pikku CLI discovers that single export and generates a catch-all
|
|
112
|
+
* `auth.gen.ts` that wires `${basePath}/**` to one shared handler, registers the
|
|
113
|
+
* better-auth session middleware, and emits a `wireSecret` for every configured
|
|
114
|
+
* social provider — so the auth routes and secret requirements flow through
|
|
115
|
+
* normal inspection into the deploy manifest.
|
|
116
|
+
*
|
|
117
|
+
* This add-wiring records the exported binding name, source file, basePath, the
|
|
118
|
+
* `socialProviders` keys, whether email/password is enabled, and the services
|
|
119
|
+
* the factory touches. Exactly one `pikkuBetterAuth` is allowed per codebase.
|
|
120
|
+
*/
|
|
121
|
+
export const addAuth: AddWiring = (logger, node, _checker, state) => {
|
|
122
|
+
if (!ts.isCallExpression(node)) return
|
|
123
|
+
|
|
124
|
+
const expression = node.expression
|
|
125
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
|
|
126
|
+
return
|
|
20
127
|
|
|
21
128
|
const sourceFile = node.getSourceFile().fileName
|
|
22
|
-
state.auth.files.add(sourceFile)
|
|
23
129
|
|
|
24
|
-
|
|
130
|
+
// Walk up to the `export const <name> = pikkuBetterAuth(...)` binding.
|
|
131
|
+
const varDecl = node.parent
|
|
132
|
+
if (!ts.isVariableDeclaration(varDecl) || !ts.isIdentifier(varDecl.name)) {
|
|
133
|
+
logger.critical(
|
|
134
|
+
ErrorCode.AUTH_NOT_EXPORTED,
|
|
135
|
+
`pikkuBetterAuth(...) must be assigned to an exported const, e.g. \`export const auth = pikkuBetterAuth((services) => betterAuth({...}))\` in ${sourceFile}`
|
|
136
|
+
)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
const exportName = varDecl.name.text
|
|
140
|
+
|
|
141
|
+
// VariableDeclaration -> VariableDeclarationList -> VariableStatement
|
|
142
|
+
const declList = varDecl.parent
|
|
143
|
+
const varStatement = declList?.parent
|
|
144
|
+
const isConst =
|
|
145
|
+
ts.isVariableDeclarationList(declList) &&
|
|
146
|
+
(declList.flags & ts.NodeFlags.Const) !== 0
|
|
147
|
+
const isExported =
|
|
148
|
+
varStatement &&
|
|
149
|
+
ts.isVariableStatement(varStatement) &&
|
|
150
|
+
varStatement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
151
|
+
if (!isExported || !isConst) {
|
|
152
|
+
logger.critical(
|
|
153
|
+
ErrorCode.AUTH_NOT_EXPORTED,
|
|
154
|
+
`pikkuBetterAuth(...) must be assigned to an exported const so the CLI can import it. Use \`export const ${exportName} = pikkuBetterAuth(...)\` in ${sourceFile}`
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (state.auth.definition) {
|
|
160
|
+
logger.critical(
|
|
161
|
+
ErrorCode.DUPLICATE_AUTH_DEFINITION,
|
|
162
|
+
`Only one pikkuBetterAuth(...) is allowed per codebase. Found a second in ${sourceFile} (first: ${state.auth.definition.sourceFile}).`
|
|
163
|
+
)
|
|
25
164
|
return
|
|
26
165
|
}
|
|
27
166
|
|
|
28
|
-
|
|
167
|
+
// The single argument must be the factory: (services) => betterAuth({...}).
|
|
168
|
+
const factory = node.arguments[0]
|
|
169
|
+
if (
|
|
170
|
+
!factory ||
|
|
171
|
+
(!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory))
|
|
172
|
+
) {
|
|
29
173
|
logger.critical(
|
|
30
174
|
ErrorCode.MISSING_NAME,
|
|
31
|
-
|
|
175
|
+
`pikkuBetterAuth(...) must take a factory function returning betterAuth(...), e.g. \`pikkuBetterAuth((services) => betterAuth({...}))\` in ${sourceFile}`
|
|
32
176
|
)
|
|
33
177
|
return
|
|
34
178
|
}
|
|
35
179
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
180
|
+
state.auth.files.add(sourceFile)
|
|
181
|
+
|
|
182
|
+
// Derive the services the factory touches from its destructured first param —
|
|
183
|
+
// the same convention as every other pikku wiring. A non-destructured param
|
|
184
|
+
// yields optimized:false (handler gets all singleton services), with the
|
|
185
|
+
// standard diagnostic steering the user to destructure.
|
|
186
|
+
const services: FunctionServicesMeta = extractServicesFromFunction(factory)
|
|
187
|
+
|
|
188
|
+
// Find the inner betterAuth({...}) call to read providers/basePath/credentials.
|
|
189
|
+
let basePath = DEFAULT_BASE_PATH
|
|
190
|
+
let hasCredentials = false
|
|
191
|
+
const betterAuthCall = findBetterAuthCall(factory)
|
|
192
|
+
const config = betterAuthCall?.arguments[0]
|
|
193
|
+
|
|
194
|
+
if (config && ts.isObjectLiteralExpression(config)) {
|
|
195
|
+
basePath = readStringProp(config, 'basePath') ?? DEFAULT_BASE_PATH
|
|
196
|
+
|
|
197
|
+
const emailAndPassword = readObjectProp(config, 'emailAndPassword')
|
|
198
|
+
if (emailAndPassword) {
|
|
199
|
+
// `emailAndPassword: { enabled: true }` — treat a present block without an
|
|
200
|
+
// explicit `enabled: false` as credentials being available.
|
|
201
|
+
const enabledProp = emailAndPassword.properties.find(
|
|
202
|
+
(p) =>
|
|
203
|
+
ts.isPropertyAssignment(p) &&
|
|
204
|
+
ts.isIdentifier(p.name) &&
|
|
205
|
+
p.name.text === 'enabled'
|
|
206
|
+
) as ts.PropertyAssignment | undefined
|
|
207
|
+
hasCredentials =
|
|
208
|
+
!enabledProp ||
|
|
209
|
+
enabledProp.initializer.kind !== ts.SyntaxKind.FalseKeyword
|
|
44
210
|
}
|
|
45
|
-
|
|
46
|
-
|
|
211
|
+
|
|
212
|
+
const socialProviders = readObjectProp(config, 'socialProviders')
|
|
213
|
+
if (socialProviders) {
|
|
214
|
+
for (const prop of socialProviders.properties) {
|
|
215
|
+
const key =
|
|
216
|
+
(ts.isPropertyAssignment(prop) ||
|
|
217
|
+
ts.isShorthandPropertyAssignment(prop)) &&
|
|
218
|
+
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
|
|
219
|
+
? prop.name.text
|
|
220
|
+
: undefined
|
|
221
|
+
if (key && !state.auth.providers.includes(key)) {
|
|
222
|
+
state.auth.providers.push(key)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const plugins = readArrayProp(config, 'plugins')
|
|
228
|
+
if (plugins) {
|
|
229
|
+
for (const el of plugins.elements) {
|
|
230
|
+
const id = readPluginId(el)
|
|
231
|
+
if (id && !state.auth.plugins.includes(id)) {
|
|
232
|
+
state.auth.plugins.push(id)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
47
235
|
}
|
|
236
|
+
} else {
|
|
237
|
+
logger.warn(
|
|
238
|
+
`pikkuBetterAuth in ${sourceFile}: could not statically find a betterAuth({...}) call inside the factory — social provider secrets will not be auto-wired.`
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
state.auth.definition = {
|
|
243
|
+
exportName,
|
|
244
|
+
sourceFile,
|
|
245
|
+
basePath,
|
|
246
|
+
hasCredentials,
|
|
247
|
+
plugins: [...state.auth.plugins],
|
|
248
|
+
services,
|
|
48
249
|
}
|
|
49
250
|
}
|
|
@@ -27,10 +27,15 @@ function makeLogger() {
|
|
|
27
27
|
* Mirrors what schema.d.ts emits so the TypeScript program sees the correct
|
|
28
28
|
* structural brand type even without @pikku/core being importable from /tmp.
|
|
29
29
|
*/
|
|
30
|
+
// Optional `__classification__?` mirrors what @pikku/core and `pikku db migrate`
|
|
31
|
+
// actually emit (optional so plain values stay assignable to branded columns).
|
|
32
|
+
// The `Secret`-in-sessioned-function cases below double as the level-fidelity
|
|
33
|
+
// guard: they only pass if `findPiiPaths` reads the level union-aware
|
|
34
|
+
// ('secret' | undefined), not via a naive `.value`.
|
|
30
35
|
const BRAND_TYPES = `
|
|
31
|
-
type Private<T> = T & { readonly __classification__
|
|
32
|
-
type Pii<T> = T & { readonly __classification__
|
|
33
|
-
type Secret<T> = T & { readonly __classification__
|
|
36
|
+
type Private<T> = T & { readonly __classification__?: 'private' }
|
|
37
|
+
type Pii<T> = T & { readonly __classification__?: 'pii' }
|
|
38
|
+
type Secret<T> = T & { readonly __classification__?: 'secret' }
|
|
34
39
|
`
|
|
35
40
|
|
|
36
41
|
async function runInspect(sourceCode: string) {
|
package/src/error-codes.ts
CHANGED
|
@@ -37,6 +37,10 @@ export enum ErrorCode {
|
|
|
37
37
|
FUNCTION_METADATA_NOT_FOUND = 'PKU559',
|
|
38
38
|
HANDLER_NOT_RESOLVED = 'PKU568',
|
|
39
39
|
|
|
40
|
+
// Auth errors
|
|
41
|
+
DUPLICATE_AUTH_DEFINITION = 'PKU581',
|
|
42
|
+
AUTH_NOT_EXPORTED = 'PKU582',
|
|
43
|
+
|
|
40
44
|
// HTTP Route errors
|
|
41
45
|
ROUTE_PARAM_MISMATCH = 'PKU571',
|
|
42
46
|
ROUTE_QUERY_MISMATCH = 'PKU572',
|
package/src/index.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 { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js'
|
|
6
7
|
export {
|
|
7
8
|
serializeInspectorState,
|
|
8
9
|
deserializeInspectorState,
|
package/src/inspector.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { getFilesAndMethods } from './utils/get-files-and-methods.js'
|
|
|
11
11
|
import { findCommonAncestor } from './utils/find-root-dir.js'
|
|
12
12
|
import {
|
|
13
13
|
aggregateRequiredServices,
|
|
14
|
+
stampAuthHandlerServices,
|
|
14
15
|
validateAgentModels,
|
|
15
16
|
validateSecretOverrides,
|
|
16
17
|
validateVariableOverrides,
|
|
@@ -147,7 +148,9 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
|
|
|
147
148
|
},
|
|
148
149
|
auth: {
|
|
149
150
|
providers: [],
|
|
151
|
+
plugins: [],
|
|
150
152
|
files: new Set(),
|
|
153
|
+
definition: null,
|
|
151
154
|
},
|
|
152
155
|
secrets: {
|
|
153
156
|
definitions: [],
|
|
@@ -333,6 +336,9 @@ export const inspect = async (
|
|
|
333
336
|
|
|
334
337
|
if (!options.setupOnly) {
|
|
335
338
|
const startAggregate = performance.now()
|
|
339
|
+
// Apply the inspected auth handler service set before aggregation so it
|
|
340
|
+
// flows into requiredServices (the generated handler's own func is opaque).
|
|
341
|
+
stampAuthHandlerServices(state)
|
|
336
342
|
aggregateRequiredServices(state)
|
|
337
343
|
logger.debug(
|
|
338
344
|
`Aggregate required services completed in ${(performance.now() - startAggregate).toFixed(2)}ms`
|
package/src/types.ts
CHANGED
|
@@ -301,6 +301,38 @@ export interface InspectorDiagnostic {
|
|
|
301
301
|
position: number
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/** A single discovered `export const X = pikkuBetterAuth((services) => betterAuth({...}))`. */
|
|
305
|
+
export interface AuthDefinition {
|
|
306
|
+
/** The exported binding name the CLI imports (`export const <exportName>`). */
|
|
307
|
+
exportName: string
|
|
308
|
+
/** Absolute path of the file declaring it. */
|
|
309
|
+
sourceFile: string
|
|
310
|
+
/** better-auth base path (the `basePath` option, default `/api/auth`). */
|
|
311
|
+
basePath: string
|
|
312
|
+
/** Whether email/password auth is enabled (`emailAndPassword.enabled`). Written
|
|
313
|
+
* into the generated `auth-meta.gen.json` so the console knows credentials are
|
|
314
|
+
* available alongside the OAuth providers. */
|
|
315
|
+
hasCredentials: boolean
|
|
316
|
+
/** better-auth plugin ids used in the config's `plugins: [...]` array, read
|
|
317
|
+
* from each entry's callee name (e.g. `bearer()` → `'bearer'`). Written into
|
|
318
|
+
* `auth-meta.gen.json` so the console SSO page can show which plugins are
|
|
319
|
+
* enabled. */
|
|
320
|
+
plugins: string[]
|
|
321
|
+
/**
|
|
322
|
+
* Singleton services the generated auth handler must have available at
|
|
323
|
+
* runtime — the services the `pikkuBetterAuth` factory reaches for (either
|
|
324
|
+
* destructured from its first param, or accessed as `services.<name>` in its
|
|
325
|
+
* body). better-auth's factory typically touches `secrets` and the DB.
|
|
326
|
+
*
|
|
327
|
+
* The generated `authHandler` calls `createAuthHandler(...).func`, an opaque
|
|
328
|
+
* property access the inspector can't see through; without this stamp the
|
|
329
|
+
* deployed auth worker would instantiate none of these services and the
|
|
330
|
+
* factory would receive an undefined `kysely`. Re-derived every inspect and
|
|
331
|
+
* applied to the handler meta before service aggregation runs.
|
|
332
|
+
*/
|
|
333
|
+
services: FunctionServicesMeta
|
|
334
|
+
}
|
|
335
|
+
|
|
304
336
|
export interface InspectorState {
|
|
305
337
|
rootDir: string // Root directory inferred from source files
|
|
306
338
|
singletonServicesTypeImportMap: PathToNameAndType
|
|
@@ -384,7 +416,12 @@ export interface InspectorState {
|
|
|
384
416
|
}
|
|
385
417
|
auth: {
|
|
386
418
|
providers: string[]
|
|
419
|
+
plugins: string[]
|
|
387
420
|
files: Set<string>
|
|
421
|
+
/** The single `export const X = pikkuBetterAuth({...})` discovered in the
|
|
422
|
+
* codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
|
|
423
|
+
* More than one `pikkuBetterAuth` is a critical error. */
|
|
424
|
+
definition: AuthDefinition | null
|
|
388
425
|
}
|
|
389
426
|
secrets: {
|
|
390
427
|
definitions: SecretDefinitions
|
|
@@ -29,8 +29,12 @@ export function findPiiPaths(
|
|
|
29
29
|
seen.add(type)
|
|
30
30
|
|
|
31
31
|
// ── Is this type itself branded? ─────────────────────────────────────────
|
|
32
|
-
// Private<T> = T & { readonly __classification__
|
|
33
|
-
// where one constituent has a `__classification__` property whose type is a
|
|
32
|
+
// Private<T> = T & { readonly __classification__?: 'private' } → isIntersection()
|
|
33
|
+
// where one constituent has a `__classification__` property whose type is a
|
|
34
|
+
// string literal. The marker is OPTIONAL (so plain values stay assignable to
|
|
35
|
+
// branded columns), which means its resolved type is `'private' | undefined` —
|
|
36
|
+
// a union, not a bare literal. Read the level union-aware via `literalString`,
|
|
37
|
+
// otherwise pii/secret silently downgrade to the `'private'` fallback.
|
|
34
38
|
if (type.isIntersection()) {
|
|
35
39
|
for (const t of type.types) {
|
|
36
40
|
const classificationProp = t
|
|
@@ -41,9 +45,9 @@ export function findPiiPaths(
|
|
|
41
45
|
classificationProp.valueDeclaration ??
|
|
42
46
|
classificationProp.declarations?.[0]
|
|
43
47
|
const classification = decl
|
|
44
|
-
? ((
|
|
45
|
-
checker.getTypeOfSymbolAtLocation(classificationProp, decl)
|
|
46
|
-
)
|
|
48
|
+
? (literalString(
|
|
49
|
+
checker.getTypeOfSymbolAtLocation(classificationProp, decl)
|
|
50
|
+
) ?? 'private')
|
|
47
51
|
: 'private'
|
|
48
52
|
return [{ path: path || '<return value>', classification }]
|
|
49
53
|
}
|
|
@@ -96,3 +100,21 @@ export function findPiiPaths(
|
|
|
96
100
|
|
|
97
101
|
return violations
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Recover a string-literal value from a type that may be the literal itself or a
|
|
106
|
+
* union containing it (e.g. `'private' | undefined`, produced by the optional
|
|
107
|
+
* `__classification__?` marker). Returns undefined when no string literal is
|
|
108
|
+
* present so the caller can apply its own fallback.
|
|
109
|
+
*/
|
|
110
|
+
function literalString(type: ts.Type): string | undefined {
|
|
111
|
+
const value = (type as { value?: unknown }).value
|
|
112
|
+
if (typeof value === 'string') return value
|
|
113
|
+
if (type.isUnion()) {
|
|
114
|
+
for (const member of type.types) {
|
|
115
|
+
const found = literalString(member)
|
|
116
|
+
if (found !== undefined) return found
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
@@ -12,6 +12,33 @@ import type {
|
|
|
12
12
|
} from '@pikku/core'
|
|
13
13
|
import { extractTypeKeys } from './type-utils.js'
|
|
14
14
|
import { ErrorCode } from '../error-codes.js'
|
|
15
|
+
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
19
|
+
* handler's function meta.
|
|
20
|
+
*
|
|
21
|
+
* The CLI generates `export const authHandler = pikkuSessionlessFunc({ func:
|
|
22
|
+
* createAuthHandler(...).func })`. That `func` is an opaque property access, so
|
|
23
|
+
* normal extraction records zero services for the handler — which would leave
|
|
24
|
+
* the deployed auth worker without `kysely`/`variables`/`secrets` and break
|
|
25
|
+
* `authorize` at runtime. `add-auth` already computed the real dependency set
|
|
26
|
+
* (from the pikkuBetterAuth source) into `state.auth.definition.services`; copy it
|
|
27
|
+
* onto the handler meta. Re-derived every inspect and ordered BEFORE
|
|
28
|
+
* `aggregateRequiredServices` so it flows into `requiredServices`.
|
|
29
|
+
*/
|
|
30
|
+
export function stampAuthHandlerServices(
|
|
31
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
32
|
+
): void {
|
|
33
|
+
const definition = state.auth.definition
|
|
34
|
+
if (!definition) return
|
|
35
|
+
const handlerMeta = state.functions.meta[AUTH_HANDLER_FUNC_ID]
|
|
36
|
+
if (!handlerMeta) return
|
|
37
|
+
handlerMeta.services = {
|
|
38
|
+
optimized: definition.services.optimized,
|
|
39
|
+
services: [...definition.services.services],
|
|
40
|
+
}
|
|
41
|
+
}
|
|
15
42
|
|
|
16
43
|
/**
|
|
17
44
|
* Helper to extract wire-level middleware/permission names from metadata.
|
|
@@ -183,7 +183,9 @@ export interface SerializableInspectorState {
|
|
|
183
183
|
}
|
|
184
184
|
auth: {
|
|
185
185
|
providers: string[]
|
|
186
|
+
plugins: string[]
|
|
186
187
|
files: string[]
|
|
188
|
+
definition: InspectorState['auth']['definition']
|
|
187
189
|
}
|
|
188
190
|
secrets: {
|
|
189
191
|
definitions: InspectorState['secrets']['definitions']
|
|
@@ -389,7 +391,9 @@ export function serializeInspectorState(
|
|
|
389
391
|
},
|
|
390
392
|
auth: {
|
|
391
393
|
providers: state.auth.providers,
|
|
394
|
+
plugins: state.auth.plugins,
|
|
392
395
|
files: Array.from(state.auth.files),
|
|
396
|
+
definition: state.auth.definition,
|
|
393
397
|
},
|
|
394
398
|
secrets: {
|
|
395
399
|
definitions: state.secrets.definitions,
|
|
@@ -566,7 +570,9 @@ export function deserializeInspectorState(
|
|
|
566
570
|
},
|
|
567
571
|
auth: {
|
|
568
572
|
providers: data.auth?.providers || [],
|
|
573
|
+
plugins: data.auth?.plugins || [],
|
|
569
574
|
files: new Set(data.auth?.files || []),
|
|
575
|
+
definition: data.auth?.definition || null,
|
|
570
576
|
},
|
|
571
577
|
secrets: {
|
|
572
578
|
definitions: data.secrets?.definitions || [],
|