@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.
- package/dist/add/add-auth.d.ts +1 -9
- package/dist/add/add-auth.js +64 -17
- package/dist/add/add-cli.js +4 -0
- package/dist/add/add-functions.js +27 -31
- package/dist/add/add-keyed-wiring.js +22 -2
- package/dist/add/add-middleware.js +17 -16
- package/dist/add/add-permission.d.ts +0 -3
- package/dist/add/add-permission.js +213 -22
- package/dist/add/add-scope.d.ts +1 -7
- package/dist/add/add-scope.js +1 -21
- package/dist/add/check-indirect-registrations.d.ts +9 -0
- package/dist/add/check-indirect-registrations.js +114 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/finalize.d.ts +10 -0
- package/dist/finalize.js +102 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +6 -5
- package/dist/initial-state.d.ts +7 -0
- package/dist/initial-state.js +216 -0
- package/dist/inspector.d.ts +6 -4
- package/dist/inspector.js +28 -302
- package/dist/lite.d.ts +26 -0
- package/dist/lite.js +25 -0
- package/dist/source-file-cache.d.ts +11 -0
- package/dist/source-file-cache.js +34 -10
- package/dist/types-map.js +2 -1
- package/dist/types.d.ts +32 -0
- package/dist/utils/apply-declared-scopes.d.ts +8 -0
- package/dist/utils/apply-declared-scopes.js +21 -0
- package/dist/utils/canonical-type-string.d.ts +14 -0
- package/dist/utils/canonical-type-string.js +162 -0
- package/dist/utils/context-based-id.d.ts +7 -0
- package/dist/utils/context-based-id.js +9 -0
- package/dist/utils/custom-types-generator.d.ts +15 -1
- package/dist/utils/custom-types-generator.js +17 -5
- package/dist/utils/ensure-function-metadata.d.ts +0 -14
- package/dist/utils/ensure-function-metadata.js +4 -1
- package/dist/utils/extract-function-name.d.ts +1 -1
- package/dist/utils/extract-function-name.js +1 -3
- package/dist/utils/filter-inspector-state.js +31 -1
- package/dist/utils/get-property-value.d.ts +5 -0
- package/dist/utils/get-property-value.js +14 -0
- package/dist/utils/inspector-constants.d.ts +16 -0
- package/dist/utils/inspector-constants.js +16 -0
- package/dist/utils/post-process.d.ts +1 -2
- package/dist/utils/post-process.js +27 -27
- package/dist/utils/read-jsx-literal-text.js +31 -1
- package/dist/utils/resolve-callee-name.d.ts +27 -0
- package/dist/utils/resolve-callee-name.js +147 -0
- package/dist/utils/resolve-service-types.d.ts +8 -0
- package/dist/utils/resolve-service-types.js +21 -0
- package/dist/utils/schema-generator.js +29 -2
- package/dist/utils/serialize-inspector-state.d.ts +7 -1
- package/dist/utils/serialize-inspector-state.js +16 -1
- package/dist/utils/workflow/dsl/patterns.d.ts +1 -8
- package/dist/utils/workflow/dsl/patterns.js +1 -7
- package/dist/visit.js +2 -0
- package/package.json +25 -9
- package/CHANGELOG.md +0 -4368
- package/OPTIMIZATION-PLAN.md +0 -201
- package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +0 -22
- package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +0 -20
- package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +0 -27
- package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +0 -20
- package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +0 -25
- package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +0 -20
- package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +0 -27
- package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +0 -27
- package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +0 -22
- package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +0 -29
- package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +0 -21
- package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +0 -21
- package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +0 -25
- package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +0 -19
- package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +0 -23
- package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +0 -24
- package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +0 -18
- package/knowledge/decisions/index.md +0 -38
- package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +0 -20
- package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +0 -19
- package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +0 -23
- package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +0 -23
- package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +0 -22
- package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +0 -28
- package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +0 -14
- package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +0 -30
- package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +0 -26
- package/knowledge/index.md +0 -14
- package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +0 -26
- package/knowledge/questions/index.md +0 -16
- package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +0 -23
- package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +0 -28
- package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +0 -22
- package/run-tests.sh +0 -68
- 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
|
|
35
|
-
|
|
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
|
|
49
|
-
if (!live.has(fileName))
|
|
50
|
-
cache.delete(
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 './
|
|
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,
|
|
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
|
*
|