@pikku/inspector 0.12.17 → 0.12.19
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 +54 -0
- package/dist/add/add-functions.js +10 -1
- package/dist/add/add-keyed-wiring.js +12 -0
- package/dist/add/add-workflow.js +0 -6
- package/dist/utils/check-pii-output.js +26 -3
- package/dist/utils/filter-inspector-state.js +13 -7
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +0 -1
- package/package.json +2 -2
- package/src/add/add-functions.test.ts +52 -0
- package/src/add/add-functions.ts +13 -1
- package/src/add/add-keyed-wiring.ts +17 -0
- package/src/add/add-workflow.ts +0 -7
- package/src/add/pii-check.test.ts +8 -3
- package/src/utils/check-pii-output.ts +27 -5
- package/src/utils/filter-inspector-state.ts +15 -8
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
+
## 0.12.19
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- fe70fe0: fix(db): make classified columns usable in Kysely queries and emit real zod
|
|
6
|
+
|
|
7
|
+
Two fixes so data-classified DB columns (`@private`/`@pii`/`@secret`, default
|
|
8
|
+
`private`) are usable end-to-end instead of poisoning ordinary app code:
|
|
9
|
+
1. **Brand marker is now optional** (`{ readonly __classification__?: ... }`)
|
|
10
|
+
in both `@pikku/core` and the `pikku db migrate` schema header. A required
|
|
11
|
+
marker made a plain value (e.g. `string`) unassignable to a branded column
|
|
12
|
+
(`Private<string>`), breaking every Kysely `where`/insert/`.set()` operand —
|
|
13
|
+
any project with classified columns failed to type-check. Optional keeps the
|
|
14
|
+
brand structurally present (so the inspector's PKU910 output check still
|
|
15
|
+
detects it) while letting plain values flow IN. The inspector's level read is
|
|
16
|
+
now union-aware (`'pii' | undefined`) so pii/secret no longer silently
|
|
17
|
+
downgrade to private.
|
|
18
|
+
2. **Zod codegen resolves classified `ColumnType<>`** to proper scalars instead
|
|
19
|
+
of `z.unknown()`. `pikku db migrate` emits `<Table>Z`/`InsertZ`/`PatchZ` from
|
|
20
|
+
the Select slot, unwrapping the brand and honoring insert-optionality from the
|
|
21
|
+
Insert slot's `| undefined`. Public `Generated<T>`/bare/nested shapes are
|
|
22
|
+
unchanged.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [fe70fe0]
|
|
25
|
+
- @pikku/core@0.12.31
|
|
26
|
+
|
|
27
|
+
## 0.12.18
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 20750fd: feat(workflow): decide step dispatch purely per-function
|
|
32
|
+
|
|
33
|
+
Workflow step execution (inline vs queue dispatch) is now decided entirely by
|
|
34
|
+
the step's function `inline` flag — the workflow-level / run-level `inline`
|
|
35
|
+
meta no longer participates in per-step dispatch.
|
|
36
|
+
- Steps default to **inline**, so a normally-started (queue-backed) workflow
|
|
37
|
+
runs its whole chain in one orchestrator pass instead of one queue
|
|
38
|
+
round-trip per step.
|
|
39
|
+
- A function marked `inline: false` is dispatched via the queue (its own
|
|
40
|
+
worker, retry isolation). When `inline: false` but no `queueService` is
|
|
41
|
+
configured, the step falls back to inline and emits a `logger.warn` instead
|
|
42
|
+
of silently swallowing the misconfiguration.
|
|
43
|
+
- Removed the now-unused workflow-level `inline` from `WorkflowsMeta` /
|
|
44
|
+
`WorkflowRuntimeMeta`, the inspector's workflow extraction, the DSL→graph
|
|
45
|
+
converter, and the deploy analyzer / service inference (which now key off
|
|
46
|
+
the per-function flag). Run-level `inline` is retained: it still controls
|
|
47
|
+
whether a whole run executes in-process without queue infrastructure.
|
|
48
|
+
|
|
49
|
+
- Updated dependencies [cd101a5]
|
|
50
|
+
- Updated dependencies [ac16265]
|
|
51
|
+
- Updated dependencies [a05e864]
|
|
52
|
+
- Updated dependencies [20750fd]
|
|
53
|
+
- @pikku/core@0.12.30
|
|
54
|
+
|
|
1
55
|
## 0.12.17
|
|
2
56
|
|
|
3
57
|
### Patch Changes
|
|
@@ -488,6 +488,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
488
488
|
const genericTypes = (typeArguments ?? [])
|
|
489
489
|
.map((tn) => checker.getTypeFromTypeNode(tn))
|
|
490
490
|
.map((t) => unwrapPromise(checker, t));
|
|
491
|
+
// pikkuChannelConnectionFunc<Out> declares a single generic that is the
|
|
492
|
+
// OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
|
|
493
|
+
// Every other wrapper reads generic[0] as INPUT, so without this guard the
|
|
494
|
+
// connect handler's output generic is mis-recorded as inputSchemaName and the
|
|
495
|
+
// empty WS handshake fails input validation at connect (1008/403).
|
|
496
|
+
const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text);
|
|
491
497
|
const capitalizedName = funcIdToTypeName(name);
|
|
492
498
|
// --- Input Extraction ---
|
|
493
499
|
let inputNames = [];
|
|
@@ -513,7 +519,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
513
519
|
inputTypes = [filterType];
|
|
514
520
|
}
|
|
515
521
|
}
|
|
516
|
-
else if (!
|
|
522
|
+
else if (!isChannelConnectionFunc &&
|
|
523
|
+
!isListFunc &&
|
|
524
|
+
genericTypes.length >= 1 &&
|
|
525
|
+
genericTypes[0]) {
|
|
517
526
|
// Fall back to extracting from generic type arguments
|
|
518
527
|
const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
|
|
519
528
|
inputNames = result.names;
|
|
@@ -2,6 +2,7 @@ import * as ts from 'typescript';
|
|
|
2
2
|
import { getPropertyValue, 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
|
+
import { parseDurationString } from '@pikku/core';
|
|
5
6
|
export const createAddKeyedWiring = (config) => {
|
|
6
7
|
return (logger, node, checker, state, _options) => {
|
|
7
8
|
if (!ts.isCallExpression(node)) {
|
|
@@ -24,6 +25,7 @@ export const createAddKeyedWiring = (config) => {
|
|
|
24
25
|
const nameValue = getPropertyValue(obj, 'name');
|
|
25
26
|
const displayNameValue = getPropertyValue(obj, 'displayName');
|
|
26
27
|
const descriptionValue = getPropertyValue(obj, 'description');
|
|
28
|
+
const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod');
|
|
27
29
|
const idValue = getPropertyValue(obj, config.idField);
|
|
28
30
|
let schemaVariableName = null;
|
|
29
31
|
let schemaSourceFile = null;
|
|
@@ -74,6 +76,15 @@ export const createAddKeyedWiring = (config) => {
|
|
|
74
76
|
logger.critical(ErrorCode.MISSING_NAME, `${config.label} '${nameValue}' is missing the required 'schema' property or schema is not a variable reference.`);
|
|
75
77
|
return;
|
|
76
78
|
}
|
|
79
|
+
if (rotationPeriodValue) {
|
|
80
|
+
try {
|
|
81
|
+
parseDurationString(rotationPeriodValue);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
logger.critical(ErrorCode.INVALID_VALUE, `${config.label} '${nameValue}' has an invalid 'rotationPeriod': '${rotationPeriodValue}'. Use a duration like '1d', '30day', or '1w'.`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
77
88
|
const sourceFile = node.getSourceFile().fileName;
|
|
78
89
|
const wiringState = config.getState(state);
|
|
79
90
|
wiringState.files.add(sourceFile);
|
|
@@ -90,6 +101,7 @@ export const createAddKeyedWiring = (config) => {
|
|
|
90
101
|
name: nameValue,
|
|
91
102
|
displayName: displayNameValue,
|
|
92
103
|
description: descriptionValue || undefined,
|
|
104
|
+
rotationPeriod: rotationPeriodValue || undefined,
|
|
93
105
|
[config.idField]: idValue,
|
|
94
106
|
schema: schemaLookupName,
|
|
95
107
|
sourceFile,
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -182,7 +182,6 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
182
182
|
let summary;
|
|
183
183
|
let description;
|
|
184
184
|
let errors;
|
|
185
|
-
let inline;
|
|
186
185
|
let expose;
|
|
187
186
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
188
187
|
const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger, checker);
|
|
@@ -192,10 +191,6 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
192
191
|
summary = metadata.summary;
|
|
193
192
|
description = metadata.description;
|
|
194
193
|
errors = metadata.errors;
|
|
195
|
-
const inlineProp = getPropertyValue(firstArg, 'inline');
|
|
196
|
-
if (inlineProp === true) {
|
|
197
|
-
inline = true;
|
|
198
|
-
}
|
|
199
194
|
expose = getPropertyValue(firstArg, 'expose');
|
|
200
195
|
}
|
|
201
196
|
// Validate that we got a valid function
|
|
@@ -279,7 +274,6 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
279
274
|
description,
|
|
280
275
|
errors,
|
|
281
276
|
tags,
|
|
282
|
-
inline,
|
|
283
277
|
expose,
|
|
284
278
|
};
|
|
285
279
|
// Workflow functions require platform services that aren't visible
|
|
@@ -17,8 +17,12 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
|
|
|
17
17
|
return [];
|
|
18
18
|
seen.add(type);
|
|
19
19
|
// ── Is this type itself branded? ─────────────────────────────────────────
|
|
20
|
-
// Private<T> = T & { readonly __classification__
|
|
21
|
-
// where one constituent has a `__classification__` property whose type is a
|
|
20
|
+
// Private<T> = T & { readonly __classification__?: 'private' } → isIntersection()
|
|
21
|
+
// where one constituent has a `__classification__` property whose type is a
|
|
22
|
+
// string literal. The marker is OPTIONAL (so plain values stay assignable to
|
|
23
|
+
// branded columns), which means its resolved type is `'private' | undefined` —
|
|
24
|
+
// a union, not a bare literal. Read the level union-aware via `literalString`,
|
|
25
|
+
// otherwise pii/secret silently downgrade to the `'private'` fallback.
|
|
22
26
|
if (type.isIntersection()) {
|
|
23
27
|
for (const t of type.types) {
|
|
24
28
|
const classificationProp = t
|
|
@@ -28,7 +32,7 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
|
|
|
28
32
|
const decl = classificationProp.valueDeclaration ??
|
|
29
33
|
classificationProp.declarations?.[0];
|
|
30
34
|
const classification = decl
|
|
31
|
-
? (checker.getTypeOfSymbolAtLocation(classificationProp, decl)
|
|
35
|
+
? (literalString(checker.getTypeOfSymbolAtLocation(classificationProp, decl)) ?? 'private')
|
|
32
36
|
: 'private';
|
|
33
37
|
return [{ path: path || '<return value>', classification }];
|
|
34
38
|
}
|
|
@@ -71,3 +75,22 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
|
|
|
71
75
|
}
|
|
72
76
|
return violations;
|
|
73
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Recover a string-literal value from a type that may be the literal itself or a
|
|
80
|
+
* union containing it (e.g. `'private' | undefined`, produced by the optional
|
|
81
|
+
* `__classification__?` marker). Returns undefined when no string literal is
|
|
82
|
+
* present so the caller can apply its own fallback.
|
|
83
|
+
*/
|
|
84
|
+
function literalString(type) {
|
|
85
|
+
const value = type.value;
|
|
86
|
+
if (typeof value === 'string')
|
|
87
|
+
return value;
|
|
88
|
+
if (type.isUnion()) {
|
|
89
|
+
for (const member of type.types) {
|
|
90
|
+
const found = literalString(member);
|
|
91
|
+
if (found !== undefined)
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
@@ -760,10 +760,15 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
760
760
|
}
|
|
761
761
|
filteredState.requiredSchemas = prunedSchemas;
|
|
762
762
|
}
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
763
|
+
// Step dispatch is decided purely per-function: a workflow step runs via the
|
|
764
|
+
// queue only when its function opts out of inline execution (inline: false).
|
|
765
|
+
// Such a unit needs workflowService + queueService injected even though the
|
|
766
|
+
// function itself doesn't reference them. Check the ORIGINAL graph meta
|
|
767
|
+
// (before filtering pruned it).
|
|
766
768
|
const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta));
|
|
769
|
+
const resolveFuncId = (rpcName) => filteredState.rpc.internalMeta[rpcName] ??
|
|
770
|
+
filteredState.rpc.exposedMeta[rpcName] ??
|
|
771
|
+
rpcName;
|
|
767
772
|
// Use the snapshot taken before filtering
|
|
768
773
|
for (const graph of Object.values(originalGraphMeta)) {
|
|
769
774
|
if (!graph.nodes)
|
|
@@ -772,11 +777,12 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
772
777
|
if (!('rpcName' in node) || !node.rpcName)
|
|
773
778
|
continue;
|
|
774
779
|
const rpcName = node.rpcName;
|
|
775
|
-
|
|
780
|
+
const funcId = resolveFuncId(rpcName);
|
|
781
|
+
if (!survivingFuncIds.has(funcId) && !survivingFuncIds.has(rpcName))
|
|
776
782
|
continue;
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
if (
|
|
783
|
+
const funcMeta = (filteredState.functions.meta[funcId] ??
|
|
784
|
+
filteredState.functions.meta[rpcName]);
|
|
785
|
+
if (funcMeta?.inline === false) {
|
|
780
786
|
filteredState.serviceAggregation.requiredServices.add('workflowService');
|
|
781
787
|
filteredState.serviceAggregation.requiredServices.add('queueService');
|
|
782
788
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.19",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.31",
|
|
39
39
|
"path-to-regexp": "^8.3.0",
|
|
40
40
|
"ts-json-schema-generator": "^2.5.0",
|
|
41
41
|
"tsx": "^4.21.0",
|
|
@@ -316,3 +316,55 @@ describe('addFunctions implementationHash', () => {
|
|
|
316
316
|
}
|
|
317
317
|
})
|
|
318
318
|
})
|
|
319
|
+
|
|
320
|
+
describe('pikkuChannelConnectionFunc generic mapping', () => {
|
|
321
|
+
// Regression: pikkuChannelConnectionFunc<Out> has a single generic that is the
|
|
322
|
+
// OUTPUT type (input is always void). The inspector must NOT record that generic
|
|
323
|
+
// as inputSchemaName — otherwise the empty WS handshake is validated against an
|
|
324
|
+
// input schema requiring the send-payload shape and the connect is rejected 403.
|
|
325
|
+
test('does not map the output generic to inputSchemaName', async () => {
|
|
326
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-channel-connect-'))
|
|
327
|
+
const file = join(rootDir, 'channel.ts')
|
|
328
|
+
|
|
329
|
+
await writeFile(
|
|
330
|
+
file,
|
|
331
|
+
[
|
|
332
|
+
'type Sessionless<In, Out> = (',
|
|
333
|
+
' services: any,',
|
|
334
|
+
' data: In,',
|
|
335
|
+
' interaction: any',
|
|
336
|
+
') => Promise<Out>',
|
|
337
|
+
'export const pikkuChannelConnectionFunc = <Out = unknown>(',
|
|
338
|
+
' func: Sessionless<void, Out>',
|
|
339
|
+
') => ({ func })',
|
|
340
|
+
'export const onCardsConnect = pikkuChannelConnectionFunc<{',
|
|
341
|
+
" type: 'hello'",
|
|
342
|
+
' count: number',
|
|
343
|
+
'}>(async (_services, _data, _interaction) => {})',
|
|
344
|
+
].join('\n')
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const logger: InspectorLogger = {
|
|
348
|
+
debug: () => {},
|
|
349
|
+
info: () => {},
|
|
350
|
+
warn: () => {},
|
|
351
|
+
error: () => {},
|
|
352
|
+
critical: () => {},
|
|
353
|
+
hasCriticalErrors: () => false,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const state = await inspect(logger, [file], { rootDir })
|
|
358
|
+
const meta = state.functions.meta['onCardsConnect']
|
|
359
|
+
assert.ok(meta, 'onCardsConnect meta should exist')
|
|
360
|
+
assert.strictEqual(
|
|
361
|
+
meta!.inputSchemaName,
|
|
362
|
+
null,
|
|
363
|
+
'connect input must be void (no input schema), not the output generic'
|
|
364
|
+
)
|
|
365
|
+
assert.deepStrictEqual(meta!.inputs, [])
|
|
366
|
+
} finally {
|
|
367
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
})
|
package/src/add/add-functions.ts
CHANGED
|
@@ -671,6 +671,13 @@ export const addFunctions: AddWiring = (
|
|
|
671
671
|
.map((tn) => checker.getTypeFromTypeNode(tn))
|
|
672
672
|
.map((t) => unwrapPromise(checker, t))
|
|
673
673
|
|
|
674
|
+
// pikkuChannelConnectionFunc<Out> declares a single generic that is the
|
|
675
|
+
// OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
|
|
676
|
+
// Every other wrapper reads generic[0] as INPUT, so without this guard the
|
|
677
|
+
// connect handler's output generic is mis-recorded as inputSchemaName and the
|
|
678
|
+
// empty WS handshake fails input validation at connect (1008/403).
|
|
679
|
+
const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text)
|
|
680
|
+
|
|
674
681
|
const capitalizedName = funcIdToTypeName(name)
|
|
675
682
|
|
|
676
683
|
// --- Input Extraction ---
|
|
@@ -708,7 +715,12 @@ export const addFunctions: AddWiring = (
|
|
|
708
715
|
} else {
|
|
709
716
|
inputTypes = [filterType]
|
|
710
717
|
}
|
|
711
|
-
} else if (
|
|
718
|
+
} else if (
|
|
719
|
+
!isChannelConnectionFunc &&
|
|
720
|
+
!isListFunc &&
|
|
721
|
+
genericTypes.length >= 1 &&
|
|
722
|
+
genericTypes[0]
|
|
723
|
+
) {
|
|
712
724
|
// Fall back to extracting from generic type arguments
|
|
713
725
|
const result = getNamesAndTypes(
|
|
714
726
|
checker,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import type { AddWiring, InspectorState } from '../types.js'
|
|
7
7
|
import { ErrorCode } from '../error-codes.js'
|
|
8
8
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
|
|
9
|
+
import { parseDurationString } from '@pikku/core'
|
|
9
10
|
|
|
10
11
|
export interface KeyedWiringConfig {
|
|
11
12
|
functionName: string
|
|
@@ -52,6 +53,9 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
52
53
|
const descriptionValue = getPropertyValue(obj, 'description') as
|
|
53
54
|
| string
|
|
54
55
|
| null
|
|
56
|
+
const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod') as
|
|
57
|
+
| string
|
|
58
|
+
| null
|
|
55
59
|
const idValue = getPropertyValue(obj, config.idField) as string | null
|
|
56
60
|
|
|
57
61
|
let schemaVariableName: string | null = null
|
|
@@ -123,6 +127,18 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
123
127
|
return
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
if (rotationPeriodValue) {
|
|
131
|
+
try {
|
|
132
|
+
parseDurationString(rotationPeriodValue)
|
|
133
|
+
} catch {
|
|
134
|
+
logger.critical(
|
|
135
|
+
ErrorCode.INVALID_VALUE,
|
|
136
|
+
`${config.label} '${nameValue}' has an invalid 'rotationPeriod': '${rotationPeriodValue}'. Use a duration like '1d', '30day', or '1w'.`
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
const sourceFile = node.getSourceFile().fileName
|
|
127
143
|
|
|
128
144
|
const wiringState = config.getState(state)
|
|
@@ -148,6 +164,7 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
148
164
|
name: nameValue,
|
|
149
165
|
displayName: displayNameValue,
|
|
150
166
|
description: descriptionValue || undefined,
|
|
167
|
+
rotationPeriod: rotationPeriodValue || undefined,
|
|
151
168
|
[config.idField]: idValue,
|
|
152
169
|
schema: schemaLookupName,
|
|
153
170
|
sourceFile,
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -209,7 +209,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
209
209
|
let summary: string | undefined
|
|
210
210
|
let description: string | undefined
|
|
211
211
|
let errors: string[] | undefined
|
|
212
|
-
let inline: boolean | undefined
|
|
213
212
|
let expose: boolean | undefined
|
|
214
213
|
|
|
215
214
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
@@ -226,11 +225,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
226
225
|
description = metadata.description
|
|
227
226
|
errors = metadata.errors
|
|
228
227
|
|
|
229
|
-
const inlineProp = getPropertyValue(firstArg, 'inline')
|
|
230
|
-
if (inlineProp === true) {
|
|
231
|
-
inline = true
|
|
232
|
-
}
|
|
233
|
-
|
|
234
228
|
expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
|
|
235
229
|
}
|
|
236
230
|
|
|
@@ -337,7 +331,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
|
|
|
337
331
|
description,
|
|
338
332
|
errors,
|
|
339
333
|
tags,
|
|
340
|
-
inline,
|
|
341
334
|
expose,
|
|
342
335
|
}
|
|
343
336
|
|
|
@@ -27,10 +27,15 @@ function makeLogger() {
|
|
|
27
27
|
* Mirrors what schema.d.ts emits so the TypeScript program sees the correct
|
|
28
28
|
* structural brand type even without @pikku/core being importable from /tmp.
|
|
29
29
|
*/
|
|
30
|
+
// Optional `__classification__?` mirrors what @pikku/core and `pikku db migrate`
|
|
31
|
+
// actually emit (optional so plain values stay assignable to branded columns).
|
|
32
|
+
// The `Secret`-in-sessioned-function cases below double as the level-fidelity
|
|
33
|
+
// guard: they only pass if `findPiiPaths` reads the level union-aware
|
|
34
|
+
// ('secret' | undefined), not via a naive `.value`.
|
|
30
35
|
const BRAND_TYPES = `
|
|
31
|
-
type Private<T> = T & { readonly __classification__
|
|
32
|
-
type Pii<T> = T & { readonly __classification__
|
|
33
|
-
type Secret<T> = T & { readonly __classification__
|
|
36
|
+
type Private<T> = T & { readonly __classification__?: 'private' }
|
|
37
|
+
type Pii<T> = T & { readonly __classification__?: 'pii' }
|
|
38
|
+
type Secret<T> = T & { readonly __classification__?: 'secret' }
|
|
34
39
|
`
|
|
35
40
|
|
|
36
41
|
async function runInspect(sourceCode: string) {
|
|
@@ -29,8 +29,12 @@ export function findPiiPaths(
|
|
|
29
29
|
seen.add(type)
|
|
30
30
|
|
|
31
31
|
// ── Is this type itself branded? ─────────────────────────────────────────
|
|
32
|
-
// Private<T> = T & { readonly __classification__
|
|
33
|
-
// where one constituent has a `__classification__` property whose type is a
|
|
32
|
+
// Private<T> = T & { readonly __classification__?: 'private' } → isIntersection()
|
|
33
|
+
// where one constituent has a `__classification__` property whose type is a
|
|
34
|
+
// string literal. The marker is OPTIONAL (so plain values stay assignable to
|
|
35
|
+
// branded columns), which means its resolved type is `'private' | undefined` —
|
|
36
|
+
// a union, not a bare literal. Read the level union-aware via `literalString`,
|
|
37
|
+
// otherwise pii/secret silently downgrade to the `'private'` fallback.
|
|
34
38
|
if (type.isIntersection()) {
|
|
35
39
|
for (const t of type.types) {
|
|
36
40
|
const classificationProp = t
|
|
@@ -41,9 +45,9 @@ export function findPiiPaths(
|
|
|
41
45
|
classificationProp.valueDeclaration ??
|
|
42
46
|
classificationProp.declarations?.[0]
|
|
43
47
|
const classification = decl
|
|
44
|
-
? ((
|
|
45
|
-
checker.getTypeOfSymbolAtLocation(classificationProp, decl)
|
|
46
|
-
)
|
|
48
|
+
? (literalString(
|
|
49
|
+
checker.getTypeOfSymbolAtLocation(classificationProp, decl)
|
|
50
|
+
) ?? 'private')
|
|
47
51
|
: 'private'
|
|
48
52
|
return [{ path: path || '<return value>', classification }]
|
|
49
53
|
}
|
|
@@ -96,3 +100,21 @@ export function findPiiPaths(
|
|
|
96
100
|
|
|
97
101
|
return violations
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Recover a string-literal value from a type that may be the literal itself or a
|
|
106
|
+
* union containing it (e.g. `'private' | undefined`, produced by the optional
|
|
107
|
+
* `__classification__?` marker). Returns undefined when no string literal is
|
|
108
|
+
* present so the caller can apply its own fallback.
|
|
109
|
+
*/
|
|
110
|
+
function literalString(type: ts.Type): string | undefined {
|
|
111
|
+
const value = (type as { value?: unknown }).value
|
|
112
|
+
if (typeof value === 'string') return value
|
|
113
|
+
if (type.isUnion()) {
|
|
114
|
+
for (const member of type.types) {
|
|
115
|
+
const found = literalString(member)
|
|
116
|
+
if (found !== undefined) return found
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
@@ -1061,21 +1061,28 @@ export function filterInspectorState(
|
|
|
1061
1061
|
filteredState.requiredSchemas = prunedSchemas
|
|
1062
1062
|
}
|
|
1063
1063
|
|
|
1064
|
-
//
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1064
|
+
// Step dispatch is decided purely per-function: a workflow step runs via the
|
|
1065
|
+
// queue only when its function opts out of inline execution (inline: false).
|
|
1066
|
+
// Such a unit needs workflowService + queueService injected even though the
|
|
1067
|
+
// function itself doesn't reference them. Check the ORIGINAL graph meta
|
|
1068
|
+
// (before filtering pruned it).
|
|
1067
1069
|
const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta))
|
|
1070
|
+
const resolveFuncId = (rpcName: string): string =>
|
|
1071
|
+
filteredState.rpc.internalMeta[rpcName] ??
|
|
1072
|
+
filteredState.rpc.exposedMeta[rpcName] ??
|
|
1073
|
+
rpcName
|
|
1068
1074
|
// Use the snapshot taken before filtering
|
|
1069
1075
|
for (const graph of Object.values(originalGraphMeta)) {
|
|
1070
1076
|
if (!graph.nodes) continue
|
|
1071
1077
|
for (const node of Object.values(graph.nodes)) {
|
|
1072
1078
|
if (!('rpcName' in node) || !node.rpcName) continue
|
|
1073
1079
|
const rpcName = node.rpcName as string
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1080
|
+
const funcId = resolveFuncId(rpcName)
|
|
1081
|
+
if (!survivingFuncIds.has(funcId) && !survivingFuncIds.has(rpcName))
|
|
1082
|
+
continue
|
|
1083
|
+
const funcMeta = (filteredState.functions.meta[funcId] ??
|
|
1084
|
+
filteredState.functions.meta[rpcName]) as { inline?: boolean }
|
|
1085
|
+
if (funcMeta?.inline === false) {
|
|
1079
1086
|
filteredState.serviceAggregation.requiredServices.add('workflowService')
|
|
1080
1087
|
filteredState.serviceAggregation.requiredServices.add('queueService')
|
|
1081
1088
|
}
|