@pikku/inspector 0.12.106 → 0.12.108

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.
Files changed (96) hide show
  1. package/dist/add/add-auth.d.ts +1 -9
  2. package/dist/add/add-auth.js +64 -17
  3. package/dist/add/add-cli.js +4 -0
  4. package/dist/add/add-functions.js +27 -31
  5. package/dist/add/add-keyed-wiring.js +22 -2
  6. package/dist/add/add-middleware.js +17 -16
  7. package/dist/add/add-permission.d.ts +0 -3
  8. package/dist/add/add-permission.js +213 -22
  9. package/dist/add/add-scope.d.ts +1 -7
  10. package/dist/add/add-scope.js +1 -21
  11. package/dist/add/check-indirect-registrations.d.ts +9 -0
  12. package/dist/add/check-indirect-registrations.js +114 -0
  13. package/dist/error-codes.d.ts +1 -0
  14. package/dist/error-codes.js +1 -0
  15. package/dist/finalize.d.ts +10 -0
  16. package/dist/finalize.js +102 -0
  17. package/dist/index.d.ts +8 -6
  18. package/dist/index.js +6 -5
  19. package/dist/initial-state.d.ts +7 -0
  20. package/dist/initial-state.js +216 -0
  21. package/dist/inspector.d.ts +6 -4
  22. package/dist/inspector.js +28 -302
  23. package/dist/lite.d.ts +26 -0
  24. package/dist/lite.js +25 -0
  25. package/dist/source-file-cache.d.ts +11 -0
  26. package/dist/source-file-cache.js +34 -10
  27. package/dist/types-map.js +2 -1
  28. package/dist/types.d.ts +32 -0
  29. package/dist/utils/apply-declared-scopes.d.ts +8 -0
  30. package/dist/utils/apply-declared-scopes.js +21 -0
  31. package/dist/utils/canonical-type-string.d.ts +14 -0
  32. package/dist/utils/canonical-type-string.js +162 -0
  33. package/dist/utils/context-based-id.d.ts +7 -0
  34. package/dist/utils/context-based-id.js +9 -0
  35. package/dist/utils/custom-types-generator.d.ts +15 -1
  36. package/dist/utils/custom-types-generator.js +17 -5
  37. package/dist/utils/ensure-function-metadata.d.ts +0 -14
  38. package/dist/utils/ensure-function-metadata.js +4 -1
  39. package/dist/utils/extract-function-name.d.ts +1 -1
  40. package/dist/utils/extract-function-name.js +1 -3
  41. package/dist/utils/filter-inspector-state.js +31 -1
  42. package/dist/utils/get-property-value.d.ts +5 -0
  43. package/dist/utils/get-property-value.js +14 -0
  44. package/dist/utils/inspector-constants.d.ts +16 -0
  45. package/dist/utils/inspector-constants.js +16 -0
  46. package/dist/utils/post-process.d.ts +1 -2
  47. package/dist/utils/post-process.js +27 -27
  48. package/dist/utils/read-jsx-literal-text.js +31 -1
  49. package/dist/utils/resolve-callee-name.d.ts +27 -0
  50. package/dist/utils/resolve-callee-name.js +147 -0
  51. package/dist/utils/resolve-service-types.d.ts +8 -0
  52. package/dist/utils/resolve-service-types.js +21 -0
  53. package/dist/utils/schema-generator.js +29 -2
  54. package/dist/utils/serialize-inspector-state.d.ts +7 -1
  55. package/dist/utils/serialize-inspector-state.js +16 -1
  56. package/dist/utils/workflow/dsl/patterns.d.ts +1 -8
  57. package/dist/utils/workflow/dsl/patterns.js +1 -7
  58. package/dist/visit.js +2 -0
  59. package/package.json +25 -9
  60. package/CHANGELOG.md +0 -4368
  61. package/OPTIMIZATION-PLAN.md +0 -201
  62. package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +0 -22
  63. package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +0 -20
  64. package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +0 -27
  65. package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +0 -20
  66. package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +0 -25
  67. package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +0 -20
  68. package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +0 -27
  69. package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +0 -27
  70. package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +0 -22
  71. package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +0 -29
  72. package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +0 -21
  73. package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +0 -21
  74. package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +0 -25
  75. package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +0 -19
  76. package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +0 -23
  77. package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +0 -24
  78. package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +0 -18
  79. package/knowledge/decisions/index.md +0 -38
  80. package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +0 -20
  81. package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +0 -19
  82. package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +0 -23
  83. package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +0 -23
  84. package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +0 -22
  85. package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +0 -28
  86. package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +0 -14
  87. package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +0 -30
  88. package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +0 -26
  89. package/knowledge/index.md +0 -14
  90. package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +0 -26
  91. package/knowledge/questions/index.md +0 -16
  92. package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +0 -23
  93. package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +0 -28
  94. package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +0 -22
  95. package/run-tests.sh +0 -68
  96. package/src/utils/test-data/inspector-state.json +0 -1687
