@pikku/core 0.12.134 → 0.12.136

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 (72) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/analytics/analytics.types.d.ts +20 -0
  3. package/dist/analytics/anonymous-analytics-identity.d.ts +2 -0
  4. package/dist/analytics/anonymous-analytics-identity.js +2 -0
  5. package/dist/analytics/compose-analytics-identity.d.ts +2 -0
  6. package/dist/analytics/compose-analytics-identity.js +2 -0
  7. package/dist/analytics/cookie-analytics-identity.d.ts +2 -0
  8. package/dist/analytics/cookie-analytics-identity.js +2 -0
  9. package/dist/analytics/define-analytics-events.d.ts +3 -0
  10. package/dist/analytics/define-analytics-events.js +2 -0
  11. package/dist/analytics/fan-out-analytics.d.ts +2 -0
  12. package/dist/analytics/fan-out-analytics.js +2 -0
  13. package/dist/analytics/logger-analytics-service.d.ts +2 -0
  14. package/dist/analytics/logger-analytics-service.js +2 -0
  15. package/dist/analytics/mint-cookie.d.ts +8 -1
  16. package/dist/analytics/mint-cookie.js +8 -4
  17. package/dist/function/compensation-name.d.ts +4 -0
  18. package/dist/function/compensation-name.js +6 -0
  19. package/dist/function/function-meta.types.d.ts +7 -0
  20. package/dist/function/function-runner.js +17 -0
  21. package/dist/function/functions.types.d.ts +18 -0
  22. package/dist/function/functions.types.js +11 -0
  23. package/dist/middleware/require-origin.d.ts +2 -0
  24. package/dist/middleware/require-origin.js +2 -0
  25. package/dist/permissions.d.ts +5 -0
  26. package/dist/permissions.js +5 -0
  27. package/dist/testing/service-tests/queued-workflow-harness.d.ts +61 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.js +151 -0
  29. package/dist/testing/service-tests/workflow-compensation-queued-tests.d.ts +11 -0
  30. package/dist/testing/service-tests/workflow-compensation-queued-tests.js +683 -0
  31. package/dist/testing/service-tests.d.ts +4 -0
  32. package/dist/testing/service-tests.js +4 -0
  33. package/dist/utils/hmac.d.ts +0 -33
  34. package/dist/utils/hmac.js +0 -61
  35. package/dist/utils.d.ts +7 -0
  36. package/dist/utils.js +16 -0
  37. package/dist/wirings/flag/define-feature-flags.d.ts +1 -13
  38. package/dist/wirings/flag/define-feature-flags.js +1 -13
  39. package/dist/wirings/http/http-runner.js +1 -3
  40. package/dist/wirings/rpc/rpc-runner.js +21 -1
  41. package/dist/wirings/trigger/webhook-source-runner.d.ts +2 -2
  42. package/dist/wirings/trigger/webhook-source-runner.js +15 -15
  43. package/dist/wirings/trigger/webhook-source.types.d.ts +9 -12
  44. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +38 -6
  45. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -1
  46. package/dist/wirings/workflow/graph/graph-node.js +2 -1
  47. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  48. package/dist/wirings/workflow/graph/graph-runner.js +104 -67
  49. package/dist/wirings/workflow/graph/graph-validation.js +2 -2
  50. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +20 -1
  51. package/dist/wirings/workflow/index.d.ts +1 -1
  52. package/dist/wirings/workflow/pikku-workflow-service.d.ts +13 -0
  53. package/dist/wirings/workflow/pikku-workflow-service.js +113 -143
  54. package/dist/wirings/workflow/run-timeline.d.ts +2 -0
  55. package/dist/wirings/workflow/run-timeline.js +3 -0
  56. package/dist/wirings/workflow/workflow-child-step.d.ts +17 -0
  57. package/dist/wirings/workflow/workflow-child-step.js +31 -0
  58. package/dist/wirings/workflow/workflow-compensation.d.ts +68 -0
  59. package/dist/wirings/workflow/workflow-compensation.js +282 -0
  60. package/dist/wirings/workflow/workflow-constants.d.ts +5 -0
  61. package/dist/wirings/workflow/workflow-constants.js +9 -0
  62. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +12 -0
  63. package/dist/wirings/workflow/workflow-dsl-pass.js +75 -0
  64. package/dist/wirings/workflow/workflow-queue-routing.js +3 -3
  65. package/dist/wirings/workflow/workflow-run-status.js +13 -1
  66. package/dist/wirings/workflow/workflow-status-stream.js +2 -0
  67. package/dist/wirings/workflow/workflow-unwind-plan.d.ts +38 -0
  68. package/dist/wirings/workflow/workflow-unwind-plan.js +120 -0
  69. package/dist/wirings/workflow/workflow.types.d.ts +9 -2
  70. package/knowledge/decisions/internals/workflow-step-compensation-runs-as-its-own-durable-step.md +21 -18
  71. package/package.json +1 -1
  72. package/src/public-surface.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 0.12.136
