@pikku/inspector 0.12.58 → 0.12.59
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 +55 -0
- package/dist/add/add-wire-addon.js +10 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/utils/post-process.js +18 -6
- package/dist/utils/serialize-inspector-state.d.ts +2 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +2 -1
- package/package.json +3 -3
- package/src/add/add-functions.ts +1 -1
- package/src/add/add-scenario-step.test.ts +2 -1
- package/src/add/add-wire-addon.test.ts +31 -0
- package/src/add/add-wire-addon.ts +8 -0
- package/src/error-codes.ts +1 -0
- package/src/types.ts +4 -0
- package/src/utils/post-process.test.ts +67 -0
- package/src/utils/post-process.ts +28 -6
- package/src/utils/serialize-inspector-state.ts +2 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +1 -1
- package/src/utils/workflow/graph/workflow-graph.types.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
+
## 0.12.59
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 02c4fe5: fix(core,inspector): let a host grant an addon secrets it could not declare
|
|
6
|
+
|
|
7
|
+
Scoping an addon's `SecretService` to its `declaredSecrets` left generic addons
|
|
8
|
+
with nothing readable: `declaredSecrets` is derived from the addon package's own
|
|
9
|
+
source, but the secrets an addon like `@pikku/addon-graph` reads are named by the
|
|
10
|
+
consuming app's workflow nodes at runtime. Every authenticated `graph:httpRequest`
|
|
11
|
+
threw.
|
|
12
|
+
|
|
13
|
+
`wireAddon` now takes `secretGrants: string[]` and `credentialGrants: string[]`,
|
|
14
|
+
completing the grant family alongside `secretOverrides` (grant + rename) and
|
|
15
|
+
`globalSecrets` (grant everything, with a reason). Grants name the secret as the
|
|
16
|
+
addon reads it, since the scope check runs before the override map renames it —
|
|
17
|
+
which is also why an override's key grants and its value does not.
|
|
18
|
+
|
|
19
|
+
A grant naming a secret the project does not declare is an `INVALID_VALUE`
|
|
20
|
+
critical at codegen, resolved through the override map before lookup.
|
|
21
|
+
|
|
22
|
+
- 438b776: Move the scenario and feature surface off `@pikku/core/workflow` and onto
|
|
23
|
+
`@pikku/core/scenario`. Scenarios extend workflows, so the production workflow
|
|
24
|
+
wiring no longer names a scenario module in its import graph. Feature and
|
|
25
|
+
scenario types are declared in their own `scenario.types.ts` rather than in
|
|
26
|
+
`workflow.types.ts`. Import `requireActor`, `requireScenarioEnv`, `pollUntil`,
|
|
27
|
+
`createCookieJar`, `addFeature`, `ScenarioHttpResponse` and the rest from
|
|
28
|
+
`@pikku/core/scenario`; `HttpPersonasConfig` now comes from
|
|
29
|
+
`@pikku/core/persona` rather than `@pikku/core/services`.
|
|
30
|
+
- ad63f47: feat(cli): warn before codegen when a linked dependency splits a package's type identity
|
|
31
|
+
|
|
32
|
+
`pikku all` now runs the split-type-identity check as a preflight, beside the
|
|
33
|
+
existing `@pikku/core` one, and warns with `PKU719` naming each package, both
|
|
34
|
+
versions and both paths.
|
|
35
|
+
|
|
36
|
+
It has to run _before_ the work rather than after it fails. The failure it
|
|
37
|
+
explains is a V8 heap OOM, which aborts the process — `process.on('exit')`,
|
|
38
|
+
`uncaughtException` and `finally` never run, so nothing printed after the fact is
|
|
39
|
+
ever seen. By the time there is a symptom, the only thing that can help is
|
|
40
|
+
already on screen above it. Without this the user sees a codegen step that dies
|
|
41
|
+
of memory pressure with no indication that two copies of one package are the
|
|
42
|
+
reason, and the obvious next move is to raise `--max-old-space-size`, which
|
|
43
|
+
hides it further.
|
|
44
|
+
|
|
45
|
+
Warns rather than throws: a skewed linked dependency is a strong signal, not a
|
|
46
|
+
certainty, and refusing to build on a heuristic would break working setups.
|
|
47
|
+
`PIKKU_SKIP_TYPE_IDENTITY_CHECK=1` silences it, matching
|
|
48
|
+
`PIKKU_ALLOW_DUPLICATE_CORE`. It also swallows its own errors — it runs on every
|
|
49
|
+
codegen, so it must never be the reason a build stops.
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [02c4fe5]
|
|
52
|
+
- Updated dependencies [438b776]
|
|
53
|
+
- Updated dependencies [438b776]
|
|
54
|
+
- @pikku/core@0.12.83
|
|
55
|
+
|
|
1
56
|
## 0.12.58
|
|
2
57
|
|
|
3
58
|
### Patch Changes
|
|
@@ -53,6 +53,8 @@ export function addWireAddon(node, state, logger) {
|
|
|
53
53
|
let secretOverrides;
|
|
54
54
|
let variableOverrides;
|
|
55
55
|
let credentialOverrides;
|
|
56
|
+
let secretGrants;
|
|
57
|
+
let credentialGrants;
|
|
56
58
|
let globalSecrets;
|
|
57
59
|
let globalCredentials;
|
|
58
60
|
for (const prop of firstArg.properties) {
|
|
@@ -96,6 +98,12 @@ export function addWireAddon(node, state, logger) {
|
|
|
96
98
|
ts.isObjectLiteralExpression(prop.initializer)) {
|
|
97
99
|
credentialOverrides = parseStringRecord(prop.initializer);
|
|
98
100
|
}
|
|
101
|
+
else if (key === 'secretGrants') {
|
|
102
|
+
secretGrants = parseStringArray(prop.initializer);
|
|
103
|
+
}
|
|
104
|
+
else if (key === 'credentialGrants') {
|
|
105
|
+
credentialGrants = parseStringArray(prop.initializer);
|
|
106
|
+
}
|
|
99
107
|
else if (key === 'globalSecrets') {
|
|
100
108
|
// The grant is real at runtime whatever the reason evaluates to, so a
|
|
101
109
|
// non-literal is recorded as its source text rather than dropped.
|
|
@@ -122,6 +130,8 @@ export function addWireAddon(node, state, logger) {
|
|
|
122
130
|
secretOverrides,
|
|
123
131
|
variableOverrides,
|
|
124
132
|
credentialOverrides,
|
|
133
|
+
secretGrants,
|
|
134
|
+
credentialGrants,
|
|
125
135
|
globalSecrets,
|
|
126
136
|
globalCredentials,
|
|
127
137
|
});
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ export declare enum ErrorCode {
|
|
|
69
69
|
FUNCTION_DYNAMIC_IMPORT = "PKU498",
|
|
70
70
|
DUPLICATE_CORE_VERSION = "PKU717",
|
|
71
71
|
CORE_VERSION_SKEW = "PKU718",
|
|
72
|
+
SPLIT_TYPE_IDENTITY = "PKU719",
|
|
72
73
|
PII_IN_OUTPUT = "PKU910",
|
|
73
74
|
ADDON_WIRING_NOT_ALLOWED = "PKU920",
|
|
74
75
|
ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
|
package/dist/error-codes.js
CHANGED
|
@@ -90,6 +90,7 @@ export var ErrorCode;
|
|
|
90
90
|
// Dependency integrity errors
|
|
91
91
|
ErrorCode["DUPLICATE_CORE_VERSION"] = "PKU717";
|
|
92
92
|
ErrorCode["CORE_VERSION_SKEW"] = "PKU718";
|
|
93
|
+
ErrorCode["SPLIT_TYPE_IDENTITY"] = "PKU719";
|
|
93
94
|
// Data classification errors
|
|
94
95
|
ErrorCode["PII_IN_OUTPUT"] = "PKU910";
|
|
95
96
|
// Addon authoring errors
|
package/dist/types.d.ts
CHANGED
|
@@ -521,6 +521,10 @@ export interface InspectorState {
|
|
|
521
521
|
secretOverrides?: Record<string, string>;
|
|
522
522
|
variableOverrides?: Record<string, string>;
|
|
523
523
|
credentialOverrides?: Record<string, string>;
|
|
524
|
+
/** Secrets the app lends this instance, named as the addon reads them. */
|
|
525
|
+
secretGrants?: string[];
|
|
526
|
+
/** Credentials the app lends this instance, named as the addon reads them. */
|
|
527
|
+
credentialGrants?: string[];
|
|
524
528
|
/**
|
|
525
529
|
* The app's stated reason for handing this instance the whole
|
|
526
530
|
* `SecretService` instead of one scoped to its declared secrets.
|
|
@@ -317,14 +317,21 @@ export function validateSecretOverrides(logger, state) {
|
|
|
317
317
|
// older meta without a secretId field.
|
|
318
318
|
const secretIds = new Set(state.secrets.definitions.map((d) => d.secretId ?? d.name));
|
|
319
319
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
320
|
-
|
|
321
|
-
continue;
|
|
322
|
-
for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides)) {
|
|
320
|
+
for (const [logicalName, resolvedName] of Object.entries(addonDecl.secretOverrides ?? {})) {
|
|
323
321
|
if (!secretIds.has(resolvedName)) {
|
|
324
322
|
const availableSecrets = Array.from(secretIds);
|
|
325
323
|
logger.critical(ErrorCode.INVALID_VALUE, `Secret override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
|
|
326
324
|
}
|
|
327
325
|
}
|
|
326
|
+
// A grant names the secret as the addon reads it, so it resolves through
|
|
327
|
+
// the override map before it can be looked up in the project.
|
|
328
|
+
for (const logicalName of addonDecl.secretGrants ?? []) {
|
|
329
|
+
const resolvedName = addonDecl.secretOverrides?.[logicalName] ?? logicalName;
|
|
330
|
+
if (!secretIds.has(resolvedName)) {
|
|
331
|
+
const availableSecrets = Array.from(secretIds);
|
|
332
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
328
335
|
}
|
|
329
336
|
}
|
|
330
337
|
export function validateCredentialOverrides(logger, state) {
|
|
@@ -333,14 +340,19 @@ export function validateCredentialOverrides(logger, state) {
|
|
|
333
340
|
return;
|
|
334
341
|
const credentialNames = new Set(state.credentials?.definitions.map((d) => d.name) ?? []);
|
|
335
342
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
336
|
-
|
|
337
|
-
continue;
|
|
338
|
-
for (const [logicalName, resolvedName] of Object.entries(addonDecl.credentialOverrides)) {
|
|
343
|
+
for (const [logicalName, resolvedName] of Object.entries(addonDecl.credentialOverrides ?? {})) {
|
|
339
344
|
if (!credentialNames.has(resolvedName)) {
|
|
340
345
|
const availableCredentials = Array.from(credentialNames);
|
|
341
346
|
logger.critical(ErrorCode.INVALID_VALUE, `Credential override '${logicalName}' -> '${resolvedName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
|
|
342
347
|
}
|
|
343
348
|
}
|
|
349
|
+
for (const logicalName of addonDecl.credentialGrants ?? []) {
|
|
350
|
+
const resolvedName = addonDecl.credentialOverrides?.[logicalName] ?? logicalName;
|
|
351
|
+
if (!credentialNames.has(resolvedName)) {
|
|
352
|
+
const availableCredentials = Array.from(credentialNames);
|
|
353
|
+
logger.critical(ErrorCode.INVALID_VALUE, `Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
344
356
|
}
|
|
345
357
|
}
|
|
346
358
|
export function validateVariableOverrides(logger, state) {
|
|
@@ -195,6 +195,8 @@ export interface SerializableInspectorState {
|
|
|
195
195
|
secretOverrides?: Record<string, string>;
|
|
196
196
|
variableOverrides?: Record<string, string>;
|
|
197
197
|
credentialOverrides?: Record<string, string>;
|
|
198
|
+
secretGrants?: string[];
|
|
199
|
+
credentialGrants?: string[];
|
|
198
200
|
globalSecrets?: string;
|
|
199
201
|
globalCredentials?: string;
|
|
200
202
|
}
|
|
@@ -67,7 +67,8 @@ export interface NodeOptions {
|
|
|
67
67
|
* Flow node types for control flow (no RPC call)
|
|
68
68
|
*/
|
|
69
69
|
export type FlowType = 'sleep' | 'suspend' | 'approval' | 'branch' | 'parallel' | 'fanout' | 'inline' | 'switch' | 'filter' | 'arrayPredicate' | 'return' | 'cancel' | 'set';
|
|
70
|
-
import type { ContextVariable,
|
|
70
|
+
import type { ContextVariable, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/workflow';
|
|
71
|
+
import type { ScenarioStepPhase } from '@pikku/core/scenario';
|
|
71
72
|
export type { ContextVariable, WorkflowContext };
|
|
72
73
|
/**
|
|
73
74
|
* Base node properties shared by all node types
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.59",
|
|
4
4
|
"description": "TypeScript inspector for Pikku — discovers functions and wirings to drive code generation",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"zod-to-ts": "^2.1.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@pikku/core": "^0.12.
|
|
48
|
+
"@pikku/core": "^0.12.83"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@pikku/core": "^0.12.
|
|
51
|
+
"@pikku/core": "^0.12.83",
|
|
52
52
|
"@types/node": "^24.11.0"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
package/src/add/add-functions.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { extractFunctionNode } from '../utils/extract-function-node.js'
|
|
10
10
|
import { extractUsedWires } from '../utils/extract-services.js'
|
|
11
11
|
import type { AuditDurability, FunctionServicesMeta } from '@pikku/core'
|
|
12
|
-
import type { ScenarioStepKind, ScenarioSurface } from '@pikku/core/
|
|
12
|
+
import type { ScenarioStepKind, ScenarioSurface } from '@pikku/core/scenario'
|
|
13
13
|
import { formatVersionedId, parseVersionedId } from '@pikku/core'
|
|
14
14
|
|
|
15
15
|
/** Binding keys a `pikkuScenarioStep` may declare, in report order. */
|
|
@@ -5,7 +5,8 @@ import { tmpdir } from 'node:os'
|
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import { inspect } from '../inspector.js'
|
|
7
7
|
import type { InspectorLogger } from '../types.js'
|
|
8
|
-
import type {
|
|
8
|
+
import type { WorkflowStepMeta } from '@pikku/core/workflow'
|
|
9
|
+
import type { ScenarioStepMeta } from '@pikku/core/scenario'
|
|
9
10
|
|
|
10
11
|
function makeLogger(
|
|
11
12
|
criticals: Array<{ code: string; message: string }>
|
|
@@ -61,11 +61,42 @@ describe('addWireAddon', () => {
|
|
|
61
61
|
secretOverrides: undefined,
|
|
62
62
|
variableOverrides: undefined,
|
|
63
63
|
credentialOverrides: undefined,
|
|
64
|
+
secretGrants: undefined,
|
|
65
|
+
credentialGrants: undefined,
|
|
64
66
|
globalSecrets: undefined,
|
|
65
67
|
globalCredentials: undefined,
|
|
66
68
|
})
|
|
67
69
|
})
|
|
68
70
|
|
|
71
|
+
test('captures the secrets and credentials the host lends an addon', () => {
|
|
72
|
+
const declarations = inspect(`
|
|
73
|
+
wireAddon({
|
|
74
|
+
name: 'graph',
|
|
75
|
+
package: '@pikku/addon-graph',
|
|
76
|
+
secretGrants: ['STRIPE_KEY', 'GITHUB_TOKEN'],
|
|
77
|
+
credentialGrants: ['slack'],
|
|
78
|
+
})
|
|
79
|
+
`)
|
|
80
|
+
|
|
81
|
+
assert.deepEqual(declarations.get('graph').secretGrants, [
|
|
82
|
+
'STRIPE_KEY',
|
|
83
|
+
'GITHUB_TOKEN',
|
|
84
|
+
])
|
|
85
|
+
assert.deepEqual(declarations.get('graph').credentialGrants, ['slack'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('drops a grant list that is not statically knowable', () => {
|
|
89
|
+
const declarations = inspect(`
|
|
90
|
+
wireAddon({
|
|
91
|
+
name: 'graph',
|
|
92
|
+
package: '@pikku/addon-graph',
|
|
93
|
+
secretGrants: someRuntimeList,
|
|
94
|
+
})
|
|
95
|
+
`)
|
|
96
|
+
|
|
97
|
+
assert.equal(declarations.get('graph').secretGrants, undefined)
|
|
98
|
+
})
|
|
99
|
+
|
|
69
100
|
test('captures the reason an addon was exempted from credential scoping', () => {
|
|
70
101
|
const declarations = inspect(`
|
|
71
102
|
wireAddon({
|
|
@@ -60,6 +60,8 @@ export function addWireAddon(
|
|
|
60
60
|
let secretOverrides: Record<string, string> | undefined
|
|
61
61
|
let variableOverrides: Record<string, string> | undefined
|
|
62
62
|
let credentialOverrides: Record<string, string> | undefined
|
|
63
|
+
let secretGrants: string[] | undefined
|
|
64
|
+
let credentialGrants: string[] | undefined
|
|
63
65
|
let globalSecrets: string | undefined
|
|
64
66
|
let globalCredentials: string | undefined
|
|
65
67
|
|
|
@@ -104,6 +106,10 @@ export function addWireAddon(
|
|
|
104
106
|
ts.isObjectLiteralExpression(prop.initializer)
|
|
105
107
|
) {
|
|
106
108
|
credentialOverrides = parseStringRecord(prop.initializer)
|
|
109
|
+
} else if (key === 'secretGrants') {
|
|
110
|
+
secretGrants = parseStringArray(prop.initializer)
|
|
111
|
+
} else if (key === 'credentialGrants') {
|
|
112
|
+
credentialGrants = parseStringArray(prop.initializer)
|
|
107
113
|
} else if (key === 'globalSecrets') {
|
|
108
114
|
// The grant is real at runtime whatever the reason evaluates to, so a
|
|
109
115
|
// non-literal is recorded as its source text rather than dropped.
|
|
@@ -130,6 +136,8 @@ export function addWireAddon(
|
|
|
130
136
|
secretOverrides,
|
|
131
137
|
variableOverrides,
|
|
132
138
|
credentialOverrides,
|
|
139
|
+
secretGrants,
|
|
140
|
+
credentialGrants,
|
|
133
141
|
globalSecrets,
|
|
134
142
|
globalCredentials,
|
|
135
143
|
})
|
package/src/error-codes.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -588,6 +588,10 @@ export interface InspectorState {
|
|
|
588
588
|
secretOverrides?: Record<string, string>
|
|
589
589
|
variableOverrides?: Record<string, string>
|
|
590
590
|
credentialOverrides?: Record<string, string>
|
|
591
|
+
/** Secrets the app lends this instance, named as the addon reads them. */
|
|
592
|
+
secretGrants?: string[]
|
|
593
|
+
/** Credentials the app lends this instance, named as the addon reads them. */
|
|
594
|
+
credentialGrants?: string[]
|
|
591
595
|
/**
|
|
592
596
|
* The app's stated reason for handing this instance the whole
|
|
593
597
|
* `SecretService` instead of one scoped to its declared secrets.
|
|
@@ -59,6 +59,33 @@ const makeOverrideState = (
|
|
|
59
59
|
},
|
|
60
60
|
}) as unknown as Omit<InspectorState, 'typesLookup'>
|
|
61
61
|
|
|
62
|
+
const makeGrantState = (
|
|
63
|
+
kind: 'secrets' | 'credentials',
|
|
64
|
+
declaredNames: string[],
|
|
65
|
+
decl: {
|
|
66
|
+
secretGrants?: string[]
|
|
67
|
+
credentialGrants?: string[]
|
|
68
|
+
secretOverrides?: Record<string, string>
|
|
69
|
+
credentialOverrides?: Record<string, string>
|
|
70
|
+
}
|
|
71
|
+
): Omit<InspectorState, 'typesLookup'> =>
|
|
72
|
+
({
|
|
73
|
+
rpc: {
|
|
74
|
+
wireAddonDeclarations: new Map([
|
|
75
|
+
['slack-marketing', { package: '@addon/slack', ...decl }],
|
|
76
|
+
]),
|
|
77
|
+
},
|
|
78
|
+
secrets: {
|
|
79
|
+
definitions:
|
|
80
|
+
kind === 'secrets' ? declaredNames.map((name) => ({ name })) : [],
|
|
81
|
+
},
|
|
82
|
+
credentials: {
|
|
83
|
+
definitions:
|
|
84
|
+
kind === 'credentials' ? declaredNames.map((name) => ({ name })) : [],
|
|
85
|
+
},
|
|
86
|
+
variables: { definitions: [] },
|
|
87
|
+
}) as unknown as Omit<InspectorState, 'typesLookup'>
|
|
88
|
+
|
|
62
89
|
function makeState(
|
|
63
90
|
overrides: {
|
|
64
91
|
usedFunctions?: string[]
|
|
@@ -414,6 +441,46 @@ describe('override validation resolves the override target (value), not the logi
|
|
|
414
441
|
validateVariableOverrides(logger, state)
|
|
415
442
|
assert.deepEqual(criticals, [])
|
|
416
443
|
})
|
|
444
|
+
|
|
445
|
+
test('validateSecretOverrides accepts a grant naming a declared secret', () => {
|
|
446
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
447
|
+
const state = makeGrantState('secrets', ['STRIPE_KEY'], {
|
|
448
|
+
secretGrants: ['STRIPE_KEY'],
|
|
449
|
+
})
|
|
450
|
+
validateSecretOverrides(logger, state)
|
|
451
|
+
assert.deepEqual(criticals, [])
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('validateSecretOverrides flags a grant naming no declared secret', () => {
|
|
455
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
456
|
+
const state = makeGrantState('secrets', ['STRIPE_KEY'], {
|
|
457
|
+
secretGrants: ['STIRPE_KEY'],
|
|
458
|
+
})
|
|
459
|
+
validateSecretOverrides(logger, state)
|
|
460
|
+
assert.equal(criticals.length, 1)
|
|
461
|
+
assert.match(criticals[0]!.message, /STIRPE_KEY/)
|
|
462
|
+
assert.match(criticals[0]!.message, /STRIPE_KEY/)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('validateSecretOverrides resolves a grant through its override before looking it up', () => {
|
|
466
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
467
|
+
const state = makeGrantState('secrets', ['PROD_EMAIL_KEY'], {
|
|
468
|
+
secretGrants: ['MAILGUN_KEY'],
|
|
469
|
+
secretOverrides: { MAILGUN_KEY: 'PROD_EMAIL_KEY' },
|
|
470
|
+
})
|
|
471
|
+
validateSecretOverrides(logger, state)
|
|
472
|
+
assert.deepEqual(criticals, [])
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('validateCredentialOverrides flags a grant naming no declared credential', () => {
|
|
476
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
477
|
+
const state = makeGrantState('credentials', ['marketing_cred'], {
|
|
478
|
+
credentialGrants: ['ghost_cred'],
|
|
479
|
+
})
|
|
480
|
+
validateCredentialOverrides(logger, state)
|
|
481
|
+
assert.equal(criticals.length, 1)
|
|
482
|
+
assert.match(criticals[0]!.message, /ghost_cred/)
|
|
483
|
+
})
|
|
417
484
|
})
|
|
418
485
|
|
|
419
486
|
const makeRemoteState = (
|
|
@@ -383,10 +383,8 @@ export function validateSecretOverrides(
|
|
|
383
383
|
)
|
|
384
384
|
|
|
385
385
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
386
|
-
if (!addonDecl.secretOverrides) continue
|
|
387
|
-
|
|
388
386
|
for (const [logicalName, resolvedName] of Object.entries(
|
|
389
|
-
addonDecl.secretOverrides
|
|
387
|
+
addonDecl.secretOverrides ?? {}
|
|
390
388
|
)) {
|
|
391
389
|
if (!secretIds.has(resolvedName)) {
|
|
392
390
|
const availableSecrets = Array.from(secretIds)
|
|
@@ -396,6 +394,20 @@ export function validateSecretOverrides(
|
|
|
396
394
|
)
|
|
397
395
|
}
|
|
398
396
|
}
|
|
397
|
+
|
|
398
|
+
// A grant names the secret as the addon reads it, so it resolves through
|
|
399
|
+
// the override map before it can be looked up in the project.
|
|
400
|
+
for (const logicalName of addonDecl.secretGrants ?? []) {
|
|
401
|
+
const resolvedName =
|
|
402
|
+
addonDecl.secretOverrides?.[logicalName] ?? logicalName
|
|
403
|
+
if (!secretIds.has(resolvedName)) {
|
|
404
|
+
const availableSecrets = Array.from(secretIds)
|
|
405
|
+
logger.critical(
|
|
406
|
+
ErrorCode.INVALID_VALUE,
|
|
407
|
+
`Secret grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a secret that does not exist. Available secrets: ${availableSecrets.join(', ') || 'none'}`
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
399
411
|
}
|
|
400
412
|
}
|
|
401
413
|
|
|
@@ -411,10 +423,8 @@ export function validateCredentialOverrides(
|
|
|
411
423
|
)
|
|
412
424
|
|
|
413
425
|
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
414
|
-
if (!addonDecl.credentialOverrides) continue
|
|
415
|
-
|
|
416
426
|
for (const [logicalName, resolvedName] of Object.entries(
|
|
417
|
-
addonDecl.credentialOverrides
|
|
427
|
+
addonDecl.credentialOverrides ?? {}
|
|
418
428
|
)) {
|
|
419
429
|
if (!credentialNames.has(resolvedName)) {
|
|
420
430
|
const availableCredentials = Array.from(credentialNames)
|
|
@@ -424,6 +434,18 @@ export function validateCredentialOverrides(
|
|
|
424
434
|
)
|
|
425
435
|
}
|
|
426
436
|
}
|
|
437
|
+
|
|
438
|
+
for (const logicalName of addonDecl.credentialGrants ?? []) {
|
|
439
|
+
const resolvedName =
|
|
440
|
+
addonDecl.credentialOverrides?.[logicalName] ?? logicalName
|
|
441
|
+
if (!credentialNames.has(resolvedName)) {
|
|
442
|
+
const availableCredentials = Array.from(credentialNames)
|
|
443
|
+
logger.critical(
|
|
444
|
+
ErrorCode.INVALID_VALUE,
|
|
445
|
+
`Credential grant '${logicalName}' in addon '${namespace}' (${addonDecl.package}) targets a credential that does not exist. Available credentials: ${availableCredentials.join(', ') || 'none'}`
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
427
449
|
}
|
|
428
450
|
}
|
|
429
451
|
|
|
@@ -170,6 +170,8 @@ export interface SerializableInspectorState {
|
|
|
170
170
|
secretOverrides?: Record<string, string>
|
|
171
171
|
variableOverrides?: Record<string, string>
|
|
172
172
|
credentialOverrides?: Record<string, string>
|
|
173
|
+
secretGrants?: string[]
|
|
174
|
+
credentialGrants?: string[]
|
|
173
175
|
globalSecrets?: string
|
|
174
176
|
globalCredentials?: string
|
|
175
177
|
},
|
|
@@ -15,13 +15,13 @@ import type {
|
|
|
15
15
|
SwitchCaseMeta,
|
|
16
16
|
FilterStepMeta,
|
|
17
17
|
ArrayPredicateStepMeta,
|
|
18
|
-
ScenarioStepMeta,
|
|
19
18
|
InputSource,
|
|
20
19
|
OutputBinding,
|
|
21
20
|
Condition,
|
|
22
21
|
WorkflowContext,
|
|
23
22
|
ContextVariable,
|
|
24
23
|
} from '@pikku/core/workflow'
|
|
24
|
+
import type { ScenarioStepMeta } from '@pikku/core/scenario'
|
|
25
25
|
import {
|
|
26
26
|
isWorkflowDoCall,
|
|
27
27
|
isWorkflowExpectEventuallyCall,
|
|
@@ -113,10 +113,10 @@ export type FlowType =
|
|
|
113
113
|
// Import and re-export context types from core
|
|
114
114
|
import type {
|
|
115
115
|
ContextVariable,
|
|
116
|
-
ScenarioStepPhase,
|
|
117
116
|
WorkflowContext,
|
|
118
117
|
WorkflowPlannedStep,
|
|
119
118
|
} from '@pikku/core/workflow'
|
|
119
|
+
import type { ScenarioStepPhase } from '@pikku/core/scenario'
|
|
120
120
|
|
|
121
121
|
export type { ContextVariable, WorkflowContext }
|
|
122
122
|
|