@pikku/core 0.12.94 → 0.12.96
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 +179 -0
- package/dist/dev/hot-reload.js +24 -4
- package/dist/dev/module-runner.d.ts +20 -3
- package/dist/dev/module-runner.js +17 -4
- package/dist/services/email-template.d.ts +43 -0
- package/dist/services/email-template.js +139 -0
- package/dist/services/http-personas.d.ts +6 -1
- package/dist/services/http-personas.js +4 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/wirings/agent/agent-prepare.d.ts +14 -0
- package/dist/wirings/agent/agent-prepare.js +24 -0
- package/dist/wirings/agent/index.d.ts +1 -1
- package/dist/wirings/agent/index.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +0 -1
- package/dist/wirings/virtual-user/index.d.ts +1 -0
- package/dist/wirings/virtual-user/index.js +1 -0
- package/dist/wirings/virtual-user/virtual-user-derive.js +9 -0
- package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +267 -0
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +400 -0
- package/dist/wirings/workflow/index.d.ts +1 -0
- package/dist/wirings/workflow/index.js +1 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +3 -9
- package/dist/wirings/workflow/scenario-prose.d.ts +23 -1
- package/dist/wirings/workflow/scenario-prose.js +12 -3
- package/dist/wirings/workflow/scenario-run.types.d.ts +7 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +18 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +35 -0
- package/dist/wirings/workflow/workflow-status-stream.d.ts +28 -0
- package/dist/wirings/workflow/workflow-status-stream.js +105 -0
- package/package.json +1 -1
- package/src/dev/hot-reload.test.ts +42 -0
- package/src/dev/hot-reload.ts +30 -4
- package/src/dev/module-runner.test.ts +56 -13
- package/src/dev/module-runner.ts +32 -10
- package/src/public-surface.json +17 -1
- package/src/services/email-template.test.ts +311 -0
- package/src/services/email-template.ts +254 -0
- package/src/services/http-personas.ts +10 -2
- package/src/services/index.ts +8 -0
- package/src/services/persona-sign-in.test.ts +22 -0
- package/src/wirings/agent/agent-helpers.test.ts +63 -0
- package/src/wirings/agent/agent-prepare.ts +25 -0
- package/src/wirings/agent/index.ts +1 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +178 -0
- package/src/wirings/scheduler/scheduler-runner.ts +0 -1
- package/src/wirings/virtual-user/index.ts +20 -0
- package/src/wirings/virtual-user/virtual-user-derive.test.ts +33 -5
- package/src/wirings/virtual-user/virtual-user-derive.ts +9 -0
- package/src/wirings/virtual-user/virtual-user-scaffold.test.ts +795 -0
- package/src/wirings/virtual-user/virtual-user-scaffold.ts +634 -0
- package/src/wirings/workflow/index.ts +4 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +71 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +5 -11
- package/src/wirings/workflow/scenario-prose.test.ts +134 -9
- package/src/wirings/workflow/scenario-prose.ts +37 -2
- package/src/wirings/workflow/scenario-run.types.ts +7 -0
- package/src/wirings/workflow/workflow-child-run-session.test.ts +79 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +44 -0
- package/src/wirings/workflow/workflow-status-stream.test.ts +354 -0
- package/src/wirings/workflow/workflow-status-stream.ts +144 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,182 @@
|
|
|
1
|
+
## 0.12.96
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 88629af: Say why a hot-reload import failed instead of only that it did.
|
|
6
|
+
|
|
7
|
+
The dev module runner caught every failure bare and returned `null`, and the reloader turned that into a single line: `Failed to import: … (keeping old code)`. Keeping the old code is the right call, but it leaves the running process disagreeing with the file on disk, and the only symptom is a function returning stale output while the editor shows the new source — `tsc` passes, every import resolves, and there is nothing anywhere to explain it.
|
|
8
|
+
|
|
9
|
+
`run` now returns `{ ok: true, exports }` or `{ ok: false, error }`, so the failure case cannot be read past, and the reloader prints the error's message and stack under the existing line. A failure matching pikku's own documented limitation — a file using top-level `await`, which the `cjs` emit cannot express — says so outright, because in that case nothing is wrong with the file and re-reading it will never reveal that.
|
|
10
|
+
|
|
11
|
+
- f1ccfe3: A step ladder reads as one paragraph, not a list of restatements
|
|
12
|
+
|
|
13
|
+
Every step prefixed its actor with `the `, named that actor again, and repeated
|
|
14
|
+
the phase keyword. A three-step run by one person said their name three times
|
|
15
|
+
and `Given` three times, only read as English when the persona key happened to
|
|
16
|
+
be a role noun, and never said who that person was — the fabric template's own
|
|
17
|
+
placeholder came out as `the nadia opens /app`.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Given yasser (the founder) signs in
|
|
21
|
+
When yasser opens the dashboard
|
|
22
|
+
And sees the audit log
|
|
23
|
+
And nadia reviews the invite
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The article is gone: the actor key is the subject verbatim, so a persona named
|
|
27
|
+
after a person reads as that person. A repeated phase reads as `And`, the way
|
|
28
|
+
Gherkin has always written it. A step that continues both the phase and the
|
|
29
|
+
actor drops the repeated subject, because English drops a repeated subject in a
|
|
30
|
+
compound predicate — it takes both, since eliding across a phase change gives
|
|
31
|
+
`When opens the dashboard`, and a pronoun rather than a name would give `they
|
|
32
|
+
sees`, step templates being authored in the third person singular.
|
|
33
|
+
|
|
34
|
+
An actor is introduced once, by the persona's `jobTitle` — prose someone wrote
|
|
35
|
+
for a reader. `roles` is authorisation, so a persona whose only description is a
|
|
36
|
+
`reviewer` grant gets no introduction rather than one assembled out of grants.
|
|
37
|
+
A row carries `sentenceWithRole` alongside `sentence`, set only where an actor
|
|
38
|
+
is first named, so a renderer can offer the introduction as a toggle without
|
|
39
|
+
parsing a composed sentence back apart.
|
|
40
|
+
|
|
41
|
+
`{placeholder}` filling, the `#ordinal` lookup for repeated step names and an
|
|
42
|
+
actorless step reading as its description alone are all unchanged.
|
|
43
|
+
|
|
44
|
+
## 0.12.95
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- 1cc50ef: Queue a workflow step that names another workflow, instead of running it inside its parent.
|
|
49
|
+
|
|
50
|
+
`dispatchStep` decided by reading `workflowQueued` off `rpc` meta, but `addWorkflow` never registers there, so a child workflow could never be queued. It always took the inline path: the parent started the child with `inline: true` and then sat in an unbounded `awaitRunEnd` poll, holding its run lock and that lock's connection until the child ended — and the child, being inline, ran its own `sleep` as a real in-process wait rather than a suspension. A parent whose child polled for fifteen minutes held two lock connections for fifteen minutes, and enough of them exhausted the lock pool and stalled every other run behind it.
|
|
51
|
+
|
|
52
|
+
A step naming a workflow now queues whenever a queue service exists, reaching the `ChildWorkflowStartedException` path that already unwinds the parent and resumes it when the child completes. An inline parent still runs its children inline.
|
|
53
|
+
|
|
54
|
+
- a3deea4: Stop the scheduler declaring `auth: false` for every task.
|
|
55
|
+
|
|
56
|
+
A task whose middleware sets a session runs a session-taking function, and the
|
|
57
|
+
hardcoded `auth: false` made the runner log "requires a session but auth was
|
|
58
|
+
explicitly disabled — use pikkuSessionlessFunc instead" on every single run.
|
|
59
|
+
Nothing else changes: a task with no session still throws `MissingSessionError`
|
|
60
|
+
when its function needs one.
|
|
61
|
+
|
|
62
|
+
- 2a02288: Let a virtual user run against a deployed stage.
|
|
63
|
+
|
|
64
|
+
Until now the scaffolded run could only sign its personas in with
|
|
65
|
+
`SCENARIO_ACTOR_SECRET`, which only `pikku dev` serves — so a run against a
|
|
66
|
+
deployed target failed before its first turn. `runVirtualUser` now takes an
|
|
67
|
+
optional short-lived Fabric operator token, handed in by whoever starts the run
|
|
68
|
+
and passed through to `createPersonas` as `operator`.
|
|
69
|
+
|
|
70
|
+
Handed in rather than fetched on demand: a stage that could ask for a token
|
|
71
|
+
would be holding a credential able to mint admin sessions for itself for as long
|
|
72
|
+
as the box lives. It holds one receipt, for one run, and the receipt expires. It
|
|
73
|
+
is never written to the run record — only `FABRIC_OPERATOR_TOKEN` in the
|
|
74
|
+
environment is read, and only as the fallback for a run nobody handed a token to.
|
|
75
|
+
|
|
76
|
+
`HttpPersonasConfig.signInPath` now applies to the operator path too, so an app
|
|
77
|
+
that mounts auth under `/api` can say so once.
|
|
78
|
+
|
|
79
|
+
The framework's own virtual-user RPCs no longer enter a virtual user's
|
|
80
|
+
catalogue. A persona whose role carries `virtualUser:*` could otherwise start
|
|
81
|
+
further runs, read back every run's transcript — an adversarial run's steps are
|
|
82
|
+
working exploits against the same app — and put a persona on a schedule that
|
|
83
|
+
outlives it.
|
|
84
|
+
|
|
85
|
+
The scheduled tick now runs as the platform user, and starts its runs through
|
|
86
|
+
the same door a person uses.
|
|
87
|
+
|
|
88
|
+
The scaffolded `startVirtualUserRun` RPC is gone — not the `startVirtualUserRun`
|
|
89
|
+
helper `@pikku/core/virtual-user` now exports, which is the shared record-writer
|
|
90
|
+
`runVirtualUser` calls. The RPC existed only so the tick could record a run
|
|
91
|
+
without holding a session, which meant the persona checks, the
|
|
92
|
+
production-disposition rule and the record lived in two places that would
|
|
93
|
+
eventually disagree. The tick calls `runVirtualUser` over RPC instead, and the
|
|
94
|
+
scaffold emits `virtualUserPlatformSession` to give it an identity:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
wireScheduler({
|
|
98
|
+
name: 'virtualUsers',
|
|
99
|
+
schedule: '0 * * * *',
|
|
100
|
+
middleware: [virtualUserPlatformSession],
|
|
101
|
+
func: tickVirtualUserSchedules,
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`pikku-platform` is the platform's own principal and already exists for exactly
|
|
106
|
+
this — a reserved user row created with no credential account of any kind, so no
|
|
107
|
+
sign-in method can resolve it, and one the user directory already filters out, so
|
|
108
|
+
unlike a seeded service account it costs no phantom member in any list, seat
|
|
109
|
+
count or bill.
|
|
110
|
+
|
|
111
|
+
The middleware is attached to the task rather than declared as tag middleware
|
|
112
|
+
over `/rpc`, which cannot set a session at all: `runScheduledTask` builds its
|
|
113
|
+
wire with a `sessionService`, so the session set here is the one the function is
|
|
114
|
+
frozen with. A tick wired without it is refused for want of a session, and one
|
|
115
|
+
carrying the wrong scope is refused on `virtualUser:run` — both now covered by
|
|
116
|
+
tests.
|
|
117
|
+
|
|
118
|
+
A Fabric operator can now actually start the run it signs in to start.
|
|
119
|
+
|
|
120
|
+
`fabric()` granted its operator row `admin` and nothing else. `admin` is this
|
|
121
|
+
package's own root — pikku's parent-grant rule walks down from a root that is
|
|
122
|
+
held, and the virtual-user scaffold declares `virtualUser` as a root of its own
|
|
123
|
+
precisely so a role can carry `virtualUser:run` without also implying
|
|
124
|
+
administration. So the operator was refused by `runVirtualUser`, the one
|
|
125
|
+
function the operator sign-in exists to reach.
|
|
126
|
+
|
|
127
|
+
The operator is now granted the roots in `OPERATOR_SCOPE_ROOTS`
|
|
128
|
+
(`admin`, `virtualUser`) rather than a bare `admin`. Listed rather than
|
|
129
|
+
collapsed to `*`, which would make every operator a superuser on every app for
|
|
130
|
+
the sake of one function: an operator still holds nothing in the application's
|
|
131
|
+
own domain, and a root the app never declared is skipped rather than stored.
|
|
132
|
+
|
|
133
|
+
The grant is also re-checked on every operator sign-in instead of only when the
|
|
134
|
+
row is created. It is deliberately logged rather than thrown, so a single
|
|
135
|
+
failure used to leave that operator permanently unprivileged with nothing to
|
|
136
|
+
retry it, and a root added to the set later would never have reached the
|
|
137
|
+
operators that already existed.
|
|
138
|
+
|
|
139
|
+
The scaffolds no longer keep their logic inside the CLI's template strings.
|
|
140
|
+
|
|
141
|
+
Code written as text inside a template literal is never compiled, never linted,
|
|
142
|
+
and testable only by matching the source the CLI emits — so a dead branch or a
|
|
143
|
+
duplicated loop survives there indefinitely. Five scaffolds were carrying real
|
|
144
|
+
logic that way, and it now lives in `@pikku/core` alongside the types it uses,
|
|
145
|
+
leaving each serializer to emit only what is genuinely per-application.
|
|
146
|
+
|
|
147
|
+
- **virtual-user** — 677 lines: the run driver, the persona and disposition
|
|
148
|
+
rules, the schedule writer and the serializers, now
|
|
149
|
+
`@pikku/core/virtual-user`. The guarantee that an operator token never
|
|
150
|
+
reaches the run record used to be a regex over emitted text; it is now
|
|
151
|
+
structural, because `startVirtualUserRun` has no parameter to pass one to.
|
|
152
|
+
- **workflow** — the two status streams were an ~80-line poll loop each,
|
|
153
|
+
identical apart from three fields, now one `streamWorkflowRunStatus` told
|
|
154
|
+
whether to be detailed. Fixes three latent bugs both copies shared: a
|
|
155
|
+
`setInterval(async …)` whose poll threw produced an unhandled rejection; a
|
|
156
|
+
poll that threw left the channel open rather than ending the stream; and the
|
|
157
|
+
interval fired whether or not the previous poll had returned, so a slow store
|
|
158
|
+
put two polls in flight and sent the init frame twice.
|
|
159
|
+
- **emails** — ~190 lines of HTML escaping, trusted-root allowlist and
|
|
160
|
+
single-pass substitution, now `renderEmail` in `@pikku/core/services`. This
|
|
161
|
+
was the security-sensitive one, and compiling it surfaced a bug the template
|
|
162
|
+
string had been hiding: `{{ content }}` was written unescaped in every render
|
|
163
|
+
rather than only in the layout it is the slot for, so a caller passing
|
|
164
|
+
`data.content` to a template that named it got raw HTML out. Nested lookups
|
|
165
|
+
also used `in`, which walks the prototype chain; nothing inherited actually
|
|
166
|
+
reached the output — every step past a prototype hit lands on a function,
|
|
167
|
+
which is neither traversed nor written — so that one is a closed door rather
|
|
168
|
+
than a fixed leak.
|
|
169
|
+
- **agent** — both callers built the same options object; now
|
|
170
|
+
`agentCallOptions`, typed against `AgentInput` rather than a second copy of
|
|
171
|
+
its shape.
|
|
172
|
+
- **console** — two branches that could only survive uncompiled: a catch block
|
|
173
|
+
identical to its try, and an if/else whose arms were the same call.
|
|
174
|
+
|
|
175
|
+
Behaviour is unchanged throughout, and the emitted modules are the same modules
|
|
176
|
+
— the emails scaffold's ten escaping tests pass untouched through core. The five
|
|
177
|
+
serializers shrink from 1,936 lines to 1,281, and what they used to emit is now
|
|
178
|
+
covered by 75 tests that run the code rather than by regexes over the text.
|
|
179
|
+
|
|
1
180
|
## 0.12.94
|
|
2
181
|
|
|
3
182
|
### Patch Changes
|
package/dist/dev/hot-reload.js
CHANGED
|
@@ -6,7 +6,7 @@ import { clearMiddlewareCache } from '../middleware-runner.js';
|
|
|
6
6
|
import { clearPermissionsCache } from '../permissions.js';
|
|
7
7
|
import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js';
|
|
8
8
|
import { httpRouter } from '../wirings/http/routers/http-router.js';
|
|
9
|
-
import { createModuleRunner } from './module-runner.js';
|
|
9
|
+
import { createModuleRunner, isTopLevelAwaitLimitation, } from './module-runner.js';
|
|
10
10
|
export { reloadGeneratedMeta, reconcileAddonRegistry } from './reload-meta.js';
|
|
11
11
|
const isFunctionConfig = (value) => {
|
|
12
12
|
return (typeof value === 'object' &&
|
|
@@ -40,6 +40,20 @@ const isWatchedTsFile = (filename) => {
|
|
|
40
40
|
// Hidden files: editor/sed atomic-write temps must never trigger a reload.
|
|
41
41
|
!basename(filename).startsWith('.'));
|
|
42
42
|
};
|
|
43
|
+
/** Not every reload failure is a mistake in the file: pikku's reloader emits
|
|
44
|
+
* `cjs`, which has no way to express top-level `await`, so a perfectly valid
|
|
45
|
+
* module can fail here forever. Saying so outright saves the reader from
|
|
46
|
+
* hunting a bug that is not in their code. The stack is dropped in that case
|
|
47
|
+
* because it points into esbuild rather than at anything actionable. */
|
|
48
|
+
const reloadFailureReason = (error) => {
|
|
49
|
+
if (isTopLevelAwaitLimitation(error)) {
|
|
50
|
+
return (` ${error.message}\n` +
|
|
51
|
+
' This is a pikku limitation, not a mistake in your file: the hot-reloader compiles to `cjs`, ' +
|
|
52
|
+
'which cannot express top-level `await`. Move the awaited work into a function, or restart the ' +
|
|
53
|
+
'dev server to pick the file up.');
|
|
54
|
+
}
|
|
55
|
+
return ` ${error.stack ?? error.message}`;
|
|
56
|
+
};
|
|
43
57
|
export async function pikkuDevReloader(options) {
|
|
44
58
|
const { srcDirectories, logger, pikkuDir = '.pikku' } = options;
|
|
45
59
|
const absSrcDirs = srcDirectories.map((d) => resolve(d));
|
|
@@ -56,11 +70,17 @@ export async function pikkuDevReloader(options) {
|
|
|
56
70
|
return;
|
|
57
71
|
const compiledFile = await findCompiledFile(changedTsFile, srcDir, absPikkuDir);
|
|
58
72
|
const importPath = compiledFile ?? changedTsFile;
|
|
59
|
-
const
|
|
60
|
-
if (!
|
|
61
|
-
|
|
73
|
+
const result = await moduleRunner.run(importPath);
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
// Keeping the old code leaves the process disagreeing with the file on
|
|
76
|
+
// disk, and the only symptom is stale output from a function that looks
|
|
77
|
+
// correct in the editor — so the reason has to be printed here, where it
|
|
78
|
+
// is still known, rather than left for the developer to reconstruct.
|
|
79
|
+
logger.error(`Failed to import: ${relative(process.cwd(), importPath)} (keeping old code)\n` +
|
|
80
|
+
reloadFailureReason(result.error));
|
|
62
81
|
return;
|
|
63
82
|
}
|
|
83
|
+
const mod = result.exports;
|
|
64
84
|
// knowledge: decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md
|
|
65
85
|
for (const [exportName, exportValue] of Object.entries(mod)) {
|
|
66
86
|
if (!isFunctionConfig(exportValue))
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
+
/** The outcome of one run. A failure carries its error rather than collapsing
|
|
2
|
+
* to `null`: the caller keeps serving the previously-loaded code, so unless the
|
|
3
|
+
* reason travels with the failure the running process silently disagrees with
|
|
4
|
+
* the file on disk and nothing anywhere says why. */
|
|
5
|
+
export type PikkuModuleRunResult = {
|
|
6
|
+
ok: true;
|
|
7
|
+
exports: Record<string, unknown>;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
error: Error;
|
|
11
|
+
};
|
|
1
12
|
export interface PikkuModuleRunner {
|
|
2
13
|
/** Run a user module by absolute path. Repeated runs of one path overwrite a
|
|
3
|
-
* single registry slot.
|
|
4
|
-
* previously-loaded code
|
|
5
|
-
|
|
14
|
+
* single registry slot. Failure is returned, not thrown, so the caller can
|
|
15
|
+
* keep the previously-loaded code — and the discriminant makes that case
|
|
16
|
+
* impossible to read past by accident. */
|
|
17
|
+
run: (absPath: string) => Promise<PikkuModuleRunResult>;
|
|
6
18
|
evict: (absPath: string) => void;
|
|
7
19
|
clear: () => void;
|
|
8
20
|
readonly size: number;
|
|
9
21
|
}
|
|
22
|
+
/** esbuild states pikku's one documented reload limitation only in the text of
|
|
23
|
+
* its transform error. Matching it is worth the fragility: the developer's file
|
|
24
|
+
* is correct, and no amount of re-reading it will reveal that the reloader —
|
|
25
|
+
* not the file — is what cannot cope. */
|
|
26
|
+
export declare const isTopLevelAwaitLimitation: (error: Error) => boolean;
|
|
10
27
|
export declare const createModuleRunner: () => PikkuModuleRunner;
|
|
@@ -13,6 +13,11 @@ const loadTransform = async () => {
|
|
|
13
13
|
transformSync = esbuild.transformSync;
|
|
14
14
|
return transformSync;
|
|
15
15
|
};
|
|
16
|
+
/** esbuild states pikku's one documented reload limitation only in the text of
|
|
17
|
+
* its transform error. Matching it is worth the fragility: the developer's file
|
|
18
|
+
* is correct, and no amount of re-reading it will reveal that the reloader —
|
|
19
|
+
* not the file — is what cannot cope. */
|
|
20
|
+
export const isTopLevelAwaitLimitation = (error) => /top-level await/i.test(error.message);
|
|
16
21
|
export const createModuleRunner = () => {
|
|
17
22
|
const registry = new Map();
|
|
18
23
|
const run = async (filePath) => {
|
|
@@ -30,12 +35,20 @@ export const createModuleRunner = () => {
|
|
|
30
35
|
const moduleObj = { exports: {} };
|
|
31
36
|
fn(require, moduleObj.exports, moduleObj, absPath, dirname(absPath));
|
|
32
37
|
registry.set(absPath, moduleObj.exports);
|
|
33
|
-
return moduleObj.exports;
|
|
38
|
+
return { ok: true, exports: moduleObj.exports };
|
|
34
39
|
}
|
|
35
|
-
catch {
|
|
40
|
+
catch (thrown) {
|
|
36
41
|
// A bad edit, or the one known limitation: a file using top-level
|
|
37
|
-
// `await`, which cannot be emitted in `cjs` form.
|
|
38
|
-
|
|
42
|
+
// `await`, which cannot be emitted in `cjs` form. Normalised to an
|
|
43
|
+
// `Error` so the caller always has a message and a stack to print
|
|
44
|
+
// without re-deriving them; a non-`Error` throw keeps its original value
|
|
45
|
+
// as the `cause`.
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
error: thrown instanceof Error
|
|
49
|
+
? thrown
|
|
50
|
+
: new Error(String(thrown), { cause: thrown }),
|
|
51
|
+
};
|
|
39
52
|
}
|
|
40
53
|
};
|
|
41
54
|
return {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer behind a generated `pikku-emails.gen.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The generated module supplies the assets — theme, locale strings, partials and
|
|
5
|
+
* the templates themselves — and a typed wrapper over `renderEmail`. Everything
|
|
6
|
+
* here is the same for every application, which is why it lives in core rather
|
|
7
|
+
* than in the string the CLI writes: this is HTML escaping, and code inside a
|
|
8
|
+
* template literal is never compiled, never linted, and testable only by
|
|
9
|
+
* matching the text it emits.
|
|
10
|
+
*/
|
|
11
|
+
export interface EmailTemplateHashes {
|
|
12
|
+
contentHash: string;
|
|
13
|
+
htmlHash: string;
|
|
14
|
+
subjectHash: string;
|
|
15
|
+
textHash: string;
|
|
16
|
+
}
|
|
17
|
+
export interface EmailTemplateAssets {
|
|
18
|
+
html: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
text: string;
|
|
21
|
+
variables: ReadonlyArray<string>;
|
|
22
|
+
hashes: Record<string, EmailTemplateHashes>;
|
|
23
|
+
}
|
|
24
|
+
export interface EmailAssets {
|
|
25
|
+
theme: Record<string, unknown>;
|
|
26
|
+
locales: Record<string, Record<string, unknown>>;
|
|
27
|
+
partials: Record<string, string>;
|
|
28
|
+
templates: Record<string, EmailTemplateAssets>;
|
|
29
|
+
}
|
|
30
|
+
export interface RenderEmailRequest {
|
|
31
|
+
name: string;
|
|
32
|
+
locale?: string;
|
|
33
|
+
data?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export interface RenderedEmailResult {
|
|
36
|
+
locale: string;
|
|
37
|
+
subject: string;
|
|
38
|
+
html: string;
|
|
39
|
+
text?: string;
|
|
40
|
+
variables: ReadonlyArray<string>;
|
|
41
|
+
hash: string;
|
|
42
|
+
}
|
|
43
|
+
export declare const renderEmail: ({ theme, locales, partials, templates }: EmailAssets, { name, locale: requestedLocale, data }: RenderEmailRequest) => RenderedEmailResult;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer behind a generated `pikku-emails.gen.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The generated module supplies the assets — theme, locale strings, partials and
|
|
5
|
+
* the templates themselves — and a typed wrapper over `renderEmail`. Everything
|
|
6
|
+
* here is the same for every application, which is why it lives in core rather
|
|
7
|
+
* than in the string the CLI writes: this is HTML escaping, and code inside a
|
|
8
|
+
* template literal is never compiled, never linted, and testable only by
|
|
9
|
+
* matching the text it emits.
|
|
10
|
+
*/
|
|
11
|
+
const HTML_ESCAPES = {
|
|
12
|
+
'&': '&',
|
|
13
|
+
'<': '<',
|
|
14
|
+
'>': '>',
|
|
15
|
+
'"': '"',
|
|
16
|
+
"'": ''',
|
|
17
|
+
};
|
|
18
|
+
const escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char);
|
|
19
|
+
// Matches the raw {{{ value }}} form before the escaped {{ value }} form, so the
|
|
20
|
+
// opt-in escape hatch is never mistaken for a normal substitution.
|
|
21
|
+
const TEMPLATE_TOKEN = /\{\{\{\s*([^{}]+?)\s*\}\}\}|\{\{\s*([^}]+?)\s*\}\}/g;
|
|
22
|
+
const PARTIAL_TOKEN = /\{\{\s*>\s*([a-zA-Z0-9-_/.]+)\s*\}\}/g;
|
|
23
|
+
const MAX_TEMPLATE_DEPTH = 5;
|
|
24
|
+
// theme.json and the locale files ship with the templates, so they are treated as
|
|
25
|
+
// template-author input: expanded before caller data and allowed to contain their
|
|
26
|
+
// own placeholders. Everything else is caller-supplied.
|
|
27
|
+
const TRUSTED_ROOTS = ['theme', 't'];
|
|
28
|
+
const isTrustedKey = (key) => TRUSTED_ROOTS.includes(String(key.split('.')[0]));
|
|
29
|
+
const getNestedValue = (source, path) => {
|
|
30
|
+
const segments = path.split('.');
|
|
31
|
+
let current = source;
|
|
32
|
+
for (const segment of segments) {
|
|
33
|
+
// `hasOwn`, not `in`: `in` walks the prototype chain, so a path is answered
|
|
34
|
+
// by what an object inherits rather than only by what it carries. Nothing
|
|
35
|
+
// inherited reaches the output today — every step past a prototype hit lands
|
|
36
|
+
// on a function, which is neither traversed nor written — so this closes the
|
|
37
|
+
// lookup rather than fixing a value that escapes through it.
|
|
38
|
+
if (!current ||
|
|
39
|
+
typeof current !== 'object' ||
|
|
40
|
+
!Object.hasOwn(current, segment)) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
current = current[segment];
|
|
44
|
+
}
|
|
45
|
+
return typeof current === 'string' || typeof current === 'number'
|
|
46
|
+
? String(current)
|
|
47
|
+
: '';
|
|
48
|
+
};
|
|
49
|
+
const readToken = (rawTriple, rawDouble) => {
|
|
50
|
+
const raw = typeof rawTriple === 'string';
|
|
51
|
+
return { raw, key: String(raw ? rawTriple : rawDouble).trim() };
|
|
52
|
+
};
|
|
53
|
+
const expandPartials = (source, partials, depth = 0) => {
|
|
54
|
+
if (depth >= MAX_TEMPLATE_DEPTH)
|
|
55
|
+
return source;
|
|
56
|
+
let found = false;
|
|
57
|
+
const expanded = source.replace(PARTIAL_TOKEN, (_match, partialName) => {
|
|
58
|
+
found = true;
|
|
59
|
+
const partial = partials[String(partialName).trim()];
|
|
60
|
+
return typeof partial === 'string' ? partial : '';
|
|
61
|
+
});
|
|
62
|
+
return found ? expandPartials(expanded, partials, depth + 1) : expanded;
|
|
63
|
+
};
|
|
64
|
+
const expandTrusted = (source, context, escape) => {
|
|
65
|
+
let rendered = source;
|
|
66
|
+
for (let i = 0; i < MAX_TEMPLATE_DEPTH; i += 1) {
|
|
67
|
+
let found = false;
|
|
68
|
+
const next = rendered.replace(TEMPLATE_TOKEN, (match, rawTriple, rawDouble) => {
|
|
69
|
+
const { raw, key } = readToken(rawTriple, rawDouble);
|
|
70
|
+
if (!isTrustedKey(key))
|
|
71
|
+
return match;
|
|
72
|
+
found = true;
|
|
73
|
+
const value = getNestedValue(context, key);
|
|
74
|
+
return raw || !escape ? value : escapeHtml(value);
|
|
75
|
+
});
|
|
76
|
+
if (!found || next === rendered)
|
|
77
|
+
break;
|
|
78
|
+
rendered = next;
|
|
79
|
+
}
|
|
80
|
+
return rendered;
|
|
81
|
+
};
|
|
82
|
+
// A single substitution pass — the replacement text is never rescanned, so a
|
|
83
|
+
// caller-supplied value can never be reinterpreted as a template.
|
|
84
|
+
const substitute = (source, context, escape, slot) => source.replace(TEMPLATE_TOKEN, (_match, rawTriple, rawDouble) => {
|
|
85
|
+
const { raw, key } = readToken(rawTriple, rawDouble);
|
|
86
|
+
// `content` is the layout's slot for the body that was already rendered and
|
|
87
|
+
// escaped, so it is the one value written in raw. Only the layout gets it:
|
|
88
|
+
// honouring it everywhere would let a caller pass `data.content` into a
|
|
89
|
+
// template that happens to name it and have it emitted unescaped.
|
|
90
|
+
if (slot !== undefined && key === slot) {
|
|
91
|
+
return typeof context[slot] === 'string' ? context[slot] : '';
|
|
92
|
+
}
|
|
93
|
+
if (key.startsWith('>')) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
const value = getNestedValue(context, key);
|
|
97
|
+
return raw || !escape ? value : escapeHtml(value);
|
|
98
|
+
});
|
|
99
|
+
const renderTemplate = (source, partials, context, escape, slot) => {
|
|
100
|
+
const composed = expandTrusted(expandPartials(source, partials), context, escape);
|
|
101
|
+
return substitute(composed, context, escape, slot);
|
|
102
|
+
};
|
|
103
|
+
export const renderEmail = ({ theme, locales, partials, templates }, { name, locale: requestedLocale, data }) => {
|
|
104
|
+
const locale = requestedLocale ?? 'en';
|
|
105
|
+
const template = templates[name];
|
|
106
|
+
if (!template) {
|
|
107
|
+
throw new Error(`Unknown email template: ${name}`);
|
|
108
|
+
}
|
|
109
|
+
const strings = locales[locale];
|
|
110
|
+
if (!strings) {
|
|
111
|
+
throw new Error(`Unknown email locale: ${locale}`);
|
|
112
|
+
}
|
|
113
|
+
const values = data ?? {};
|
|
114
|
+
const appName = (typeof values.appName === 'string' && values.appName) ||
|
|
115
|
+
getNestedValue(theme, 'appName');
|
|
116
|
+
const baseContext = {
|
|
117
|
+
...values,
|
|
118
|
+
locale,
|
|
119
|
+
theme,
|
|
120
|
+
t: strings,
|
|
121
|
+
appName,
|
|
122
|
+
};
|
|
123
|
+
const subject = renderTemplate(template.subject, partials, baseContext, false).trim();
|
|
124
|
+
const htmlBody = renderTemplate(template.html, partials, { ...baseContext, subject }, true);
|
|
125
|
+
const html = partials.layout
|
|
126
|
+
? renderTemplate(partials.layout, partials, { ...baseContext, subject, content: htmlBody }, true, 'content')
|
|
127
|
+
: htmlBody;
|
|
128
|
+
const text = template.text
|
|
129
|
+
? renderTemplate(template.text, partials, { ...baseContext, subject }, false).trim()
|
|
130
|
+
: undefined;
|
|
131
|
+
return {
|
|
132
|
+
locale,
|
|
133
|
+
subject,
|
|
134
|
+
html,
|
|
135
|
+
...(text ? { text } : {}),
|
|
136
|
+
variables: template.variables,
|
|
137
|
+
hash: template.hashes[locale]?.contentHash ?? '',
|
|
138
|
+
};
|
|
139
|
+
};
|
|
@@ -27,7 +27,12 @@ export interface HttpPersonasConfig {
|
|
|
27
27
|
operator?: OperatorSignInOptions;
|
|
28
28
|
/** Persona id → the declaration with its address filled in. */
|
|
29
29
|
personas: Record<string, ResolvedPersona>;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Sign-in path under apiUrl, for whichever of the two paths is in use — an
|
|
32
|
+
* app that mounts auth under `/api` moves both. Default: the actor plugin's
|
|
33
|
+
* `/auth/sign-in/actor`, or `/auth/sign-in/fabric` for an operator.
|
|
34
|
+
* {@link OperatorSignInOptions.signInPath} overrides it.
|
|
35
|
+
*/
|
|
31
36
|
signInPath?: string;
|
|
32
37
|
/** Where the session (and its roles) is read back. Default `/auth/get-session`. */
|
|
33
38
|
sessionPath?: string;
|
|
@@ -32,7 +32,10 @@ export class HttpPersona {
|
|
|
32
32
|
this.config = config;
|
|
33
33
|
this.jar = createCookieJar(config.apiUrl);
|
|
34
34
|
if (config.operator) {
|
|
35
|
-
this.signIn = new OperatorSignIn(config.apiUrl,
|
|
35
|
+
this.signIn = new OperatorSignIn(config.apiUrl, {
|
|
36
|
+
...config.operator,
|
|
37
|
+
signInPath: config.operator.signInPath ?? config.signInPath,
|
|
38
|
+
});
|
|
36
39
|
}
|
|
37
40
|
else if (config.secret) {
|
|
38
41
|
this.signIn = new ActorSignIn(config.apiUrl, config.secret, config.signInPath ?? '/auth/sign-in/actor');
|
package/dist/services/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type { ContentService, SignContentKeyArgs, SignURLArgs, GetUploadURLArgs,
|
|
|
21
21
|
export type { ScenarioPersona, ResolvedPersona, ScenarioPersonas, } from './personas-service.js';
|
|
22
22
|
export type { JWTService } from './jwt-service.js';
|
|
23
23
|
export type { EmailService, SendEmailInput, SendEmailResult, SendHTMLEmailInput, SendTemplateEmailInput, SendTextEmailInput, } from './email-service.js';
|
|
24
|
+
export { renderEmail, type EmailAssets, type EmailTemplateAssets, type EmailTemplateHashes, type RenderEmailRequest, type RenderedEmailResult, } from './email-template.js';
|
|
24
25
|
export { DEFAULT_WEBHOOK_RETRIES, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, type SendWebhookInput, type SendWebhookResult, type WebhookAttemptResult, type WebhookDeliveryRecord, type WebhookDeliveryWithAttempts, type WebhookJobData, type WebhookServiceConfig, } from './webhook-service.js';
|
|
25
26
|
export type { Logger } from './logger.js';
|
|
26
27
|
export type { SecretService, SecretValues } from './secret-service.js';
|
package/dist/services/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
|
17
17
|
export { InMemoryAgentRunStateService } from './in-memory-agent-run-state-service.js';
|
|
18
18
|
export { LocalGatewayService } from './local-gateway-service.js';
|
|
19
19
|
export { FileScenarioRunStore, scenarioArtifactContentType, scenarioRunSummary, } from './file-scenario-run-store.js';
|
|
20
|
+
export { renderEmail, } from './email-template.js';
|
|
20
21
|
export { DEFAULT_WEBHOOK_RETRIES, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, } from './webhook-service.js';
|
|
21
22
|
export { SchedulerService } from './scheduler-service.js';
|
|
22
23
|
export { TypedCredentialService } from './typed-credential-service.js';
|
|
@@ -28,6 +28,20 @@ export declare function canAccessThread(storedResourceId: string, session: {
|
|
|
28
28
|
userId?: string;
|
|
29
29
|
orgId?: string;
|
|
30
30
|
} | undefined): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* An agent call with the fields nobody supplied left out.
|
|
33
|
+
*
|
|
34
|
+
* Omitted rather than passed as `undefined`, because an explicit `undefined`
|
|
35
|
+
* overrides the agent's own declared default with nothing — a request that
|
|
36
|
+
* names no model would silently unset the one the agent declares.
|
|
37
|
+
*
|
|
38
|
+
* Shared by the scaffolded `run` and `stream` routes, which receive the same
|
|
39
|
+
* input and differ only in what they do with the reply. `agentName` is not part
|
|
40
|
+
* of it: both callers pass that separately, because `rpc.agent.run` and
|
|
41
|
+
* `rpc.agent.stream` take it as their first argument and type the rest
|
|
42
|
+
* against it.
|
|
43
|
+
*/
|
|
44
|
+
export declare const agentCallOptions: (input: AgentInput) => AgentInput;
|
|
31
45
|
export type StreamAgentOptions = {
|
|
32
46
|
requiresToolApproval?: 'all' | 'explicit' | false;
|
|
33
47
|
onRunCreated?: (runId: string) => void;
|
|
@@ -65,6 +65,30 @@ export function canAccessThread(storedResourceId, session) {
|
|
|
65
65
|
return false;
|
|
66
66
|
return principals.some((principal) => isOwnedByPrincipal(storedResourceId, principal));
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* An agent call with the fields nobody supplied left out.
|
|
70
|
+
*
|
|
71
|
+
* Omitted rather than passed as `undefined`, because an explicit `undefined`
|
|
72
|
+
* overrides the agent's own declared default with nothing — a request that
|
|
73
|
+
* names no model would silently unset the one the agent declares.
|
|
74
|
+
*
|
|
75
|
+
* Shared by the scaffolded `run` and `stream` routes, which receive the same
|
|
76
|
+
* input and differ only in what they do with the reply. `agentName` is not part
|
|
77
|
+
* of it: both callers pass that separately, because `rpc.agent.run` and
|
|
78
|
+
* `rpc.agent.stream` take it as their first argument and type the rest
|
|
79
|
+
* against it.
|
|
80
|
+
*/
|
|
81
|
+
export const agentCallOptions = (input) => ({
|
|
82
|
+
message: input.message,
|
|
83
|
+
threadId: input.threadId,
|
|
84
|
+
resourceId: input.resourceId,
|
|
85
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
86
|
+
...(input.model ? { model: input.model } : {}),
|
|
87
|
+
...(input.temperature !== undefined
|
|
88
|
+
? { temperature: input.temperature }
|
|
89
|
+
: {}),
|
|
90
|
+
...(input.context ? { context: input.context } : {}),
|
|
91
|
+
});
|
|
68
92
|
export const APPROVAL_REQUIRED = Symbol('pikku.ai.approvalRequired');
|
|
69
93
|
/**
|
|
70
94
|
* In-process brand proving a credential request was produced by pikku itself and
|
|
@@ -7,6 +7,6 @@ export { voiceInput, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } fr
|
|
|
7
7
|
export { voiceOutput, unspeakableScripts, voiceForText, type SpeakableScripts, type VoiceOutputState, } from './voice-output.js';
|
|
8
8
|
export { AgentInterruptedError, signalRunInterrupt } from './agent-interrupt.js';
|
|
9
9
|
export type { AgentInterruption, AgentInterruptResult, InterruptibleRunHandle, } from './agent-interrupt.js';
|
|
10
|
-
export { type RunAgentParams, type StreamAgentOptions, ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, threadOwnerConstraint, } from './agent-prepare.js';
|
|
10
|
+
export { type RunAgentParams, type StreamAgentOptions, ToolApprovalRequired, ToolCredentialRequired, agentCallOptions, canAccessThread, isOwnedByPrincipal, threadOwnerConstraint, } from './agent-prepare.js';
|
|
11
11
|
export { addAgent } from './agent-registry.js';
|
|
12
12
|
export type { AgentInput, AgentsMeta, AgentMemoryConfig, AgentStep, AgentContentPart, AgentRunRow, AgentRunService, AgentRunState, AgentMessage, AgentStreamChannel, AgentStreamEvent, AgentThread, CoreAgent, PendingApproval, PikkuAgentMiddlewareHooks, } from './agent.types.js';
|
|
@@ -6,5 +6,5 @@ export { streamAgent, resumeAgent, interruptAgent } from './agent-stream.js';
|
|
|
6
6
|
export { voiceInput, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } from './voice-input.js';
|
|
7
7
|
export { voiceOutput, unspeakableScripts, voiceForText, } from './voice-output.js';
|
|
8
8
|
export { AgentInterruptedError, signalRunInterrupt } from './agent-interrupt.js';
|
|
9
|
-
export { ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, threadOwnerConstraint, } from './agent-prepare.js';
|
|
9
|
+
export { ToolApprovalRequired, ToolCredentialRequired, agentCallOptions, canAccessThread, isOwnedByPrincipal, threadOwnerConstraint, } from './agent-prepare.js';
|
|
10
10
|
export { addAgent } from './agent-registry.js';
|
|
@@ -68,7 +68,6 @@ export async function runScheduledTask({ name, session, traceId, }) {
|
|
|
68
68
|
await runPikkuFunc('scheduler', meta.name, meta.pikkuFuncId, {
|
|
69
69
|
singletonServices,
|
|
70
70
|
createWireServices,
|
|
71
|
-
auth: false,
|
|
72
71
|
data: () => undefined,
|
|
73
72
|
inheritedMiddleware: meta.middleware,
|
|
74
73
|
wireMiddleware: task.middleware,
|
|
@@ -29,3 +29,4 @@ export { type AgentReachability, type ReachableAgent, } from './virtual-user-age
|
|
|
29
29
|
export { IntentStack, intentsForPersona } from './virtual-user-intents.js';
|
|
30
30
|
export { deriveCatalogue, deriveIntents, type SchemaMap, } from './virtual-user-derive.js';
|
|
31
31
|
export { personaVirtualUserTarget, type PersonaTargetOptions, } from './virtual-user-target.js';
|
|
32
|
+
export { executeVirtualUserRun, logVirtualUserTick, requireVirtualUserRunStore, requireVirtualUserScheduleStore, runnablePersona, serializeVirtualUserRun, serializeVirtualUserSchedule, serializeVirtualUserSteps, signInPathFor, startVirtualUserRun, VIRTUAL_USER_VARIABLES, virtualUserScheduleRunInput, writeVirtualUserSchedule, type ExecuteVirtualUserRunParams, type ScaffoldPersonas, type StartedVirtualUserRun, type StartVirtualUserRunParams, type WriteVirtualUserScheduleParams, } from './virtual-user-scaffold.js';
|
|
@@ -7,3 +7,4 @@ export { catalogueClassification, catalogueLookup, isReadOnly, reachableCatalogu
|
|
|
7
7
|
export { IntentStack, intentsForPersona } from './virtual-user-intents.js';
|
|
8
8
|
export { deriveCatalogue, deriveIntents, } from './virtual-user-derive.js';
|
|
9
9
|
export { personaVirtualUserTarget, } from './virtual-user-target.js';
|
|
10
|
+
export { executeVirtualUserRun, logVirtualUserTick, requireVirtualUserRunStore, requireVirtualUserScheduleStore, runnablePersona, serializeVirtualUserRun, serializeVirtualUserSchedule, serializeVirtualUserSteps, signInPathFor, startVirtualUserRun, VIRTUAL_USER_VARIABLES, virtualUserScheduleRunInput, writeVirtualUserSchedule, } from './virtual-user-scaffold.js';
|
|
@@ -24,6 +24,15 @@ export const deriveCatalogue = (functions, schemas = {}) => {
|
|
|
24
24
|
// knowledge: decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md
|
|
25
25
|
if (meta.expose !== true)
|
|
26
26
|
continue;
|
|
27
|
+
// A virtual user is not offered the machinery that runs virtual users. A
|
|
28
|
+
// persona whose role carries `virtualUser:*` would otherwise be able to
|
|
29
|
+
// start further runs, read every run's findings — an adversarial run's
|
|
30
|
+
// transcript is working exploits against this same app — and put a persona
|
|
31
|
+
// on a schedule that outlives it. Same reasoning as the scenario-step rule
|
|
32
|
+
// above: the tool is about the run, not about the product being explored.
|
|
33
|
+
if (meta.scopes?.some((scope) => scope.split(':')[0] === 'virtualUser')) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
27
36
|
const inputSchema = meta.inputSchemaName
|
|
28
37
|
? schemas[meta.inputSchemaName]
|
|
29
38
|
: undefined;
|