2
+
3
+ ### Patch Changes
4
+
5
+ - 149faae: Webhook `receive` steps lose their boilerplate. `pikkuWebhookReceive` (from `#pikku/trigger` or `#pikku/addon/trigger`) declares one: it is public, typed to the raw request with a required `http`, and never registered as an RPC, and the inspector rejects a `receive` declared with any other wrapper. `parseJson` (in `@pikku/core/utils`, generated as `#pikku/utils` and `#pikku/addon/utils`) parses text or bytes and answers a body that is not JSON with a 400. A webhook source whose `method` includes `'head'` answers HEAD probes itself with a 200.
6
+
7
+ `receive` no longer returns `{ respond }`: a handshake returns nothing and writes its answer to `http.response`, like any other HTTP function. The webhook route no longer returns a fetch `Response` either. An HTTP function that returns nothing is now answered with whatever status its response has (200 unless it set another), no longer a forced 204. A webhook receive is also left out of contract versioning, since nothing but its own route calls it.
8
+
9
+ ## 0.12.135
10
+
11
+ ### Patch Changes
12
+
13
+ - ce3e5f5: Saga compensation for workflows. A function declares `compensate` inline; when a workflow step fails, completed steps are undone newest-first as durable `<step>:compensate` steps. New run statuses `compensating`, `compensated` and `compensation_failed`, `workflow.milestone(name)` to bound the unwind, `{ compensate: false }` to opt a call out, nested-workflow unwinding, and `PikkuWorkflowService.cancelRun` which unwinds too. Graph nodes replace `onError` with `recover` (`nodeId`, `nodeId[]` or `'ignore'`), exposing the error as `wire.graph.recoveringFrom`. **Breaking:** the DSL `onError` step option and the graph `onError` node field are removed; `getRunSteps` is now abstract on `PikkuWorkflowService`.
14
+ - fb36c37: Remove `WebhookSigningSecret` from `@pikku/core/hmac`: declare `verify` on `wireTriggerWebhookSource` instead, or use `hmacDigest`, `verifyHmacSignature`, `verifyPublicKeySignature` and `timingSafeStringEqual` directly. The trigger skill and the online-shop snippet now teach declarative `verify`, and the `verify` JSDoc describes bodiless requests correctly.
15
+
1
16
  ## 0.12.134
2
17
 
3
18
  ### Patch Changes
@@ -8,6 +8,7 @@ import type { CoreUserSession, PikkuWire, PikkuWiringTypes } from '../types/core
8
8
  * events carry more than a fixed set of props.
9
9
  */
10
10
  export type AnalyticsEventBase = {
11
+ /** The event name, as declared in `defineAnalyticsEvents`. */
11
12
  name: string;
12
13
  } & Record<string, unknown>;
