@amalgm/automations 0.2.1 → 0.2.3

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 (64) hide show
  1. package/AXIOMS.md +39 -17
  2. package/PURPOSE.md +26 -11
  3. package/README.md +33 -4
  4. package/dist/host/config.d.ts +2 -0
  5. package/dist/host/config.js +8 -0
  6. package/dist/host/main.js +10 -5
  7. package/dist/host/server.d.ts +2 -0
  8. package/dist/host/server.js +25 -7
  9. package/dist/src/automations.d.ts +15 -9
  10. package/dist/src/automations.js +33 -76
  11. package/dist/src/cli.d.ts +1 -1
  12. package/dist/src/cli.js +5 -2
  13. package/dist/src/client.d.ts +1 -0
  14. package/dist/src/client.js +4 -2
  15. package/dist/src/contract.d.ts +11 -31
  16. package/dist/src/crud/context.d.ts +3 -1
  17. package/dist/src/crud/context.js +2 -1
  18. package/dist/src/crud/repository.d.ts +17 -6
  19. package/dist/src/crud/runs.d.ts +1 -1
  20. package/dist/src/crud/runs.js +24 -1
  21. package/dist/src/crud/triggers.js +18 -9
  22. package/dist/src/crud.d.ts +4 -2
  23. package/dist/src/crud.js +5 -2
  24. package/dist/src/events-http.d.ts +3 -5
  25. package/dist/src/events-http.js +22 -26
  26. package/dist/src/executor.d.ts +12 -3
  27. package/dist/src/executor.js +163 -44
  28. package/dist/src/http.js +8 -1
  29. package/dist/src/index.d.ts +6 -4
  30. package/dist/src/index.js +6 -4
  31. package/dist/src/machine-client.d.ts +1 -0
  32. package/dist/src/machine-client.js +2 -0
  33. package/dist/src/machine.d.ts +2 -1
  34. package/dist/src/machine.js +6 -1
  35. package/dist/src/mcp.d.ts +3 -0
  36. package/dist/src/mcp.js +61 -50
  37. package/dist/src/plan.d.ts +3 -0
  38. package/dist/src/plan.js +48 -0
  39. package/dist/src/run-contract.d.ts +52 -0
  40. package/dist/src/run-contract.js +1 -0
  41. package/dist/src/run-journal.d.ts +8 -0
  42. package/dist/src/run-journal.js +56 -0
  43. package/dist/src/runner.d.ts +14 -0
  44. package/dist/src/runner.js +70 -0
  45. package/dist/src/schema.d.ts +31 -15
  46. package/dist/src/schema.js +27 -4
  47. package/dist/src/supabase-crud/mappers.d.ts +3 -2
  48. package/dist/src/supabase-crud/mappers.js +5 -0
  49. package/dist/src/supabase-crud/rows.d.ts +3 -0
  50. package/dist/src/supabase-crud/workflow-runs.d.ts +1 -1
  51. package/dist/src/supabase-crud/workflow-runs.js +10 -0
  52. package/dist/src/supabase-machine.js +2 -0
  53. package/dist/src/supabase-store.d.ts +3 -6
  54. package/dist/src/supabase-store.js +13 -34
  55. package/dist/src/tool-surface.d.ts +47 -0
  56. package/dist/src/tool-surface.js +126 -0
  57. package/dist/src/types.d.ts +8 -24
  58. package/dist/src/webhook.d.ts +15 -1
  59. package/dist/src/webhook.js +86 -0
  60. package/package.json +3 -3
  61. package/skills/automations/SKILL.md +29 -16
  62. package/supabase/migrations/20260830010000_durable_step_retries.sql +332 -0
  63. package/supabase/migrations/20260831010000_public_webhook_endpoints.sql +287 -0
  64. package/supabase/migrations/20260831020000_manual_run_admission.sql +68 -0
@@ -1,6 +1,8 @@
1
1
  export type Json = null | boolean | number | string | Json[] | {
2
2
  [key: string]: Json;
3
3
  };
