@pikku/inspector 0.12.13 → 0.12.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## 0.12.14
2
+
3
+ ### Patch Changes
4
+
5
+ - 4b5c75b: feat(auth-js): wire OIDC config (issuer/tenantId) as variables, expand provider registry
6
+ - Move `issuer` and `tenantId` out of the secret blob for OIDC providers (auth0, okta, azure-ad, keycloak, cognito, microsoft-entra-id) — they are public config URLs, not secrets. Now registered via `wireVariable` and loaded at runtime via `services.variables.get()`.
7
+ - Expand provider registry from 13 to 31 providers: reddit, notion, instagram, zoom, figma, tiktok, threads, patreon, dropbox, bitbucket, hubspot, salesforce, atlassian, strava, keycloak, cognito, microsoft-entra-id added.
8
+ - `serialize-auth-gen` emits `wireVariable({...})` declarations and `services.variables.get()` calls in the generated factory for OIDC providers.
9
+ - Integration verifier exercises real `/auth/providers` endpoint with `LocalSecretService` + `LocalVariablesService`, including a spy test proving `services.variables.get('AUTH0_ISSUER')` is called at request time.
10
+
11
+ - 4b5c75b: Add end-to-end data classification for SQLite and Postgres projects.
12
+
13
+ **Core (`@pikku/core`):** New `Private<T>` and `Secret<T>` intersection brands, `ClassificationManifest`, `ColumnClassification`, and `AnonymizeStrategy` types exported from `data-classification.ts`.
14
+
15
+ **CLI (`@pikku/cli`):**
16
+ - SQL comment annotations: `-- @public`, `-- @private[:strategy]`, `-- @secret[:strategy]` on `CREATE TABLE` columns and `ALTER TABLE ... ADD COLUMN` statements. Unannotated columns default to `private`.
17
+ - `pikku db migrate` now emits a `classification.gen.ts` manifest alongside `schema.d.ts`.
18
+ - New `pikku db audit` command — prints a per-column classification summary and warns on `private`/`secret` columns with no anonymize strategy.
19
+ - Postgres dialect support in `resolveDb`, `PostgresMigrationExecutor`, and `PostgresIntrospector`.
20
+
21
+ **Inspector (`@pikku/inspector`):** New PKU910 check — `findPiiPaths()` walks inferred function return types looking for `__pii__` brands (including inside `Array<T>`, `Record<K,V>`, and index signatures) and fails the build if a function exposes branded fields in its output.
22
+
23
+ - Updated dependencies [4b5c75b]
24
+ - Updated dependencies [4b5c75b]
25
+ - @pikku/core@0.12.27
26
+
1
27
  ## 0.12.13
2
28
 
3
29
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ import type { AddWiring } from '../types.js';
2
+ export declare const addAuth: AddWiring;
@@ -0,0 +1,34 @@
1
+ import * as ts from 'typescript';
2
+ import { ErrorCode } from '../error-codes.js';
3
+ export const addAuth = (logger, node, _checker, state) => {
4
+ if (!ts.isCallExpression(node))
5
+ return;
6
+ const expression = node.expression;
7
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireAuth')
8
+ return;
9
+ const firstArg = node.arguments[0];
10
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
11
+ return;
12
+ const providersProp = firstArg.properties.find((p) => ts.isPropertyAssignment(p) &&
13
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
14
+ p.name.text === 'providers');
15
+ const sourceFile = node.getSourceFile().fileName;
16
+ state.auth.files.add(sourceFile);
17
+ if (!providersProp) {
18
+ return;
19
+ }
20
+ if (!ts.isArrayLiteralExpression(providersProp.initializer)) {
21
+ logger.critical(ErrorCode.MISSING_NAME, 'wireAuth: providers must be an array literal of string literals.');
22
+ return;
23
+ }
24
+ for (const element of providersProp.initializer
25
+ .elements) {
26
+ if (!ts.isStringLiteral(element)) {
27
+ logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `wireAuth: each provider must be a string literal. Found: ${element.getText()}`);
28
+ return;
29
+ }
30
+ if (!state.auth.providers.includes(element.text)) {
31
+ state.auth.providers.push(element.text);
32
+ }
33
+ }
34
+ };
@@ -629,7 +629,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
629
629
  }
630
630
  // ── PII brand check ───────────────────────────────────────────────────────
631
631
  // Walk the function body's ACTUAL inferred return type looking for Private<T>
