@pikku/core 0.12.74 → 0.12.78

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.
Files changed (150) hide show
  1. package/CHANGELOG.md +397 -0
  2. package/dist/column-form.d.ts +32 -0
  3. package/dist/column-form.js +42 -0
  4. package/dist/crypto-utils.d.ts +15 -4
  5. package/dist/crypto-utils.js +18 -2
  6. package/dist/data-classification.d.ts +44 -0
  7. package/dist/function/functions.types.d.ts +23 -10
  8. package/dist/function/index.d.ts +1 -1
  9. package/dist/index.d.ts +6 -3
  10. package/dist/index.js +3 -1
  11. package/dist/middleware/auth-bearer.js +2 -1
  12. package/dist/middleware/remote-auth.js +1 -1
  13. package/dist/remote.js +1 -1
  14. package/dist/secret-value.d.ts +56 -0
  15. package/dist/secret-value.js +46 -0
  16. package/dist/services/audit-service.d.ts +74 -4
  17. package/dist/services/audit-service.js +7 -5
  18. package/dist/services/credential-wire-service.d.ts +5 -0
  19. package/dist/services/credential-wire-service.js +9 -1
  20. package/dist/services/email-service.d.ts +2 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +1 -0
  22. package/dist/services/in-memory-workflow-service.js +20 -0
  23. package/dist/services/index.d.ts +3 -3
  24. package/dist/services/index.js +1 -1
  25. package/dist/services/local-content-request-handler.d.ts +29 -0
  26. package/dist/services/local-content-request-handler.js +176 -0
  27. package/dist/services/local-secrets.d.ts +4 -3
  28. package/dist/services/local-secrets.js +7 -3
  29. package/dist/services/logger.d.ts +22 -5
  30. package/dist/services/queue-webhook-service.js +1 -1
  31. package/dist/services/scoped-secret-service.d.ts +4 -3
  32. package/dist/services/secret-service.d.ts +8 -3
  33. package/dist/services/typed-secret-service.d.ts +5 -4
  34. package/dist/services/webhook-service.d.ts +2 -1
  35. package/dist/testing/service-tests.js +6 -6
  36. package/dist/types/core.types.d.ts +25 -4
  37. package/dist/wirings/ai-agent/ai-agent-agui.js +13 -1
  38. package/dist/wirings/ai-agent/ai-agent-prepare.js +7 -1
  39. package/dist/wirings/ai-agent/ai-agent-runner.js +14 -2
  40. package/dist/wirings/ai-agent/ai-agent-stream.js +27 -1
  41. package/dist/wirings/ai-agent/ai-agent.types.d.ts +40 -0
  42. package/dist/wirings/ai-agent/index.d.ts +1 -1
  43. package/dist/wirings/ai-agent/index.js +1 -1
  44. package/dist/wirings/ai-agent/voice-input.d.ts +20 -0
  45. package/dist/wirings/ai-agent/voice-input.js +44 -9
  46. package/dist/wirings/ai-agent/voice-output.d.ts +15 -0
  47. package/dist/wirings/ai-agent/voice-output.js +10 -1
  48. package/dist/wirings/cli/channel/cli-raw-client-runner.d.ts +21 -3
  49. package/dist/wirings/cli/channel/cli-raw-client-runner.js +13 -5
  50. package/dist/wirings/cli/channel/index.d.ts +1 -0
  51. package/dist/wirings/persona/define-personas.d.ts +4 -0
  52. package/dist/wirings/persona/define-personas.js +4 -0
  53. package/dist/wirings/persona/persona.types.d.ts +11 -0
  54. package/dist/wirings/queue/queue-identity.js +2 -1
  55. package/dist/wirings/queue/queue.types.d.ts +2 -1
  56. package/dist/wirings/queue/signed-queue-service.d.ts +2 -1
  57. package/dist/wirings/rpc/remote-addon-auth.d.ts +2 -1
  58. package/dist/wirings/rpc/remote-addon-auth.js +6 -2
  59. package/dist/wirings/virtual-user/index.d.ts +3 -0
  60. package/dist/wirings/virtual-user/index.js +2 -0
  61. package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +54 -0
  62. package/dist/wirings/virtual-user/prepare-virtual-user-run.js +49 -0
  63. package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +90 -0
  64. package/dist/wirings/virtual-user/virtual-user-run-store.js +1 -0
  65. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +14 -10
  66. package/dist/wirings/workflow/pikku-scenario-service.js +1 -2
  67. package/dist/wirings/workflow/pikku-workflow-service.d.ts +35 -0
  68. package/dist/wirings/workflow/pikku-workflow-service.js +51 -0
  69. package/dist/wirings/workflow/scenario-prose.js +1 -1
  70. package/dist/wirings/workflow/scenario-step.types.d.ts +13 -7
  71. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  72. package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md +48 -0
  73. package/knowledge/decisions/internals/core-column-form-is-an-axis-of-its-own.md +84 -0
  74. package/knowledge/decisions/internals/core-data-classification-brand-is-an-optional-property.md +9 -2
  75. package/knowledge/decisions/internals/index.md +4 -0
  76. package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +53 -0
  77. package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +48 -0
  78. package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +43 -0
  79. package/package.json +3 -2
  80. package/src/column-form.test.ts +97 -0
  81. package/src/column-form.ts +58 -0
  82. package/src/crypto-utils.ts +25 -6
  83. package/src/data-classification.ts +44 -0
  84. package/src/function/functions.types.ts +47 -10
  85. package/src/function/index.ts +1 -0
  86. package/src/index.ts +24 -2
  87. package/src/middleware/auth-bearer.test.ts +3 -2
  88. package/src/middleware/auth-bearer.ts +2 -1
  89. package/src/middleware/remote-auth.test.ts +2 -1
  90. package/src/middleware/remote-auth.ts +1 -1
  91. package/src/remote.test.ts +2 -1
  92. package/src/remote.ts +1 -1
  93. package/src/secret-value.test.ts +204 -0
  94. package/src/secret-value.ts +111 -0
  95. package/src/services/audit-service.ts +87 -9
  96. package/src/services/credential-wire-service.ts +9 -1
  97. package/src/services/email-service.ts +3 -1
  98. package/src/services/in-memory-workflow-service.ts +29 -0
  99. package/src/services/index.ts +3 -3
  100. package/src/services/local-content-request-handler.test.ts +202 -0
  101. package/src/services/local-content-request-handler.ts +267 -0
  102. package/src/services/local-secrets.test.ts +20 -5
  103. package/src/services/local-secrets.ts +15 -7
  104. package/src/services/logger.ts +27 -7
  105. package/src/services/queue-webhook-service.test.ts +2 -1
  106. package/src/services/queue-webhook-service.ts +1 -1
  107. package/src/services/scoped-secret-service.ts +4 -3
  108. package/src/services/secret-service.ts +8 -3
  109. package/src/services/typed-secret-service.ts +11 -7
  110. package/src/services/webhook-service.ts +4 -1
  111. package/src/testing/service-tests.ts +6 -6
  112. package/src/types/core.types.ts +25 -4
  113. package/src/wirings/ai-agent/ai-agent-agui.test.ts +16 -0
  114. package/src/wirings/ai-agent/ai-agent-agui.ts +14 -1
  115. package/src/wirings/ai-agent/ai-agent-prepare.ts +7 -1
  116. package/src/wirings/ai-agent/ai-agent-runner.ts +18 -2
  117. package/src/wirings/ai-agent/ai-agent-stream.ts +32 -1
  118. package/src/wirings/ai-agent/ai-agent.types.ts +45 -1
  119. package/src/wirings/ai-agent/index.ts +2 -0
  120. package/src/wirings/ai-agent/voice-input.test.ts +65 -0
  121. package/src/wirings/ai-agent/voice-input.ts +48 -9
  122. package/src/wirings/ai-agent/voice-output.test.ts +91 -1
  123. package/src/wirings/ai-agent/voice-output.ts +28 -1
  124. package/src/wirings/cli/channel/cli-raw-client-runner.ts +39 -9
  125. package/src/wirings/cli/channel/index.ts +4 -0
  126. package/src/wirings/persona/define-personas.ts +4 -0
  127. package/src/wirings/persona/persona.types.ts +11 -0
  128. package/src/wirings/queue/queue-identity.test.ts +2 -1
  129. package/src/wirings/queue/queue-identity.ts +4 -1
  130. package/src/wirings/queue/queue.types.ts +6 -1
  131. package/src/wirings/queue/signed-queue-service.ts +2 -1
  132. package/src/wirings/rpc/remote-addon-auth.ts +8 -3
  133. package/src/wirings/rpc/rpc-runner.test.ts +6 -4
  134. package/src/wirings/virtual-user/index.ts +12 -0
  135. package/src/wirings/virtual-user/prepare-virtual-user-run.test.ts +115 -0
  136. package/src/wirings/virtual-user/prepare-virtual-user-run.ts +95 -0
  137. package/src/wirings/virtual-user/virtual-user-run-store.ts +98 -0
  138. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +14 -16
  139. package/src/wirings/workflow/pikku-scenario-service.ts +1 -10
  140. package/src/wirings/workflow/pikku-workflow-service.ts +67 -0
  141. package/src/wirings/workflow/scenario-prose.test.ts +5 -7
  142. package/src/wirings/workflow/scenario-prose.ts +1 -1
  143. package/src/wirings/workflow/scenario-service.test.ts +0 -1
  144. package/src/wirings/workflow/scenario-step.test.ts +4 -5
  145. package/src/wirings/workflow/scenario-step.types.ts +13 -7
  146. package/src/wirings/workflow/scenario-surface.test.ts +6 -5
  147. package/src/wirings/workflow/workflow-stalled-recovery.test.ts +106 -0
  148. package/src/wirings/workflow/workflow.types.ts +7 -0
  149. package/tsconfig.tsbuildinfo +1 -1
  150. package/tsconfig.type-tests.json +12 -0