4
+ import type { AutomationRun, ListRuns, RunAutomationNow } from './run-contract.js';
5
+ export type { AutomationPlan, AutomationRun, AutomationRunJournal, AutomationStepRun, AutomationStepStatus, ListRuns, RunAutomationNow, RunStatus, ToolActionStep, } from './run-contract.js';
4
6
  export type AutomationScope = 'automations:read' | 'automations:write' | 'runs:read' | 'runs:execute' | '*';
5
7
  /** Identity resolved by Amalgm before the SDK is bound to a caller. */
6
8
  export interface AutomationPrincipal {
@@ -66,6 +68,8 @@ export interface WebhookTrigger extends TriggerBase {
66
68
  kind: 'webhook';
67
69
  source: string;
68
70
  event: string;
71
+ /** Bearer-capability URL. Treat it as a credential and rotate it if exposed. */
72
+ webhookUrl: string;
69
73
  secretConfigured: boolean;
70
74
  }
71
75
  export type Trigger = ScheduleTrigger | WebhookTrigger;
@@ -84,26 +88,21 @@ export interface UpdateScheduleTrigger {
84
88
  /** Set a new finite budget, or null to make the schedule unbounded. */
85
89
  maxOccurrences?: number | null;
86
90
  }
87
- export interface ToolActionStep {
88
- id: string;
89
- actionId: string;
90
- input: Json;
91
- }
92
- export interface AutomationPlan {
93
- version: 1;
94
- steps: ToolActionStep[];
95
- }
96
91
  export interface CreateWebhookTrigger {
97
92
  id?: string;
98
93
  source?: string;
99
94
  event?: string;
100
- secret: string;
95
+ /** Optional provider signing secret; the generated webhook URL authenticates admission. */
96
+ secret?: string;
101
97
  enabled?: boolean;
102
98
  }
103
99
  export interface UpdateWebhookTrigger {
104
100
  source?: string;
105
101
  event?: string;
106
- secret?: string;
102
+ /** Set null to remove provider-signature verification. */
103
+ secret?: string | null;
104
+ /** Invalidates the existing URL and returns its replacement. */
105
+ rotateUrl?: boolean;
107
106
  enabled?: boolean;
108
107
  }
109
108
  export interface Workflow {
@@ -132,25 +131,6 @@ export interface UpdateWorkflow {
132
131
  allowlist?: Json | null;
133
132
  limits?: Json | null;
134
133
  }
135
- export type RunStatus = 'pending' | 'sent' | 'running' | 'completed' | 'failed';
136
- export interface AutomationRun {
137
- id: string;
138
- automationId: string;
139
- triggerId: string;
140
- workflowId: string;
141
- targetId: string;
142
- status: RunStatus;
143
- input: Json;
144
- createdAt: string;
145
- sentAt?: string;
146
- startedAt?: string;
147
- finishedAt?: string;
148
- output?: Json;
149
- error?: string;
150
- }
151
- export interface ListRuns extends PageQuery {
152
- status?: RunStatus;
153
- }
154
134
  export interface AutomationCrud {
155
135
  readonly automations: {
156
136
  create(input: CreateAutomation): Promise<Automation>;
@@ -183,6 +163,7 @@ export interface AutomationCrud {
183
163
  delete(automationId: string): Promise<void>;
184
164
  };
185
165
  readonly runs: {
166
+ runNow(automationId: string, input?: RunAutomationNow): Promise<AutomationRun>;
186
167
  list(automationId: string, query?: ListRuns): Promise<Page<AutomationRun>>;
187
168
  get(automationId: string, runId: string): Promise<AutomationRun | null>;
188
169
  };
@@ -190,4 +171,3 @@ export interface AutomationCrud {
190
171
  export interface AutomationCrudService {
191
172
  for(principal: AutomationPrincipal): AutomationCrud;
192
173
  }
193
- export {};
@@ -1,14 +1,16 @@
1
1
  import type { AutomationPrincipal } from '../contract.js';
2
2
  import type { AutomationCrudRepository } from './repository.js';
3
+ import type { WebhookEndpoints } from '../webhook.js';
3
4
  export interface CrudContext {
4
5
  repository: AutomationCrudRepository;
5
6
  principal: AutomationPrincipal;
6
7
  clock: () => Date;
8
+ webhooks: WebhookEndpoints;
7
9
  read(): void;
8
10
  write(): void;
9
11
  runsRead(): void;
10
12
  exists(automationId: string): Promise<void>;
11
13
  }
12
- export declare function createContext(repository: AutomationCrudRepository, principal: AutomationPrincipal, clock: () => Date): CrudContext;
14
+ export declare function createContext(repository: AutomationCrudRepository, principal: AutomationPrincipal, clock: () => Date, webhooks: WebhookEndpoints): CrudContext;
13
15
  export declare function assertTarget(principal: AutomationPrincipal, targetId: string): void;
14
16
  export declare function resolveTarget(principal: AutomationPrincipal, targetId?: string): string;
@@ -1,11 +1,12 @@
1
1
  import { ForbiddenError, NotFoundError, ValidationError } from '../errors.js';
2
2
  import { id, requiredText } from '../validation.js';
3
- export function createContext(repository, principal, clock) {
3
+ export function createContext(repository, principal, clock, webhooks) {
4
4
  assertPrincipal(principal);
5
5
  return {
6
6
  repository,
7
7
  principal,
8
8
  clock,
9
+ webhooks,
9
10
  read: () => assertScope(principal, 'automations:read'),
10
11
  write: () => assertScope(principal, 'automations:write'),
11
12
  runsRead: () => assertScope(principal, 'runs:read'),
@@ -1,4 +1,4 @@
1
- import type { Automation, AutomationRun, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, ListAutomations, ListRuns, Page, PageQuery, ScheduleTrigger, Trigger, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow } from '../contract.js';
1
+ import type { Automation, AutomationRun, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, Json, ListAutomations, ListRuns, Page, PageQuery, ScheduleTrigger, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow } from '../contract.js';
2
2
  export type StoredScheduleCreate = Omit<Required<CreateScheduleTrigger>, 'maxOccurrences'> & {
3
3
  maxOccurrences: number | null;
4
4
  nextRunAt: string;
@@ -7,6 +7,16 @@ export type StoredScheduleUpdate = UpdateScheduleTrigger & {
7
7
  nextRunAt?: string;
8
8
  remainingOccurrences?: number | null;
9
9
  };
10
+ export type WebhookTriggerRecord = Omit<WebhookTrigger, 'webhookUrl'> & {
11
+ endpointId: string;
12
+ };
13
+ export type StoredWebhookCreate = Omit<Required<CreateWebhookTrigger>, 'secret'> & {
14
+ secret: string | null;
15
+ endpointId: string;
16
+ };
17
+ export type StoredWebhookUpdate = Omit<UpdateWebhookTrigger, 'rotateUrl'> & {
18
+ endpointId?: string;
19
+ };
10
20
  export type NormalizedListAutomations = Required<PageQuery> & Omit<ListAutomations, keyof PageQuery>;
11
21
  export type NormalizedListRuns = Required<PageQuery> & Omit<ListRuns, keyof PageQuery>;
12
22
  export interface AutomationCrudRepository {
@@ -20,16 +30,17 @@ export interface AutomationCrudRepository {
20
30
  getScheduleTrigger(userId: string, automationId: string, triggerId: string): Promise<ScheduleTrigger | null>;
21
31
  updateScheduleTrigger(userId: string, automationId: string, triggerId: string, patch: StoredScheduleUpdate): Promise<ScheduleTrigger | null>;
22
32
  deleteScheduleTrigger(userId: string, automationId: string, triggerId: string): Promise<boolean>;
23
- createWebhookTrigger(userId: string, automationId: string, input: Required<CreateWebhookTrigger>): Promise<WebhookTrigger>;
24
- listWebhookTriggers(userId: string, automationId: string, query: Required<PageQuery>): Promise<Page<WebhookTrigger>>;
25
- getWebhookTrigger(userId: string, automationId: string, triggerId: string): Promise<WebhookTrigger | null>;
26
- updateWebhookTrigger(userId: string, automationId: string, triggerId: string, patch: UpdateWebhookTrigger): Promise<WebhookTrigger | null>;
33
+ createWebhookTrigger(userId: string, automationId: string, input: StoredWebhookCreate): Promise<WebhookTriggerRecord>;
34
+ listWebhookTriggers(userId: string, automationId: string, query: Required<PageQuery>): Promise<Page<WebhookTriggerRecord>>;
35
+ getWebhookTrigger(userId: string, automationId: string, triggerId: string): Promise<WebhookTriggerRecord | null>;
36
+ updateWebhookTrigger(userId: string, automationId: string, triggerId: string, patch: StoredWebhookUpdate): Promise<WebhookTriggerRecord | null>;
27
37
  deleteWebhookTrigger(userId: string, automationId: string, triggerId: string): Promise<boolean>;
28
- listTriggers(userId: string, automationId: string): Promise<Trigger[]>;
38
+ listTriggers(userId: string, automationId: string): Promise<Array<ScheduleTrigger | WebhookTriggerRecord>>;
29
39
  createWorkflow(userId: string, automationId: string, input: Required<CreateWorkflow>): Promise<Workflow>;
30
40
  getWorkflow(userId: string, automationId: string): Promise<Workflow | null>;
31
41
  updateWorkflow(userId: string, automationId: string, patch: UpdateWorkflow): Promise<Workflow | null>;
32
42
  deleteWorkflow(userId: string, automationId: string): Promise<boolean>;
33
43
  listRuns(userId: string, automationId: string, query: NormalizedListRuns): Promise<Page<AutomationRun>>;
34
44
  getRun(userId: string, automationId: string, runId: string): Promise<AutomationRun | null>;
45
+ runAutomationNow(userId: string, automationId: string, input: Json, idempotencyKey: string | null, now: Date): Promise<AutomationRun | null>;
35
46
  }
@@ -1,3 +1,3 @@
1
1
  import type { AutomationCrud } from '../contract.js';
2
- import type { CrudContext } from './context.js';
2
+ import { type CrudContext } from './context.js';
3
3
  export declare function runOperations(context: CrudContext): AutomationCrud['runs'];
@@ -1,8 +1,31 @@
1
- import { parseListRuns } from '../schema.js';
1
+ import { AutomationError, NotFoundError, ValidationError } from '../errors.js';
2
+ import { compiledAutomationPlan } from '../plan.js';
3
+ import { parseListRuns, parseRunAutomationNow } from '../schema.js';
2
4
  import { id, page } from '../validation.js';
5
+ import { assertTarget } from './context.js';
3
6
  export function runOperations(context) {
4
7
  const { principal, repository } = context;
5
8
  return {
9
+ runNow: async (automationId, input = {}) => {
10
+ context.write();
11
+ const automationKey = id(automationId, 'Automation id');
12
+ const parsed = parseRunAutomationNow(input);
13
+ const automation = await repository.getAutomation(principal.userId, automationKey);
14
+ if (!automation)
15
+ throw new NotFoundError('Automation');
16
+ assertTarget(principal, automation.targetId);
17
+ if (!automation.enabled)
18
+ throw new ValidationError('Automation must be enabled before it can run');
19
+ const workflow = await repository.getWorkflow(principal.userId, automationKey);
20
+ if (!workflow?.compiled)
21
+ throw new ValidationError('Automation needs a compiled workflow before it can run');
22
+ compiledAutomationPlan(workflow.compiled);
23
+ const run = await repository.runAutomationNow(principal.userId, automationKey, { kind: 'manual', payload: parsed.input ?? {} }, parsed.idempotencyKey ?? null, context.clock());
24
+ if (!run) {
25
+ throw new AutomationError('conflict', 'Automation changed before the run was admitted; inspect it and try again');
26
+ }
27
+ return run;
28
+ },
6
29
  list: async (automationId, query = {}) => {
7
30
  context.runsRead();
8
31
  const parsed = parseListRuns(query);
@@ -3,7 +3,7 @@ import { parseCreateScheduleTrigger, parseCreateWebhookTrigger, parsePageQuery,
3
3
  import { nextCronAt } from '../schedule.js';
4
4
  import { id, newId, page, schedule } from '../validation.js';
5
5
  export function triggerOperations(context) {
6
- const { principal, repository } = context;
6
+ const { principal, repository, webhooks } = context;
7
7
  const unique = async (automationId, triggerId) => {
8
8
  const exists = (await repository.listTriggers(principal.userId, automationId))
9
9
  .some((trigger) => trigger.id === triggerId);
@@ -14,7 +14,7 @@ export function triggerOperations(context) {
14
14
  list: async (automationId) => {
15
15
  context.read();
16
16
  await context.exists(automationId);
17
- return repository.listTriggers(principal.userId, automationId);
17
+ return (await repository.listTriggers(principal.userId, automationId)).map((trigger) => (trigger.kind === 'webhook' ? publicWebhook(trigger, webhooks) : trigger));
18
18
  },
19
19
  schedule: {
20
20
  create: async (automationId, input) => {
@@ -80,31 +80,36 @@ export function triggerOperations(context) {
80
80
  const parsed = parseCreateWebhookTrigger(input);
81
81
  if (parsed.id)
82
82
  await unique(automationId, parsed.id);
83
- return repository.createWebhookTrigger(principal.userId, automationId, {
83
+ return publicWebhook(await repository.createWebhookTrigger(principal.userId, automationId, {
84
84
  id: parsed.id ? id(parsed.id, 'Webhook trigger id') : newId('webhook'),
85
85
  source: parsed.source || '*',
86
86
  event: parsed.event || '*',
87
- secret: parsed.secret,
87
+ secret: parsed.secret ?? null,
88
88
  enabled: parsed.enabled !== false,
89
- });
89
+ endpointId: webhooks.createId(),
90
+ }), webhooks);
90
91
  },
91
92
  list: async (automationId, query = {}) => {
92
93
  context.read();
93
94
  await context.exists(automationId);
94
- return repository.listWebhookTriggers(principal.userId, automationId, page(parsePageQuery(query)));
95
+ const result = await repository.listWebhookTriggers(principal.userId, automationId, page(parsePageQuery(query)));
96
+ return { ...result, items: result.items.map((trigger) => publicWebhook(trigger, webhooks)) };
95
97
  },
96
98
  get: async (automationId, triggerId) => {
97
99
  context.read();
98
100
  await context.exists(automationId);
99
- return repository.getWebhookTrigger(principal.userId, automationId, id(triggerId, 'Webhook trigger id'));
101
+ const trigger = await repository.getWebhookTrigger(principal.userId, automationId, id(triggerId, 'Webhook trigger id'));
102
+ return trigger ? publicWebhook(trigger, webhooks) : null;
100
103
  },
101
104
  update: async (automationId, triggerId, patch) => {
102
105
  context.write();
103
106
  await context.exists(automationId);
104
- const updated = await repository.updateWebhookTrigger(principal.userId, automationId, id(triggerId, 'Webhook trigger id'), parseUpdateWebhookTrigger(patch));
107
+ const parsed = parseUpdateWebhookTrigger(patch);
108
+ const { rotateUrl, ...changes } = parsed;
109
+ const updated = await repository.updateWebhookTrigger(principal.userId, automationId, id(triggerId, 'Webhook trigger id'), { ...changes, ...(rotateUrl ? { endpointId: webhooks.createId() } : {}) });
105
110
  if (!updated)
106
111
  throw new NotFoundError('Webhook trigger');
107
- return updated;
112
+ return publicWebhook(updated, webhooks);
108
113
  },
109
114
  delete: async (automationId, triggerId) => {
110
115
  context.write();
@@ -116,3 +121,7 @@ export function triggerOperations(context) {
116
121
  },
117
122
  };
118
123
  }
124
+ function publicWebhook(trigger, webhooks) {
125
+ const { endpointId, ...publicTrigger } = trigger;
126
+ return { ...publicTrigger, webhookUrl: webhooks.url(endpointId) };
127
+ }
@@ -1,9 +1,11 @@
1
1
  import type { AutomationCrud, AutomationCrudService as AutomationCrudServiceContract, AutomationPrincipal } from './contract.js';
2
2
  import type { AutomationCrudRepository } from './crud/repository.js';
3
- export type { AutomationCrudRepository, NormalizedListAutomations, NormalizedListRuns, StoredScheduleCreate, StoredScheduleUpdate, } from './crud/repository.js';
3
+ import { WebhookEndpoints } from './webhook.js';
4
+ export type { AutomationCrudRepository, NormalizedListAutomations, NormalizedListRuns, StoredScheduleCreate, StoredScheduleUpdate, StoredWebhookCreate, StoredWebhookUpdate, WebhookTriggerRecord, } from './crud/repository.js';
4
5
  export declare class AutomationCrudService implements AutomationCrudServiceContract {
6
+ #private;
5
7
  private readonly repository;
6
8
  private readonly clock;
7
- constructor(repository: AutomationCrudRepository, clock?: () => Date);
9
+ constructor(repository: AutomationCrudRepository, clock?: () => Date, webhooks?: WebhookEndpoints);
8
10
  for(principal: AutomationPrincipal): AutomationCrud;
9
11
  }
package/dist/src/crud.js CHANGED
@@ -3,15 +3,18 @@ import { createContext } from './crud/context.js';
3
3
  import { runOperations } from './crud/runs.js';
4
4
  import { triggerOperations } from './crud/triggers.js';
5
5
  import { workflowOperations } from './crud/workflow.js';
6
+ import { WebhookEndpoints } from './webhook.js';
6
7
  export class AutomationCrudService {
7
8
  repository;
8
9
  clock;
9
- constructor(repository, clock = () => new Date()) {
10
+ #webhooks;
11
+ constructor(repository, clock = () => new Date(), webhooks) {
10
12
  this.repository = repository;
11
13
  this.clock = clock;
14
+ this.#webhooks = webhooks ?? new WebhookEndpoints('http://automations.local', Buffer.from('local-automations-webhook-key-32-bytes-minimum'));
12
15
  }
13
16
  for(principal) {
14
- const context = createContext(this.repository, principal, this.clock);
17
+ const context = createContext(this.repository, principal, this.clock, this.#webhooks);
15
18
  return {
16
19
  automations: automationOperations(context),
17
20
  triggers: triggerOperations(context),
@@ -1,19 +1,17 @@
1
1
  import { Automations } from './automations.js';
2
- import type { AutomationTarget } from './types.js';
2
+ import { type WebhookEndpoints } from './webhook.js';
3
3
  export interface AutomationEventReceipt {
4
4
  receivedAt: string;
5
- targetId: string;
6
5
  source: string;
7
6
  event: string;
8
- runIds: string[];
7
+ runId?: string;
9
8
  status: 'pending' | 'unmatched';
10
9
  }
11
10
  export interface AutomationEventsApiConfig {
12
11
  delivery: Pick<Automations, 'receiveEvent'>;
13
- target(request: Request): Promise<AutomationTarget>;
12
+ endpoints: Pick<WebhookEndpoints, 'resolve'>;
14
13
  now?: () => Date;
15
14
  maxBodyBytes?: number;
16
- recentLimit?: number;
17
15
  }
18
16
  export type AutomationEventsApi = (request: Request) => Promise<Response>;
19
17
  export declare function createAutomationEventsApi(config: AutomationEventsApiConfig): AutomationEventsApi;
@@ -1,59 +1,55 @@
1
- import { Automations, EventRejectedError } from './automations.js';
2
- import { eventReference } from './webhook.js';
1
+ import { Automations, EventEndpointNotFoundError, EventRejectedError } from './automations.js';
2
+ import { webhookDeliveryKey } from './webhook.js';
3
3
  // Engine reference: amalgm-engine/runtime/scripts/amalgm-mcp/events/ingress.js.
4
- // The 2 MiB transport bound and safe recent-events door are preserved. The
5
- // authority changes deliberately: success now means matching runs are durable
6
- // in Supabase, never that an envelope was written to a local SQLite inbox.
4
+ // The 2 MiB transport bound is preserved. Success means the matching run is
5
+ // durable in Supabase, never that an envelope was written to a local inbox.
7
6
  const DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
8
- const DEFAULT_RECENT_LIMIT = 200;
9
7
  export function createAutomationEventsApi(config) {
10
- const recent = [];
11
8
  const now = config.now ?? (() => new Date());
12
9
  const maxBodyBytes = positiveInteger(config.maxBodyBytes, DEFAULT_MAX_BODY_BYTES);
13
- const recentLimit = positiveInteger(config.recentLimit, DEFAULT_RECENT_LIMIT);
14
10
  return async (request) => {
15
11
  const pathname = new URL(request.url).pathname;
16
- if (pathname !== '/events')
12
+ const token = pathname.match(/^\/e\/([^/]+)$/)?.[1];
13
+ if (!token)
17
14
  return json(404, { error: 'not found' });
18
- if (request.method === 'GET')
19
- return json(200, { events: recent.slice(-50) });
20
15
  if (request.method !== 'POST')
21
16
  return json(405, { error: 'method not allowed' });
22
17
  try {
18
+ const endpointId = config.endpoints.resolve(token);
19
+ if (!endpointId)
20
+ throw new EventEndpointNotFoundError();
23
21
  const body = await readLimitedBody(request, maxBodyBytes);
24
22
  const payload = parsePayload(body);
25
23
  const headers = Object.fromEntries(request.headers.entries());
26
- const target = await config.target(request);
27
24
  const receivedAt = now();
28
- const runs = await config.delivery.receiveEvent({
29
- target,
25
+ const admission = await config.delivery.receiveEvent({
26
+ endpointId,
30
27
  headers,
31
28
  body,
32
29
  payload,
30
+ deliveryKey: webhookDeliveryKey(headers, payload),
33
31
  now: receivedAt,
34
32
  });
35
- const firstInput = runs[0]?.input;
36
- const reference = firstInput?.kind === 'event'
37
- ? { source: firstInput.source, event: firstInput.event }
38
- : eventReference(headers, payload);
39
33
  const receipt = {
40
34
  receivedAt: receivedAt.toISOString(),
41
- targetId: target.targetId,
42
- source: reference.source,
43
- event: reference.event,
44
- runIds: runs.map(({ id }) => id),
45
- status: runs.length ? 'pending' : 'unmatched',
35
+ source: admission.source,
36
+ event: admission.event,
37
+ ...(admission.run ? { runId: admission.run.id } : {}),
38
+ status: admission.run ? 'pending' : 'unmatched',
46
39
  };
47
- recent.push(receipt);
48
- if (recent.length > recentLimit)
49
- recent.splice(0, recent.length - recentLimit);
50
40
  return json(202, { ok: true, accepted: true, ...receipt });
51
41
  }
52
42
  catch (error) {
43
+ if (error instanceof EventEndpointNotFoundError)
44
+ return json(404, { error: error.message });
53
45
  if (error instanceof EventRejectedError)
54
46
  return json(401, { error: error.message });
55
47
  if (error instanceof EventHttpError)
56
48
  return json(error.status, { error: error.message });
49
+ const status = Number(error?.status);
50
+ if (Number.isInteger(status) && status >= 400 && status <= 499) {
51
+ return json(status, { error: error instanceof Error ? error.message : String(error) });
52
+ }
57
53
  return json(500, { error: error instanceof Error ? error.message : String(error) });
58
54
  }
59
55
  };
@@ -1,16 +1,25 @@
1
- import type { AutomationPlan, Json } from './contract.js';
1
+ import type { Json } from './contract.js';
2
2
  import type { ClaimedAutomationRun, MachineRuns } from './machine.js';
3
+ export { automationPlan } from './plan.js';
3
4
  export interface AutomationActionPort {
4
5
  call(input: Readonly<{
5
6
  actionId: string;
6
7
  payload: Json;
7
8
  idempotencyKey: string;
9
+ signal: AbortSignal;
8
10
  }>): Promise<Json>;
9
11
  }
12
+ export interface AutomationRunExecutorOptions {
13
+ readonly now?: () => Date;
14
+ readonly heartbeatMs?: number;
15
+ readonly maxAttempts?: number;
16
+ readonly retryDelayMs?: (attempt: number) => number;
17
+ }
10
18
  export declare class AutomationRunExecutor {
19
+ #private;
11
20
  private readonly runs;
12
21
  private readonly actions;
13
- constructor(runs: MachineRuns, actions: AutomationActionPort);
22
+ constructor(runs: MachineRuns, actions: AutomationActionPort, options?: AutomationRunExecutorOptions);
14
23
  execute(run: ClaimedAutomationRun): Promise<void>;
15
24
  }
16
- export declare function automationPlan(snapshot: Json): AutomationPlan;
25
+ export declare function transientFailure(error: unknown): boolean;