@pikku/inspector 0.12.106 → 0.12.107
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/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
package/dist/add/add-auth.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import type { AddWiring } from '../types.js';
|
|
2
|
-
|
|
3
|
-
* The pikku function id of the single shared auth handler the CLI generates
|
|
4
|
-
* (`export const authHandler = pikkuSessionlessFunc(...)` in auth.gen.ts). An
|
|
5
|
-
* exported top-level const collapses the catch-all `/api/auth/**` route onto one
|
|
6
|
-
* worker, and the export name becomes the function id. Shared with the CLI
|
|
7
|
-
* codegen and the post-process service stamp so all three agree on the same id
|
|
8
|
-
* without the inspector having to import `@pikku/better-auth`.
|
|
9
|
-
*/
|
|
10
|
-
export declare const AUTH_HANDLER_FUNC_ID = "authHandler";
|
|
2
|
+
export { AUTH_HANDLER_FUNC_ID } from '../utils/inspector-constants.js';
|
|
11
3
|
/**
|
|
12
4
|
* Detects `pikkuBetterAuth((services) => betterAuth({...}))` calls.
|
|
13
5
|
*
|
package/dist/add/add-auth.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { ErrorCode } from '../error-codes.js';
|
|
3
3
|
import { extractServicesFromFunction } from '../utils/extract-services.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* worker, and the export name becomes the function id. Shared with the CLI
|
|
9
|
-
* codegen and the post-process service stamp so all three agree on the same id
|
|
10
|
-
* without the inspector having to import `@pikku/better-auth`.
|
|
11
|
-
*/
|
|
12
|
-
export const AUTH_HANDLER_FUNC_ID = 'authHandler';
|
|
4
|
+
import { resolveCalleeName } from '../utils/resolve-callee-name.js';
|
|
5
|
+
const isBetterAuth = (name) => name === 'betterAuth';
|
|
6
|
+
const isPikkuBetterAuth = (name) => name === 'pikkuBetterAuth';
|
|
7
|
+
export { AUTH_HANDLER_FUNC_ID } from '../utils/inspector-constants.js';
|
|
13
8
|
/** The default better-auth base path when `basePath` is not configured. */
|
|
14
9
|
const DEFAULT_BASE_PATH = '/api/auth';
|
|
15
10
|
/**
|
|
@@ -17,14 +12,12 @@ const DEFAULT_BASE_PATH = '/api/auth';
|
|
|
17
12
|
* factory body. Supports both `(s) => betterAuth({...})` and
|
|
18
13
|
* `(s) => { ...; return betterAuth({...}) }`.
|
|
19
14
|
*/
|
|
20
|
-
const findBetterAuthCall = (node) => {
|
|
15
|
+
const findBetterAuthCall = (node, checker) => {
|
|
21
16
|
let found;
|
|
22
17
|
const visit = (n) => {
|
|
23
18
|
if (found)
|
|
24
19
|
return;
|
|
25
|
-
if (ts.isCallExpression(n) &&
|
|
26
|
-
ts.isIdentifier(n.expression) &&
|
|
27
|
-
n.expression.text === 'betterAuth') {
|
|
20
|
+
if (ts.isCallExpression(n) && resolveCalleeName(n, checker, isBetterAuth)) {
|
|
28
21
|
found = n;
|
|
29
22
|
return;
|
|
30
23
|
}
|
|
@@ -42,6 +35,30 @@ const readStringProp = (obj, name) => {
|
|
|
42
35
|
return prop.initializer.text;
|
|
43
36
|
return undefined;
|
|
44
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Whether a property is written at all, literal or not — so a value the
|
|
40
|
+
* inspector cannot read can be told apart from one that was never set.
|
|
41
|
+
*/
|
|
42
|
+
const hasProp = (obj, name) => obj.properties.some((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
|
43
|
+
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
44
|
+
p.name.text === name);
|
|
45
|
+
/**
|
|
46
|
+
* True when a spread comes after the last assignment of `name` in `obj`, so the
|
|
47
|
+
* value written there may not be the one that applies. A spread before it is
|
|
48
|
+
* harmless: the property wins.
|
|
49
|
+
*/
|
|
50
|
+
const isOverriddenBySpread = (obj, name) => {
|
|
51
|
+
let assigned = -1;
|
|
52
|
+
obj.properties.forEach((p, index) => {
|
|
53
|
+
if ((ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
|
54
|
+
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
55
|
+
p.name.text === name) {
|
|
56
|
+
assigned = index;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return (assigned >= 0 &&
|
|
60
|
+
obj.properties.slice(assigned + 1).some(ts.isSpreadAssignment));
|
|
61
|
+
};
|
|
45
62
|
/** Find an object-literal-valued property off an object literal, if present. */
|
|
46
63
|
const readObjectProp = (obj, name) => {
|
|
47
64
|
const prop = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
|
|
@@ -159,11 +176,10 @@ and set password.`;
|
|
|
159
176
|
* `socialProviders` keys, whether email/password is enabled, and the services
|
|
160
177
|
* the factory touches. Exactly one `pikkuBetterAuth` is allowed per codebase.
|
|
161
178
|
*/
|
|
162
|
-
export const addAuth = (logger, node,
|
|
179
|
+
export const addAuth = (logger, node, checker, state) => {
|
|
163
180
|
if (!ts.isCallExpression(node))
|
|
164
181
|
return;
|
|
165
|
-
|
|
166
|
-
if (!ts.isIdentifier(expression) || expression.text !== 'pikkuBetterAuth')
|
|
182
|
+
if (!resolveCalleeName(node, checker, isPikkuBetterAuth))
|
|
167
183
|
return;
|
|
168
184
|
const sourceFile = node.getSourceFile().fileName;
|
|
169
185
|
// Walk up to the `export const <name> = pikkuBetterAuth(...)` binding.
|
|
@@ -206,7 +222,21 @@ export const addAuth = (logger, node, _checker, state) => {
|
|
|
206
222
|
let basePath = DEFAULT_BASE_PATH;
|
|
207
223
|
let hasCredentials = false;
|
|
208
224
|
let cookieCache = false;
|
|
209
|
-
|
|
225
|
+
let cookiePrefix;
|
|
226
|
+
let cookieCacheStrategy;
|
|
227
|
+
let cookieCacheVersion;
|
|
228
|
+
const unreadCookieOptions = [];
|
|
229
|
+
const readCookieOption = (block, name, path, enclosingOverridden) => {
|
|
230
|
+
if (!hasProp(block, name))
|
|
231
|
+
return undefined;
|
|
232
|
+
const value = enclosingOverridden || isOverriddenBySpread(block, name)
|
|
233
|
+
? undefined
|
|
234
|
+
: readStringProp(block, name);
|
|
235
|
+
if (value === undefined)
|
|
236
|
+
unreadCookieOptions.push(path);
|
|
237
|
+
return value;
|
|
238
|
+
};
|
|
239
|
+
const betterAuthCall = findBetterAuthCall(factory, checker);
|
|
210
240
|
const config = betterAuthCall?.arguments[0];
|
|
211
241
|
if (config && ts.isObjectLiteralExpression(config)) {
|
|
212
242
|
basePath = readStringProp(config, 'basePath') ?? DEFAULT_BASE_PATH;
|
|
@@ -221,8 +251,22 @@ export const addAuth = (logger, node, _checker, state) => {
|
|
|
221
251
|
cookieCache =
|
|
222
252
|
!enabledProp ||
|
|
223
253
|
enabledProp.initializer.kind !== ts.SyntaxKind.FalseKeyword;
|
|
254
|
+
const sessionOverridden = isOverriddenBySpread(config, 'session') ||
|
|
255
|
+
isOverriddenBySpread(session, 'cookieCache');
|
|
256
|
+
cookieCacheStrategy = readCookieOption(cookieCacheBlock, 'strategy', 'session.cookieCache.strategy', sessionOverridden);
|
|
257
|
+
cookieCacheVersion = readCookieOption(cookieCacheBlock, 'version', 'session.cookieCache.version', sessionOverridden);
|
|
224
258
|
}
|
|
225
259
|
}
|
|
260
|
+
// The stateless middleware never builds the auth instance, so it has to be
|
|
261
|
+
// told how the cookie was written: read the parts that change its name and
|
|
262
|
+
// its verification, and say so when one is not a literal.
|
|
263
|
+
const advanced = readObjectProp(config, 'advanced');
|
|
264
|
+
if (advanced) {
|
|
265
|
+
cookiePrefix = readCookieOption(advanced, 'cookiePrefix', 'advanced.cookiePrefix', isOverriddenBySpread(config, 'advanced'));
|
|
266
|
+
}
|
|
267
|
+
if (cookieCache && unreadCookieOptions.length > 0) {
|
|
268
|
+
logger.warn(`pikkuBetterAuth in ${sourceFile}: ${unreadCookieOptions.join(', ')} is not a string literal, or a later spread can replace it, so the generated betterAuthStatelessSession cannot read the session cookie the way it is written. Write it as a literal, or register betterAuthStatelessSession({ cookiePrefix, cookieCache: { strategy, version } }) yourself with addGlobalMiddleware — yours runs first and the generated one then does nothing.`);
|
|
269
|
+
}
|
|
226
270
|
const emailAndPassword = readObjectProp(config, 'emailAndPassword');
|
|
227
271
|
if (emailAndPassword) {
|
|
228
272
|
// `emailAndPassword: { enabled: true }` — treat a present block without an
|
|
@@ -272,6 +316,9 @@ export const addAuth = (logger, node, _checker, state) => {
|
|
|
272
316
|
basePath,
|
|
273
317
|
hasCredentials,
|
|
274
318
|
cookieCache,
|
|
319
|
+
...(cookiePrefix !== undefined && { cookiePrefix }),
|
|
320
|
+
...(cookieCacheStrategy !== undefined && { cookieCacheStrategy }),
|
|
321
|
+
...(cookieCacheVersion !== undefined && { cookieCacheVersion }),
|
|
275
322
|
plugins: [...state.auth.plugins],
|
|
276
323
|
services,
|
|
277
324
|
};
|
package/dist/add/add-cli.js
CHANGED
|
@@ -499,6 +499,7 @@ function extractEnumFromPropertyType(type, propertyName, typeChecker) {
|
|
|
499
499
|
const enumValues = [];
|
|
500
500
|
// Check if it's a union type (e.g., 'debug' | 'info' | 'warn')
|
|
501
501
|
if (propertyType.isUnion()) {
|
|
502
|
+
let fromEnum = false;
|
|
502
503
|
for (const unionType of propertyType.types) {
|
|
503
504
|
// Check if it's a string literal type
|
|
504
505
|
if (unionType.flags & ts.TypeFlags.StringLiteral) {
|
|
@@ -507,6 +508,7 @@ function extractEnumFromPropertyType(type, propertyName, typeChecker) {
|
|
|
507
508
|
}
|
|
508
509
|
// Check if it's an enum member (could be string or number enum)
|
|
509
510
|
else if (unionType.flags & ts.TypeFlags.EnumLiteral) {
|
|
511
|
+
fromEnum = true;
|
|
510
512
|
const enumLiteralType = unionType;
|
|
511
513
|
// For string enums, use the value directly
|
|
512
514
|
if (typeof enumLiteralType.value === 'string') {
|
|
@@ -521,6 +523,8 @@ function extractEnumFromPropertyType(type, propertyName, typeChecker) {
|
|
|
521
523
|
}
|
|
522
524
|
}
|
|
523
525
|
}
|
|
526
|
+
if (!fromEnum)
|
|
527
|
+
enumValues.sort();
|
|
524
528
|
}
|
|
525
529
|
// Check if it's an enum type directly
|
|
526
530
|
else if (propertyType.flags & ts.TypeFlags.Enum) {
|
|
@@ -25,6 +25,11 @@ const SCENARIO_STEP_KINDS = new Map([
|
|
|
25
25
|
['pikkuPlatformScenarioStep', 'platform'],
|
|
26
26
|
['pikkuAddonScenarioStep', 'addon'],
|
|
27
27
|
]);
|
|
28
|
+
const PIKKU_FUNC_PATTERN = /pikku.*func/i;
|
|
29
|
+
const isFunctionHelper = (name) => PIKKU_FUNC_PATTERN.test(name) ||
|
|
30
|
+
name === 'pikkuScenario' ||
|
|
31
|
+
name === 'pikkuWebhookReceive' ||
|
|
32
|
+
SCENARIO_STEP_KINDS.has(name);
|
|
28
33
|
import { getPropertyValue, getArrayPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
|
|
29
34
|
import { canonicalJSON, hashString } from '../utils/hash.js';
|
|
30
35
|
import { resolveMiddleware } from '../utils/middleware.js';
|
|
@@ -34,6 +39,7 @@ import { ErrorCode } from '../error-codes.js';
|
|
|
34
39
|
import { findPiiPaths } from '../utils/check-pii-output.js';
|
|
35
40
|
import { findClassifiedSinks } from '../utils/check-classified-sinks.js';
|
|
36
41
|
import { isScenarioInstrumentationFunction } from './scenario-instrumentation.js';
|
|
42
|
+
import { resolveCalleeName } from '../utils/resolve-callee-name.js';
|
|
37
43
|
const isValidVariableName = (name) => {
|
|
38
44
|
const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
39
45
|
return regex.test(name);
|
|
@@ -351,37 +357,27 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
351
357
|
if (!ts.isCallExpression(node))
|
|
352
358
|
return;
|
|
353
359
|
const { expression, arguments: args, typeArguments } = node;
|
|
354
|
-
//
|
|
355
|
-
|
|
360
|
+
// Match helpers whose name contains both "pikku" and "func" (case
|
|
361
|
+
// insensitive), plus pikkuScenario and the three scenario step wrappers
|
|
362
|
+
// (workflow wrappers without "func" in their names). The callee is matched
|
|
363
|
+
// by what it resolves to, so an aliased `pikkuFunc` is still a pikkuFunc —
|
|
364
|
+
// and still requires a session.
|
|
365
|
+
const callee = resolveCalleeName(node, checker, isFunctionHelper);
|
|
366
|
+
if (!callee)
|
|
356
367
|
return;
|
|
357
|
-
}
|
|
358
|
-
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
359
|
-
// plus pikkuScenario and the three scenario step wrappers (workflow wrappers
|
|
360
|
-
// without "func" in their names)
|
|
361
|
-
const pikkuFuncPattern = /pikku.*func/i;
|
|
362
|
-
if (!pikkuFuncPattern.test(expression.text) &&
|
|
363
|
-
expression.text !== 'pikkuScenario' &&
|
|
364
|
-
expression.text !== 'pikkuWebhookReceive' &&
|
|
365
|
-
!SCENARIO_STEP_KINDS.has(expression.text)) {
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
368
|
// A scenario step is bundled like any invoked function, but is deliberately
|
|
369
369
|
// never RPC-registered: a step may drive a browser, so exposing it over the
|
|
370
370
|
// network would put a test harness on the public API surface.
|
|
371
|
-
const scenarioStepKind = SCENARIO_STEP_KINDS.get(
|
|
371
|
+
const scenarioStepKind = SCENARIO_STEP_KINDS.get(callee);
|
|
372
372
|
const isScenarioStep = scenarioStepKind !== undefined;
|
|
373
373
|
// A scenario's own body. Marked so the codegen partitions and the deploy
|
|
374
374
|
// analyzer can tell it apart from an application workflow without having to
|
|
375
375
|
// reach into the workflow graph.
|
|
376
|
-
const isScenario =
|
|
376
|
+
const isScenario = callee === 'pikkuScenario';
|
|
377
377
|
// A webhook source's `receive`. Only its source's route runs it, with the
|
|
378
378
|
// raw request, so like a scenario step it is bundled but never RPC-registered.
|
|
379
|
-
const isWebhookReceive =
|
|
379
|
+
const isWebhookReceive = callee === 'pikkuWebhookReceive';
|
|
380
380
|
const notRpc = isScenarioStep || isWebhookReceive;
|
|
381
|
-
// only handle calls like pikkuFunc(...)
|
|
382
|
-
if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
381
|
if (args.length === 0)
|
|
386
382
|
return;
|
|
387
383
|
let { pikkuFuncId, name, explicitName, exportedName } = extractFunctionName(node, checker, state.rootDir);
|
|
@@ -540,7 +536,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
540
536
|
// take a single `func`, and a binding key here is a coded error rather
|
|
541
537
|
// than a convention nobody reads.
|
|
542
538
|
if (declaredSurfaces.length > 0) {
|
|
543
|
-
logger.error(`• ${
|
|
539
|
+
logger.error(`• ${callee}('${name}') declares ${declaredSurfaces.join('/')} — a ${scenarioStepKind} step has one implementation, declared as \`func\`. Surfaces are how a person drives the system, and nobody drives this one.`);
|
|
544
540
|
return;
|
|
545
541
|
}
|
|
546
542
|
// The phase rule depends on it: an assertion runs every witness it has and
|
|
@@ -665,7 +661,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
665
661
|
// A remote channel capability is a contract with no body: the connected
|
|
666
662
|
// client answers it. Everything past this point reads the handler, so it gets
|
|
667
663
|
// its own path rather than a nullable handler threaded through all of it.
|
|
668
|
-
if (
|
|
664
|
+
if (callee === 'pikkuRemoteChannelFunc') {
|
|
669
665
|
addRemoteChannelFunc({
|
|
670
666
|
state,
|
|
671
667
|
logger,
|
|
@@ -679,8 +675,8 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
679
675
|
});
|
|
680
676
|
return;
|
|
681
677
|
}
|
|
682
|
-
const isMCPToolFunc =
|
|
683
|
-
const isListFunc =
|
|
678
|
+
const isMCPToolFunc = callee === 'pikkuMCPToolFunc';
|
|
679
|
+
const isListFunc = callee === 'pikkuListFunc';
|
|
684
680
|
const mcpEnabled = mcp || isMCPToolFunc;
|
|
685
681
|
// Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
|
|
686
682
|
const handler = resolvedFunc &&
|
|
@@ -777,7 +773,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
777
773
|
// Every other wrapper reads generic[0] as INPUT, so without this guard the
|
|
778
774
|
// connect handler's output generic is mis-recorded as inputSchemaName and the
|
|
779
775
|
// empty WS handshake fails input validation at connect (1008/403).
|
|
780
|
-
const isChannelConnectionFunc = /ChannelConnection/i.test(
|
|
776
|
+
const isChannelConnectionFunc = /ChannelConnection/i.test(callee);
|
|
781
777
|
const capitalizedName = funcIdToTypeName(name);
|
|
782
778
|
// --- Input Extraction ---
|
|
783
779
|
let inputNames = [];
|
|
@@ -857,7 +853,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
857
853
|
pikkuMCPToolFunc: 'MCPToolResponse',
|
|
858
854
|
pikkuMCPPromptFunc: 'MCPPromptResponse',
|
|
859
855
|
};
|
|
860
|
-
const mcpOutputType = mcpOutputTypes[
|
|
856
|
+
const mcpOutputType = mcpOutputTypes[callee];
|
|
861
857
|
if (mcpOutputType && outputNames[0] !== mcpOutputType) {
|
|
862
858
|
let resolved = false;
|
|
863
859
|
const rawSymbol = checker.getSymbolAtLocation(expression);
|
|
@@ -925,7 +921,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
925
921
|
}
|
|
926
922
|
}
|
|
927
923
|
}
|
|
928
|
-
const sessionless =
|
|
924
|
+
const sessionless = callee !== 'pikkuFunc';
|
|
929
925
|
// ── Classification brand check ─────────────────────────────────────────────
|
|
930
926
|
// Walk the function body's ACTUAL inferred return type looking for classification
|
|
931
927
|
// brands (__classification__ property on Private<T>, Pii<T>, Secret<T>).
|
|
@@ -1017,7 +1013,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1017
1013
|
}
|
|
1018
1014
|
}
|
|
1019
1015
|
const implementationHash = computeImplementationHash({
|
|
1020
|
-
wrapper: expression
|
|
1016
|
+
wrapper: ts.isIdentifier(expression) ? callee : callee,
|
|
1021
1017
|
handler,
|
|
1022
1018
|
objectNode,
|
|
1023
1019
|
isDirectFunction,
|
|
@@ -1051,7 +1047,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1051
1047
|
state.functions.meta[pikkuFuncId] = {
|
|
1052
1048
|
pikkuFuncId,
|
|
1053
1049
|
functionType: 'user',
|
|
1054
|
-
funcWrapper:
|
|
1050
|
+
funcWrapper: callee,
|
|
1055
1051
|
sessionless,
|
|
1056
1052
|
name,
|
|
1057
1053
|
services,
|
|
@@ -1162,7 +1158,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1162
1158
|
}
|
|
1163
1159
|
// Workflow functions don't get registered as RPC functions,
|
|
1164
1160
|
// they are their own type handled by add-workflow
|
|
1165
|
-
if (
|
|
1161
|
+
if (callee.includes('Workflow')) {
|
|
1166
1162
|
return;
|
|
1167
1163
|
}
|
|
1168
1164
|
// Trigger and channel connect/disconnect functions are not callable via RPC
|
|
@@ -1171,7 +1167,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1171
1167
|
/ChannelConnection/i,
|
|
1172
1168
|
/ChannelDisconnection/i,
|
|
1173
1169
|
];
|
|
1174
|
-
if (nonRPCPatterns.some((pattern) => pattern.test(
|
|
1170
|
+
if (nonRPCPatterns.some((pattern) => pattern.test(callee))) {
|
|
1175
1171
|
return;
|
|
1176
1172
|
}
|
|
1177
1173
|
// Store function file location for wiring generation
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { getPropertyValue, getArrayPropertyValue, assertStringLiteralProperty, } from '../utils/get-property-value.js';
|
|
2
|
+
import { getPropertyValue, getArrayPropertyValue, getObjectPropertyLiteral, assertStringLiteralProperty, } from '../utils/get-property-value.js';
|
|
3
3
|
import { ErrorCode } from '../error-codes.js';
|
|
4
4
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
|
|
5
5
|
import { parseDurationString } from '@pikku/core/utils';
|
|
6
|
+
import { SECRET_GENERATE_MIN_SIZE } from '@pikku/core/secret';
|
|
6
7
|
export const createAddKeyedWiring = (config) => {
|
|
7
8
|
return (logger, node, checker, state, _options) => {
|
|
8
9
|
if (!ts.isCallExpression(node)) {
|
|
@@ -79,7 +80,7 @@ export const createAddKeyedWiring = (config) => {
|
|
|
79
80
|
logger.critical(ErrorCode.MISSING_NAME, `${config.label} '${nameValue}' is missing the required 'schema' property or schema is not a variable reference.`);
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
|
-
if (rotationPeriodValue) {
|
|
83
|
+
if (rotationPeriodValue && rotationPeriodValue !== 'never') {
|
|
83
84
|
try {
|
|
84
85
|
parseDurationString(rotationPeriodValue);
|
|
85
86
|
}
|
|
@@ -88,6 +89,24 @@ export const createAddKeyedWiring = (config) => {
|
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
let generateValue;
|
|
93
|
+
const generateObj = config.idField === 'secretId'
|
|
94
|
+
? getObjectPropertyLiteral(obj, 'generate')
|
|
95
|
+
: null;
|
|
96
|
+
if (generateObj) {
|
|
97
|
+
const sizeValue = getPropertyValue(generateObj, 'size');
|
|
98
|
+
if (typeof sizeValue !== 'number' ||
|
|
99
|
+
!Number.isInteger(sizeValue) ||
|
|
100
|
+
sizeValue < SECRET_GENERATE_MIN_SIZE) {
|
|
101
|
+
logger.critical(ErrorCode.INVALID_VALUE, `${config.label} '${nameValue}' needs a 'generate' size of at least ${SECRET_GENERATE_MIN_SIZE} bytes.`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!rotationPeriodValue) {
|
|
105
|
+
logger.critical(ErrorCode.INVALID_VALUE, `${config.label} '${nameValue}' is generated, so it needs a 'rotationPeriod' such as '30day', or 'never' to generate it once.`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
generateValue = { size: sizeValue };
|
|
109
|
+
}
|
|
91
110
|
const sourceFile = node.getSourceFile().fileName;
|
|
92
111
|
const wiringState = config.getState(state);
|
|
93
112
|
wiringState.files.add(sourceFile);
|
|
@@ -105,6 +124,7 @@ export const createAddKeyedWiring = (config) => {
|
|
|
105
124
|
displayName: displayNameValue,
|
|
106
125
|
description: descriptionValue || undefined,
|
|
107
126
|
rotationPeriod: rotationPeriodValue || undefined,
|
|
127
|
+
generate: generateValue,
|
|
108
128
|
docsUrl: docsUrlValue || undefined,
|
|
109
129
|
allowedHosts: allowedHostsValue || undefined,
|
|
110
130
|
[config.idField]: idValue,
|
|
@@ -4,6 +4,7 @@ import { extractServicesFromFunction, extractUsedWires, } from '../utils/extract
|
|
|
4
4
|
import { extractMiddlewareRefs } from '../utils/middleware.js';
|
|
5
5
|
import { getPropertyValue } from '../utils/get-property-value.js';
|
|
6
6
|
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
|
|
7
|
+
import { MIDDLEWARE_HELPERS, resolveCalleeName, } from '../utils/resolve-callee-name.js';
|
|
7
8
|
/**
|
|
8
9
|
* Record a middleware group, keeping any registration already stored under the
|
|
9
10
|
* same pattern or tag.
|
|
@@ -113,11 +114,11 @@ function renameTempDefinitions(state, definitionIds, groupType, groupKey, storeK
|
|
|
113
114
|
export const addMiddleware = (logger, node, checker, state) => {
|
|
114
115
|
if (!ts.isCallExpression(node))
|
|
115
116
|
return;
|
|
116
|
-
const {
|
|
117
|
-
|
|
117
|
+
const { arguments: args } = node;
|
|
118
|
+
const callee = resolveCalleeName(node, checker, MIDDLEWARE_HELPERS);
|
|
119
|
+
if (!callee)
|
|
118
120
|
return;
|
|
119
|
-
|
|
120
|
-
if (expression.text === 'pikkuMiddleware') {
|
|
121
|
+
if (callee === 'pikkuMiddleware') {
|
|
121
122
|
const arg = args[0];
|
|
122
123
|
if (!arg)
|
|
123
124
|
return;
|
|
@@ -174,7 +175,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
174
175
|
logger.debug(`• Found middleware with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`);
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
177
|
-
if (
|
|
178
|
+
if (callee === 'pikkuMiddlewareFactory') {
|
|
178
179
|
const factoryNode = args[0];
|
|
179
180
|
if (!factoryNode)
|
|
180
181
|
return;
|
|
@@ -192,8 +193,8 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
192
193
|
let description;
|
|
193
194
|
const findPikkuMiddlewareCall = (node) => {
|
|
194
195
|
if (ts.isCallExpression(node)) {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
if (resolveCalleeName(node, checker, MIDDLEWARE_HELPERS) ===
|
|
197
|
+
'pikkuMiddleware') {
|
|
197
198
|
return node;
|
|
198
199
|
}
|
|
199
200
|
}
|
|
@@ -260,7 +261,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
260
261
|
logger.debug(`• Found middleware factory with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}`);
|
|
261
262
|
return;
|
|
262
263
|
}
|
|
263
|
-
if (
|
|
264
|
+
if (callee === 'addTagMiddleware') {
|
|
264
265
|
const tagArg = args[0];
|
|
265
266
|
const middlewareArrayArg = args[1];
|
|
266
267
|
if (!tagArg || !middlewareArrayArg)
|
|
@@ -335,7 +336,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
335
336
|
logger.debug(`• Found tag middleware group: ${tag} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
|
|
336
337
|
return;
|
|
337
338
|
}
|
|
338
|
-
if (
|
|
339
|
+
if (callee === 'addGlobalMiddleware') {
|
|
339
340
|
const middlewareArrayArg = args[0];
|
|
340
341
|
if (!middlewareArrayArg ||
|
|
341
342
|
!ts.isArrayLiteralExpression(middlewareArrayArg)) {
|
|
@@ -362,7 +363,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
362
363
|
logger.debug(`• Found global middleware group with ${refs.length} entries`);
|
|
363
364
|
return;
|
|
364
365
|
}
|
|
365
|
-
if (
|
|
366
|
+
if (callee === 'addHTTPMiddleware') {
|
|
366
367
|
const patternArg = args[0];
|
|
367
368
|
const middlewareArrayArg = args[1];
|
|
368
369
|
if (!patternArg || !middlewareArrayArg)
|
|
@@ -438,7 +439,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
438
439
|
logger.debug(`• Found HTTP route middleware group: ${pattern} -> [${instanceIds.join(', ')}] (${isFactory ? 'factory' : 'direct'})`);
|
|
439
440
|
return;
|
|
440
441
|
}
|
|
441
|
-
if (
|
|
442
|
+
if (callee === 'pikkuChannelMiddleware') {
|
|
442
443
|
const arg = args[0];
|
|
443
444
|
if (!arg)
|
|
444
445
|
return;
|
|
@@ -476,7 +477,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
476
477
|
logger.debug(`• Found channel middleware with services: ${services.services.join(', ')}`);
|
|
477
478
|
return;
|
|
478
479
|
}
|
|
479
|
-
if (
|
|
480
|
+
if (callee === 'pikkuChannelMiddlewareFactory') {
|
|
480
481
|
const factoryNode = args[0];
|
|
481
482
|
if (!factoryNode)
|
|
482
483
|
return;
|
|
@@ -488,8 +489,8 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
488
489
|
let services = { optimized: false, services: [] };
|
|
489
490
|
const findPikkuChannelMiddlewareCall = (n) => {
|
|
490
491
|
if (ts.isCallExpression(n)) {
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
if (resolveCalleeName(n, checker, MIDDLEWARE_HELPERS) ===
|
|
493
|
+
'pikkuChannelMiddleware') {
|
|
493
494
|
return n;
|
|
494
495
|
}
|
|
495
496
|
}
|
|
@@ -539,7 +540,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
539
540
|
logger.debug(`• Found channel middleware factory with services: ${services.services.join(', ')}`);
|
|
540
541
|
return;
|
|
541
542
|
}
|
|
542
|
-
if (
|
|
543
|
+
if (callee === 'pikkuAgentMiddleware') {
|
|
543
544
|
const arg = args[0];
|
|
544
545
|
if (!arg)
|
|
545
546
|
return;
|
|
@@ -587,7 +588,7 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
587
588
|
logger.debug(`• Found AI middleware with services: ${services.services.join(', ')}`);
|
|
588
589
|
return;
|
|
589
590
|
}
|
|
590
|
-
if (
|
|
591
|
+
if (callee === 'addChannelMiddleware') {
|
|
591
592
|
const tagArg = args[0];
|
|
592
593
|
const middlewareArrayArg = args[1];
|
|
593
594
|
if (!tagArg || !middlewareArrayArg)
|