@@ -4,13 +4,14 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
4
4
  import type { PikkuError } from '../errors/error-handler.js';
5
5
  import type { CoreNodeConfig } from '../wirings/node/node.types.js';
6
6
  import type { ScenarioSurface } from '../wirings/workflow/scenario-step.types.js';
7
+ import type { Safe } from '../secret-value.js';
7
8
  /**
8
9
  * @deprecated Use StandardSchemaV1 from @standard-schema/spec instead.
9
10
  * This alias exists only for backward compatibility with generated code.
10
11
  */
11
12
  export type ZodLike<T = any> = StandardSchemaV1<T, T>;
12
- export type CorePikkuFunction<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, true, Session, PikkuRPC, null, string> = PikkuWire<In, Out, true, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Out> : Promise<Out> | Promise<void>;
13
- export type CorePikkuFunctionSessionless<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, false, Session, PikkuRPC, null, string> = PikkuWire<In, Out, false, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Out> : Promise<Out> | Promise<void>;
13
+ export type CorePikkuFunction<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, true, Session, PikkuRPC, null, string> = PikkuWire<In, Out, true, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Safe<Out>> : Promise<Safe<Out>> | Promise<void>;
14
+ export type CorePikkuFunctionSessionless<In, Out, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession, Wire extends PikkuWire<In, Out, false, Session, PikkuRPC, null, string> = PikkuWire<In, Out, false, Session>> = (services: Services, data: In, wire: Wire) => Wire['channel'] extends null ? Promise<Safe<Out>> : Promise<Safe<Out>> | Promise<void>;
14
15
  export type CorePikkuPermission<In = any, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Wire extends PikkuWire<In, never, false, any, PikkuRPC, never, never> = PikkuWire<In, never, false, any, PikkuRPC, never, never>> = (services: Services, data: In, wire: Wire) => Promise<boolean>;