13
14
  export interface AnalyticsEventInput {
@@ -20,8 +21,11 @@ export interface AnalyticsEventInput {
20
21
  * which is what makes an unauthenticated ingest safe to expose.
21
22
  */
22
23
  export interface AnalyticsIdentity {
24
+ /** The session's user id, or null for a visitor with no session. */
23
25
  userId: string | null;
26
+ /** The session's organization, when it has one. */
24
27
  orgId?: string;
28
+ /** The pikku user the session resolves to; absent without a session. */
25
29
  pikkuUserId?: string;
26
30
  /**
27
31
  * Identifiers a destination keys on that pikku does not mint — GA4's
@@ -80,14 +84,23 @@ resolved?: Pick<AnalyticsIdentity, 'vendorIds' | 'consent' | 'anonymousId'>) =>
80
84
  * through, so a sink never has to trust — or re-derive — any of them.
81
85
  */
82
86
  export interface AnalyticsRecord {
87
+ /** The event name. */
83
88
  name: string;
89
+ /** The validated props the event carried. */
84
90
  props?: Record<string, unknown>;
91
+ /** When the server accepted the event, as an ISO timestamp. */
85
92
  occurredAt: string;
93
+ /** When a browser says it happened, in epoch milliseconds; only on relayed events. */
86
94
  at?: number;
95
+ /** Who the event is attributed to, stamped from the session and cookies. */
87
96
  userIdentity: AnalyticsIdentity;
97
+ /** The trace the emitting invocation belongs to. */
88
98
  traceId?: string;
99
+ /** The function that recorded the event, when one did. */
89
100
  functionId?: string;
101
+ /** The kind of wire the event came in on. */
90
102
  wireType?: PikkuWiringTypes;
103
+ /** `client` when relayed from a browser, `server` when a function recorded it. */
91
104
  source: 'server' | 'client';
92
105
  }
93
106
  /**
@@ -100,6 +113,7 @@ export interface AnalyticsRecord {
100
113
  * to implement and every caller had to choose between.
101
114
  */
102
115
  export interface AnalyticsService {
116
+ /** Delivers one batch of accepted records; a rejection is logged, never surfaced to the caller. */
103
117
  write(batch: AnalyticsRecord[]): Promise<void>;
104
118
  }
105
119
  /**
@@ -111,7 +125,9 @@ export interface AnalyticsService {
111
125
  * should look like once it gets there is the sink's, and lives in its mapper.
112
126
  */
113
127
  export interface AnalyticsSink {
128
+ /** The destination the filtered records are written to. */
114
129
  service: AnalyticsService;
130
+ /** Returns true for the records this destination should get; omit to send everything. */
115
131
  accepts?: (record: AnalyticsRecord) => boolean;
116
132
  }
117
133
  /**
@@ -119,6 +135,7 @@ export interface AnalyticsSink {
119
135
  * beacon is free to omit `at`.
120
136
  */
121
137
  export interface AnalyticsClientContext {
138
+ /** When the browser says the event happened, in epoch milliseconds. */
122
139
  at?: number;
123
140
  }
124
141
  /**
@@ -127,8 +144,11 @@ export interface AnalyticsClientContext {
127
144
  * narrow `Events` in its own `SingletonServices`.
128
145
  */
129
146
  export interface AnalyticsLog<Events extends AnalyticsEventBase = AnalyticsEventBase> {
147
+ /** Buffers an event for the invocation; pass `client` when relaying one from a browser. */
130
148
  record(event: Events, client?: AnalyticsClientContext): Promise<void>;
149
+ /** Writes what is buffered to the service now, rather than when the invocation ends. */
131
150
  flush(): Promise<void>;
151
+ /** Flushes and stops accepting events; called for you when the invocation ends. */
132
152
  close(): Promise<void>;
133
153
  }
134
154
  /**
@@ -19,5 +19,7 @@ export interface AnonymousAnalyticsIdentityOptions {
19
19
  * browser script needs it, and a cookie scripts cannot touch is both harder to
20
20
  * misuse and not subject to the seven-day cap browsers place on script-set
21
21
  * ones. An app that wants a vendor SDK to read it must opt out deliberately.
22
+ *
23
+ * @example snippet: analyticsIdentity
22
24
  */
23
25
  export declare const anonymousAnalyticsIdentity: (options?: AnonymousAnalyticsIdentityOptions) => AnalyticsIdentityResolver;
@@ -14,6 +14,8 @@ const DEFAULT_COOKIE = {
14
14
  * browser script needs it, and a cookie scripts cannot touch is both harder to
15
15
  * misuse and not subject to the seven-day cap browsers place on script-set
16
16
  * ones. An app that wants a vendor SDK to read it must opt out deliberately.
17
+ *
18
+ * @example snippet: analyticsIdentity
17
19
  */
18
20
  export const anonymousAnalyticsIdentity = (options = {}) => {
19
21
  return (wire, resolved) => {
@@ -10,5 +10,7 @@ import type { AnalyticsIdentityResolver } from './analytics.types.js';
10
10
  *
11
11
  * A later resolver wins a key it sets, so a minter's freshly created id
12
12
  * replaces the absent one the cookie reader could not find.
13
+ *
14
+ * @example snippet: analyticsIdentity
13
15
  */
14
16
  export declare const composeAnalyticsIdentity: (...resolvers: AnalyticsIdentityResolver[]) => AnalyticsIdentityResolver;
@@ -9,6 +9,8 @@
9
9
  *
10
10
  * A later resolver wins a key it sets, so a minter's freshly created id
11
11
  * replaces the absent one the cookie reader could not find.
12
+ *
13
+ * @example snippet: analyticsIdentity
12
14
  */
13
15
  export const composeAnalyticsIdentity = (...resolvers) => {
14
16
  return (wire, initial) => {
@@ -20,5 +20,7 @@ export interface CookieAnalyticsIdentityOptions {
20
20
  * Resolves nothing off an HTTP wire, which is correct: a cron task and a queue
21
21
  * worker have no browser behind them, so any vendor id they produced would be
22
22
  * invented.
23
+ *
24
+ * @example snippet: analyticsIdentity
23
25
  */
24
26
  export declare const cookieAnalyticsIdentity: (options: CookieAnalyticsIdentityOptions) => AnalyticsIdentityResolver;
@@ -9,6 +9,8 @@ const DENIALS = new Set(['0', 'false', 'denied', 'deny', 'no']);
9
9
  * Resolves nothing off an HTTP wire, which is correct: a cron task and a queue
10
10
  * worker have no browser behind them, so any vendor id they produced would be
11
11
  * invented.
12
+ *
13
+ * @example snippet: analyticsIdentity
12
14
  */
13
15
  export const cookieAnalyticsIdentity = (options) => {
14
16
  return (wire) => {
@@ -7,6 +7,7 @@ import type { StandardSchemaV1 } from '@standard-schema/spec';
7
7
  * and saying so here fails at the declaration rather than in generated code.
8
8
  */
9
9
  export type AnalyticsEventPropsSchema = StandardSchemaV1 & {
10
+ /** The object schema's fields, which the CLI reads to list an event's props. */
10
11
  shape: Record<string, unknown>;
11
12
  };
12
13
  /** Events keyed by name; each value is the schema for that event's props. */
@@ -18,5 +19,7 @@ export type AnalyticsEventDefinitions = Record<string, AnalyticsEventPropsSchema
18
19
  * It must stay an exported const — the schema pipeline reads the value by name.
19
20
  * It registers nothing at runtime: where events go is an `AnalyticsService` on
20
21
  * singleton services.
22
+ *
23
+ * @example snippet: analyticsEvents
21
24
  */
22
25
  export declare const defineAnalyticsEvents: <const Events extends AnalyticsEventDefinitions>(events: Events) => Events;
@@ -5,5 +5,7 @@
5
5
  * It must stay an exported const — the schema pipeline reads the value by name.
6
6
  * It registers nothing at runtime: where events go is an `AnalyticsService` on
7
7
  * singleton services.
8
+ *
9
+ * @example snippet: analyticsEvents
8
10
  */
9
11
  export const defineAnalyticsEvents = (events) => events;
@@ -11,5 +11,7 @@ import type { AnalyticsService, AnalyticsSink } from './analytics.types.js';
11
11
  * is down must not cost the others their events. A destination that throws is
12
12
  * reported by the caller's existing flush guard, which already treats analytics
13
13
  * as best-effort.
14
+ *
15
+ * @example snippet: shopServices
14
16
  */
15
17
  export declare const fanOutAnalytics: (sinks: ReadonlyArray<AnalyticsSink | AnalyticsService>) => AnalyticsService;
@@ -10,6 +10,8 @@
10
10
  * is down must not cost the others their events. A destination that throws is
11
11
  * reported by the caller's existing flush guard, which already treats analytics
12
12
  * as best-effort.
13
+ *
14
+ * @example snippet: shopServices
13
15
  */
14
16
  export const fanOutAnalytics = (sinks) => {
15
17
  const resolved = sinks.map((sink) => 'service' in sink ? sink : { service: sink });
@@ -9,6 +9,8 @@ import type { AnalyticsRecord, AnalyticsService } from './analytics.types.js';
9
9
  */
10
10
  export declare class LoggerAnalyticsService implements AnalyticsService {
11
11
  private readonly logger;
12
+ /** @param logger Receives each event at `debug`. */
12
13
  constructor(logger: Logger);
14
+ /** Logs each record in the batch at `debug`. */
13
15
  write(batch: AnalyticsRecord[]): Promise<void>;
14
16
  }
@@ -7,9 +7,11 @@
7
7
  */
8
8
  export class LoggerAnalyticsService {
9
9
  logger;
10
+ /** @param logger Receives each event at `debug`. */
10
11
  constructor(logger) {
11
12
  this.logger = logger;
12
13
  }
14
+ /** Logs each record in the batch at `debug`. */
13
15
  async write(batch) {
14
16
  for (const event of batch) {
15
17
  this.logger.debug(`analytics: ${event.name}`, {
@@ -10,6 +10,7 @@ export interface MintCookieOptions {
10
10
  * is answered has already done the thing the send gate was meant to prevent.
11
11
  */
12
12
  requires?: string[];
13
+ /** What the visitor agreed to, as resolved by an earlier resolver; `requires` is checked against it. */
13
14
  consent?: Record<string, boolean>;
14
15
  /**
15
16
  * Replace the cookie already on the device rather than returning it.
@@ -37,8 +38,14 @@ export interface MintCookieOptions {
37
38
  * has been sent; a cron task and a queue worker have no browser to store it,
38
39
  * and a stream's headers are long gone. Both return undefined rather than
39
40
  * pretending.
41
+ *
42
+ * @example snippet: mintVendorCookie
40
43
  */
41
44
  export declare const mintCookie: (wire: AnyWire, name: string, options: MintCookieOptions, mint: () => string) => string | undefined;
42
- /** Cryptographically random digits, the shape both vendor formats use. */
45
+ /**
46
+ * Cryptographically random digits, the shape both vendor formats use.
47
+ *
48
+ * @example snippet: mintVendorCookie
49
+ */
43
50
  export declare const randomDigits: (length: number) => string;
44
51
  export {};
@@ -21,11 +21,11 @@ const permitted = (requires, consent) => {
21
21
  * has been sent; a cron task and a queue worker have no browser to store it,
22
22
  * and a stream's headers are long gone. Both return undefined rather than
23
23
  * pretending.
24
+ *
25
+ * @example snippet: mintVendorCookie
24
26
  */
25
27
  export const mintCookie = (wire, name, options, mint) => {
26
- const existing = options.overwrite
27
- ? null
28
- : wire.http?.request?.cookie(name);
28
+ const existing = options.overwrite ? null : wire.http?.request?.cookie(name);
29
29
  if (existing)
30
30
  return existing;
31
31
  const cache = minted.get(wire) ?? new Map();
@@ -43,7 +43,11 @@ export const mintCookie = (wire, name, options, mint) => {
43
43
  minted.set(wire, cache);
44
44
  return value;
45
45
  };
46
- /** Cryptographically random digits, the shape both vendor formats use. */
46
+ /**
47
+ * Cryptographically random digits, the shape both vendor formats use.
48
+ *
49
+ * @example snippet: mintVendorCookie
50
+ */
47
51
  export const randomDigits = (length) => {
48
52
  const bytes = new Uint8Array(length);
49
53
  crypto.getRandomValues(bytes);
@@ -0,0 +1,4 @@
1
+ export declare const COMPENSATION_STEP_SUFFIX = ":compensate";
2
+ export declare const compensationStepName: (stepName: string) => string;
3
+ export declare const isCompensationStepName: (stepName: string) => boolean;
4
+ export declare const forwardStepName: (stepName: string) => string;
@@ -0,0 +1,6 @@
1
+ export const COMPENSATION_STEP_SUFFIX = ':compensate';
2
+ export const compensationStepName = (stepName) => `${stepName}${COMPENSATION_STEP_SUFFIX}`;
3
+ export const isCompensationStepName = (stepName) => stepName.endsWith(COMPENSATION_STEP_SUFFIX);
4
+ export const forwardStepName = (stepName) => isCompensationStepName(stepName)
5
+ ? stepName.slice(0, -COMPENSATION_STEP_SUFFIX.length)
6
+ : stepName;
@@ -49,6 +49,11 @@ export type FunctionRuntimeMeta = {
49
49
  * everywhere else, so it is never network-callable.
50
50
  */
51
51
  scenarioStep?: boolean;
52
+ /**
53
+ * A webhook source's `receive` step, declared with `pikkuWebhookReceive`.
54
+ * Only its source's route runs it, so it is never RPC-callable.
55
+ */
56
+ webhookReceive?: boolean;
52
57
  /**
53
58
  * The body of a `pikkuScenario(...)`. Only ever run by `pikku scenario run`,
54
59
  * so it is held back from the app bootstrap and from every deployed unit.
@@ -58,6 +63,8 @@ export type FunctionRuntimeMeta = {
58
63
  readonly?: boolean;
59
64
  deploy?: 'serverless' | 'server' | 'auto';
60
65
  sessionless?: boolean;
66
+ /** The function declares a `compensate`, run as the sibling `<id>:compensate` when a workflow unwinds. */
67
+ compensate?: boolean;
61
68
  /** When true, workflow steps calling this function are dispatched via the queue. No queue service configured is a hard error. */
62
69
  workflowQueued?: boolean;
63
70
  /** Retry count when this function is used as a workflow step. */
@@ -1,4 +1,5 @@
1
1
  import { beginChanges } from './abort-scope.js';
2
+ import { forwardStepName, isCompensationStepName } from './compensation-name.js';
2
3
  import { runMiddleware, combineMiddleware } from '../middleware-runner.js';
3
4
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../wirings/channel/channel-middleware-runner.js';
4
5
  import { runPermissions } from '../permissions.js';
@@ -71,6 +72,22 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
71
72
  let funcConfig = funcMap.get(funcName);
72
73
  const allMeta = pikkuState(packageName, 'function', 'meta');
73
74
  let funcMeta = allMeta[funcName];
75
+ if ((!funcConfig || !funcMeta) && isCompensationStepName(funcName)) {
76
+ const forwardName = forwardStepName(funcName);
77
+ const forward = funcMap.get(forwardName);
78
+ const forwardMeta = allMeta[forwardName];
79
+ if (forward?.compensate && forwardMeta) {
80
+ const { compensate, ...rest } = forward;
81
+ funcConfig = { ...rest, func: compensate };
82
+ funcMeta = {
83
+ ...forwardMeta,
84
+ pikkuFuncId: funcName,
85
+ outputs: null,
86
+ expose: false,
87
+ compensate: undefined,
88
+ };
89
+ }
90
+ }
74
91
  if (!funcConfig || !funcMeta) {
75
92
  const { baseName, version } = parseVersionedId(funcName);
76
93
  if (version !== null) {
@@ -17,12 +17,23 @@ export type CorePikkuPermissionConfig<In = any, Services extends CoreSecretlessS
17
17
  };
18
18
  export declare const pikkuPermission: <In = any, Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Wire extends PickRequired<PikkuWire<In, never, false, any, PikkuRPC, never, never>, 'session'> = PickRequired<PikkuWire<In, never, false, any, PikkuRPC, never, never>, 'session'>>(permission: CorePikkuPermission<In, Services, Wire> | CorePikkuPermissionConfig<In, Services, Wire>) => CorePikkuPermission<In, Services, Wire>;
19
19
  export type CorePikkuPermissionFactory<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>> = (input: In) => CorePikkuPermission<any, Services, Wire>;
20
+ /**
21
+ * Declares a permission that takes configuration, so one check serves many
22
+ * call sites: `hasProfileRole({ role: 'support' })`.
23
+ *
24
+ * @example snippet: permissionFactory
25
+ */
20
26
  export declare const pikkuPermissionFactory: <In = any>(factory: CorePikkuPermissionFactory<In>) => CorePikkuPermissionFactory<In>;
21
27
  /**
22
28
  * Renders a human-readable approval prompt for an AI agent, in place of the
23
29
  * raw tool arguments.
24
30
  */
25
31
  export type CorePikkuApprovalDescription<In = any, Services extends CoreSecretlessSingletonServices = CoreSecretlessSingletonServices> = (services: Services, data: In) => Promise<string>;
32
+ /**
33
+ * Declares the approval prompt a function shows in place of its raw arguments.
34
+ *
35
+ * @example snippet: approvalDescription
36
+ */
26
37
  export declare const pikkuApprovalDescription: <In = any, Services extends CoreSecretlessSingletonServices = CoreSecretlessSingletonServices>(fn: CorePikkuApprovalDescription<In, Services>) => CorePikkuApprovalDescription<In, Services>;
27
38
  export type CorePikkuAuth<Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession> = (services: Services, session: Session) => Promise<boolean> | boolean;
28
39
  export type CorePikkuAuthConfig<Services extends CoreSecretlessSingletonServices = SecretlessServices<CoreServices>, Session extends CoreUserSession = CoreUserSession> = {
@@ -83,6 +94,13 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
83
94
  approvalRequired?: boolean;
84
95
  /** When true, workflow steps calling this function are dispatched via the queue. No queue service configured is a hard error. Defaults to false (inline). */
85
96
  workflowQueued?: boolean;
97
+ /**
98
+ * Undoes this function's effect when a workflow that called it unwinds. It
99
+ * receives the function's own input, and `wire.workflow.compensatingFor`
100
+ * carries the forward output (or the error, when the forward step failed).
101
+ * It is never callable on its own — only the workflow engine runs it.
102
+ */
103
+ compensate?: (services: any, data: any, wire: any) => Promise<any> | any;
86
104
  /** Number of retry attempts when this function is used as a workflow step. */
87
105
  workflowRetries?: number;
88
106
  /** Timeout for this function when used as a workflow step (e.g. '30s', '5m'). */
@@ -1,9 +1,20 @@
1
1
  export const pikkuPermission = (permission) => {
2
2
  return typeof permission === 'function' ? permission : permission.func;
3
3
  };
4
+ /**
5
+ * Declares a permission that takes configuration, so one check serves many
6
+ * call sites: `hasProfileRole({ role: 'support' })`.
7
+ *
8
+ * @example snippet: permissionFactory
9
+ */
4
10
  export const pikkuPermissionFactory = (factory) => {
5
11
  return factory;
6
12
  };
13
+ /**
14
+ * Declares the approval prompt a function shows in place of its raw arguments.
15
+ *
16
+ * @example snippet: approvalDescription
17
+ */
7
18
  export const pikkuApprovalDescription = (fn) => {
8
19
  return fn;
9
20
  };
@@ -16,6 +16,8 @@ export declare const isAllowedOrigin: (requestOrigin: string | null, hostOrigin:
16
16
  * before the function body. It stops another site's page from posting to an unauthed
17
17
  * route — it is not flood control, because `Origin` is trusted from nobody but a browser.
18
18
  * A missing `Origin` is rejected too: a real browser sets one on a cross-origin-capable POST.
19
+ *
20
+ * @example snippet: requireOrigin
19
21
  */
20
22
  export declare const requireOrigin: import("./middleware.types.js").CorePikkuMiddlewareFactory<{
21
23
  /** Extra allowed origins beyond the request's own host, or a resolver for them. */
@@ -33,6 +33,8 @@ export const isAllowedOrigin = (requestOrigin, hostOrigin, configuredOrigins) =>
33
33
  * before the function body. It stops another site's page from posting to an unauthed
34
34
  * route — it is not flood control, because `Origin` is trusted from nobody but a browser.
35
35
  * A missing `Origin` is rejected too: a real browser sets one on a cross-origin-capable POST.
36
+ *
37
+ * @example snippet: requireOrigin
36
38
  */
37
39
  export const requireOrigin = pikkuMiddlewareFactory(({ origins = [] } = {}) => pikkuMiddleware({
38
40
  name: 'requireOrigin',
@@ -8,6 +8,11 @@ import type { PikkuRPC } from './wirings/rpc/rpc-types.js';
8
8
  export type PermissionWire = PikkuWire<any, never, false, any, PikkuRPC, never, never>;
9
9
  import type { CorePermissionGroup, CorePikkuPermission } from './function/functions.types.js';
10
10
  export declare const clearPermissionsCache: () => void;
11
+ /**
12
+ * Applies permissions to every function, ahead of the per-function ones.
13
+ *
14
+ * @example snippet: globalPermission
15
+ */
11
16
  export declare const addGlobalPermission: (permissions: CorePermissionGroup | CorePikkuPermission[], packageName?: string | null) => CorePermissionGroup | CorePikkuPermission[];
12
17
  export declare const runPermissions: ({ funcPermissions, services, wire, data, packageName, label, }: {
13
18
  funcPermissions?: CorePermissionGroup | CorePikkuPermission[];
@@ -29,6 +29,11 @@ export const clearPermissionsCache = () => {
29
29
  delete globalPermissionsCache[key];
30
30
  }
31
31
  };
32
+ /**
33
+ * Applies permissions to every function, ahead of the per-function ones.
34
+ *
35
+ * @example snippet: globalPermission
36
+ */
32
37
  export const addGlobalPermission = (permissions, packageName = null) => {
33
38
  const state = pikkuState(packageName, 'permissions', 'global');
34
39
  if (Array.isArray(permissions)) {
@@ -0,0 +1,61 @@
1
+ import type { PikkuWorkflowService } from '../../wirings/workflow/pikku-workflow-service.js';
2
+ import type { CompensatingFor } from '../../wirings/workflow/dsl/workflow-dsl.types.js';
3
+ export type Handler = {
4
+ forward: (data: any, wire: any) => Promise<any> | any;
5
+ compensate?: (data: any, context: CompensatingFor) => Promise<void> | void;
6
+ queued?: boolean;
7
+ };
8
+ interface Job {
9
+ queue: string;
10
+ data: any;
11
+ attempts?: number;
12
+ attempt?: number;
13
+ }
14
+ /**
15
+ * Drives a workflow the way a deployment does: every orchestration and every
16
+ * `workflowQueued` step goes through a queue, and a pump plays the workers.
17
+ * Delays are ignored, so a retry runs on the next pass.
18
+ */
19
+ export declare class QueuedWorkflowHarness {
20
+ handlers: Record<string, Handler>;
21
+ log: string[];
22
+ contexts: Array<{
23
+ rpc: string;
24
+ context: CompensatingFor | undefined;
25
+ }>;
26
+ recovering: Array<{
27
+ rpc: string;
28
+ from: any;
29
+ }>;
30
+ queue: Job[];
31
+ jobsRun: Array<{
32
+ queue: string;
33
+ step?: string;
34
+ }>;
35
+ ws: PikkuWorkflowService;
36
+ /** Return true to drop the job instead of running it, simulating a worker that never ran. */
37
+ dropWhen?: (job: Job) => boolean;
38
+ /** Make `queue.add` throw for a job, simulating the broker being unreachable. */
39
+ rejectAddWhen?: (queue: string, data: any) => boolean;
40
+ constructor(service: PikkuWorkflowService);
41
+ readonly rpc: {
42
+ rpcWithWire: (rpcName: string, data: any, wire: any) => Promise<any>;
43
+ };
44
+ register(name: string, handler: Handler): void;
45
+ defineDsl(name: string, body: (workflow: any, input: any) => Promise<any>): void;
46
+ defineGraph(name: string, entry: string, nodes: Record<string, any>): void;
47
+ start(name: string, input?: any): Promise<string>;
48
+ /** Run jobs until the queue is empty. */
49
+ pump(limit?: number): Promise<void>;
50
+ run(runId: string): Promise<import("../../wirings/workflow/workflow.types.js").WorkflowRun>;
51
+ steps(runId: string): Promise<(import("../../wirings/workflow/workflow.types.js").StepState & {
52
+ stepName: string;
53
+ rpcName?: string;
54
+ data?: any;
55
+ })[]>;
56
+ get undone(): string[];
57
+ ok(output?: any): Handler;
58
+ boom(message?: string): Handler;
59
+ undoable(output?: any): Handler;
60
+ }
61
+ export {};