632
- // / Secret<T> brands (__pii__ property). This runs for every function,
632
+ // / Pii<T> / Secret<T> brands (__classification__ property). This runs for every function,
633
633
  // including those with a Zod output schema, because the TS return type
634
634
  // reflects what the body actually returns before any Zod coercion.
635
635
  {
package/dist/inspector.js CHANGED
@@ -115,6 +115,10 @@ export function getInitialInspectorState(rootDir) {
115
115
  meta: {},
116
116
  files: new Set(),
117
117
  },
118
+ auth: {
119
+ providers: [],
120
+ files: new Set(),
121
+ },
118
122
  secrets: {
119
123
  definitions: [],
120
124
  files: new Set(),
package/dist/types.d.ts CHANGED
@@ -339,6 +339,10 @@ export interface InspectorState {
339
339
  meta: NodesMeta;
340
340
  files: Set<string>;
341
341
  };
342
+ auth: {
343
+ providers: string[];
344
+ files: Set<string>;
345
+ };
342
346
  secrets: {
343
347
  definitions: SecretDefinitions;
344
348
  files: Set<string>;
@@ -1,12 +1,12 @@
1
1
  import * as ts from 'typescript';
2
2
  /**
3
- * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
3
+ * Recursively walks a resolved TypeScript type looking for `__classification__` brands —
4
4
  * the structural marker emitted by `Private<T>` and `Secret<T>`.
5
5
  *
6
- * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
7
- * system as an intersection whose constituents include a type with a `__pii__`
6
+ * `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
7
+ * system as an intersection whose constituents include a type with a `__classification__`
8
8
  * property. We detect that by checking whether any constituent of an
9
- * intersection exposes a property named `__pii__`.
9
+ * intersection exposes a property named `__classification__`.
10
10
  *
11
11
  * Returns the list of dotted field paths where a brand was found
12
12
  * (e.g. `['email', 'address.phone']`). An empty array means clean.
@@ -1,12 +1,12 @@
1
1
  import * as ts from 'typescript';
2
2
  /**
3
- * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
3
+ * Recursively walks a resolved TypeScript type looking for `__classification__` brands —
4
4
  * the structural marker emitted by `Private<T>` and `Secret<T>`.
5
5
  *
6
- * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
7
- * system as an intersection whose constituents include a type with a `__pii__`
6
+ * `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
7
+ * system as an intersection whose constituents include a type with a `__classification__`
8
8
  * property. We detect that by checking whether any constituent of an
9
- * intersection exposes a property named `__pii__`.
9
+ * intersection exposes a property named `__classification__`.
10
10
  *
11
11
  * Returns the list of dotted field paths where a brand was found
12
12
  * (e.g. `['email', 'address.phone']`). An empty array means clean.
@@ -16,10 +16,10 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
16
16
  return [];
17
17
  seen.add(type);
18
18
  // ── Is this type itself branded? ─────────────────────────────────────────
19
- // Private<T> = T & { readonly __pii__: 'private' } → isIntersection()
20
- // where one constituent has a `__pii__` property.
19
+ // Private<T> = T & { readonly __classification__: 'private' } → isIntersection()
20
+ // where one constituent has a `__classification__` property.
21
21
  if (type.isIntersection()) {
22
- const branded = type.types.some((t) => t.getProperties().some((p) => p.name === '__pii__'));
22
+ const branded = type.types.some((t) => t.getProperties().some((p) => p.name === '__classification__'));
23
23
  if (branded) {
24
24
  return [path || '<return value>'];
25
25
  }
@@ -203,6 +203,10 @@ export interface SerializableInspectorState {
203
203
  meta: InspectorState['nodes']['meta'];
204
204
  files: string[];
205
205
  };
206
+ auth: {
207
+ providers: string[];
208
+ files: string[];
209
+ };
206
210
  secrets: {
207
211
  definitions: InspectorState['secrets']['definitions'];
208
212
  files: string[];
@@ -98,6 +98,10 @@ export function serializeInspectorState(state) {
98
98
  meta: state.nodes.meta,
99
99
  files: Array.from(state.nodes.files),
100
100
  },
101
+ auth: {
102
+ providers: state.auth.providers,
103
+ files: Array.from(state.auth.files),
104
+ },
101
105
  secrets: {
102
106
  definitions: state.secrets.definitions,
103
107
  files: Array.from(state.secrets.files),
@@ -246,6 +250,10 @@ export function deserializeInspectorState(data) {
246
250
  meta: data.nodes?.meta || {},
247
251
  files: new Set(data.nodes?.files || []),
248
252
  },
253
+ auth: {
254
+ providers: data.auth?.providers || [],
255
+ files: new Set(data.auth?.files || []),
256
+ },
249
257
  secrets: {
250
258
  definitions: data.secrets?.definitions || [],
251
259
  files: new Set(data.secrets?.files || []),
package/dist/visit.js CHANGED
@@ -17,6 +17,7 @@ import { addWireAddon } from './add/add-wire-addon.js';
17
17
  import { addMiddleware } from './add/add-middleware.js';
18
18
  import { addPermission } from './add/add-permission.js';
19
19
  import { addCLI, addCLIRenderers } from './add/add-cli.js';
20
+ import { addAuth } from './add/add-auth.js';
20
21
  import { addSecret } from './add/add-secret.js';
21
22
  import { addCredential } from './add/add-credential.js';
22
23
  import { addVariable } from './add/add-variable.js';
@@ -41,6 +42,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
41
42
  };
42
43
  export const visitRoutes = (logger, checker, node, state, options) => {
43
44
  addFunctions(logger, node, checker, state, options);
45
+ addAuth(logger, node, checker, state, options);
44
46
  addSecret(logger, node, checker, state, options);
45
47
  addCredential(logger, node, checker, state, options);
46
48
  addVariable(logger, node, checker, state, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.13",
3
+ "version": "0.12.14",
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.25",
38
+ "@pikku/core": "^0.12.27",
39
39
  "path-to-regexp": "^8.3.0",
40
40
  "ts-json-schema-generator": "^2.5.0",
41
41
  "tsx": "^4.21.0",
@@ -0,0 +1,175 @@
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
+ critical: (code: ErrorCode, message: string) => {
17
+ criticals.push({ code, message })
18
+ },
19
+ hasCriticalErrors: () => criticals.length > 0,
20
+ }) satisfies InspectorLogger
21
+
22
+ describe('addAuth inspector', () => {
23
+ test('extracts provider string literals from wireAuth call', async () => {
24
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-'))
25
+ const file = join(rootDir, 'auth.ts')
26
+
27
+ await writeFile(
28
+ file,
29
+ [
30
+ "import { wireAuth } from '@pikku/auth-js'",
31
+ "wireAuth({ providers: ['github', 'google'] })",
32
+ ].join('\n')
33
+ )
34
+
35
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
36
+ try {
37
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
38
+ assert.equal(criticals.length, 0)
39
+ assert.deepEqual(state.auth.providers, ['github', 'google'])
40
+ } finally {
41
+ await rm(rootDir, { recursive: true, force: true })
42
+ }
43
+ })
44
+
45
+ test('deduplicates providers across multiple wireAuth calls', async () => {
46
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-dedup-'))
47
+ const file = join(rootDir, 'auth.ts')
48
+
49
+ await writeFile(
50
+ file,
51
+ [
52
+ "import { wireAuth } from '@pikku/auth-js'",
53
+ "wireAuth({ providers: ['github'] })",
54
+ "wireAuth({ providers: ['github', 'google'] })",
55
+ ].join('\n')
56
+ )
57
+
58
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
59
+ try {
60
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
61
+ assert.equal(criticals.length, 0)
62
+ assert.deepEqual(state.auth.providers, ['github', 'google'])
63
+ } finally {
64
+ await rm(rootDir, { recursive: true, force: true })
65
+ }
66
+ })
67
+
68
+ test('logs critical error when a provider is a non-literal reference', async () => {
69
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-nonlit-'))
70
+ const file = join(rootDir, 'auth.ts')
71
+
72
+ await writeFile(
73
+ file,
74
+ [
75
+ "import { wireAuth } from '@pikku/auth-js'",
76
+ "const PROVIDER = 'github'",
77
+ 'wireAuth({ providers: [PROVIDER] })',
78
+ ].join('\n')
79
+ )
80
+
81
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
82
+ try {
83
+ await inspect(makeLogger(criticals), [file], { rootDir })
84
+ const hit = criticals.find(
85
+ (e) => e.code === ErrorCode.NON_LITERAL_WIRE_NAME
86
+ )
87
+ assert.ok(hit, 'expected NON_LITERAL_WIRE_NAME critical')
88
+ assert.match(hit!.message, /PROVIDER/)
89
+ } finally {
90
+ await rm(rootDir, { recursive: true, force: true })
91
+ }
92
+ })
93
+
94
+ test('does not error when providers is absent (credentials-only wireAuth)', async () => {
95
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-creds-only-'))
96
+ const file = join(rootDir, 'auth.ts')
97
+
98
+ await writeFile(
99
+ file,
100
+ [
101
+ "import { wireAuth } from '@pikku/auth-js'",
102
+ 'wireAuth({ credentials: { authorize: async () => null } })',
103
+ ].join('\n')
104
+ )
105
+
106
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
107
+ try {
108
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
109
+ assert.equal(
110
+ criticals.length,
111
+ 0,
112
+ 'credentials-only wireAuth must not produce errors'
113
+ )
114
+ assert.deepEqual(
115
+ state.auth.providers,
116
+ [],
117
+ 'no providers should be extracted'
118
+ )
119
+ assert.ok(state.auth.files.has(file), 'source file still tracked')
120
+ } finally {
121
+ await rm(rootDir, { recursive: true, force: true })
122
+ }
123
+ })
124
+
125
+ test('logs critical error when providers is not an array literal', async () => {
126
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-nonarray-'))
127
+ const file = join(rootDir, 'auth.ts')
128
+
129
+ await writeFile(
130
+ file,
131
+ [
132
+ "import { wireAuth } from '@pikku/auth-js'",
133
+ "const PROVIDERS = ['github']",
134
+ 'wireAuth({ providers: PROVIDERS })',
135
+ ].join('\n')
136
+ )
137
+
138
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
139
+ try {
140
+ await inspect(makeLogger(criticals), [file], { rootDir })
141
+ const hit = criticals.find((e) => e.code === ErrorCode.MISSING_NAME)
142
+ assert.ok(
143
+ hit,
144
+ 'expected MISSING_NAME critical for non-array-literal providers'
145
+ )
146
+ } finally {
147
+ await rm(rootDir, { recursive: true, force: true })
148
+ }
149
+ })
150
+
151
+ test('tracks source file in state.auth.files', async () => {
152
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-auth-files-'))
153
+ const file = join(rootDir, 'auth.wiring.ts')
154
+
155
+ await writeFile(
156
+ file,
157
+ [
158
+ "import { wireAuth } from '@pikku/auth-js'",
159
+ "wireAuth({ providers: ['discord'] })",
160
+ ].join('\n')
161
+ )
162
+
163
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
164
+ try {
165
+ const state = await inspect(makeLogger(criticals), [file], { rootDir })
166
+ assert.equal(criticals.length, 0)
167
+ assert.ok(
168
+ state.auth.files.has(file),
169
+ 'source file should be tracked in state.auth.files'
170
+ )
171
+ } finally {
172
+ await rm(rootDir, { recursive: true, force: true })
173
+ }
174
+ })
175
+ })
@@ -0,0 +1,49 @@
1
+ import * as ts from 'typescript'
2
+ import type { AddWiring } from '../types.js'
3
+ import { ErrorCode } from '../error-codes.js'
4
+
5
+ export const addAuth: AddWiring = (logger, node, _checker, state) => {
6
+ if (!ts.isCallExpression(node)) return
7
+
8
+ const expression = node.expression
9
+ if (!ts.isIdentifier(expression) || expression.text !== 'wireAuth') return
10
+
11
+ const firstArg = node.arguments[0]
12
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg)) return
13
+
14
+ const providersProp = firstArg.properties.find(
15
+ (p) =>
16
+ ts.isPropertyAssignment(p) &&
17
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
18
+ p.name.text === 'providers'
19
+ ) as ts.PropertyAssignment | undefined
20
+
21
+ const sourceFile = node.getSourceFile().fileName
22
+ state.auth.files.add(sourceFile)
23
+
24
+ if (!providersProp) {
25
+ return
26
+ }
27
+
28
+ if (!ts.isArrayLiteralExpression(providersProp.initializer)) {
29
+ logger.critical(
30
+ ErrorCode.MISSING_NAME,
31
+ 'wireAuth: providers must be an array literal of string literals.'
32
+ )
33
+ return
34
+ }
35
+
36
+ for (const element of (providersProp.initializer as ts.ArrayLiteralExpression)
37
+ .elements) {
38
+ if (!ts.isStringLiteral(element)) {
39
+ logger.critical(
40
+ ErrorCode.NON_LITERAL_WIRE_NAME,
41
+ `wireAuth: each provider must be a string literal. Found: ${element.getText()}`
42
+ )
43
+ return
44
+ }
45
+ if (!state.auth.providers.includes(element.text)) {
46
+ state.auth.providers.push(element.text)
47
+ }
48
+ }
49
+ }
@@ -885,7 +885,7 @@ export const addFunctions: AddWiring = (
885
885
 
886
886
  // ── PII brand check ───────────────────────────────────────────────────────
887
887
  // Walk the function body's ACTUAL inferred return type looking for Private<T>
888
- // / Secret<T> brands (__pii__ property). This runs for every function,
888
+ // / Pii<T> / Secret<T> brands (__classification__ property). This runs for every function,
889
889
  // including those with a Zod output schema, because the TS return type
890
890
  // reflects what the body actually returns before any Zod coercion.
891
891
  {
@@ -23,13 +23,14 @@ function makeLogger() {
23
23
  }
24
24
 
25
25
  /**
26
- * Inline Private<T>/Secret<T> definitions that the test source files use.
26
+ * Inline Private<T>/Pii<T>/Secret<T> definitions that the test source files use.
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
30
  const BRAND_TYPES = `
31
- type Private<T> = T & { readonly __pii__: 'private' }
32
- type Secret<T> = T & { readonly __pii__: 'secret' }
31
+ type Private<T> = T & { readonly __classification__: 'private' }
32
+ type Pii<T> = T & { readonly __classification__: 'pii' }
33
+ type Secret<T> = T & { readonly __classification__: 'secret' }
33
34
  `
34
35
 
35
36
  async function runInspect(sourceCode: string) {
@@ -104,7 +105,11 @@ export const getPublicData = pikkuFunc({
104
105
  })
105
106
  `)
106
107
  const hit = criticals.find((c) => c.code === ErrorCode.PII_IN_OUTPUT)
107
- assert.equal(hit, undefined, `Expected no PKU910 but got: ${JSON.stringify(hit)}`)
108
+ assert.equal(
109
+ hit,
110
+ undefined,
111
+ `Expected no PKU910 but got: ${JSON.stringify(hit)}`
112
+ )
108
113
  })
109
114
 
110
115
  test('does not flag a void-returning function', async () => {
package/src/inspector.ts CHANGED
@@ -145,6 +145,10 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
145
145
  meta: {},
146
146
  files: new Set(),
147
147
  },
148
+ auth: {
149
+ providers: [],
150
+ files: new Set(),
151
+ },
148
152
  secrets: {
149
153
  definitions: [],
150
154
  files: new Set(),
package/src/types.ts CHANGED
@@ -382,6 +382,10 @@ export interface InspectorState {
382
382
  meta: NodesMeta
383
383
  files: Set<string>
384
384
  }
385
+ auth: {
386
+ providers: string[]
387
+ files: Set<string>
388
+ }
385
389
  secrets: {
386
390
  definitions: SecretDefinitions
387
391
  files: Set<string>
@@ -1,13 +1,13 @@
1
1
  import * as ts from 'typescript'
2
2
 
3
3
  /**
4
- * Recursively walks a resolved TypeScript type looking for `__pii__` brands —
4
+ * Recursively walks a resolved TypeScript type looking for `__classification__` brands —
5
5
  * the structural marker emitted by `Private<T>` and `Secret<T>`.
6
6
  *
7
- * `Private<T> = T & { readonly __pii__: 'private' }` shows up in the TS type
8
- * system as an intersection whose constituents include a type with a `__pii__`
7
+ * `Private<T> = T & { readonly __classification__: 'private' }` shows up in the TS type
8
+ * system as an intersection whose constituents include a type with a `__classification__`
9
9
  * property. We detect that by checking whether any constituent of an
10
- * intersection exposes a property named `__pii__`.
10
+ * intersection exposes a property named `__classification__`.
11
11
  *
12
12
  * Returns the list of dotted field paths where a brand was found
13
13
  * (e.g. `['email', 'address.phone']`). An empty array means clean.
@@ -23,11 +23,11 @@ export function findPiiPaths(
23
23
  seen.add(type)
24
24
 
25
25
  // ── Is this type itself branded? ─────────────────────────────────────────
26
- // Private<T> = T & { readonly __pii__: 'private' } → isIntersection()
27
- // where one constituent has a `__pii__` property.
26
+ // Private<T> = T & { readonly __classification__: 'private' } → isIntersection()
27
+ // where one constituent has a `__classification__` property.
28
28
  if (type.isIntersection()) {
29
29
  const branded = type.types.some((t) =>
30
- t.getProperties().some((p) => p.name === '__pii__')
30
+ t.getProperties().some((p) => p.name === '__classification__')
31
31
  )
32
32
  if (branded) {
33
33
  return [path || '<return value>']
@@ -54,12 +54,16 @@ export function findPiiPaths(
54
54
  const numberIndex = checker.getIndexTypeOfType(type, ts.IndexKind.Number)
55
55
  if (numberIndex) {
56
56
  const idxPath = path ? `${path}[]` : '[]'
57
- violations.push(...findPiiPaths(checker, numberIndex, idxPath, depth + 1, seen))
57
+ violations.push(
58
+ ...findPiiPaths(checker, numberIndex, idxPath, depth + 1, seen)
59
+ )
58
60
  }
59
61
  const stringIndex = checker.getIndexTypeOfType(type, ts.IndexKind.String)
60
62
  if (stringIndex) {
61
63
  const idxPath = path ? `${path}[*]` : '[*]'
62
- violations.push(...findPiiPaths(checker, stringIndex, idxPath, depth + 1, seen))
64
+ violations.push(
65
+ ...findPiiPaths(checker, stringIndex, idxPath, depth + 1, seen)
66
+ )
63
67
  }
64
68
 
65
69
  for (const prop of type.getProperties()) {
@@ -68,7 +72,9 @@ export function findPiiPaths(
68
72
  if (!decl) continue
69
73
  const propType = checker.getTypeOfSymbolAtLocation(prop, decl)
70
74
  const subPath = path ? `${path}.${prop.name}` : prop.name
71
- violations.push(...findPiiPaths(checker, propType, subPath, depth + 1, seen))
75
+ violations.push(
76
+ ...findPiiPaths(checker, propType, subPath, depth + 1, seen)
77
+ )
72
78
  }
73
79
  }
74
80
 
@@ -181,6 +181,10 @@ export interface SerializableInspectorState {
181
181
  meta: InspectorState['nodes']['meta']
182
182
  files: string[]
183
183
  }
184
+ auth: {
185
+ providers: string[]
186
+ files: string[]
187
+ }
184
188
  secrets: {
185
189
  definitions: InspectorState['secrets']['definitions']
186
190
  files: string[]
@@ -383,6 +387,10 @@ export function serializeInspectorState(
383
387
  meta: state.nodes.meta,
384
388
  files: Array.from(state.nodes.files),
385
389
  },
390
+ auth: {
391
+ providers: state.auth.providers,
392
+ files: Array.from(state.auth.files),
393
+ },
386
394
  secrets: {
387
395
  definitions: state.secrets.definitions,
388
396
  files: Array.from(state.secrets.files),
@@ -556,6 +564,10 @@ export function deserializeInspectorState(
556
564
  meta: data.nodes?.meta || {},
557
565
  files: new Set(data.nodes?.files || []),
558
566
  },
567
+ auth: {
568
+ providers: data.auth?.providers || [],
569
+ files: new Set(data.auth?.files || []),
570
+ },
559
571
  secrets: {
560
572
  definitions: data.secrets?.definitions || [],
561
573
  files: new Set(data.secrets?.files || []),
package/src/visit.ts CHANGED
@@ -22,6 +22,7 @@ import { addWireAddon } from './add/add-wire-addon.js'
22
22
  import { addMiddleware } from './add/add-middleware.js'
23
23
  import { addPermission } from './add/add-permission.js'
24
24
  import { addCLI, addCLIRenderers } from './add/add-cli.js'
25
+ import { addAuth } from './add/add-auth.js'
25
26
  import { addSecret } from './add/add-secret.js'
26
27
  import { addCredential } from './add/add-credential.js'
27
28
  import { addVariable } from './add/add-variable.js'
@@ -106,6 +107,7 @@ export const visitRoutes = (
106
107
  options: InspectorOptions
107
108
  ) => {
108
109
  addFunctions(logger, node, checker, state, options)
110
+ addAuth(logger, node, checker, state, options)
109
111
  addSecret(logger, node, checker, state, options)
110
112
  addCredential(logger, node, checker, state, options)
111
113
  addVariable(logger, node, checker, state, options)