@pikku/core 0.12.29 → 0.12.30
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 +64 -0
- package/dist/services/meta-service.js +15 -7
- package/dist/types/core.types.d.ts +9 -1
- package/dist/wirings/secret/secret.types.d.ts +7 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +5 -1
- package/dist/wirings/workflow/graph/graph-runner.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +11 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +37 -23
- package/dist/wirings/workflow/workflow.types.d.ts +0 -3
- package/package.json +1 -1
- package/src/services/meta-service.ts +19 -7
- package/src/types/core.types.ts +9 -0
- package/src/wirings/secret/secret.types.ts +7 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +5 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +1 -2
- package/src/wirings/workflow/graph/graph-runner.ts +1 -2
- package/src/wirings/workflow/pikku-workflow-service.test.ts +168 -19
- package/src/wirings/workflow/pikku-workflow-service.ts +41 -25
- package/src/wirings/workflow/workflow.types.ts +0 -3
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
## 0.12.30
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- cd101a5: feat(core): add `auditLog` service slot for per-invocation audit logs
|
|
6
|
+
|
|
7
|
+
`CoreSingletonServices` now declares `auditLog?: AuditLog`, giving the
|
|
8
|
+
per-request audit log returned by `createInvocationAudit` a typed home in the
|
|
9
|
+
service container. Apps wire it in `createWireServices`
|
|
10
|
+
(`return { auditLog, kysely: createAuditedKysely(kysely, { audit: auditLog }) }`)
|
|
11
|
+
and the runner flushes its buffer via `close()` when the invocation ends.
|
|
12
|
+
|
|
13
|
+
Previously there was no slot to return it from: `audit` is typed `AuditService`
|
|
14
|
+
(the durable sink, `.audit()`), while `createInvocationAudit` returns an
|
|
15
|
+
`AuditLog` (the request-scoped buffer, `.write/.flush/.close`). Returning the
|
|
16
|
+
buffer under `audit` was a type error, so audited-Kysely wiring could not
|
|
17
|
+
type-check. `auditLog` is distinct from `audit` and never shadows it.
|
|
18
|
+
|
|
19
|
+
- ac16265: fix(core): read email template assets from the absolute `emailsMeta.src` directly
|
|
20
|
+
|
|
21
|
+
`getEmailTemplateAssets` passed an absolute `baseDir` (e.g. `/project/emails`) into
|
|
22
|
+
`readProjectFile`, which resolves `join(basePath, '..', relativePath)`. Because
|
|
23
|
+
`path.join` does not treat an absolute second segment as a root reset, this produced
|
|
24
|
+
a non-existent compound path (`/project/packages/functions/project/emails/...`), so
|
|
25
|
+
every asset read returned `null` and the email preview reported all source files
|
|
26
|
+
(`theme, locale, html, subject, text`) as missing. Read the assets directly via
|
|
27
|
+
`readFile(join(baseDir, rel))` instead, which resolves correctly for an absolute
|
|
28
|
+
base. Verified live: a previously all-missing preview now renders.
|
|
29
|
+
|
|
30
|
+
- a05e864: fix(core): allow multiple independent suspend points in one workflow
|
|
31
|
+
|
|
32
|
+
`getSuspendStepName()` returned the constant `'__workflow_suspend'` for every
|
|
33
|
+
`workflow.suspend()` call, so all suspends in a run shared a single step row.
|
|
34
|
+
Once the first suspend resolved (row → `succeeded`), every later `suspend()`
|
|
35
|
+
read that same `succeeded` row and fell straight through without pausing — so a
|
|
36
|
+
workflow could only ever have one working suspend point, and a second one (e.g.
|
|
37
|
+
wait-for-build, then wait-for-approval) was silently skipped.
|
|
38
|
+
|
|
39
|
+
The suspend step is now keyed on its `reason` (used raw, just namespaced so it
|
|
40
|
+
can't collide with a `do`/`sleep` step of the same name), so each distinct
|
|
41
|
+
reason is its own step row. A workflow can now have multiple independent
|
|
42
|
+
suspends, including dynamic reasons in loops (`suspend(`Wait for ${i}`)`),
|
|
43
|
+
exactly like dynamic `do()` step names. As with `do()`/`sleep()`, the reason is
|
|
44
|
+
the suspend's stable identity and must be derived deterministically so it
|
|
45
|
+
matches on replay. `suspend(reason)` is unchanged at the call site.
|
|
46
|
+
|
|
47
|
+
- 20750fd: feat(workflow): decide step dispatch purely per-function
|
|
48
|
+
|
|
49
|
+
Workflow step execution (inline vs queue dispatch) is now decided entirely by
|
|
50
|
+
the step's function `inline` flag — the workflow-level / run-level `inline`
|
|
51
|
+
meta no longer participates in per-step dispatch.
|
|
52
|
+
- Steps default to **inline**, so a normally-started (queue-backed) workflow
|
|
53
|
+
runs its whole chain in one orchestrator pass instead of one queue
|
|
54
|
+
round-trip per step.
|
|
55
|
+
- A function marked `inline: false` is dispatched via the queue (its own
|
|
56
|
+
worker, retry isolation). When `inline: false` but no `queueService` is
|
|
57
|
+
configured, the step falls back to inline and emits a `logger.warn` instead
|
|
58
|
+
of silently swallowing the misconfiguration.
|
|
59
|
+
- Removed the now-unused workflow-level `inline` from `WorkflowsMeta` /
|
|
60
|
+
`WorkflowRuntimeMeta`, the inspector's workflow extraction, the DSL→graph
|
|
61
|
+
converter, and the deploy analyzer / service inference (which now key off
|
|
62
|
+
the per-function flag). Run-level `inline` is retained: it still controls
|
|
63
|
+
whether a whole run executes in-process without queue infrastructure.
|
|
64
|
+
|
|
1
65
|
## 0.12.29
|
|
2
66
|
|
|
3
67
|
### Patch Changes
|
|
@@ -306,6 +306,20 @@ export class LocalMetaService {
|
|
|
306
306
|
readEmailFile(`templates/${templateName}.subject.txt`),
|
|
307
307
|
readEmailFile(`templates/${templateName}.text.txt`),
|
|
308
308
|
]);
|
|
309
|
+
const missing = [
|
|
310
|
+
...(themeRaw ? [] : ['theme']),
|
|
311
|
+
...(localeRaw ? [] : ['locale']),
|
|
312
|
+
...(templateHtml ? [] : ['html']),
|
|
313
|
+
...(templateSubject ? [] : ['subject']),
|
|
314
|
+
...(templateText ? [] : ['text']),
|
|
315
|
+
];
|
|
316
|
+
if (missing.length > 0) {
|
|
317
|
+
// Diagnostic for the intermittent "Missing source files" report: logs which
|
|
318
|
+
// process and which resolved baseDir saw the gap. Different pids across
|
|
319
|
+
// failures => multiple runtime workers with divergent cached `src`; same pid
|
|
320
|
+
// + transient absence => files being rewritten under us (e.g. regeneration).
|
|
321
|
+
console.warn(`[email-assets] pid=${process.pid} template=${templateName} locale=${locale} baseDir=${baseDir} missing=${missing.join(',')}`);
|
|
322
|
+
}
|
|
309
323
|
return {
|
|
310
324
|
theme: themeRaw ? JSON.parse(themeRaw) : {},
|
|
311
325
|
strings: localeRaw
|
|
@@ -318,13 +332,7 @@ export class LocalMetaService {
|
|
|
318
332
|
html: templateHtml ?? '',
|
|
319
333
|
subject: templateSubject ?? '',
|
|
320
334
|
text: templateText ?? '',
|
|
321
|
-
missing
|
|
322
|
-
...(themeRaw ? [] : ['theme']),
|
|
323
|
-
...(localeRaw ? [] : ['locale']),
|
|
324
|
-
...(templateHtml ? [] : ['html']),
|
|
325
|
-
...(templateSubject ? [] : ['subject']),
|
|
326
|
-
...(templateText ? [] : ['text']),
|
|
327
|
-
],
|
|
335
|
+
missing,
|
|
328
336
|
};
|
|
329
337
|
}
|
|
330
338
|
async getServicesMeta() {
|
|
@@ -28,7 +28,7 @@ import type { CredentialService } from '../services/credential-service.js';
|
|
|
28
28
|
import type { EmailService } from '../services/email-service.js';
|
|
29
29
|
import type { MetaService } from '../services/meta-service.js';
|
|
30
30
|
import type { SessionStore } from '../services/session-store.js';
|
|
31
|
-
import type { AuditDurability, AuditService } from '../services/audit-service.js';
|
|
31
|
+
import type { AuditDurability, AuditLog, AuditService } from '../services/audit-service.js';
|
|
32
32
|
export type PikkuWiringTypes = 'http' | 'scheduler' | 'trigger' | 'channel' | 'rpc' | 'queue' | 'mcp' | 'cli' | 'workflow' | 'agent' | 'gateway';
|
|
33
33
|
export interface FunctionServicesMeta {
|
|
34
34
|
optimized: boolean;
|
|
@@ -196,6 +196,14 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
196
196
|
metaService?: MetaService;
|
|
197
197
|
/** Audit service for durable or staged audit event capture */
|
|
198
198
|
audit?: AuditService;
|
|
199
|
+
/**
|
|
200
|
+
* Per-invocation audit log. Typically created in `createWireServices` via
|
|
201
|
+
* `createInvocationAudit(audit, wire)` and returned as a wire service so the
|
|
202
|
+
* runner flushes its buffer (via `close()`) when the invocation ends. Distinct
|
|
203
|
+
* from `audit` (the durable sink): this is the request-scoped buffer that
|
|
204
|
+
* writes into it.
|
|
205
|
+
*/
|
|
206
|
+
auditLog?: AuditLog;
|
|
199
207
|
/** Session store for persisting user sessions keyed by pikkuUserId */
|
|
200
208
|
sessionStore?: SessionStore;
|
|
201
209
|
}
|
|
@@ -4,6 +4,12 @@ export type CoreSecret<T = unknown> = {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
secretId: string;
|
|
6
6
|
schema: T;
|
|
7
|
+
/**
|
|
8
|
+
* Optional rotation cadence for this secret, e.g. '1d', '30day', '1w'.
|
|
9
|
+
* Stored in the generated secrets metadata so consumers can tell when a
|
|
10
|
+
* secret was last updated and whether it is due for rotation.
|
|
11
|
+
*/
|
|
12
|
+
rotationPeriod?: string;
|
|
7
13
|
};
|
|
8
14
|
export type OAuth2CredentialConfig = {
|
|
9
15
|
tokenSecretId: string;
|
|
@@ -20,6 +26,7 @@ export type SecretDefinitionMeta = {
|
|
|
20
26
|
secretId: string;
|
|
21
27
|
schema?: Record<string, unknown> | string;
|
|
22
28
|
oauth2?: OAuth2CredentialConfig;
|
|
29
|
+
rotationPeriod?: string;
|
|
23
30
|
sourceFile?: string;
|
|
24
31
|
};
|
|
25
32
|
export type SecretDefinitionsMeta = Record<string, SecretDefinitionMeta>;
|
|
@@ -33,6 +33,7 @@ export function validateAndBuildSecretDefinitionsMeta(definitions, schemaLookup)
|
|
|
33
33
|
secretId: def.secretId,
|
|
34
34
|
schema: def.schema,
|
|
35
35
|
oauth2: def.oauth2,
|
|
36
|
+
rotationPeriod: def.rotationPeriod,
|
|
36
37
|
sourceFile: def.sourceFile,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
@@ -47,6 +48,7 @@ export function validateAndBuildSecretDefinitionsMeta(definitions, schemaLookup)
|
|
|
47
48
|
secretId: def.secretId,
|
|
48
49
|
schema: def.schema,
|
|
49
50
|
oauth2: def.oauth2,
|
|
51
|
+
rotationPeriod: def.rotationPeriod,
|
|
50
52
|
sourceFile: def.sourceFile,
|
|
51
53
|
};
|
|
52
54
|
}
|
|
@@ -27,7 +27,11 @@ export type WorkflowWireDoInline = <T>(stepName: string, fn: () => Promise<T> |
|
|
|
27
27
|
*/
|
|
28
28
|
export type WorkflowWireSleep = (stepName: string, duration: string) => Promise<void>;
|
|
29
29
|
/**
|
|
30
|
-
* Type signature for workflow.suspend() - used by inspector
|
|
30
|
+
* Type signature for workflow.suspend() - used by inspector.
|
|
31
|
+
* `reason` is both the human-readable message stored on the suspended run and
|
|
32
|
+
* the suspend point's stable identity (used raw as the durable step name), so a
|
|
33
|
+
* workflow can have multiple independent suspends — including dynamic reasons in
|
|
34
|
+
* loops, like dynamic `do()` step names.
|
|
31
35
|
*/
|
|
32
36
|
export type WorkflowWireSuspend = (reason: string) => Promise<void>;
|
|
33
37
|
/**
|
|
@@ -359,7 +359,7 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
|
|
|
359
359
|
parentRunId: runId,
|
|
360
360
|
parentStepId: stepId,
|
|
361
361
|
};
|
|
362
|
-
const shouldInline =
|
|
362
|
+
const shouldInline = !getSingletonServices()?.queueService;
|
|
363
363
|
const { runId: childRunId } = await workflowService.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
|
|
364
364
|
await workflowService.setStepChildRunId(stepId, childRunId);
|
|
365
365
|
if (shouldInline) {
|
|
@@ -335,6 +335,17 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
335
335
|
private rpcStep;
|
|
336
336
|
private inlineStep;
|
|
337
337
|
private sleepStep;
|
|
338
|
+
/**
|
|
339
|
+
* Derive the durable step name for a suspend point from its `reason`, so each
|
|
340
|
+
* distinct reason is its own step row — letting one workflow have multiple
|
|
341
|
+
* independent suspends (e.g. wait-for-build, then wait-for-approval), and
|
|
342
|
+
* supporting dynamic reasons in loops (`suspend(`Wait for ${i}`)`) exactly
|
|
343
|
+
* like dynamic `do()` step names. The reason is used raw (it's just a text
|
|
344
|
+
* step name), only namespaced so it can't collide with a `do`/`sleep` step of
|
|
345
|
+
* the same name. Like `do()` / `sleep()`, the reason is the step's stable
|
|
346
|
+
* identity: it MUST be derived deterministically so it's the same every time
|
|
347
|
+
* the workflow replays through that point.
|
|
348
|
+
*/
|
|
338
349
|
private getSuspendStepName;
|
|
339
350
|
private suspendStep;
|
|
340
351
|
createWorkflowWire(name: string, runId: string, rpcService: any, addonNamespace?: string | null): PikkuWorkflowWire;
|
|
@@ -484,16 +484,25 @@ export class PikkuWorkflowService {
|
|
|
484
484
|
* through to the inline execution path.
|
|
485
485
|
*/
|
|
486
486
|
async dispatchStep(runId, stepName, rpcName, data, stepOptions) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
//
|
|
491
|
-
//
|
|
487
|
+
// Step execution is decided purely by the function's `inline` flag (default
|
|
488
|
+
// true). Only a function explicitly marked `inline: false` dispatches via
|
|
489
|
+
// the queue. The run-level inline state is intentionally NOT consulted
|
|
490
|
+
// here: default steps run inline even inside a queue-dispatched run, so a
|
|
491
|
+
// normally-started workflow executes its steps in one orchestrator-worker
|
|
492
|
+
// pass instead of one queue round-trip per step.
|
|
492
493
|
const functionsMeta = pikkuState(null, 'function', 'meta');
|
|
493
494
|
const rpcFuncId = pikkuState(null, 'rpc', 'meta')[rpcName];
|
|
494
495
|
const rpcMeta = typeof rpcFuncId === 'string' ? functionsMeta[rpcFuncId] : undefined;
|
|
495
496
|
const forceQueue = rpcMeta?.inline === false;
|
|
496
|
-
if (!forceQueue
|
|
497
|
+
if (!forceQueue) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
// The function opted out of inline execution (`inline: false`) but no queue
|
|
501
|
+
// service is configured to dispatch it. Fall back to inline so the workflow
|
|
502
|
+
// still progresses, but warn loudly — silently swallowing this hides a real
|
|
503
|
+
// misconfiguration (the step won't get its own worker/retry isolation).
|
|
504
|
+
if (!getSingletonServices()?.queueService) {
|
|
505
|
+
getSingletonServices()?.logger.warn(`Workflow step '${stepName}' (function '${rpcName}') is marked 'inline: false' but no queue service is configured — running it inline instead of dispatching to a queue.`);
|
|
497
506
|
return false;
|
|
498
507
|
}
|
|
499
508
|
const retries = stepOptions?.retries ?? 0;
|
|
@@ -553,9 +562,7 @@ export class PikkuWorkflowService {
|
|
|
553
562
|
}
|
|
554
563
|
if (workflowMeta.source === 'graph' ||
|
|
555
564
|
workflowMeta.source === 'dynamic-workflow') {
|
|
556
|
-
const shouldInline = options?.inline ||
|
|
557
|
-
workflowMeta.inline ||
|
|
558
|
-
!getSingletonServices()?.queueService;
|
|
565
|
+
const shouldInline = options?.inline || !getSingletonServices()?.queueService;
|
|
559
566
|
return runWorkflowGraph(this, name, input, rpcService, shouldInline, options?.startNode, wire, workflowMeta);
|
|
560
567
|
}
|
|
561
568
|
// DSL workflow - check registration exists
|
|
@@ -567,9 +574,7 @@ export class PikkuWorkflowService {
|
|
|
567
574
|
if (!workflowMeta.graphHash) {
|
|
568
575
|
throw new Error(`Missing workflow graphHash for '${name}'`);
|
|
569
576
|
}
|
|
570
|
-
const shouldInline = options?.inline ||
|
|
571
|
-
workflowMeta.inline ||
|
|
572
|
-
!getSingletonServices()?.queueService;
|
|
577
|
+
const shouldInline = options?.inline || !getSingletonServices()?.queueService;
|
|
573
578
|
const runId = await this.createRun(name, input, shouldInline, workflowMeta.graphHash, wire, {
|
|
574
579
|
deterministic: workflowMeta.deterministic,
|
|
575
580
|
plannedSteps: workflowMeta.plannedSteps,
|
|
@@ -808,7 +813,7 @@ export class PikkuWorkflowService {
|
|
|
808
813
|
parentRunId: runId,
|
|
809
814
|
parentStepId: stepState.stepId,
|
|
810
815
|
};
|
|
811
|
-
const shouldInline =
|
|
816
|
+
const shouldInline = !getSingletonServices()?.queueService;
|
|
812
817
|
const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
|
|
813
818
|
await this.setStepChildRunId(stepState.stepId, childRunId);
|
|
814
819
|
if (shouldInline) {
|
|
@@ -1115,26 +1120,34 @@ export class PikkuWorkflowService {
|
|
|
1115
1120
|
await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
|
|
1116
1121
|
await this.setStepResult(stepState.stepId, null);
|
|
1117
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Derive the durable step name for a suspend point from its `reason`, so each
|
|
1125
|
+
* distinct reason is its own step row — letting one workflow have multiple
|
|
1126
|
+
* independent suspends (e.g. wait-for-build, then wait-for-approval), and
|
|
1127
|
+
* supporting dynamic reasons in loops (`suspend(`Wait for ${i}`)`) exactly
|
|
1128
|
+
* like dynamic `do()` step names. The reason is used raw (it's just a text
|
|
1129
|
+
* step name), only namespaced so it can't collide with a `do`/`sleep` step of
|
|
1130
|
+
* the same name. Like `do()` / `sleep()`, the reason is the step's stable
|
|
1131
|
+
* identity: it MUST be derived deterministically so it's the same every time
|
|
1132
|
+
* the workflow replays through that point.
|
|
1133
|
+
*/
|
|
1118
1134
|
getSuspendStepName(reason) {
|
|
1119
|
-
|
|
1120
|
-
return '__workflow_suspend';
|
|
1121
|
-
}
|
|
1122
|
-
return '__workflow_suspend';
|
|
1135
|
+
return `__workflow_suspend:${reason}`;
|
|
1123
1136
|
}
|
|
1124
1137
|
async suspendStep(runId, reason) {
|
|
1125
|
-
const
|
|
1126
|
-
await this.withStepLock(runId,
|
|
1138
|
+
const suspendStepName = this.getSuspendStepName(reason);
|
|
1139
|
+
await this.withStepLock(runId, suspendStepName, async () => {
|
|
1127
1140
|
let stepState;
|
|
1128
1141
|
try {
|
|
1129
|
-
stepState = await this.getStepState(runId,
|
|
1142
|
+
stepState = await this.getStepState(runId, suspendStepName);
|
|
1130
1143
|
}
|
|
1131
1144
|
catch {
|
|
1132
|
-
stepState = await this.insertStepState(runId,
|
|
1145
|
+
stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
|
|
1133
1146
|
reason,
|
|
1134
1147
|
});
|
|
1135
1148
|
}
|
|
1136
1149
|
if (!stepState.stepId) {
|
|
1137
|
-
stepState = await this.insertStepState(runId,
|
|
1150
|
+
stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
|
|
1138
1151
|
reason,
|
|
1139
1152
|
});
|
|
1140
1153
|
}
|
|
@@ -1175,7 +1188,8 @@ export class PikkuWorkflowService {
|
|
|
1175
1188
|
await this.sleepStep(runId, stepName, getDurationInMilliseconds(duration));
|
|
1176
1189
|
},
|
|
1177
1190
|
suspend: async (reason) => {
|
|
1178
|
-
|
|
1191
|
+
this.verifyStepName(reason);
|
|
1192
|
+
await this.suspendStep(runId, reason);
|
|
1179
1193
|
},
|
|
1180
1194
|
};
|
|
1181
1195
|
return workflowWire;
|
|
@@ -228,7 +228,6 @@ export type WorkflowsMeta = Record<string, CommonWireMeta & {
|
|
|
228
228
|
steps: WorkflowStepMeta[];
|
|
229
229
|
context?: WorkflowContext;
|
|
230
230
|
dsl?: boolean;
|
|
231
|
-
inline?: boolean;
|
|
232
231
|
expose?: boolean;
|
|
233
232
|
}>;
|
|
234
233
|
/**
|
|
@@ -247,8 +246,6 @@ export interface WorkflowRuntimeMeta {
|
|
|
247
246
|
description?: string;
|
|
248
247
|
/** Tags for organization */
|
|
249
248
|
tags?: string[];
|
|
250
|
-
/** If true, workflow always executes inline without queues */
|
|
251
|
-
inline?: boolean;
|
|
252
249
|
/** Serialized nodes */
|
|
253
250
|
nodes?: Record<string, any>;
|
|
254
251
|
/** Entry node IDs for graph workflows (computed at build time) */
|
package/package.json
CHANGED
|
@@ -562,6 +562,24 @@ export class LocalMetaService implements MetaService {
|
|
|
562
562
|
readEmailFile(`templates/${templateName}.text.txt`),
|
|
563
563
|
])
|
|
564
564
|
|
|
565
|
+
const missing = [
|
|
566
|
+
...(themeRaw ? [] : ['theme']),
|
|
567
|
+
...(localeRaw ? [] : ['locale']),
|
|
568
|
+
...(templateHtml ? [] : ['html']),
|
|
569
|
+
...(templateSubject ? [] : ['subject']),
|
|
570
|
+
...(templateText ? [] : ['text']),
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
if (missing.length > 0) {
|
|
574
|
+
// Diagnostic for the intermittent "Missing source files" report: logs which
|
|
575
|
+
// process and which resolved baseDir saw the gap. Different pids across
|
|
576
|
+
// failures => multiple runtime workers with divergent cached `src`; same pid
|
|
577
|
+
// + transient absence => files being rewritten under us (e.g. regeneration).
|
|
578
|
+
console.warn(
|
|
579
|
+
`[email-assets] pid=${process.pid} template=${templateName} locale=${locale} baseDir=${baseDir} missing=${missing.join(',')}`
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
|
|
565
583
|
return {
|
|
566
584
|
theme: themeRaw ? (JSON.parse(themeRaw) as Record<string, unknown>) : {},
|
|
567
585
|
strings: localeRaw
|
|
@@ -574,13 +592,7 @@ export class LocalMetaService implements MetaService {
|
|
|
574
592
|
html: templateHtml ?? '',
|
|
575
593
|
subject: templateSubject ?? '',
|
|
576
594
|
text: templateText ?? '',
|
|
577
|
-
missing
|
|
578
|
-
...(themeRaw ? [] : ['theme']),
|
|
579
|
-
...(localeRaw ? [] : ['locale']),
|
|
580
|
-
...(templateHtml ? [] : ['html']),
|
|
581
|
-
...(templateSubject ? [] : ['subject']),
|
|
582
|
-
...(templateText ? [] : ['text']),
|
|
583
|
-
],
|
|
595
|
+
missing,
|
|
584
596
|
}
|
|
585
597
|
}
|
|
586
598
|
|
package/src/types/core.types.ts
CHANGED
|
@@ -40,6 +40,7 @@ import type { MetaService } from '../services/meta-service.js'
|
|
|
40
40
|
import type { SessionStore } from '../services/session-store.js'
|
|
41
41
|
import type {
|
|
42
42
|
AuditDurability,
|
|
43
|
+
AuditLog,
|
|
43
44
|
AuditService,
|
|
44
45
|
} from '../services/audit-service.js'
|
|
45
46
|
|
|
@@ -255,6 +256,14 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
255
256
|
metaService?: MetaService
|
|
256
257
|
/** Audit service for durable or staged audit event capture */
|
|
257
258
|
audit?: AuditService
|
|
259
|
+
/**
|
|
260
|
+
* Per-invocation audit log. Typically created in `createWireServices` via
|
|
261
|
+
* `createInvocationAudit(audit, wire)` and returned as a wire service so the
|
|
262
|
+
* runner flushes its buffer (via `close()`) when the invocation ends. Distinct
|
|
263
|
+
* from `audit` (the durable sink): this is the request-scoped buffer that
|
|
264
|
+
* writes into it.
|
|
265
|
+
*/
|
|
266
|
+
auditLog?: AuditLog
|
|
258
267
|
/** Session store for persisting user sessions keyed by pikkuUserId */
|
|
259
268
|
sessionStore?: SessionStore
|
|
260
269
|
}
|
|
@@ -4,6 +4,12 @@ export type CoreSecret<T = unknown> = {
|
|
|
4
4
|
description?: string
|
|
5
5
|
secretId: string
|
|
6
6
|
schema: T
|
|
7
|
+
/**
|
|
8
|
+
* Optional rotation cadence for this secret, e.g. '1d', '30day', '1w'.
|
|
9
|
+
* Stored in the generated secrets metadata so consumers can tell when a
|
|
10
|
+
* secret was last updated and whether it is due for rotation.
|
|
11
|
+
*/
|
|
12
|
+
rotationPeriod?: string
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export type OAuth2CredentialConfig = {
|
|
@@ -22,6 +28,7 @@ export type SecretDefinitionMeta = {
|
|
|
22
28
|
secretId: string
|
|
23
29
|
schema?: Record<string, unknown> | string
|
|
24
30
|
oauth2?: OAuth2CredentialConfig
|
|
31
|
+
rotationPeriod?: string
|
|
25
32
|
sourceFile?: string
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -57,6 +57,7 @@ export function validateAndBuildSecretDefinitionsMeta(
|
|
|
57
57
|
secretId: def.secretId,
|
|
58
58
|
schema: def.schema,
|
|
59
59
|
oauth2: def.oauth2,
|
|
60
|
+
rotationPeriod: def.rotationPeriod,
|
|
60
61
|
sourceFile: def.sourceFile,
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -73,6 +74,7 @@ export function validateAndBuildSecretDefinitionsMeta(
|
|
|
73
74
|
secretId: def.secretId,
|
|
74
75
|
schema: def.schema,
|
|
75
76
|
oauth2: def.oauth2,
|
|
77
|
+
rotationPeriod: def.rotationPeriod,
|
|
76
78
|
sourceFile: def.sourceFile,
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -46,7 +46,11 @@ export type WorkflowWireSleep = (
|
|
|
46
46
|
) => Promise<void>
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Type signature for workflow.suspend() - used by inspector
|
|
49
|
+
* Type signature for workflow.suspend() - used by inspector.
|
|
50
|
+
* `reason` is both the human-readable message stored on the suspended run and
|
|
51
|
+
* the suspend point's stable identity (used raw as the durable step name), so a
|
|
52
|
+
* workflow can have multiple independent suspends — including dynamic reasons in
|
|
53
|
+
* loops, like dynamic `do()` step names.
|
|
50
54
|
*/
|
|
51
55
|
export type WorkflowWireSuspend = (reason: string) => Promise<void>
|
|
52
56
|
|
|
@@ -485,7 +485,7 @@ describe('graph-runner bugs', () => {
|
|
|
485
485
|
delete metaState['testInlineRpcMissing']
|
|
486
486
|
})
|
|
487
487
|
|
|
488
|
-
test('graph workflow with inline
|
|
488
|
+
test('graph workflow started with the inline flag should run inline', async () => {
|
|
489
489
|
const ws = new InMemoryWorkflowService()
|
|
490
490
|
let executed = false
|
|
491
491
|
|
|
@@ -511,7 +511,6 @@ describe('graph-runner bugs', () => {
|
|
|
511
511
|
name: 'testInlineMetaGraph',
|
|
512
512
|
pikkuFuncId: 'testInlineMetaGraph',
|
|
513
513
|
source: 'graph',
|
|
514
|
-
inline: true,
|
|
515
514
|
entryNodeIds: ['a'],
|
|
516
515
|
graphHash: 'inline-meta-graph-hash',
|
|
517
516
|
nodes: {
|
|
@@ -497,8 +497,7 @@ export async function executeGraphStep(
|
|
|
497
497
|
parentRunId: runId,
|
|
498
498
|
parentStepId: stepId,
|
|
499
499
|
}
|
|
500
|
-
const shouldInline =
|
|
501
|
-
subWorkflowMeta.inline || !getSingletonServices()?.queueService
|
|
500
|
+
const shouldInline = !getSingletonServices()?.queueService
|
|
502
501
|
const { runId: childRunId } = await workflowService.startWorkflow(
|
|
503
502
|
rpcName,
|
|
504
503
|
data,
|