15
16
  export type CorePikkuPermissionConfig<In = any, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Wire extends PikkuWire<In, never, false, any, PikkuRPC, never, never> = PikkuWire<In, never, false, any, PikkuRPC, never, never>> = {
16
17
  func: CorePikkuPermission<In, Services, Wire>;
@@ -49,16 +50,18 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
49
50
  tags?: string[];
50
51
  expose?: boolean;
51
52
  /**
52
- * The function performs its own authorization in its body — verifying a
53
+ * The permission check for this function lives in its body — verifying a
53
54
  * signed token, checking a webhook signature, matching an invite code — so
54
- * it is not open despite carrying no session, scope or permission.
55
+ * it is not open despite declaring no session, scope or permission.
55
56
  *
56
- * Purely declarative: nothing at runtime reads it, and it grants nothing. It
57
- * exists so the claim is written down where an audit can find it, and so
58
- * codegen stops warning about a gate it has no way to see. Asserting it
59
- * falsely disables the one check that would have caught the mistake.
57
+ * A last resort. Prefer `permissions`, which are declared, inspectable, and
58
+ * reusable; reach for this only when the check cannot be expressed as one.
59
+ *
60
+ * Purely declarative it grants nothing, and asserting it falsely disables
61
+ * the audit that would have caught the mistake. Requires
62
+ * `allow.permissionsInBody` in `pikku.config.json`.
60
63
  */
61
- selfAuthenticated?: boolean;
64
+ permissionsInBody?: boolean;
62
65
  remote?: boolean;
63
66
  mcp?: boolean;
64
67
  readonly?: boolean;
@@ -103,7 +106,8 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
103
106
  * Scopes the session must hold; all are required (AND) and checked before
104
107
  * `permissions`, which OR together — a scope can only narrow access.
105
108
  * Narrowed to the generated `ScopeId` union, so an undeclared scope is a
106
- * compile error.
109
+ * compile error. Requires a session — see
110
+ * {@link CorePikkuSessionlessFunctionConfig}.
107
111
  */
108
112
  scopes?: Scope[];
109
113
  permissions?: CorePermissionGroup<PikkuPermission>;
@@ -113,3 +117,12 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
113
117
  node?: CoreNodeConfig;
114
118
  errors?: Array<typeof PikkuError>;
115
119
  };
120
+ /**
121
+ * {@link CorePikkuFunctionConfig} for a function that runs without a session.
122
+ *
123
+ * Identical, minus `scopes`. Scopes are AND-ed and `verifyScopes` fails closed,
124
+ * so an anonymous caller holds none and satisfies none — a sessionless function
125
+ * with scopes rejects every caller it exists to serve. Gate it with
126
+ * `permissions`, which receive the optional session and may pass anonymous.
127
+ */
128
+ export type CorePikkuSessionlessFunctionConfig<PikkuFunction extends CorePikkuFunctionSessionless<any, any, any, any, any>, PikkuPermission extends CorePikkuPermission<any, any, any> = CorePikkuPermission<any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>, InputSchema extends StandardSchemaV1 | undefined = undefined, OutputSchema extends StandardSchemaV1 | undefined = undefined> = Omit<CorePikkuFunctionConfig<PikkuFunction, PikkuPermission, PikkuMiddleware, InputSchema, OutputSchema>, 'scopes'>;
@@ -1,4 +1,4 @@
1
1
  export { addFunction, getAllFunctionNames } from './function-runner.js';
2
2
  export { pikkuAuth, pikkuPermission, pikkuPermissionFactory, pikkuApprovalDescription, } from './functions.types.js';
3
- export type { CorePikkuFunction, CorePikkuFunctionSessionless, CorePikkuFunctionConfig, CorePikkuAuth, CorePikkuAuthConfig, CorePikkuPermission, } from './functions.types.js';
3
+ export type { CorePikkuFunction, CorePikkuFunctionSessionless, CorePikkuFunctionConfig, CorePikkuSessionlessFunctionConfig, CorePikkuAuth, CorePikkuAuthConfig, CorePikkuPermission, } from './functions.types.js';
4
4
  export type { ListInput, ListOutput, Filter, LeafFilter, LeafValue, } from './list.types.js';
package/dist/index.d.ts CHANGED
@@ -36,8 +36,8 @@ export type { GatewayService } from './services/gateway-service.js';
36
36
  export type { TriggerService } from './services/trigger-service.js';
37
37
  export type { SchemaService } from './services/schema-service.js';
38
38
  export type { SessionService } from './services/user-session-service.js';
