@pikku/inspector 0.12.87 → 0.12.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/dist/add/add-functions.js +15 -10
- package/dist/utils/load-addon-functions-meta.d.ts +1 -0
- package/dist/utils/load-addon-functions-meta.js +83 -5
- package/dist/utils/post-process.js +7 -0
- package/dist/utils/validate-exposed-functions-gated.js +8 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
## 0.12.89
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- bc488cf: fix(inspector): an explicit `auth: false` declares an exposed sessionless function public, so PKU574 no longer warns about it
|
|
6
|
+
|
|
7
|
+
A genuinely public endpoint — a published programme, a health check — had no
|
|
8
|
+
honest way to quiet PKU574: the only options were an always-true permission or
|
|
9
|
+
`permissionsInBody: true`, both of which claim a gate that does not exist. The
|
|
10
|
+
inspector now records `auth` on function meta exactly as written instead of
|
|
11
|
+
dropping `false`, and the check treats an explicit `auth: false` as the author
|
|
12
|
+
declaring the function public on purpose. A sessionless function that leaves
|
|
13
|
+
`auth` out still warns.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [50b59a3]
|
|
16
|
+
- Updated dependencies [2b946e9]
|
|
17
|
+
- Updated dependencies [bc488cf]
|
|
18
|
+
- @pikku/core@0.12.123
|
|
19
|
+
|
|
20
|
+
## 0.12.88
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 55ab4d1: An addon's metadata now resolves from the package that calls `wireAddon`, as well as from the repo root, so a workspace addon listed only in `packages/functions` loads. When it still cannot be found, the warning names the package, the directories tried and the fix: add the dependency and install, or build the addon if it is installed but has no generated metadata.
|
|
25
|
+
- f6c1dd6: A fresh project with Better Auth no longer gets PKU951 for `BETTER_AUTH_SECRET` on its first `pikku all`. The secret is declared by the generated `auth-secrets.gen.ts`, which did not exist yet when the first inspection ran.
|
|
26
|
+
- 0210e96: Review fixes for the OpenAPI addon onboarding:
|
|
27
|
+
|
|
28
|
+
- A delegated sign-in whose email the upstream did not return, including a login typed as an email, never links to an existing user. An authenticator that omits `syntheticEmail` counts as synthetic.
|
|
29
|
+
- `pikkuActor` credentials take an optional `remove`, which drops a credential the environment no longer sets. The actor log line names the user id, not the email.
|
|
30
|
+
- Basic credentials are UTF-8 encoded, and a Swagger 2 `application` OAuth flow keeps its token URL.
|
|
31
|
+
- `pikku new addon` writes a `file:` path relative to each package when the app is not a workspace, and reports an auth.ts factory it cannot edit instead of half-wiring it.
|
|
32
|
+
- The inspector reads an addon from the package that declares it before the root.
|
|
33
|
+
- The dev credentials key file is created exclusively, so two `pikku dev` processes agree on one key.
|
|
34
|
+
|
|
35
|
+
- 86c2f1d: `pikku dev` keeps stored credentials in the dev database, so connected accounts and delegated sign-ins survive a restart. The key comes from `PIKKU_DEV_CREDENTIALS_KEY`, or is generated once into `.pikku-runtime/dev-credentials.key`. A project that declares a credential now gets the credential tables in its generated migration; without them dev falls back to the in-memory store and says so once.
|
|
36
|
+
- da9b931: An inline `input`/`output` schema (PKU489) is now an `error` diagnostic instead of a critical one, so `pikku dev` keeps running. The function is validated against its TypeScript type until the schema is extracted to an exported variable; `--fail-on-error` still blocks it.
|
|
37
|
+
- Updated dependencies [e84abd0]
|
|
38
|
+
- @pikku/core@0.12.121
|
|
39
|
+
|
|
1
40
|
## 0.12.87
|
|
2
41
|
|
|
3
42
|
### Patch Changes
|
|
@@ -397,10 +397,11 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
397
397
|
let remote;
|
|
398
398
|
let mcp;
|
|
399
399
|
/**
|
|
400
|
-
* A sessionless function's own `auth
|
|
401
|
-
* baseline — a `pikkuFunc` always needs a session — while this
|
|
402
|
-
*
|
|
403
|
-
*
|
|
400
|
+
* A sessionless function's own `auth`, exactly as written. `sessionless`
|
|
401
|
+
* records the baseline — a `pikkuFunc` always needs a session — while this
|
|
402
|
+
* records what a `pikkuSessionlessFunc` says about itself: `true` tightens it
|
|
403
|
+
* to require a session, `false` declares it public on purpose. Both are
|
|
404
|
+
* otherwise invisible to anything reading meta; left out, it stays undefined.
|
|
404
405
|
*/
|
|
405
406
|
let auth;
|
|
406
407
|
/** The author's claim that the body authorizes its own callers. */
|
|
@@ -629,12 +630,16 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
629
630
|
}
|
|
630
631
|
}
|
|
631
632
|
else if (ts.isCallExpression(prop.initializer)) {
|
|
632
|
-
// Bad - it's an inline expression
|
|
633
633
|
const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
|
|
634
|
-
logger.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
`
|
|
634
|
+
logger.diagnostic({
|
|
635
|
+
severity: 'error',
|
|
636
|
+
code: ErrorCode.INLINE_SCHEMA,
|
|
637
|
+
message: `Inline schemas are not supported for '${propName}' in '${name}'; ` +
|
|
638
|
+
`it is validated against its TypeScript type only, so refinements are not enforced.\n` +
|
|
639
|
+
` Extract to an exported variable:\n` +
|
|
640
|
+
` export const ${schemaName} = ${prop.initializer.getText()}\n` +
|
|
641
|
+
` Then use: ${propName}: ${schemaName}`,
|
|
642
|
+
});
|
|
638
643
|
}
|
|
639
644
|
}
|
|
640
645
|
}
|
|
@@ -994,7 +999,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
994
999
|
inputs: inputNames.filter((n) => n !== 'void') ?? null,
|
|
995
1000
|
outputs: outputNames.filter((n) => n !== 'void') ?? null,
|
|
996
1001
|
expose: expose || undefined,
|
|
997
|
-
auth: auth
|
|
1002
|
+
auth: typeof auth === 'boolean' ? auth : undefined,
|
|
998
1003
|
permissionsInBody: permissionsInBody || undefined,
|
|
999
1004
|
audit,
|
|
1000
1005
|
remote: remote || undefined,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InspectorState, InspectorLogger } from '../types.js';
|
|
2
|
+
export declare const describeMissingAddonMeta: (namespace: string, packageName: string, dirs: string[], declFile?: string) => string;
|
|
2
3
|
/**
|
|
3
4
|
* After the setup sweep discovers wireAddon() declarations, load each addon
|
|
4
5
|
* package's function metadata so that wiring handlers (channels, HTTP routes,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { readFile, readdir } from 'fs/promises';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
|
-
import { join, dirname } from 'path';
|
|
4
|
+
import { join, dirname, parse, relative } from 'path';
|
|
5
5
|
import { ErrorCode } from '../error-codes.js';
|
|
6
6
|
const isHTTPRouteConfig = (value) => typeof value === 'object' &&
|
|
7
7
|
value !== null &&
|
|
@@ -90,6 +90,77 @@ const resolveAddonMeta = (require, packageName, baseName) => {
|
|
|
90
90
|
}
|
|
91
91
|
return null;
|
|
92
92
|
};
|
|
93
|
+
const nearestPackageDir = (file) => {
|
|
94
|
+
const root = parse(file).root;
|
|
95
|
+
let dir = dirname(file);
|
|
96
|
+
while (dir && dir !== root) {
|
|
97
|
+
if (existsSync(join(dir, 'package.json')))
|
|
98
|
+
return dir;
|
|
99
|
+
const parent = dirname(dir);
|
|
100
|
+
if (parent === dir)
|
|
101
|
+
break;
|
|
102
|
+
dir = parent;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* An addon is installed wherever the package that calls `wireAddon` lists it,
|
|
108
|
+
* which in a workspace is usually not the repo root. Resolve from the declaring
|
|
109
|
+
* package first, as its code does at runtime, then from the root.
|
|
110
|
+
*/
|
|
111
|
+
const addonResolutionDirs = (rootDir, declFile) => {
|
|
112
|
+
const declDir = declFile ? nearestPackageDir(declFile) : null;
|
|
113
|
+
return declDir && declDir !== rootDir ? [declDir, rootDir] : [rootDir];
|
|
114
|
+
};
|
|
115
|
+
const createAddonResolver = (dirs) => {
|
|
116
|
+
const requires = dirs.map((dir) => createRequire(join(dir, 'package.json')));
|
|
117
|
+
return {
|
|
118
|
+
resolve: (id) => {
|
|
119
|
+
let lastError;
|
|
120
|
+
for (const req of requires) {
|
|
121
|
+
try {
|
|
122
|
+
return req.resolve(id);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
lastError = e;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw lastError;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const findInstalledPackageDir = (dirs, packageName) => {
|
|
133
|
+
for (const start of dirs) {
|
|
134
|
+
const root = parse(start).root;
|
|
135
|
+
let dir = start;
|
|
136
|
+
while (dir) {
|
|
137
|
+
const candidate = join(dir, 'node_modules', packageName);
|
|
138
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
139
|
+
return candidate;
|
|
140
|
+
if (dir === root)
|
|
141
|
+
break;
|
|
142
|
+
const parent = dirname(dir);
|
|
143
|
+
if (parent === dir)
|
|
144
|
+
break;
|
|
145
|
+
dir = parent;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
export const describeMissingAddonMeta = (namespace, packageName, dirs, declFile) => {
|
|
151
|
+
const caller = declFile
|
|
152
|
+
? relative(dirs[dirs.length - 1], declFile) || declFile
|
|
153
|
+
: undefined;
|
|
154
|
+
const declaredIn = caller ? ` (declared in ${caller})` : '';
|
|
155
|
+
const packageDir = findInstalledPackageDir(dirs, packageName);
|
|
156
|
+
if (!packageDir) {
|
|
157
|
+
const declaringPackage = dirs[0];
|
|
158
|
+
return (`wireAddon('${namespace}')${declaredIn} names ${packageName}, which is not installed where it can be resolved — tried ${dirs.join(', ')}. ` +
|
|
159
|
+
`Add "${packageName}": "workspace:*" (or a version) to the dependencies of ${join(declaringPackage, 'package.json')} and run install.`);
|
|
160
|
+
}
|
|
161
|
+
return (`wireAddon('${namespace}')${declaredIn}: ${packageName} is installed at ${packageDir} but has not been built — ` +
|
|
162
|
+
`its dist/.pikku/addon/function/pikku-functions-meta.gen.json does not exist. Run the addon's build (pikku all && tsc && pikku dist) in ${packageDir}.`);
|
|
163
|
+
};
|
|
93
164
|
/**
|
|
94
165
|
* Give the addon's input/output type names a declaration file to be imported
|
|
95
166
|
* from, so a wiring that references an addon function can name its contract.
|
|
@@ -128,21 +199,24 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
128
199
|
const { wireAddonDeclarations } = state.rpc;
|
|
129
200
|
if (wireAddonDeclarations.size === 0)
|
|
130
201
|
return;
|
|
131
|
-
const require = createRequire(join(state.rootDir, 'package.json'));
|
|
132
202
|
for (const [namespace, decl] of wireAddonDeclarations) {
|
|
133
203
|
// Remote addons (wireRemoteAddon) ship as a devDependency: types only.
|
|
134
204
|
// Their functions, secrets, variables, schemas and services live on the
|
|
135
205
|
// HOST that runs them — never load or require any of that here.
|
|
136
206
|
if (decl.remote)
|
|
137
207
|
continue;
|
|
208
|
+
const dirs = addonResolutionDirs(state.rootDir, decl.file);
|
|
209
|
+
const require = createAddonResolver(dirs);
|
|
138
210
|
try {
|
|
139
211
|
// Verbose first. `description` is one of the fields stripped from the
|
|
140
212
|
// minimal copy, and it is what an addon's function is offered to a model
|
|
141
213
|
// under — both as an MCP tool below and as an agent tool. Read the minimal
|
|
142
214
|
// one and every addon function looks undescribed.
|
|
143
215
|
const metaPath = resolveAddonMeta(require, decl.package, 'function/pikku-functions-meta');
|
|
144
|
-
if (!metaPath)
|
|
145
|
-
|
|
216
|
+
if (!metaPath) {
|
|
217
|
+
logger.warn(describeMissingAddonMeta(namespace, decl.package, dirs, decl.file));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
146
220
|
const raw = await readFile(metaPath, 'utf-8');
|
|
147
221
|
const meta = JSON.parse(raw);
|
|
148
222
|
state.addonFunctions[namespace] = meta;
|
|
@@ -375,11 +449,11 @@ export async function loadAddonSchemas(logger, state) {
|
|
|
375
449
|
const { wireAddonDeclarations } = state.rpc;
|
|
376
450
|
if (wireAddonDeclarations.size === 0)
|
|
377
451
|
return;
|
|
378
|
-
const require = createRequire(join(state.rootDir, 'package.json'));
|
|
379
452
|
for (const [namespace, decl] of wireAddonDeclarations) {
|
|
380
453
|
// Remote addons carry no local schemas — their funcs run on the host.
|
|
381
454
|
if (decl.remote)
|
|
382
455
|
continue;
|
|
456
|
+
const require = createAddonResolver(addonResolutionDirs(state.rootDir, decl.file));
|
|
383
457
|
try {
|
|
384
458
|
const metaPath = require.resolve(`${decl.package}/.pikku/function/pikku-functions-meta.gen.json`);
|
|
385
459
|
const schemasDir = join(dirname(metaPath), '..', 'schemas', 'schemas');
|
|
@@ -400,6 +474,10 @@ export async function loadAddonSchemas(logger, state) {
|
|
|
400
474
|
}
|
|
401
475
|
}
|
|
402
476
|
catch (error) {
|
|
477
|
+
if (error?.code === 'MODULE_NOT_FOUND' ||
|
|
478
|
+
error?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
403
481
|
logger.warn(`Failed to load addon schemas for '${namespace}' (${decl.package}): ${error.message}`);
|
|
404
482
|
}
|
|
405
483
|
}
|
|
@@ -287,6 +287,9 @@ export function aggregateRequiredServices(state) {
|
|
|
287
287
|
if ((state.analytics?.length ?? 0) > 0) {
|
|
288
288
|
requiredServices.add('analyticsService');
|
|
289
289
|
}
|
|
290
|
+
if ((state.credentials?.definitions?.length ?? 0) > 0) {
|
|
291
|
+
requiredServices.add('credentialService');
|
|
292
|
+
}
|
|
290
293
|
// 7. Services that consumed addons need from the parent project.
|
|
291
294
|
const addonFnServices = new Map();
|
|
292
295
|
for (const [namespace, fns] of Object.entries(state.addonFunctions ?? {})) {
|
|
@@ -1146,6 +1149,10 @@ export function validateSecretUsage(logger, state) {
|
|
|
1146
1149
|
declared.add(definition.oauth2.tokenSecretId);
|
|
1147
1150
|
}
|
|
1148
1151
|
}
|
|
1152
|
+
// auth-secrets.gen.ts declares it, but that file is written after the first
|
|
1153
|
+
// inspection of a fresh project, so its defineSecret is not seen yet.
|
|
1154
|
+
if (state.auth?.definition)
|
|
1155
|
+
declared.add('BETTER_AUTH_SECRET');
|
|
1149
1156
|
for (const [file, usage] of state.secrets.usage) {
|
|
1150
1157
|
const relativeFile = relative(state.rootDir, file);
|
|
1151
1158
|
const undeclared = usage.keys.filter((key) => !declared.has(key));
|
|
@@ -64,8 +64,9 @@ export function validateExposedFunctionsGated(logger, state) {
|
|
|
64
64
|
(rest > 0 ? `, and ${rest} more` : '') +
|
|
65
65
|
`. Add permissions to the function, set \`auth: true\` if a session is ` +
|
|
66
66
|
`required after all, gate the whole addon with ` +
|
|
67
|
-
`wireAddon({ auth: true }),
|
|
68
|
-
`${one ? 'it was' : 'they were'} not meant to be callable from outside
|
|
67
|
+
`wireAddon({ auth: true }), drop \`expose: true\` if ` +
|
|
68
|
+
`${one ? 'it was' : 'they were'} not meant to be callable from outside, ` +
|
|
69
|
+
`or set \`auth: false\` to declare ${one ? 'it' : 'them'} public on purpose.`,
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
const gatesItself = (meta) =>
|
|
@@ -79,6 +80,11 @@ meta.sessionless === false ||
|
|
|
79
80
|
// and a warning that is usually wrong stops being read.
|
|
80
81
|
meta.permissionsInBody === true ||
|
|
81
82
|
meta.auth === true ||
|
|
83
|
+
// The author wrote `auth: false` on a sessionless function: public on
|
|
84
|
+
// purpose — a health check, a sign-up, a published catalogue. A declaration,
|
|
85
|
+
// like `permissionsInBody`, so the default-open function nobody thought
|
|
86
|
+
// about still warns and the one somebody decided about does not.
|
|
87
|
+
meta.auth === false ||
|
|
82
88
|
(meta.scopes?.length ?? 0) > 0 ||
|
|
83
89
|
(meta.permissions?.length ?? 0) > 0;
|
|
84
90
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.89",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"zod-to-ts": "^2.1.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@pikku/core": "^0.12.
|
|
58
|
+
"@pikku/core": "^0.12.123"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@pikku/core": "^0.12.
|
|
61
|
+
"@pikku/core": "^0.12.123",
|
|
62
62
|
"@types/node": "^24.13.3"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|