@@ -1,4 +1,15 @@
1
1
  import * as ts from 'typescript';
2
+ /**
3
+ * Parse through the shared cache. With `populate: false` the cache is only
4
+ * read: a short-lived program (the schema generator's) reuses what the
5
+ * inspector parsed without pinning its own files past its lifetime.
6
+ */
7
+ export declare const getCachedSourceFile: (fileName: string, text: string, languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, { populate }?: {
8
+ populate?: boolean;
9
+ }) => {
10
+ file: ts.SourceFile;
11
+ reused: boolean;
12
+ };
2
13
  export interface SourceFileCacheHost extends ts.CompilerHost {
3
14
  /** Files served from the cache by the program just built. */
4
15
  readonly reused: () => number;
@@ -17,6 +17,35 @@ import { createHash } from 'crypto';
17
17
  * worth having if it outlives the call.
18
18
  */
19
19
  const cache = new Map();
20
+ /**
21
+ * A SourceFile is only shareable between programs that would have parsed it
22
+ * the same way: the language version and the module format the program
23
+ * implies for it (ESM vs CJS) are baked into the AST. The inspector and the
24
+ * schema generator use different compiler options, so both go into the key.
25
+ */
26
+ const cacheKey = (fileName, languageVersionOrOptions) => {
27
+ if (typeof languageVersionOrOptions !== 'object') {
28
+ return `${fileName}\0${languageVersionOrOptions}`;
29
+ }
30
+ const { languageVersion, impliedNodeFormat, jsDocParsingMode } = languageVersionOrOptions;
31
+ return `${fileName}\0${languageVersion}\0${impliedNodeFormat ?? ''}\0${jsDocParsingMode ?? ''}`;
32
+ };
33
+ /**
34
+ * Parse through the shared cache. With `populate: false` the cache is only
35
+ * read: a short-lived program (the schema generator's) reuses what the
36
+ * inspector parsed without pinning its own files past its lifetime.
37
+ */
38
+ export const getCachedSourceFile = (fileName, text, languageVersionOrOptions, { populate = true } = {}) => {
39
+ const key = cacheKey(fileName, languageVersionOrOptions);
40
+ const hash = createHash('sha1').update(text).digest('hex');
41
+ const hit = cache.get(key);
42
+ if (hit && hit.hash === hash)
43
+ return { file: hit.file, reused: true };
44
+ const file = ts.createSourceFile(fileName, text, languageVersionOrOptions, true);
45
+ if (populate)
46
+ cache.set(key, { hash, file });
47
+ return { file, reused: false };
48
+ };
20
49
  export const createSourceFileCacheHost = (options) => {
21
50
  const host = ts.createCompilerHost(options, true);
22
51
  let reused = 0;
@@ -31,23 +60,18 @@ export const createSourceFileCacheHost = (options) => {
31
60
  }
32
61
  if (text === undefined)
33
62
  return undefined;
34
- const hash = createHash('sha1').update(text).digest('hex');
35
- const hit = cache.get(fileName);
36
- if (hit && hit.hash === hash) {
63
+ const { file, reused: hit } = getCachedSourceFile(fileName, text, languageVersionOrOptions);
64
+ if (hit)
37
65
  reused++;
38
- return hit.file;
39
- }
40
- const file = ts.createSourceFile(fileName, text, languageVersionOrOptions, true);
41
- cache.set(fileName, { hash, file });
42
66
  return file;
43
67
  };
44
68
  return Object.assign(host, {
45
69
  reused: () => reused,
46
70
  prune: (program) => {
47
71
  const live = new Set(program.getSourceFiles().map((sf) => sf.fileName));
48
- for (const fileName of cache.keys()) {
49
- if (!live.has(fileName))
50
- cache.delete(fileName);
72
+ for (const [key, entry] of cache) {
73
+ if (!live.has(entry.file.fileName))
74
+ cache.delete(key);
51
75
  }
52
76
  },
53
77
  });
package/dist/types-map.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { canonicalTypeString } from './utils/canonical-type-string.js';
2
3
  export class TypesMap {
3
4
  map = new Map();
4
5
  customTypes = new Map();
5
6
  addCustomType(name, type, references) {
6
- this.customTypes.set(name, { type, references });
7
+ this.customTypes.set(name, { type: canonicalTypeString(type), references });
7
8
  }
8
9
  addType(originalName, path) {
9
10
  this.map.set(originalName, { originalName, path });
package/dist/types.d.ts CHANGED
@@ -206,9 +206,21 @@ export interface InspectorPermissionInstance {
206
206
  position: number;
207
207
  isFactoryCall: boolean;
208
208
  }
209
+ /**
210
+ * One `addGlobalPermission(...)` call. It registers at module evaluation, so
211
+ * codegen must import its file — or call `exportName`, when the call sits in an
212
+ * exported zero-argument factory — or the global gate does not exist.
213
+ */
214
+ export interface InspectorGlobalPermissionRegistration {
215
+ sourceFile: string;
216
+ position: number;
217
+ exportName: string | null;
218
+ definitionIds: string[];
219
+ }
209
220
  export interface InspectorPermissionState {
210
221
  definitions: Record<string, InspectorPermissionDefinition>;
211
222
  instances: Record<string, InspectorPermissionInstance>;
223
+ global: InspectorGlobalPermissionRegistration[];
212
224
  }
213
225
  export type InspectorFilters = {
214
226
  names?: string[];
@@ -428,6 +440,12 @@ export interface AuthDefinition {
428
440
  /** Whether `session.cookieCache` is enabled — drives the stateless session
429
441
  * middleware split in the auth codegen. Absent/false ⇒ stateful middleware. */
430
442
  cookieCache?: boolean;
443
+ /** `advanced.cookiePrefix`, when written as a string literal. */
444
+ cookiePrefix?: string;
445
+ /** `session.cookieCache.strategy`, when written as a string literal. */
446
+ cookieCacheStrategy?: string;
447
+ /** `session.cookieCache.version`, when written as a string literal. */
448
+ cookieCacheVersion?: string;
431
449
  /**
432
450
  * Singleton services the generated auth handler must have available at
433
451
  * runtime — the services the `pikkuBetterAuth` factory reaches for (either
@@ -518,6 +536,14 @@ export type ScreensManifestMeta = {
518
536
  */
519
537
  scopes?: string[];
520
538
  };
539
+ export interface InspectorServiceTypes {
540
+ /** Property names of the project's SingletonServices type, sorted. */
541
+ singletonServices: string[] | null;
542
+ /** Property names of the project's Services type, unfiltered. */
543
+ services: string[] | null;
544
+ /** Singleton services that are a `SecretService` under another name. */
545
+ secretAliases: string[];
546
+ }
521
547
  export interface InspectorState {
522
548
  /** Set when this package calls `defineScreens`: the screens it ships. */
523
549
  screensManifest?: ScreensManifestMeta;
@@ -801,6 +827,12 @@ export interface InspectorState {
801
827
  channelMiddleware: InspectorChannelMiddlewareState;
802
828
  agentMiddleware: InspectorAIMiddlewareState;
803
829
  permissions: InspectorPermissionState;
830
+ /**
831
+ * What the checker concluded about the project's core service types, read
832
+ * during analysis so that finalize never needs `typesLookup`. A field is null
833
+ * when the type did not resolve.
834
+ */
835
+ serviceTypes?: InspectorServiceTypes;
804
836
  serviceAggregation: {
805
837
  requiredServices: Set<string>;
806
838
  usedFunctions: Set<string>;
@@ -0,0 +1,8 @@
1
+ import type { InspectorState } from '../types.js';
2
+ /**
3
+ * Folds `declareScopes` ids into `state.scopes.definitions`. An id the
4
+ * definitions already hold (an addon's tree, a `defineScope`) is left alone;
5
+ * a missing one is added as a bare node under its root, creating the root when
6
+ * nothing declared it.
7
+ */
8
+ export declare const applyDeclaredScopes: (state: Pick<InspectorState, "scopes">) => void;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Folds `declareScopes` ids into `state.scopes.definitions`. An id the
3
+ * definitions already hold (an addon's tree, a `defineScope`) is left alone;
4
+ * a missing one is added as a bare node under its root, creating the root when
5
+ * nothing declared it.
6
+ */
7
+ export const applyDeclaredScopes = (state) => {
8
+ for (const [id, sourceFile] of state.scopes.declared ?? []) {
9
+ const [rootName, ...rest] = id.split(':');
10
+ let root = state.scopes.definitions.find((d) => d.name === rootName);
11
+ if (!root) {
12
+ root = { name: rootName, sourceFile };
13
+ state.scopes.definitions.push(root);
14
+ }
15
+ let level = root;
16
+ for (const segment of rest) {
17
+ level.scopes ??= {};
18
+ level = level.scopes[segment] ??= {};
19
+ }
20
+ }
21
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A printed type with the members of every union in it sorted: by text, with
3
+ * `null` and `undefined` last. The checker prints a union in the order its
4
+ * members were created, which follows whichever file it happened to check
5
+ * first, and so differs between runtimes and between the TypeScript and native
6
+ * inspectors. Everything other than a union's order is left byte for byte.
7
+ *
8
+ * A level the scanner cannot split safely (a conditional type, a type
9
+ * parameter's constraint, a type predicate) keeps its order and only has the
10
+ * brackets inside it canonicalized. The native inspector's
11
+ * `typesmap.CanonicalTypeString` is the same algorithm, and both run the cases
12
+ * in `canonical-type-string.cases.json`.
13
+ */
14
+ export declare const canonicalTypeString: (type: string) => string;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * A printed type with the members of every union in it sorted: by text, with
3
+ * `null` and `undefined` last. The checker prints a union in the order its
4
+ * members were created, which follows whichever file it happened to check
5
+ * first, and so differs between runtimes and between the TypeScript and native
6
+ * inspectors. Everything other than a union's order is left byte for byte.
7
+ *
8
+ * A level the scanner cannot split safely (a conditional type, a type
9
+ * parameter's constraint, a type predicate) keeps its order and only has the
10
+ * brackets inside it canonicalized. The native inspector's
11
+ * `typesmap.CanonicalTypeString` is the same algorithm, and both run the cases
12
+ * in `canonical-type-string.cases.json`.
13
+ */
14
+ export const canonicalTypeString = (type) => {
15
+ const lead = type.length - type.replace(/^[ \t\n\r]+/, '').length;
16
+ const trail = type.length - type.replace(/[ \t\n\r]+$/, '').length;
17
+ if (lead === type.length)
18
+ return type;
19
+ const body = type.slice(lead, type.length - trail);
20
+ return (type.slice(0, lead) + canonicalBody(body) + type.slice(type.length - trail));
21
+ };
22
+ const OPENERS = '{[(<';
23
+ const CLOSE = { '{': '}', '[': ']', '(': ')', '<': '>' };
24
+ const CLOSERS = '}])>';
25
+ const QUOTES = `"'\``;
26
+ const UNSPLITTABLE = new Set(['extends', 'is', 'asserts', 'infer', 'as']);
27
+ const MAPPED_KEY = /^[A-Za-z_$][\w$]*[ \t\n\r]+in[ \t\n\r]/;
28
+ /** Where the comment starting at `i` ends (exclusive), or -1 if none starts there. */
29
+ const commentEnd = (s, i) => {
30
+ if (s[i] !== '/')
31
+ return -1;
32
+ if (s[i + 1] === '*') {
33
+ const end = s.indexOf('*/', i + 2);
34
+ return end === -1 ? s.length : end + 2;
35
+ }
36
+ if (s[i + 1] === '/') {
37
+ const end = s.indexOf('\n', i + 2);
38
+ return end === -1 ? s.length : end;
39
+ }
40
+ return -1;
41
+ };
42
+ /**
43
+ * `s` with everything inside brackets, string literals and comments blanked,
44
+ * and the outermost bracket pairs.
45
+ */
46
+ const topLevel = (s) => {
47
+ const mask = Array.from({ length: s.length }, () => ' ');
48
+ const groups = [];
49
+ let depth = 0;
50
+ let open = -1;
51
+ for (let i = 0; i < s.length; i++) {
52
+ const char = s[i];
53
+ if (QUOTES.includes(char)) {
54
+ let j = i + 1;
55
+ while (j < s.length && s[j] !== char)
56
+ j += s[j] === '\\' ? 2 : 1;
57
+ i = j;
58
+ continue;
59
+ }
60
+ const end = commentEnd(s, i);
61
+ if (end !== -1) {
62
+ i = end - 1;
63
+ continue;
64
+ }
65
+ if (OPENERS.includes(char)) {
66
+ if (depth === 0)
67
+ open = i;
68
+ depth++;
69
+ }
70
+ else if (CLOSERS.includes(char) && !(char === '>' && s[i - 1] === '=')) {
71
+ if (depth > 0)
72
+ depth--;
73
+ if (depth === 0 && open !== -1 && CLOSE[s[open]] === char) {
74
+ groups.push([open, i]);
75
+ open = -1;
76
+ }
77
+ }
78
+ else if (depth === 0) {
79
+ mask[i] = char;
80
+ }
81
+ }
82
+ return { mask: mask.join(''), groups };
83
+ };
84
+ const isNullish = (member) => member === 'null' || member === 'undefined';
85
+ const sortMembers = (members) => members.sort((a, b) => isNullish(a) !== isNullish(b)
86
+ ? isNullish(a)
87
+ ? 1
88
+ : -1
89
+ : a < b
90
+ ? -1
91
+ : a > b
92
+ ? 1
93
+ : 0);
94
+ const unsplittable = (mask) => {
95
+ if (/[?:]/.test(mask))
96
+ return true;
97
+ for (let i = 0; i < mask.length; i++) {
98
+ if (mask[i] === '=' && mask[i + 1] !== '>')
99
+ return true;
100
+ }
101
+ return mask.split(/[^A-Za-z0-9_$]+/).some((word) => UNSPLITTABLE.has(word));
102
+ };
103
+ const canonicalBody = (body) => {
104
+ const { mask } = topLevel(body);
105
+ const arrow = mask.indexOf('=>');
106
+ if (arrow !== -1 && !mask.slice(0, arrow).includes('|')) {
107
+ return `${rewriteGroups(body.slice(0, arrow))}=>${canonicalTypeString(body.slice(arrow + 2))}`;
108
+ }
109
+ const mapped = MAPPED_KEY.exec(mask);
110
+ if (mapped) {
111
+ const key = mapped[0].length;
112
+ return body.slice(0, key) + canonicalTypeString(body.slice(key));
113
+ }
114
+ if (!mask.includes('|') || unsplittable(mask))
115
+ return rewriteGroups(body);
116
+ const members = [];
117
+ let start = 0;
118
+ for (let i = 0; i <= mask.length; i++) {
119
+ if (i === mask.length || mask[i] === '|') {
120
+ const member = canonicalTypeString(body.slice(start, i)).trim();
121
+ if (member)
122
+ members.push(member);
123
+ start = i + 1;
124
+ }
125
+ }
126
+ return members.length > 1
127
+ ? sortMembers(members).join(' | ')
128
+ : rewriteGroups(body);
129
+ };
130
+ /** `s` with the contents of each outermost bracket pair canonicalized. */
131
+ const rewriteGroups = (s) => {
132
+ let out = '';
133
+ let last = 0;
134
+ for (const [open, close] of topLevel(s).groups) {
135
+ out +=
136
+ s.slice(last, open + 1) + rewriteList(s.slice(open + 1, close), s[open]);
137
+ last = close;
138
+ }
139
+ return out + s.slice(last);
140
+ };
141
+ /** A bracket's contents: members of an object, parameters, or type arguments. */
142
+ const rewriteList = (inner, opener) => {
143
+ const separators = opener === '{' ? ';,' : ',';
144
+ const { mask } = topLevel(inner);
145
+ let out = '';
146
+ let start = 0;
147
+ for (let i = 0; i <= mask.length; i++) {
148
+ if (i === mask.length || separators.includes(mask[i])) {
149
+ out += rewriteItem(inner.slice(start, i), opener) + (inner[i] ?? '');
150
+ start = i + 1;
151
+ }
152
+ }
153
+ return out;
154
+ };
155
+ const rewriteItem = (item, opener) => {
156
+ if (opener === '<')
157
+ return canonicalTypeString(item);
158
+ const colon = topLevel(item).mask.indexOf(':');
159
+ if (colon === -1)
160
+ return canonicalTypeString(item);
161
+ return `${rewriteGroups(item.slice(0, colon))}:${canonicalTypeString(item.slice(colon + 1))}`;
162
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The id of a function wired inline rather than declared on its own: the
3
+ * wiring type followed by whatever identifies the wiring (`queue:emails`,
4
+ * `http:get:/todos`). Kept apart from `extract-function-name.ts`, which needs
5
+ * the TypeScript compiler, so pure state helpers can use it without loading it.
6
+ */
7
+ export declare function makeContextBasedId(wiringType: string, ...segments: string[]): string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The id of a function wired inline rather than declared on its own: the
3
+ * wiring type followed by whatever identifies the wiring (`queue:emails`,
4
+ * `http:get:/todos`). Kept apart from `extract-function-name.ts`, which needs
5
+ * the TypeScript compiler, so pure state helpers can use it without loading it.
6
+ */
7
+ export function makeContextBasedId(wiringType, ...segments) {
8
+ return [wiringType, ...segments].join(':');
9
+ }
@@ -6,4 +6,18 @@ import type { TypesMap } from '../types-map.js';
6
6
  * ts-json-schema-generator can discover inline/custom types from typesMap.
7
7
  */
8
8
  export declare function sanitizeTypeName(name: string): string;
9
- export declare function generateCustomTypes(typesMap: TypesMap, requiredTypes: Set<string>): string;
9
+ /**
10
+ * The names `generateCustomTypes` declares, in the order it declares them. A
11
+ * file that imports from the shared custom-types file uses this to know which
12
+ * names exist there.
13
+ */
14
+ export declare function customTypeNames(typesMap: TypesMap): string[];
15
+ export interface GenerateCustomTypesOptions {
16
+ /**
17
+ * Turns the absolute path of a type's source file into the specifier the
18
+ * import line uses. Left out, the absolute path is used as-is — fine for the
19
+ * in-memory source the schema generator compiles, wrong for a file on disk.
20
+ */
21
+ resolveImportPath?: (absolutePath: string) => string;
22
+ }
23
+ export declare function generateCustomTypes(typesMap: TypesMap, requiredTypes: Set<string>, { resolveImportPath }?: GenerateCustomTypesOptions): string;
@@ -55,14 +55,26 @@ function stripClassificationWrappers(type) {
55
55
  }
56
56
  return output;
57
57
  }
58
- export function generateCustomTypes(typesMap, requiredTypes) {
59
- const typeDeclarations = Array.from(typesMap.customTypes.entries())
58
+ function emittableCustomTypes(typesMap) {
59
+ return Array.from(typesMap.customTypes.entries())
60
60
  .sort(([a], [b]) => a.localeCompare(b))
61
61
  .filter(([_name, { type }]) => {
62
62
  const hasUndefinedGeneric = /\b(Name|In|Out|Key)\b/.test(type) && /\[.*\]/.test(type);
63
63
  return !hasUndefinedGeneric;
64
- })
65
- .map(([originalName, { type, references }]) => {
64
+ });
65
+ }
66
+ /**
67
+ * The names `generateCustomTypes` declares, in the order it declares them. A
68
+ * file that imports from the shared custom-types file uses this to know which
69
+ * names exist there.
70
+ */
71
+ export function customTypeNames(typesMap) {
72
+ return emittableCustomTypes(typesMap)
73
+ .filter(([originalName, { type }]) => sanitizeTypeName(originalName) !== stripClassificationWrappers(type))
74
+ .map(([originalName]) => sanitizeTypeName(originalName));
75
+ }
76
+ export function generateCustomTypes(typesMap, requiredTypes, { resolveImportPath = (path) => path } = {}) {
77
+ const typeDeclarations = emittableCustomTypes(typesMap).map(([originalName, { type, references }]) => {
66
78
  const name = sanitizeTypeName(originalName);
67
79
  references.forEach((refName) => {
68
80
  if (refName !== '__object' &&
@@ -118,7 +130,7 @@ export function generateCustomTypes(typesMap, requiredTypes) {
118
130
  }
119
131
  }
120
132
  const importLines = Array.from(importsByPath.entries())
121
- .map(([path, types]) => `import type { ${Array.from(types).join(', ')} } from '${path}'`)
133
+ .map(([path, types]) => `import type { ${Array.from(types).join(', ')} } from '${resolveImportPath(path)}'`)
122
134
  .join('\n');
123
135
  return `${importLines}\n\n${typeDeclarations.filter(Boolean).join('\n')}`;
124
136
  }
@@ -7,18 +7,4 @@ import type { InspectorState } from '../types.js';
7
7
  * extracts tags/middleware/permissions from the pikkuFunc() config.
8
8
  */
9
9
  export declare function ensureFunctionMetadata(state: InspectorState, pikkuFuncId: string, fallbackName?: string, funcInitializer?: ts.Node, checker?: ts.TypeChecker, isHelper?: boolean): void;
10
- /**
11
- * Register metadata for a `func:` inlined into a wiring (queue/scheduler/
12
- * trigger/gateway/…).
13
- *
14
- * An inline function has no exported name, so `addFunctions` skips it (it bails
15
- * on `__temp_` ids) and only the wiring's context-based id exists. Without this
16
- * the wiring points at a function that was never registered and the worker
17
- * resolves to nothing at runtime ("Missing generated metadata for ...").
18
- *
19
- * The stub also carries no session info, so the runtime would treat it as
20
- * session-required and every invocation would fail with "Authentication
21
- * required". Derive `sessionless` from the helper the function was built with —
22
- * the same rule `addFunctions` applies to named ones (`!== 'pikkuFunc'`).
23
- */
24
10
  export declare function ensureInlineWiringFunction(state: InspectorState, pikkuFuncId: string, fallbackName: string | undefined, funcInitializer: ts.Node, checker: ts.TypeChecker, isHelper?: boolean): void;
@@ -3,6 +3,7 @@ import { funcIdToTypeName } from './extract-function-name.js';
3
3
  import { getCommonWireMetaData } from './get-property-value.js';
4
4
  import { resolveMiddleware } from './middleware.js';
5
5
  import { resolvePermissions } from './permissions.js';
6
+ import { resolveCalleeName } from './resolve-callee-name.js';
6
7
  function isVoidLike(type) {
7
8
  return !!(type.flags &
8
9
  (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike));
@@ -224,6 +225,7 @@ export function ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcIni
224
225
  * required". Derive `sessionless` from the helper the function was built with —
225
226
  * the same rule `addFunctions` applies to named ones (`!== 'pikkuFunc'`).
226
227
  */
228
+ const isPikkuFunc = (name) => name === 'pikkuFunc';
227
229
  export function ensureInlineWiringFunction(state, pikkuFuncId, fallbackName, funcInitializer, checker, isHelper) {
228
230
  ensureFunctionMetadata(state, pikkuFuncId, fallbackName, funcInitializer, checker, isHelper);
229
231
  const meta = state.functions.meta[pikkuFuncId];
@@ -231,7 +233,8 @@ export function ensureInlineWiringFunction(state, pikkuFuncId, fallbackName, fun
231
233
  meta.sessionless === undefined &&
232
234
  ts.isCallExpression(funcInitializer) &&
233
235
  ts.isIdentifier(funcInitializer.expression)) {
234
- meta.sessionless = funcInitializer.expression.text !== 'pikkuFunc';
236
+ meta.sessionless =
237
+ resolveCalleeName(funcInitializer, checker, isPikkuFunc) !== 'pikkuFunc';
235
238
  }
236
239
  }
237
240
  function populateTypesLookup(state, pikkuFuncId, funcInitializer, checker) {
@@ -8,7 +8,7 @@ export type ExtractedFunctionName = {
8
8
  isHelper: boolean;
9
9
  version: number | null;
10
10
  };
11
- export declare function makeContextBasedId(wiringType: string, ...segments: string[]): string;
11
+ export { makeContextBasedId } from './context-based-id.js';
12
12
  export declare function funcIdToTypeName(id: string): string;
13
13
  export declare function extractFunctionName(callExpr: ts.Node, checker: ts.TypeChecker, rootDir: string): ExtractedFunctionName;
14
14
  /**
@@ -18,9 +18,7 @@ function tempFuncId(callExpr, rootDir) {
18
18
  .slice(0, 16);
19
19
  return `__temp_${hash}`;
20
20
  }
21
- export function makeContextBasedId(wiringType, ...segments) {
22
- return [wiringType, ...segments].join(':');
23
- }
21
+ export { makeContextBasedId } from './context-based-id.js';
24
22
  export function funcIdToTypeName(id) {
25
23
  return id
26
24
  .split(/[^a-zA-Z0-9]/)
@@ -1,5 +1,5 @@
1
1
  import { parseVersionedId } from '@pikku/core/utils';
2
- import { makeContextBasedId } from './extract-function-name.js';
2
+ import { makeContextBasedId } from './context-based-id.js';
3
3
  import { aggregateRequiredServices } from './post-process.js';
4
4
  import { IncompatibleDeployTargetError, resolveDeployTarget, } from './resolve-deploy-target.js';
5
5
  /**
@@ -359,6 +359,11 @@ export function filterInspectorState(state, filters, logger) {
359
359
  meta: JSON.parse(JSON.stringify(state.channels.meta)),
360
360
  files: new Set(), // Will be repopulated with filtered files
361
361
  },
362
+ gateways: {
363
+ ...state.gateways,
364
+ meta: JSON.parse(JSON.stringify(state.gateways?.meta ?? {})),
365
+ files: new Set(),
366
+ },
362
367
  triggers: {
363
368
  ...state.triggers,
364
369
  meta: JSON.parse(JSON.stringify(state.triggers?.meta ?? {})),
@@ -511,6 +516,31 @@ export function filterInspectorState(state, filters, logger) {
511
516
  }
512
517
  }
513
518
  filteredState.channels.files = repopulateFileSetFromMeta(filteredState.channels.meta, state.channels.files, Object.keys(filteredState.channels.meta).length > 0);
519
+ // Filter gateways. A gateway is addressed by its own name or by the function
520
+ // it runs, so a deploy unit naming either keeps the wiring that serves it.
521
+ for (const name of Object.keys(filteredState.gateways.meta)) {
522
+ const gatewayMeta = filteredState.gateways.meta[name];
523
+ const matches = matchesFilters(filters, {
524
+ type: 'gateway',
525
+ name: gatewayMeta.pikkuFuncId,
526
+ altNames: [name],
527
+ tags: gatewayMeta.tags,
528
+ httpRoute: gatewayMeta.route,
529
+ }, logger, undefined, keptByDeploy);
530
+ if (!matches) {
531
+ delete filteredState.gateways.meta[name];
532
+ }
533
+ else {
534
+ filteredState.serviceAggregation.usedFunctions.add(gatewayMeta.pikkuFuncId);
535
+ extractWireNames(gatewayMeta.middleware).forEach((name) => filteredState.serviceAggregation.usedMiddleware.add(name));
536
+ }
537
+ }
538
+ // The meta records no source file per gateway, so a surviving gateway keeps
539
+ // every gateway file and none surviving drops them all.
540
+ filteredState.gateways.files =
541
+ Object.keys(filteredState.gateways.meta).length > 0
542
+ ? new Set(state.gateways?.files ?? [])
543
+ : new Set();
514
544
  // Filter triggers
515
545
  for (const name of Object.keys(filteredState.triggers.meta)) {
516
546
  const triggerMeta = filteredState.triggers.meta[name];
@@ -1,6 +1,11 @@
1
1
  import * as ts from 'typescript';
2
2
  import { ErrorCode } from '../error-codes.js';
3
3
  export declare const getArrayPropertyValue: (obj: ts.ObjectLiteralExpression, propertyName: string) => string[] | null;
4
+ /**
5
+ * Returns the object literal assigned to a property, or null when the property
6
+ * is absent or is not an inline object.
7
+ */
8
+ export declare const getObjectPropertyLiteral: (obj: ts.ObjectLiteralExpression, propertyName: string) => ts.ObjectLiteralExpression | null;
4
9
  /**
5
10
  * Extracts a string->string record from an object property.
6
11
  *
@@ -27,6 +27,20 @@ export const getArrayPropertyValue = (obj, propertyName) => {
27
27
  }
28
28
  return null;
29
29
  };
30
+ /**
31
+ * Returns the object literal assigned to a property, or null when the property
32
+ * is absent or is not an inline object.
33
+ */
34
+ export const getObjectPropertyLiteral = (obj, propertyName) => {
35
+ const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
36
+ ts.isIdentifier(p.name) &&
37
+ p.name.text === propertyName);
38
+ if (!property || !ts.isPropertyAssignment(property)) {
39
+ return null;
40
+ }
41
+ const initializer = unwrapAs(property.initializer);
42
+ return ts.isObjectLiteralExpression(initializer) ? initializer : null;
43
+ };
30
44
  /**
31
45
  * Extracts a string->string record from an object property.
32
46
  *
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The pikku function id of the single shared auth handler the CLI generates
3
+ * (`export const authHandler = pikkuSessionlessFunc(...)` in auth.gen.ts). An
4
+ * exported top-level const collapses the catch-all `/api/auth/**` route onto one
5
+ * worker, and the export name becomes the function id. Shared with the CLI
6
+ * codegen and the post-process service stamp so all three agree on the same id
7
+ * without the inspector having to import `@pikku/better-auth`.
8
+ */
9
+ export declare const AUTH_HANDLER_FUNC_ID = "authHandler";
10
+ /**
11
+ * Placeholder recorded when a scenario step's target isn't a static string
12
+ * literal. The step is kept rather than dropped so post-processing can raise
13
+ * PKU678 against it — a dropped step is the silent-failure mode this repo has
14
+ * hit repeatedly on workflow graph round-trips.
15
+ */
16
+ export declare const DYNAMIC_SCENARIO_STEP_TARGET = "<dynamic>";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The pikku function id of the single shared auth handler the CLI generates
3
+ * (`export const authHandler = pikkuSessionlessFunc(...)` in auth.gen.ts). An
4
+ * exported top-level const collapses the catch-all `/api/auth/**` route onto one
5
+ * worker, and the export name becomes the function id. Shared with the CLI
6
+ * codegen and the post-process service stamp so all three agree on the same id
7
+ * without the inspector having to import `@pikku/better-auth`.
8
+ */
9
+ export const AUTH_HANDLER_FUNC_ID = 'authHandler';
10
+ /**
11
+ * Placeholder recorded when a scenario step's target isn't a static string
12
+ * literal. The step is kept rather than dropped so post-processing can raise
13
+ * PKU678 against it — a dropped step is the silent-failure mode this repo has
14
+ * hit repeatedly on workflow graph round-trips.
15
+ */
16
+ export const DYNAMIC_SCENARIO_STEP_TARGET = '<dynamic>';
@@ -1,7 +1,6 @@
1
1
  import type { InspectorState, InspectorLogger, InspectorOptions } from '../types.js';
2
2
  import type { PermissionMetadata } from '@pikku/core/function';
3
3
  import type { MiddlewareMetadata } from '@pikku/core/middleware';
4
- import type ts from 'typescript';
5
4
  /**
6
5
  * Stamp the inspected authorize/callbacks service set onto the generated auth
7
6
  * handler's function meta.
@@ -180,7 +179,7 @@ export declare function validateSchemaWiringSeparation(logger: InspectorLogger,
180
179
  * a name the omit does not cover. A name-based rule misses this; the type does
181
180
  * not.
182
181
  */
183
- export declare function validateNoSecretAliasServices(logger: InspectorLogger, checker: ts.TypeChecker, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
182
+ export declare function validateNoSecretAliasServices(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
184
183
  /**
185
184
  * Reports secret reads the catalogue does not account for.
186
185
  *