39
- export { NoopAuditService, createInvocationAudit, resolveAuditActorFromWire, resolveAuditConfig, } from './services/audit-service.js';
40
- export type { AuditActor, AuditConfig, AuditDurability, AuditEvent, AuditEventBatch, AuditLog, AuditLogWriteInput, AuditOutcome, AuditService, AuditSource, ResolvedAuditConfig, } from './services/audit-service.js';
39
+ export { NoopAuditService, createInvocationAudit, resolveAuditConfig, resolveAuditUserIdentityFromWire, } from './services/audit-service.js';
40
+ export type { AuditConfig, AuditDurability, AuditEvent, AuditEventBatch, AuditFacets, AuditLog, AuditLogWriteInput, AuditOutcome, AuditQuery, AuditQueryResult, AuditService, AuditSource, AuditUserIdentity, ResolvedAuditConfig, } from './services/audit-service.js';
41
41
  export type { AIAgentRunnerService, AIEmbedManyParams, AIEmbedManyResult, AIEmbedParams, AIEmbedResult, AIGenerateImageParams, AIGenerateImagePrompt, AIGenerateImageResult, AIGenerateSpeechParams, AIGenerateSpeechResult, AIProviderOptions, AIRerankParams, AIRerankResult, AITranscriptionParams, AITranscriptionResult, } from './services/ai-agent-runner-service.js';
42
42
  export type { AIEmbeddingService } from './services/ai-embedding-service.js';
43
43
  export type { AIRunStateService } from './services/ai-run-state-service.js';
@@ -59,4 +59,7 @@ export { getSingletonServices, getCreateWireServices, setSingletonServices, } fr
59
59
  export { clearPikkuRuntimeState } from './test-utils.js';
60
60
  export { type ScheduledTaskInfo, type ScheduledTaskSummary, } from './services/scheduler-service.js';
61
61
  export { SchedulerService } from './services/scheduler-service.js';
62
- export type { Private, Pii, Secret, Classification, AnonymizeStrategy, ColumnClassification, ClassificationManifest, } from './data-classification.js';
62
+ export type { Private, Pii, Secret, Classification, AnonymizeStrategy, ColumnClassification, ClassificationManifest, ColumnForm, WrappedValue, SealedValue, HashedValue, } from './data-classification.js';
63
+ export { hashToken, unsafeAsWrapped, unsafeAsSealed, unsafeAsHashed, } from './column-form.js';
64
+ export type { SecretValue, Safe } from './secret-value.js';
65
+ export { createSecretValue, isSecretValue, SecretCoercionError, REDACTED, } from './secret-value.js';
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export { AIProviderAuthError, AIProviderNotConfiguredError, BadGatewayError, Bad
15
15
  export { PikkuError, isExpectedError } from './errors/error-handler.js';
16
16
  export { SecretAccessDeniedError, withoutSecrets, } from './services/secretless.js';
17
17
  export { SecretHostNotAllowedError, assertSecretAllowedForHost, } from './services/secret-host-binding.js';
18
- export { NoopAuditService, createInvocationAudit, resolveAuditActorFromWire, resolveAuditConfig, } from './services/audit-service.js';
18
+ export { NoopAuditService, createInvocationAudit, resolveAuditConfig, resolveAuditUserIdentityFromWire, } from './services/audit-service.js';
19
19
  export { createGraph } from './wirings/workflow/graph/graph-node.js';
20
20
  export { wireAddon } from './wirings/rpc/wire-addon.js';
21
21
  export { wireRemoteAddon } from './wirings/rpc/wire-remote-addon.js';
@@ -26,3 +26,5 @@ export { isSerializable, stopSingletonServices, pikkuServerLifecycle, } from './
26
26
  export { getSingletonServices, getCreateWireServices, setSingletonServices, } from './pikku-state.js';
27
27
  export { clearPikkuRuntimeState } from './test-utils.js';
28
28
  export { SchedulerService } from './services/scheduler-service.js';
29
+ export { hashToken, unsafeAsWrapped, unsafeAsSealed, unsafeAsHashed, } from './column-form.js';
30
+ export { createSecretValue, isSecretValue, SecretCoercionError, REDACTED, } from './secret-value.js';
@@ -33,9 +33,10 @@ export const authBearer = pikkuMiddlewareFactory(({ token } = {}) => pikkuMiddle
33
33
  }
34
34
  else {
35
35
  // An unset secret means the feature is off — never a request error.
36
- expected = await secrets
36
+ const stored = await secrets
37
37
  ?.getSecret(token.secretId)
38
38
  .catch(() => undefined);
39
+ expected = stored?.reveal();
39
40
  }
40
41
  if (expected && constantTimeEqual(bearerToken, expected)) {
41
42
  userSession = token.userSession;
@@ -7,7 +7,7 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(async ({ secrets, jwt }
7
7
  }
8
8
  let secret;
9
9
  try {
10
- secret = await secrets.getSecret('PIKKU_REMOTE_SECRET');
10
+ secret = (await secrets.getSecret('PIKKU_REMOTE_SECRET')).reveal();
11
11
  }
12
12
  catch {
13
13
  if (http.request.path().startsWith('/remote/rpc/')) {
package/dist/remote.js CHANGED
@@ -11,7 +11,7 @@ export async function buildRemoteHeaders(jwt, secrets, funcName, session, traceI
11
11
  };
12
12
  let secret;
13
13
  try {
14
- secret = await secrets?.getSecret('PIKKU_REMOTE_SECRET');
14
+ secret = (await secrets?.getSecret('PIKKU_REMOTE_SECRET'))?.reveal();
15
15
  }
16
16
  catch { }
17
17
  if (secret && jwt) {
@@ -0,0 +1,56 @@
1
+ import type { Secret } from './data-classification.js';
2
+ declare const secretValueBrand: unique symbol;
3
+ export declare const REDACTED = "[secret]";
4
+ /** Runtime marker, resilient to duplicate copies of core. */
5
+ declare const SECRET_VALUE: unique symbol;
6
+ declare const NODE_INSPECT: unique symbol;
7
+ export declare class SecretCoercionError extends Error {
8
+ constructor();
9
+ }
10
+ /**
11
+ * A vault secret. Nominal, so it is not assignable to `string` and every
12
+ * concretely-typed sink rejects it; `.reveal()` is the one way out, and every
13
+ * call is a deliberate, greppable disclosure.
14
+ *
15
+ * The revealed value carries the erasable `Secret<T>` classification brand, so
16
+ * the inspector can still follow it one hop past the call.
17
+ *
18
+ * Structured serialization redacts to `[secret]` — an audit or log write must
19
+ * stay honest about the field without crashing the request. String coercion
20
+ * throws, because a template literal or concatenation is always a leak.
21
+ */
22
+ export declare class SecretValue<T = string> {
23
+ #private;
24
+ readonly [secretValueBrand]: true;
25
+ readonly [SECRET_VALUE] = true;
26
+ constructor(value: T);
27
+ reveal(): Secret<T>;
28
+ toJSON(): string;
29
+ [NODE_INSPECT](): string;
30
+ toString(): never;
31
+ [Symbol.toPrimitive](): never;
32
+ }
33
+ export declare const createSecretValue: <T>(value: T) => SecretValue<T>;
34
+ export declare const isSecretValue: (value: unknown) => value is SecretValue<unknown>;
35
+ type IsAny<T> = 0 extends 1 & T ? true : false;
36
+ type Passthrough = Function | Date | RegExp | Error | ArrayBuffer | ArrayBufferView;
37
+ /**
38
+ * Rejects a `SecretValue` anywhere in `T`, however deeply nested, by collapsing
39
+ * it to `never`.
40
+ *
41
+ * For sinks whose parameters are `any`, `unknown` or a free generic — loggers,
42
+ * queue payloads, channel messages — where nominality alone cannot help.
43
+ * `any` is passed through untouched: it cannot be guarded, and collapsing it
44
+ * would reject every legitimate call.
45
+ *
46
+ * `Promise`, `Map` and `Set` are recursed into rather than passed through. A
47
+ * mapped type cannot reach what they hold — mapping their keys yields their
48
+ * methods, not their contents — so treating them as opaque let a secret ride
49
+ * through inside one. `Promise` is the case that bites: `getSecret()` returns
50
+ * `Promise<SecretValue<T>>`, so a forgotten `await` would otherwise log a
51
+ * secret.
52
+ */
53
+ export type Safe<T> = IsAny<T> extends true ? T : [Extract<T, SecretValue<any>>] extends [never] ? T extends Promise<infer V> ? Promise<Safe<V>> : T extends Map<infer K, infer V> ? Map<Safe<K>, Safe<V>> : T extends Set<infer V> ? Set<Safe<V>> : T extends Passthrough ? T : T extends object ? {
54
+ [K in keyof T]: Safe<T[K]>;
55
+ } : T : never;
56
+ export {};
@@ -0,0 +1,46 @@
1
+ export const REDACTED = '[secret]';
2
+ /** Runtime marker, resilient to duplicate copies of core. */
3
+ const SECRET_VALUE = Symbol.for('pikku.secretValue');
4
+ const NODE_INSPECT = Symbol.for('nodejs.util.inspect.custom');
5
+ export class SecretCoercionError extends Error {
6
+ constructor() {
7
+ super(`A secret was coerced to a string, which would write it out in the clear. Unwrap it deliberately with .reveal() at the point it reaches the wire.`);
8
+ this.name = 'SecretCoercionError';
9
+ }
10
+ }
11
+ /**
12
+ * A vault secret. Nominal, so it is not assignable to `string` and every
13
+ * concretely-typed sink rejects it; `.reveal()` is the one way out, and every
14
+ * call is a deliberate, greppable disclosure.
15
+ *
16
+ * The revealed value carries the erasable `Secret<T>` classification brand, so
17
+ * the inspector can still follow it one hop past the call.
18
+ *
19
+ * Structured serialization redacts to `[secret]` — an audit or log write must
20
+ * stay honest about the field without crashing the request. String coercion
21
+ * throws, because a template literal or concatenation is always a leak.
22
+ */
23
+ export class SecretValue {
24
+ [SECRET_VALUE] = true;
25
+ #value;
26
+ constructor(value) {
27
+ this.#value = value;
28
+ }
29
+ reveal() {
30
+ return this.#value;
31
+ }
32
+ toJSON() {
33
+ return REDACTED;
34
+ }
35
+ [NODE_INSPECT]() {
36
+ return REDACTED;
37
+ }
38
+ toString() {
39
+ throw new SecretCoercionError();
40
+ }
41
+ [Symbol.toPrimitive]() {
42
+ throw new SecretCoercionError();
43
+ }
44
+ }
45
+ export const createSecretValue = (value) => new SecretValue(value);
46
+ export const isSecretValue = (value) => typeof value === 'object' && value !== null && SECRET_VALUE in value;
@@ -1,5 +1,6 @@
1
1
  import type { CoreUserSession, PikkuWire, PikkuWiringTypes } from '../types/core.types.js';
2
2
  import type { Logger } from './logger.js';
3
+ import type { Safe } from '../secret-value.js';
3
4
  export type AuditDurability = 'best-effort' | 'transactional';
4
5
  export type AuditOutcome = 'success' | 'failed' | 'denied';
5
6
  export type AuditSource = 'auto' | 'explicit';
@@ -9,7 +10,19 @@ export type AuditConfig = boolean | {
9
10
  export type ResolvedAuditConfig = {
10
11
  durability: AuditDurability;
11
12
  };
12
- export type AuditActor = {
13
+ /**
14
+ * Who an event happened under.
15
+ *
16
+ * A user, not an "actor": in pikku an actor is a synthetic person a scenario
17
+ * drives, flagged `actor` on the user row, and the overwhelming majority of
18
+ * audited events are caused by ordinary customers. Naming this `actor` made the
19
+ * synthetic case unsayable — `actor.actor === true` — and implied every
20
+ * recorded action was a test.
21
+ *
22
+ * `pikkuUserId` is the identity pikku resolves for every wire, so it is the one
23
+ * field a signed-out caller still leaves behind.
24
+ */
25
+ export type AuditUserIdentity = {
13
26
  userId?: string;
14
27
  orgId?: string;
15
28
  pikkuUserId?: string;
@@ -26,26 +39,83 @@ export type AuditEvent = {
26
39
  traceId?: string;
27
40
  transactionId?: string | null;
28
41
  queryId?: string | null;
29
- actor?: AuditActor;
42
+ userIdentity?: AuditUserIdentity;
30
43
  input?: unknown;
31
44
  metadata?: Record<string, unknown>;
32
45
  };
33
46
  export type AuditEventBatch = AuditEvent[];
47
+ /**
48
+ * A page of the trail, newest first, narrowed by the filters a reader picked.
49
+ *
50
+ * Every field is a conjunction, and an empty array is not a filter — it is
51
+ * "match nothing", which would otherwise read as "match everything" and quietly
52
+ * widen a scoped query.
53
+ */
54
+ export type AuditQuery = {
55
+ /** Restrict to these users. */
56
+ userIds?: string[];
57
+ /** Restrict to these `AuditEvent['type']` values. */
58
+ types?: string[];
59
+ /** Restrict to one organisation. */
60
+ orgId?: string;
61
+ /** Inclusive lower bound on `occurredAt` (ISO 8601). */
62
+ from?: string;
63
+ /** Exclusive upper bound on `occurredAt` (ISO 8601). */
64
+ to?: string;
65
+ limit?: number;
66
+ offset?: number;
67
+ };
68
+ /**
69
+ * The distinct values present in the trail, for populating filter controls.
70
+ *
71
+ * Computed over the whole trail rather than the current page — a filter list
72
+ * that only offered what the current page happens to show could never be used
73
+ * to reach anything else.
74
+ */
75
+ export type AuditFacets = {
76
+ userIds: string[];
77
+ types: string[];
78
+ };
79
+ export type AuditQueryResult = {
80
+ events: AuditEvent[];
81
+ /** Offset of the next page, or `null` at the end. */
82
+ nextCursor: number | null;
83
+ /** Present only when the caller asked for it — it costs two extra scans. */
84
+ facets?: AuditFacets;
85
+ };
34
86
  export interface AuditService {
35
87
  audit(event: AuditEvent): Promise<void>;
36
88
  write?(batch: AuditEventBatch): Promise<void>;
89
+ /**
90
+ * The read side. Optional because a sink can legitimately be write-only — a
91
+ * queue producer that hands events to another system has nothing to read
92
+ * back. A reader that finds this absent should say the trail is not readable
93
+ * here rather than that it is empty; the two are very different answers.
94
+ */
95
+ query?(query: AuditQuery): Promise<AuditQueryResult>;
96
+ /** Distinct users and types across the whole trail. Paired with {@link query}. */
97
+ facets?(): Promise<AuditFacets>;
37
98
  }
38
99
  export declare class NoopAuditService implements AuditService {
39
100
  audit(_event: AuditEvent): Promise<void>;
40
101
  write(_batch: AuditEventBatch): Promise<void>;
41
102
  }
42
103
  export type AuditLogWriteInput = Omit<AuditEvent, 'occurredAt'>;
104
+ /**
105
+ * The audit an invocation writes to. `write` is `Safe<>`-guarded like the
106
+ * logger: an audit event carries `input` and `metadata` as `unknown`, so
107
+ * nominality alone cannot stop a `SecretValue` landing in one. A secret
108
+ * anywhere in the event, however deeply nested, collapses to `never`.
109
+ *
110
+ * An unrevealed `SecretValue` would serialize as `[secret]` anyway; the guard
111
+ * is what makes that an explicit choice rather than a near miss.
112
+ */
43
113
  export interface AuditLog {
44
114
  readonly config: ResolvedAuditConfig | undefined;
45
- write(event: AuditLogWriteInput): Promise<void>;
115
+ write<E extends AuditLogWriteInput>(event: Safe<E>): Promise<void>;
46
116
  flush(): Promise<void>;
47
117
  close(): Promise<void>;
48
118
  }
49
119
  export declare const resolveAuditConfig: (config?: AuditConfig) => ResolvedAuditConfig | undefined;
50
120
  export declare const createInvocationAudit: (service: AuditService, wire: PikkuWire<any, any, any, CoreUserSession>, logger?: Logger) => AuditLog;
51
- export declare const resolveAuditActorFromWire: (wire: PikkuWire<any, any, any, CoreUserSession>) => AuditActor | undefined;
121
+ export declare const resolveAuditUserIdentityFromWire: (wire: PikkuWire<any, any, any, CoreUserSession>) => AuditUserIdentity | undefined;
@@ -75,7 +75,7 @@ class InvocationAuditLog {
75
75
  wireType: this.wire.wireType,
76
76
  wireId: this.wire.wireId,
77
77
  traceId: this.wire.traceId,
78
- actor: event.actor ?? resolveAuditActorFromWire(this.wire),
78
+ userIdentity: event.userIdentity ?? resolveAuditUserIdentityFromWire(this.wire),
79
79
  ...event,
80
80
  occurredAt: new Date().toISOString(),
81
81
  };
@@ -98,15 +98,17 @@ export const createInvocationAudit = (service, wire, logger) => {
98
98
  }
99
99
  return new InvocationAuditLog(wire.audit, service, wire, logger);
100
100
  };
101
- export const resolveAuditActorFromWire = (wire) => {
101
+ export const resolveAuditUserIdentityFromWire = (wire) => {
102
102
  const session = wire.session;
103
- const actor = {
103
+ const userIdentity = {
104
104
  userId: session?.userId,
105
105
  orgId: session?.orgId,
106
106
  pikkuUserId: wire.pikkuUserId,
107
107
  };
108
- if (!actor.userId && !actor.orgId && !actor.pikkuUserId) {
108
+ if (!userIdentity.userId &&
109
+ !userIdentity.orgId &&
110
+ !userIdentity.pikkuUserId) {
109
111
  return undefined;
110
112
  }
111
- return actor;
113
+ return userIdentity;
112
114
  };
@@ -9,6 +9,11 @@ export declare class PikkuCredentialWireService {
9
9
  private loadPromise;
10
10
  constructor(credentialService?: CredentialService | undefined, wire?: PikkuRawWire | undefined, aliases?: Record<string, string> | undefined);
11
11
  private resolveName;
12
+ /**
13
+ * A credential is one of the few places vault material is meant to end up, so
14
+ * a `SecretValue` is unwrapped here rather than rejected — `get` promises the
15
+ * raw material, and storing the wrapper would make that a lie.
16
+ */
12
17
  set(name: string, value: unknown): void;
13
18
  get<T = unknown>(name: string): T | null | Promise<T | null>;
14
19
  getAll(): Record<string, unknown> | Promise<Record<string, unknown>>;
@@ -1,3 +1,4 @@
1
+ import { isSecretValue } from '../secret-value.js';
1
2
  import { defaultPikkuUserIdResolver } from './pikku-user-id.js';
2
3
  export class PikkuCredentialWireService {
3
4
  credentialService;
@@ -14,8 +15,15 @@ export class PikkuCredentialWireService {
14
15
  resolveName(name) {
15
16
  return this.aliases?.[name] ?? name;
16
17
  }
18
+ /**
19
+ * A credential is one of the few places vault material is meant to end up, so
20
+ * a `SecretValue` is unwrapped here rather than rejected — `get` promises the
21
+ * raw material, and storing the wrapper would make that a lie.
22
+ */
17
23
  set(name, value) {
18
- this.credentials[this.resolveName(name)] = value;
24
+ this.credentials[this.resolveName(name)] = isSecretValue(value)
25
+ ? value.reveal()
26
+ : value;
19
27
  }
20
28
  get(name) {
21
29
  const key = this.resolveName(name);
@@ -1,3 +1,4 @@
1
+ import type { Safe } from '../secret-value.js';
1
2
  export interface EmailTemplateReference {
2
3
  name: string;
3
4
  locale?: string;
@@ -32,5 +33,5 @@ export interface SendEmailResult {
32
33
  messageId?: string;
33
34
  }
34
35
  export interface EmailService {
35
- send(input: SendEmailInput): Promise<SendEmailResult>;
36
+ send<T extends SendEmailInput>(input: Safe<T>): Promise<SendEmailResult>;
36
37
  }
@@ -29,6 +29,7 @@ export declare class InMemoryWorkflowService extends PikkuWorkflowService implem
29
29
  protected setStepErrorImpl(stepId: string, error: Error): Promise<void>;
30
30
  protected setStepChildRunIdImpl(stepId: string, childRunId: string): Promise<void>;
31
31
  protected createRetryAttemptImpl(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
32
+ protected findStalledRunIds(before: Date, limit: number): Promise<string[]>;
32
33
  listRuns(options?: {
33
34
  workflowName?: string;
34
35
  status?: string;
@@ -199,6 +199,26 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
199
199
  this.stepHistory.set(runId, history);
200
200
  return newStep;
201
201
  }
202
+ async findStalledRunIds(before, limit) {
203
+ const stalled = [];
204
+ for (const [runId, run] of this.runs) {
205
+ if (run.status !== 'running')
206
+ continue;
207
+ const steps = this.stepHistory.get(runId) ?? [];
208
+ if (steps.some((step) => step.status === 'running' ||
209
+ step.status === 'scheduled' ||
210
+ step.status === 'suspended')) {
211
+ continue;
212
+ }
213
+ const lastActivity = steps.reduce((latest, step) => (step.updatedAt > latest ? step.updatedAt : latest), run.updatedAt);
214
+ if (lastActivity >= before)
215
+ continue;
216
+ stalled.push(runId);
217
+ if (stalled.length >= limit)
218
+ break;
219
+ }
220
+ return stalled;
221
+ }
202
222
  async listRuns(options) {
203
223
  let runs = Array.from(this.runs.values());
204
224
  if (options?.workflowName) {
@@ -22,7 +22,7 @@ export type { JWTService } from './jwt-service.js';
22
22
  export type { EmailService, EmailTemplateReference, SendEmailInput, SendEmailResult, SendHTMLEmailInput, SendTemplateEmailInput, SendTextEmailInput, } from './email-service.js';
23
23
  export { DEFAULT_WEBHOOK_RETRIES, DEFAULT_WEBHOOK_SIGNATURE_HEADER, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, type SendWebhookInput, type SendWebhookResult, type WebhookAttemptRecord, type WebhookAttemptResult, type WebhookDeliveryRecord, type WebhookDeliveryWithAttempts, type WebhookJobData, type WebhookServiceConfig, } from './webhook-service.js';
24
24
  export type { Logger } from './logger.js';
25
- export type { SecretService } from './secret-service.js';
25
+ export type { SecretService, SecretValues } from './secret-service.js';
26
26
  export type { VariablesService } from './variables-service.js';
27
27
  export type { SchemaService } from './schema-service.js';
28
28
  export type { SessionService } from './user-session-service.js';
@@ -44,8 +44,8 @@ export type { SessionStore } from './session-store.js';
44
44
  export type { ScopeService, Role } from './scope-service.js';
45
45
  export { assertRoleIsMutable, assertRoleNameAvailable, roleLockReason, } from './system-role-guard.js';
46
46
  export type { IsSystemRole } from './system-role-guard.js';
47
- export { NoopAuditService, createInvocationAudit, resolveAuditActorFromWire, resolveAuditConfig, } from './audit-service.js';
48
- export type { AuditActor, AuditConfig, AuditDurability, AuditEvent, AuditEventBatch, AuditLog, AuditLogWriteInput, AuditOutcome, AuditService, AuditSource, ResolvedAuditConfig, } from './audit-service.js';
47
+ export { NoopAuditService, createInvocationAudit, resolveAuditConfig, resolveAuditUserIdentityFromWire, } from './audit-service.js';
48
+ export type { AuditConfig, AuditDurability, AuditEvent, AuditEventBatch, AuditLog, AuditLogWriteInput, AuditOutcome, AuditService, AuditSource, AuditUserIdentity, ResolvedAuditConfig, } from './audit-service.js';
49
49
  export { InMemorySessionStore } from './in-memory-session-store.js';
50
50
  export type { MCPMeta, RPCMetaRecord, ServiceMeta, ServicesMetaRecord, MiddlewareDefinitionMeta, MiddlewareInstanceMeta, GroupMeta, MiddlewareGroupsMeta, PermissionDefinitionMeta, PermissionsGroupsMeta, FunctionsMeta, FunctionMeta, MiddlewareMeta, PermissionMeta, AgentsMeta, AgentMeta, EmailsMeta, EmailTemplateMeta, EmailTemplateLocaleMeta, EmailTemplateAssets, } from './meta-service.js';
51
51
  export type { CoverageService, CoverageSnapshot, LineHits, ScriptCoverage, FunctionCoverage, CoverageRange, CoverageStatus, FunctionCoverageEntry, FunctionCoverageReport, CoverageFunctionMeta, } from './v8-coverage-service.js';
@@ -18,7 +18,7 @@ export { LocalGatewayService } from './local-gateway-service.js';
18
18
  export { DEFAULT_WEBHOOK_RETRIES, DEFAULT_WEBHOOK_SIGNATURE_HEADER, PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME, WebhookService, } from './webhook-service.js';
19
19
  export { TypedCredentialService } from './typed-credential-service.js';
20
20
  export { assertRoleIsMutable, assertRoleNameAvailable, roleLockReason, } from './system-role-guard.js';
21
- export { NoopAuditService, createInvocationAudit, resolveAuditActorFromWire, resolveAuditConfig, } from './audit-service.js';
21
+ export { NoopAuditService, createInvocationAudit, resolveAuditConfig, resolveAuditUserIdentityFromWire, } from './audit-service.js';
22
22
  export { InMemorySessionStore } from './in-memory-session-store.js';
23
23
  export { StubTracker, createStubProxy, getStubTracker, isTestRun, stub, spy, } from './stub-tracker.js';
24
24
  export { SecretHostNotAllowedError, assertSecretAllowedForHost, } from './secret-host-binding.js';
@@ -0,0 +1,29 @@
1
+ import type { JWTService, Logger } from '@pikku/core/services';
2
+ import { type LocalContentConfig } from './local-content.js';
3
+ /**
4
+ * The server half of {@link LocalContent}.
5
+ *
6
+ * `LocalContent` hands out `PUT <uploadUrlPrefix>/<key>` upload URLs and signed
7
+ * `GET <assetUrlPrefix>/<key>` read URLs, but it cannot answer either: it is a
8
+ * `ContentService`, not a transport. Something in the serving path has to, and
9
+ * until now only `@pikku/node-http-server` did — so the very same project served
10
+ * under Bun handed the browser upload URLs that 404ed, with nothing naming the
11
+ * cause.
12
+ *
13
+ * Expressed in Web `Request`/`Response` so every runtime can share one
14
+ * implementation rather than each re-deriving the signature check. Returns
15
+ * `null` for anything that is not a content request, which is the caller's
16
+ * signal to carry on with its normal routing.
17
+ */
18
+ export type LocalContentRequestHandler = (request: Request) => Promise<Response | null>;
19
+ export type LocalContentRequestHandlerOptions = {
20
+ content: LocalContentConfig;
21
+ logger: Logger;
22
+ /**
23
+ * Resolved per request rather than passed by value: a runtime may only be
24
+ * able to reach the signing service through `singletonServices`, which is not
25
+ * populated until after the server is constructed.
26
+ */
27
+ getJWT: () => JWTService | undefined;
28
+ };
29
+ export declare const createLocalContentRequestHandler: ({ content, logger, getJWT, }: LocalContentRequestHandlerOptions) => LocalContentRequestHandler;