@amalgm/automations 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AXIOMS.md CHANGED
@@ -27,6 +27,14 @@
27
27
  automation's owner.
28
28
  11. Storage records, Supabase RPCs, HTTP details, and MCP protocol details are
29
29
  implementation concerns, not SDK concepts.
30
- 12. During cutover, legacy and replacement implementations never write the
31
- same authority concurrently. Completed cutover deletes the legacy
32
- implementation.
30
+ 12. The control-plane SDK is the only configuration write path. The delivery
31
+ rail reads that configuration and writes only schedule clocks and run
32
+ state; it never owns a second automation-definition API.
33
+ 13. Admitting a run atomically verifies the current enabled configuration,
34
+ stores a secret-free snapshot, and, for a schedule, advances exactly the
35
+ firing instant that was claimed.
36
+ 14. Legacy local automation storage is not a compatibility authority. Engine
37
+ cutover migrates callers to this SDK and then deletes the old store.
38
+ 15. Event ingress returns success only after every admitted run is durable in
39
+ Supabase. Its recent-event list is a bounded, secret-free operational view,
40
+ never a second event or run authority.
package/PURPOSE.md CHANGED
@@ -17,10 +17,18 @@ The product has two composable halves over that one state:
17
17
  skill are adapters over that contract; they do not access Supabase directly
18
18
  or implement lifecycle rules.
19
19
  - **The delivery rail** — trigger admission, scheduling, and run state: the
20
- receipt-before-auth, lease, retry, and idempotency laws that decide when a
21
- run happens. When a machine is offline, new runs remain pending; when it
22
- reconnects, every pending run is sent through Amalgm's existing transport and
23
- started by the existing host runtime. Execution itself stays on the machine.
20
+ authentication, atomic admission, schedule-claim, reconnect, and
21
+ idempotency laws that decide when a run happens. It consumes the control
22
+ plane's persisted configuration; it does not expose another definition
23
+ write path. When a machine is offline, new runs remain pending; when it
24
+ reconnects, every pending run is sent through Amalgm's existing transport
25
+ and started by the existing host runtime. Execution itself stays on the
26
+ machine.
27
+
28
+ The event HTTP adapter is one thin door over that delivery rail. It bounds and
29
+ parses the webhook body, resolves the authenticated target supplied by the
30
+ host, and returns only after matching runs have been stored in Supabase. Its
31
+ recent-event response is an ephemeral, secret-free operational projection.
24
32
 
25
33
  Amalgm supplies a resolved authenticated principal from its user session or
26
34
  HMAC-refresh flow; future API keys resolve to the same principal capability.
@@ -31,5 +39,5 @@ the other's storage or reimplements the other's decisions.
31
39
 
32
40
  This extraction is complete only when every Automations surface in Engine calls
33
41
  this product service, product state has one writer, migrated behavior has parity
34
- tests, and the legacy `amalgm-mcp` automation implementation and storage are
35
- deleted.
42
+ tests, and the local `amalgm-mcp` automation implementation and storage are
43
+ deleted rather than retained as a compatibility layer.
package/README.md CHANGED
@@ -81,8 +81,6 @@ const automations = new Automations(new SupabaseStore(getSupabase()), {
81
81
  send: (run) => existingTunnel.send(run.targetId, run),
82
82
  });
83
83
 
84
- await automations.save(authenticatedUser.id, definition);
85
-
86
84
  await automations.receiveEvent({
87
85
  target: await core.resolveEventTarget(eventRef),
88
86
  headers,
@@ -97,24 +95,38 @@ await automations.updateRun({ userId, targetId }, runId, {
97
95
  output,
98
96
  });
99
97
 
100
- const history = await automations.listRuns(userId, automationId);
98
+ const history = await sdk.runs.list(automationId);
99
+ ```
100
+
101
+ The brick also owns the `/events` HTTP contract; a host supplies only target
102
+ identity and forwards the Fetch request:
103
+
104
+ ```ts
105
+ import { createAutomationEventsApi } from '@amalgm/automations';
106
+
107
+ const events = createAutomationEventsApi({
108
+ delivery: automations,
109
+ target: async (request) => resolveAutomationTarget(request),
110
+ });
111
+
112
+ const response = await events(request);
101
113
  ```
102
114
 
103
- `save`, `receiveEvent`, `fireDueCrons`, `targetOnline`, `updateRun`, and
104
- `listRuns` own the decisions.
105
- The surrounding Engine route, MCP tool, scheduler callback, and tunnel callback
106
- only translate and call them.
115
+ Configuration is written only through the auth-bound SDK shown above. The
116
+ delivery rail exposes `receiveEvent`, `fireDueCrons`, `targetOnline`, and
117
+ `updateRun`; it has no parallel save, delete, or history API. The surrounding
118
+ Engine route, scheduler callback, and tunnel callback only translate and call
119
+ these SDK operations.
107
120
 
108
121
  Every firing stores an immutable run snapshot in Supabase. Delivery and machine
109
122
  execution update that record instead of deleting it. When a machine is offline,
110
123
  new runs remain pending; on reconnect every pending run drains through the
111
124
  existing transport.
112
125
 
113
- The current implementation covers Supabase definitions, cron and event
114
- admission, secret verification, permanent run history, and reconnect drain. The
115
- remaining extraction is the proven workflow definition, validation,
116
- orchestration, and public surface behavior still owned by `amalgm-mcp`; it must
117
- move behind this same service boundary before legacy code is removed.
126
+ The implementation covers Supabase definitions, cron and event admission,
127
+ secret verification, permanent run history, and reconnect drain. Engine
128
+ cutover must call these published contracts and delete its local automation
129
+ store; this package intentionally provides no compatibility storage path.
118
130
 
119
131
  The purpose is in [PURPOSE.md](./PURPOSE.md), and the non-negotiable ownership
120
132
  rules are in [AXIOMS.md](./AXIOMS.md).
@@ -1,4 +1,4 @@
1
- import type { AutomationDefinition, AutomationLog, AutomationRun, AutomationStore, AutomationTarget, AutomationTransport, Json, RunUpdate } from './types.js';
1
+ import type { AutomationLog, AutomationRun, AutomationStore, AutomationTarget, AutomationTransport, Json, RunUpdate } from './types.js';
2
2
  export declare class EventRejectedError extends Error {
3
3
  constructor();
4
4
  }
@@ -8,9 +8,6 @@ export declare class Automations {
8
8
  readonly transport: AutomationTransport;
9
9
  readonly log: AutomationLog;
10
10
  constructor(store: AutomationStore, transport: AutomationTransport, log?: AutomationLog);
11
- save(userId: string, definition: AutomationDefinition, now?: Date): Promise<void>;
12
- delete(userId: string, automationId: string): Promise<boolean>;
13
- listRuns(userId: string, automationId?: string): Promise<AutomationRun[]>;
14
11
  receiveEvent(input: {
15
12
  target: AutomationTarget;
16
13
  headers: Record<string, string>;
@@ -1,6 +1,5 @@
1
- import { nextCronAt, validateCron } from './schedule.js';
1
+ import { nextCronAt } from './schedule.js';
2
2
  import { eventReferences, matchesEvent, verifyEventSecret } from './webhook.js';
3
- const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/;
4
3
  const noop = () => { };
5
4
  export class EventRejectedError extends Error {
6
5
  constructor() {
@@ -18,16 +17,6 @@ export class Automations {
18
17
  this.transport = transport;
19
18
  this.log = log;
20
19
  }
21
- async save(userId, definition, now = new Date()) {
22
- const automation = prepareAutomation(userId, definition, now);
23
- await this.store.saveAutomation(automation);
24
- }
25
- delete(userId, automationId) {
26
- return this.store.deleteAutomation(userId, automationId);
27
- }
28
- listRuns(userId, automationId) {
29
- return this.store.listRuns(userId, automationId);
30
- }
31
20
  async receiveEvent(input) {
32
21
  assertJson(input.payload, 'Event payload');
33
22
  const candidates = await this.store.eventTriggers(input.target);
@@ -135,53 +124,6 @@ export class Automations {
135
124
  return sent;
136
125
  }
137
126
  }
138
- function prepareAutomation(userId, definition, now) {
139
- if (!ID.test(definition.id))
140
- throw new Error('Automation id is invalid');
141
- if (!userId)
142
- throw new Error('Automation userId is required');
143
- if (!definition.targetId)
144
- throw new Error('Automation targetId is required');
145
- if (!ID.test(definition.trigger.id))
146
- throw new Error('Trigger id is invalid');
147
- if (!ID.test(definition.workflow.id))
148
- throw new Error('Workflow id is invalid');
149
- if (!definition.workflow.script.trim())
150
- throw new Error('Workflow script is required');
151
- if (definition.workflow.compiled !== undefined) {
152
- assertJson(definition.workflow.compiled, 'Compiled workflow');
153
- }
154
- if (definition.workflow.allowlist !== undefined) {
155
- assertJson(definition.workflow.allowlist, 'Workflow allowlist');
156
- }
157
- if (definition.workflow.limits !== undefined) {
158
- assertJson(definition.workflow.limits, 'Workflow limits');
159
- }
160
- const trigger = definition.trigger.kind === 'event'
161
- ? prepareEventTrigger(definition.trigger)
162
- : prepareCronTrigger(definition.trigger, now);
163
- return { ...definition, userId, enabled: definition.enabled !== false, trigger };
164
- }
165
- function prepareEventTrigger(trigger) {
166
- if (trigger.secret.length < 16)
167
- throw new Error(`Event trigger ${trigger.id} secret is too short`);
168
- return {
169
- ...trigger,
170
- enabled: trigger.enabled !== false,
171
- source: trigger.source || '*',
172
- event: trigger.event || '*',
173
- };
174
- }
175
- function prepareCronTrigger(trigger, now) {
176
- const timezone = trigger.timezone || 'UTC';
177
- validateCron(trigger.cron, timezone);
178
- return {
179
- ...trigger,
180
- enabled: trigger.enabled !== false,
181
- timezone,
182
- nextRunAt: nextCronAt(trigger.cron, timezone, now),
183
- };
184
- }
185
127
  function assertJson(value, label) {
186
128
  if (value === null || typeof value === 'string' || typeof value === 'boolean')
187
129
  return;
@@ -1,14 +1,20 @@
1
1
  import type { Automation, AutomationCrud, AutomationCrudService as AutomationCrudServiceContract, AutomationPrincipal, AutomationRun, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, ListAutomations, ListRuns, Page, PageQuery, ScheduleTrigger, Trigger, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow } from './contract.js';
2
+ export type StoredScheduleCreate = Required<CreateScheduleTrigger> & {
3
+ nextRunAt: string;
4
+ };
5
+ export type StoredScheduleUpdate = UpdateScheduleTrigger & {
6
+ nextRunAt?: string;
7
+ };
2
8
  export interface AutomationCrudRepository {
3
9
  createAutomation(userId: string, input: Required<CreateAutomation>): Promise<Automation>;
4
10
  listAutomations(userId: string, query: NormalizedListAutomations): Promise<Page<Automation>>;
5
11
  getAutomation(userId: string, automationId: string): Promise<Automation | null>;
6
12
  updateAutomation(userId: string, automationId: string, patch: UpdateAutomation): Promise<Automation | null>;
7
13
  deleteAutomation(userId: string, automationId: string): Promise<boolean>;
8
- createScheduleTrigger(userId: string, automationId: string, input: Required<CreateScheduleTrigger>): Promise<ScheduleTrigger>;
14
+ createScheduleTrigger(userId: string, automationId: string, input: StoredScheduleCreate): Promise<ScheduleTrigger>;
9
15
  listScheduleTriggers(userId: string, automationId: string, query: Required<PageQuery>): Promise<Page<ScheduleTrigger>>;
10
16
  getScheduleTrigger(userId: string, automationId: string, triggerId: string): Promise<ScheduleTrigger | null>;
11
- updateScheduleTrigger(userId: string, automationId: string, triggerId: string, patch: UpdateScheduleTrigger): Promise<ScheduleTrigger | null>;
17
+ updateScheduleTrigger(userId: string, automationId: string, triggerId: string, patch: StoredScheduleUpdate): Promise<ScheduleTrigger | null>;
12
18
  deleteScheduleTrigger(userId: string, automationId: string, triggerId: string): Promise<boolean>;
13
19
  createWebhookTrigger(userId: string, automationId: string, input: Required<CreateWebhookTrigger>): Promise<WebhookTrigger>;
14
20
  listWebhookTriggers(userId: string, automationId: string, query: Required<PageQuery>): Promise<Page<WebhookTrigger>>;
@@ -27,6 +33,7 @@ export type NormalizedListAutomations = Required<PageQuery> & Omit<ListAutomatio
27
33
  export type NormalizedListRuns = Required<PageQuery> & Omit<ListRuns, keyof PageQuery>;
28
34
  export declare class AutomationCrudService implements AutomationCrudServiceContract {
29
35
  private readonly repository;
30
- constructor(repository: AutomationCrudRepository);
36
+ private readonly clock;
37
+ constructor(repository: AutomationCrudRepository, clock?: () => Date);
31
38
  for(principal: AutomationPrincipal): AutomationCrud;
32
39
  }
package/dist/src/crud.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { ConflictError, ForbiddenError, NotFoundError } from './errors.js';
2
2
  import { parseCreateAutomation, parseCreateScheduleTrigger, parseCreateWebhookTrigger, parseCreateWorkflow, parseListAutomations, parseListRuns, parsePageQuery, parseUpdateAutomation, parseUpdateScheduleTrigger, parseUpdateWebhookTrigger, parseUpdateWorkflow, } from './schema.js';
3
3
  import { id, newId, page, requiredText, schedule, } from './validation.js';
4
+ import { nextCronAt } from './schedule.js';
4
5
  export class AutomationCrudService {
5
6
  repository;
6
- constructor(repository) {
7
+ clock;
8
+ constructor(repository, clock = () => new Date()) {
7
9
  this.repository = repository;
10
+ this.clock = clock;
8
11
  }
9
12
  for(principal) {
10
13
  assertPrincipal(principal);
@@ -88,6 +91,7 @@ export class AutomationCrudService {
88
91
  cron: parsed.cron,
89
92
  timezone,
90
93
  enabled: parsed.enabled !== false,
94
+ nextRunAt: nextCronAt(parsed.cron, timezone, this.clock()),
91
95
  });
92
96
  },
93
97
  list: async (automationId, query = {}) => {
@@ -104,13 +108,17 @@ export class AutomationCrudService {
104
108
  write();
105
109
  await exists(automationId);
106
110
  const parsed = parseUpdateScheduleTrigger(patch);
111
+ let storedPatch = parsed;
107
112
  if (parsed.cron !== undefined || parsed.timezone !== undefined) {
108
113
  const existing = await this.repository.getScheduleTrigger(principal.userId, automationId, triggerId);
109
114
  if (!existing)
110
115
  throw new NotFoundError('Schedule trigger');
111
- schedule(parsed.cron ?? existing.cron, parsed.timezone ?? existing.timezone);
116
+ const cron = parsed.cron ?? existing.cron;
117
+ const timezone = parsed.timezone ?? existing.timezone;
118
+ schedule(cron, timezone);
119
+ storedPatch = { ...parsed, nextRunAt: nextCronAt(cron, timezone, this.clock()) };
112
120
  }
113
- const updated = await this.repository.updateScheduleTrigger(principal.userId, automationId, id(triggerId, 'Schedule trigger id'), parsed);
121
+ const updated = await this.repository.updateScheduleTrigger(principal.userId, automationId, id(triggerId, 'Schedule trigger id'), storedPatch);
114
122
  if (!updated)
115
123
  throw new NotFoundError('Schedule trigger');
116
124
  return updated;
@@ -0,0 +1,19 @@
1
+ import { Automations } from './automations.js';
2
+ import type { AutomationTarget } from './types.js';
3
+ export interface AutomationEventReceipt {
4
+ receivedAt: string;
5
+ targetId: string;
6
+ source: string;
7
+ event: string;
8
+ runIds: string[];
9
+ status: 'pending' | 'unmatched';
10
+ }
11
+ export interface AutomationEventsApiConfig {
12
+ delivery: Pick<Automations, 'receiveEvent'>;
13
+ target(request: Request): Promise<AutomationTarget>;
14
+ now?: () => Date;
15
+ maxBodyBytes?: number;
16
+ recentLimit?: number;
17
+ }
18
+ export type AutomationEventsApi = (request: Request) => Promise<Response>;
19
+ export declare function createAutomationEventsApi(config: AutomationEventsApiConfig): AutomationEventsApi;
@@ -0,0 +1,107 @@
1
+ import { Automations, EventRejectedError } from './automations.js';
2
+ import { eventReference } from './webhook.js';
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.
7
+ const DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
8
+ const DEFAULT_RECENT_LIMIT = 200;
9
+ export function createAutomationEventsApi(config) {
10
+ const recent = [];
11
+ const now = config.now ?? (() => new Date());
12
+ const maxBodyBytes = positiveInteger(config.maxBodyBytes, DEFAULT_MAX_BODY_BYTES);
13
+ const recentLimit = positiveInteger(config.recentLimit, DEFAULT_RECENT_LIMIT);
14
+ return async (request) => {
15
+ const pathname = new URL(request.url).pathname;
16
+ if (pathname !== '/events')
17
+ return json(404, { error: 'not found' });
18
+ if (request.method === 'GET')
19
+ return json(200, { events: recent.slice(-50) });
20
+ if (request.method !== 'POST')
21
+ return json(405, { error: 'method not allowed' });
22
+ try {
23
+ const body = await readLimitedBody(request, maxBodyBytes);
24
+ const payload = parsePayload(body);
25
+ const headers = Object.fromEntries(request.headers.entries());
26
+ const target = await config.target(request);
27
+ const receivedAt = now();
28
+ const runs = await config.delivery.receiveEvent({
29
+ target,
30
+ headers,
31
+ body,
32
+ payload,
33
+ now: receivedAt,
34
+ });
35
+ const firstInput = runs[0]?.input;
36
+ const reference = firstInput?.kind === 'event'
37
+ ? { source: firstInput.source, event: firstInput.event }
38
+ : eventReference(headers, payload);
39
+ const receipt = {
40
+ 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',
46
+ };
47
+ recent.push(receipt);
48
+ if (recent.length > recentLimit)
49
+ recent.splice(0, recent.length - recentLimit);
50
+ return json(202, { ok: true, accepted: true, ...receipt });
51
+ }
52
+ catch (error) {
53
+ if (error instanceof EventRejectedError)
54
+ return json(401, { error: error.message });
55
+ if (error instanceof EventHttpError)
56
+ return json(error.status, { error: error.message });
57
+ return json(500, { error: error instanceof Error ? error.message : String(error) });
58
+ }
59
+ };
60
+ }
61
+ class EventHttpError extends Error {
62
+ status;
63
+ constructor(status, message) {
64
+ super(message);
65
+ this.status = status;
66
+ this.name = 'EventHttpError';
67
+ }
68
+ }
69
+ async function readLimitedBody(request, limit) {
70
+ if (!request.body)
71
+ return Buffer.alloc(0);
72
+ const reader = request.body.getReader();
73
+ const chunks = [];
74
+ let size = 0;
75
+ while (true) {
76
+ const { done, value } = await reader.read();
77
+ if (done)
78
+ break;
79
+ const chunk = Buffer.from(value);
80
+ size += chunk.length;
81
+ if (size > limit) {
82
+ await reader.cancel();
83
+ throw new EventHttpError(413, `Event body exceeds ${limit} bytes`);
84
+ }
85
+ chunks.push(chunk);
86
+ }
87
+ return Buffer.concat(chunks);
88
+ }
89
+ function parsePayload(body) {
90
+ if (body.length === 0)
91
+ return {};
92
+ try {
93
+ return JSON.parse(body.toString('utf8'));
94
+ }
95
+ catch {
96
+ throw new EventHttpError(400, 'Event body must be valid JSON');
97
+ }
98
+ }
99
+ function positiveInteger(value, fallback) {
100
+ return Number.isInteger(value) && value > 0 ? value : fallback;
101
+ }
102
+ function json(status, body) {
103
+ return new Response(JSON.stringify(body), {
104
+ status,
105
+ headers: { 'content-type': 'application/json' },
106
+ });
107
+ }
@@ -7,8 +7,9 @@ export { createAutomationMcpServer } from './mcp.js';
7
7
  export { SupabaseAutomationCrudRepository, type SupabaseRpcClient } from './supabase-crud.js';
8
8
  export type { Automation, AutomationCrud, AutomationCrudService as AutomationCrudServiceContract, AutomationPrincipal, AutomationRun, AutomationScope, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, Json, ListAutomations, ListRuns, Page, PageQuery, RunStatus, ScheduleTrigger, Trigger, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow, } from './contract.js';
9
9
  export { Automations, EventRejectedError } from './automations.js';
10
+ export { createAutomationEventsApi, type AutomationEventReceipt, type AutomationEventsApi, type AutomationEventsApiConfig, } from './events-http.js';
10
11
  export { nextCronAt, validateCron } from './schedule.js';
11
12
  export { eventReference, eventReferences, matchesEvent, normalizeHeaders, verifyEventSecret, } from './webhook.js';
12
13
  export { SupabaseStore } from './supabase-store.js';
13
14
  export type { SupabaseRpcClient as SupabaseStoreRpcClient } from './supabase-store.js';
14
- export type { AutomationDefinition, AutomationLog, AutomationRun as AutomationRunRecord, AutomationStore, AutomationTarget, AutomationTransport, CronTrigger, DueCronTrigger, EventTrigger, PreparedAutomation, PreparedTrigger, RunInput, RunUpdate, StoredEventTrigger, Trigger as StoreTrigger, WorkflowDefinition, } from './types.js';
15
+ export type { AutomationLog, AutomationRun as AutomationRunRecord, AutomationStore, AutomationTarget, AutomationTransport, DueCronTrigger, RunInput, RunUpdate, StoredEventTrigger, } from './types.js';
package/dist/src/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // The automation product has two composable halves:
2
2
  // - the configuration service (contract/crud/http/mcp/cli) — Supabase owns
3
3
  // everything except execution;
4
- // - the delivery rail (automations/webhook/schedule/store) — the receipt,
5
- // lease, and retry laws that decide when a run happens.
4
+ // - the delivery rail (automations/webhook/schedule/store) — authenticated
5
+ // admission, schedule claims, reconnect drain, and run-state transitions.
6
6
  // Colliding names keep the contract's spelling; the rail's store-level
7
7
  // records are exported under Store-scoped aliases.
8
8
  export { createAutomationClient } from './client.js';
@@ -13,6 +13,7 @@ export { runAutomationCli } from './cli.js';
13
13
  export { createAutomationMcpServer } from './mcp.js';
14
14
  export { SupabaseAutomationCrudRepository } from './supabase-crud.js';
15
15
  export { Automations, EventRejectedError } from './automations.js';
16
+ export { createAutomationEventsApi, } from './events-http.js';
16
17
  export { nextCronAt, validateCron } from './schedule.js';
17
18
  export { eventReference, eventReferences, matchesEvent, normalizeHeaders, verifyEventSecret, } from './webhook.js';
18
19
  export { SupabaseStore } from './supabase-store.js';
@@ -1,5 +1,5 @@
1
- import type { Automation, AutomationRun, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, Page, ScheduleTrigger, Trigger, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow } from './contract.js';
2
- import type { AutomationCrudRepository, NormalizedListAutomations, NormalizedListRuns } from './crud.js';
1
+ import type { Automation, AutomationRun, CreateAutomation, CreateWebhookTrigger, CreateWorkflow, Page, ScheduleTrigger, Trigger, UpdateAutomation, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow } from './contract.js';
2
+ import type { AutomationCrudRepository, NormalizedListAutomations, NormalizedListRuns, StoredScheduleCreate, StoredScheduleUpdate } from './crud.js';
3
3
  export interface SupabaseRpcClient {
4
4
  rpc(functionName: string, arguments_: Record<string, unknown>): PromiseLike<{
5
5
  data: unknown;
@@ -18,13 +18,13 @@ export declare class SupabaseAutomationCrudRepository implements AutomationCrudR
18
18
  getAutomation(userId: string, automationId: string): Promise<Automation | null>;
19
19
  updateAutomation(userId: string, automationId: string, patch: UpdateAutomation): Promise<Automation | null>;
20
20
  deleteAutomation(userId: string, automationId: string): Promise<boolean>;
21
- createScheduleTrigger(userId: string, automationId: string, input: Required<CreateScheduleTrigger>): Promise<ScheduleTrigger>;
21
+ createScheduleTrigger(userId: string, automationId: string, input: StoredScheduleCreate): Promise<ScheduleTrigger>;
22
22
  listScheduleTriggers(userId: string, automationId: string, query: Required<{
23
23
  limit?: number;
24
24
  offset?: number;
25
25
  }>): Promise<Page<ScheduleTrigger>>;
26
26
  getScheduleTrigger(userId: string, automationId: string, triggerId: string): Promise<ScheduleTrigger | null>;
27
- updateScheduleTrigger(userId: string, automationId: string, triggerId: string, patch: UpdateScheduleTrigger): Promise<ScheduleTrigger | null>;
27
+ updateScheduleTrigger(userId: string, automationId: string, triggerId: string, patch: StoredScheduleUpdate): Promise<ScheduleTrigger | null>;
28
28
  deleteScheduleTrigger(userId: string, automationId: string, triggerId: string): Promise<boolean>;
29
29
  createWebhookTrigger(userId: string, automationId: string, input: Required<CreateWebhookTrigger>): Promise<WebhookTrigger>;
30
30
  listWebhookTriggers(userId: string, automationId: string, query: Required<{
@@ -1,4 +1,4 @@
1
- import type { AutomationStore, AutomationTarget, AutomationRun, DueCronTrigger, PreparedAutomation, RunInput, RunUpdate, StoredEventTrigger } from './types.js';
1
+ import type { AutomationStore, AutomationTarget, AutomationRun, DueCronTrigger, RunInput, RunUpdate, StoredEventTrigger } from './types.js';
2
2
  export interface SupabaseRpcClient {
3
3
  rpc(functionName: string, arguments_: Record<string, unknown>): PromiseLike<{
4
4
  data: unknown;
@@ -11,8 +11,6 @@ export declare class SupabaseStore implements AutomationStore {
11
11
  #private;
12
12
  readonly client: SupabaseRpcClient;
13
13
  constructor(client: SupabaseRpcClient);
14
- saveAutomation(automation: PreparedAutomation): Promise<void>;
15
- deleteAutomation(userId: string, automationId: string): Promise<boolean>;
16
14
  eventTriggers(target: AutomationTarget): Promise<StoredEventTrigger[]>;
17
15
  dueCronTriggers(now: Date): Promise<DueCronTrigger[]>;
18
16
  enqueueEvent(trigger: StoredEventTrigger, input: Extract<RunInput, {
@@ -20,7 +18,6 @@ export declare class SupabaseStore implements AutomationStore {
20
18
  }>, now: Date): Promise<AutomationRun | null>;
21
19
  enqueueCron(trigger: DueCronTrigger, nextRunAt: string, now: Date): Promise<AutomationRun | null>;
22
20
  pendingRuns(target: AutomationTarget): Promise<AutomationRun[]>;
23
- listRuns(userId: string, automationId?: string): Promise<AutomationRun[]>;
24
21
  markSent(runId: string, target: AutomationTarget, now: Date): Promise<boolean>;
25
22
  updateRun(target: AutomationTarget, runId: string, update: RunUpdate): Promise<AutomationRun | null>;
26
23
  }
@@ -22,15 +22,6 @@ export class SupabaseStore {
22
22
  constructor(client) {
23
23
  this.client = client;
24
24
  }
25
- async saveAutomation(automation) {
26
- await this.#call('save_amalgm_automation', { p_automation: automation });
27
- }
28
- deleteAutomation(userId, automationId) {
29
- return this.#call('delete_amalgm_automation', {
30
- p_user_id: userId,
31
- p_automation_id: automationId,
32
- });
33
- }
34
25
  async eventTriggers(target) {
35
26
  const rows = await this.#call('list_amalgm_event_triggers', {
36
27
  p_user_id: target.userId,
@@ -76,13 +67,6 @@ export class SupabaseStore {
76
67
  });
77
68
  return rows.map(automationRun);
78
69
  }
79
- async listRuns(userId, automationId) {
80
- const rows = await this.#call('list_amalgm_runs', {
81
- p_user_id: userId,
82
- p_automation_id: automationId || null,
83
- });
84
- return rows.map(automationRun);
85
- }
86
70
  markSent(runId, target, now) {
87
71
  return this.#call('mark_amalgm_run_sent', {
88
72
  p_run_id: runId,
@@ -1,58 +1,10 @@
1
- export type Json = null | boolean | number | string | Json[] | {
2
- [key: string]: Json;
3
- };
4
- interface TriggerBase {
5
- id: string;
6
- enabled?: boolean;
7
- }
8
- export interface CronTrigger extends TriggerBase {
9
- kind: 'cron';
10
- cron: string;
11
- timezone?: string;
12
- }
13
- export interface EventTrigger extends TriggerBase {
14
- kind: 'event';
15
- source?: string;
16
- event?: string;
17
- secret: string;
18
- }
19
- export type Trigger = CronTrigger | EventTrigger;
20
- export interface WorkflowDefinition {
21
- id: string;
22
- name?: string;
23
- script: string;
24
- compiled?: Json;
25
- allowlist?: Json;
26
- limits?: Json;
27
- }
28
- export interface AutomationDefinition {
29
- id: string;
30
- targetId: string;
31
- name?: string;
32
- description?: string;
33
- enabled?: boolean;
34
- trigger: Trigger;
35
- workflow: WorkflowDefinition;
36
- }
1
+ import type { Json } from './contract.js';
2
+ export type { Json } from './contract.js';
37
3
  /** The only authenticated identity Automations accepts from Core. */
38
4
  export interface AutomationTarget {
39
5
  userId: string;
40
6
  targetId: string;
41
7
  }
42
- export type PreparedTrigger = (CronTrigger & {
43
- enabled: boolean;
44
- timezone: string;
45
- nextRunAt: string;
46
- }) | (EventTrigger & {
47
- enabled: boolean;
48
- source: string;
49
- event: string;
50
- });
51
- export interface PreparedAutomation extends Omit<AutomationDefinition, 'enabled' | 'trigger'> {
52
- userId: string;
53
- enabled: boolean;
54
- trigger: PreparedTrigger;
55
- }
56
8
  interface StoredTriggerBase {
57
9
  userId: string;
58
10
  automationId: string;
@@ -106,8 +58,6 @@ export interface RunUpdate {
106
58
  error?: string;
107
59
  }
108
60
  export interface AutomationStore {
109
- saveAutomation(automation: PreparedAutomation): Promise<void>;
110
- deleteAutomation(userId: string, automationId: string): Promise<boolean>;
111
61
  eventTriggers(target: AutomationTarget): Promise<StoredEventTrigger[]>;
112
62
  dueCronTriggers(now: Date): Promise<DueCronTrigger[]>;
113
63
  enqueueEvent(trigger: StoredEventTrigger, input: Extract<RunInput, {
@@ -115,7 +65,6 @@ export interface AutomationStore {
115
65
  }>, now: Date): Promise<AutomationRun | null>;
116
66
  enqueueCron(trigger: DueCronTrigger, nextRunAt: string, now: Date): Promise<AutomationRun | null>;
117
67
  pendingRuns(target: AutomationTarget): Promise<AutomationRun[]>;
118
- listRuns(userId: string, automationId?: string): Promise<AutomationRun[]>;
119
68
  markSent(runId: string, target: AutomationTarget, now: Date): Promise<boolean>;
120
69
  updateRun(target: AutomationTarget, runId: string, update: RunUpdate): Promise<AutomationRun | null>;
121
70
  }
@@ -125,4 +74,3 @@ export interface AutomationTransport {
125
74
  send(run: AutomationRun): Promise<boolean>;
126
75
  }
127
76
  export type AutomationLog = (event: 'event.rejected' | 'run.pending' | 'drain.started' | 'run.sent' | 'run.failed', details: Readonly<Record<string, string>>) => void;
128
- export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@amalgm/automations",
3
- "version": "0.1.0",
4
- "description": "Amalgm's automation product: Supabase-backed configuration plus the delivery rail (receipts, leases, retries).",
3
+ "version": "0.1.2",
4
+ "description": "Amalgm's cloud automation SDK: Supabase-backed configuration, trigger admission, and run delivery.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
@@ -29,8 +29,8 @@
29
29
  }
30
30
  },
31
31
  "bin": {
32
- "amalgm-automations": "./dist/src/cli-main.js",
33
- "amalgm-automations-mcp": "./dist/src/mcp-main.js"
32
+ "amalgm-automations": "dist/src/cli-main.js",
33
+ "amalgm-automations-mcp": "dist/src/mcp-main.js"
34
34
  },
35
35
  "files": [
36
36
  "dist",
@@ -1,6 +1,5 @@
1
- -- Isolated integration-test schema for this service. The canonical deployment
2
- -- copy is amalgm-ui/supabase/migrations/20260806210000_create_automations.sql.
3
- -- Keep their SQL bodies equivalent.
1
+ -- Automations owns this schema contract. Deployments consume this migration;
2
+ -- no UI or local-runtime store is an authority for these records.
4
3
 
5
4
  CREATE EXTENSION IF NOT EXISTS pgcrypto;
6
5
 
@@ -40,6 +39,7 @@ CREATE TABLE amalgm_automation_triggers (
40
39
  enabled boolean NOT NULL DEFAULT true,
41
40
  cron text,
42
41
  timezone text,
42
+ next_run_at timestamptz,
43
43
  source text,
44
44
  event text,
45
45
  secret text,
@@ -50,16 +50,22 @@ CREATE TABLE amalgm_automation_triggers (
50
50
  REFERENCES amalgm_automations(user_id, id) ON DELETE CASCADE,
51
51
  CHECK (
52
52
  (kind = 'schedule' AND cron IS NOT NULL AND timezone IS NOT NULL
53
- AND source IS NULL AND event IS NULL AND secret IS NULL)
53
+ AND next_run_at IS NOT NULL AND source IS NULL AND event IS NULL
54
+ AND secret IS NULL)
54
55
  OR
55
- (kind = 'webhook' AND cron IS NULL AND timezone IS NULL AND source IS NOT NULL
56
- AND event IS NOT NULL AND length(secret) >= 16)
56
+ (kind = 'webhook' AND cron IS NULL AND timezone IS NULL
57
+ AND next_run_at IS NULL AND source IS NOT NULL AND event IS NOT NULL
58
+ AND length(secret) >= 16)
57
59
  )
58
60
  );
59
61
 
60
62
  CREATE INDEX amalgm_automation_triggers_by_automation
61
63
  ON amalgm_automation_triggers(user_id, automation_id, kind, created_at, id);
62
64
 
65
+ CREATE INDEX amalgm_automation_schedules_due
66
+ ON amalgm_automation_triggers(next_run_at, user_id, automation_id, id)
67
+ WHERE kind = 'schedule' AND enabled;
68
+
63
69
  CREATE TABLE amalgm_automation_runs (
64
70
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
65
71
  user_id uuid NOT NULL,
@@ -82,6 +88,10 @@ CREATE TABLE amalgm_automation_runs (
82
88
  CREATE INDEX amalgm_automation_runs_history
83
89
  ON amalgm_automation_runs(user_id, automation_id, created_at DESC, id DESC);
84
90
 
91
+ CREATE INDEX amalgm_automation_runs_pending
92
+ ON amalgm_automation_runs(user_id, target_id, created_at, id)
93
+ WHERE status = 'pending';
94
+
85
95
  ALTER TABLE amalgm_automations ENABLE ROW LEVEL SECURITY;
86
96
  ALTER TABLE amalgm_automation_workflows ENABLE ROW LEVEL SECURITY;
87
97
  ALTER TABLE amalgm_automation_triggers ENABLE ROW LEVEL SECURITY;
@@ -100,6 +110,7 @@ AS $$
100
110
  'enabled', p_trigger.enabled,
101
111
  'cron', p_trigger.cron,
102
112
  'timezone', p_trigger.timezone,
113
+ 'next_run_at', p_trigger.next_run_at,
103
114
  'source', p_trigger.source,
104
115
  'event', p_trigger.event,
105
116
  'secret_configured', p_trigger.secret IS NOT NULL,
@@ -221,10 +232,11 @@ DECLARE
221
232
  v_row amalgm_automation_triggers;
222
233
  BEGIN
223
234
  INSERT INTO amalgm_automation_triggers (
224
- user_id, automation_id, id, kind, enabled, cron, timezone
235
+ user_id, automation_id, id, kind, enabled, cron, timezone, next_run_at
225
236
  )
226
237
  SELECT p_user_id, p_automation_id, p_trigger->>'id', 'schedule',
227
- (p_trigger->>'enabled')::boolean, p_trigger->>'cron', p_trigger->>'timezone'
238
+ (p_trigger->>'enabled')::boolean, p_trigger->>'cron', p_trigger->>'timezone',
239
+ (p_trigger->>'nextRunAt')::timestamptz
228
240
  WHERE EXISTS (
229
241
  SELECT 1 FROM amalgm_automations
230
242
  WHERE user_id = p_user_id AND id = p_automation_id
@@ -291,6 +303,10 @@ BEGIN
291
303
  UPDATE amalgm_automation_triggers SET
292
304
  cron = CASE WHEN p_patch ? 'cron' THEN p_patch->>'cron' ELSE cron END,
293
305
  timezone = CASE WHEN p_patch ? 'timezone' THEN p_patch->>'timezone' ELSE timezone END,
306
+ next_run_at = CASE
307
+ WHEN p_patch ? 'nextRunAt' THEN (p_patch->>'nextRunAt')::timestamptz
308
+ ELSE next_run_at
309
+ END,
294
310
  enabled = CASE WHEN p_patch ? 'enabled' THEN (p_patch->>'enabled')::boolean ELSE enabled END,
295
311
  updated_at = now()
296
312
  WHERE user_id = p_user_id AND automation_id = p_automation_id
@@ -561,6 +577,208 @@ AS $$
561
577
  WHERE r.user_id = p_user_id AND r.automation_id = p_automation_id AND r.id::text = p_run_id;
562
578
  $$;
563
579
 
580
+ CREATE FUNCTION list_amalgm_event_triggers(p_user_id uuid, p_target_id text)
581
+ RETURNS TABLE (
582
+ user_id uuid,
583
+ automation_id text,
584
+ trigger_id text,
585
+ target_id text,
586
+ source text,
587
+ event text,
588
+ secret text
589
+ )
590
+ LANGUAGE sql
591
+ SECURITY DEFINER
592
+ SET search_path = public
593
+ AS $$
594
+ SELECT t.user_id, t.automation_id, t.id, a.target_id, t.source, t.event, t.secret
595
+ FROM amalgm_automation_triggers t
596
+ JOIN amalgm_automations a
597
+ ON a.user_id = t.user_id AND a.id = t.automation_id
598
+ WHERE t.kind = 'webhook' AND t.user_id = p_user_id
599
+ AND a.target_id = p_target_id AND t.enabled AND a.enabled;
600
+ $$;
601
+
602
+ CREATE FUNCTION list_due_amalgm_crons(p_now timestamptz)
603
+ RETURNS TABLE (
604
+ user_id uuid,
605
+ automation_id text,
606
+ trigger_id text,
607
+ target_id text,
608
+ cron text,
609
+ timezone text,
610
+ next_run_at timestamptz
611
+ )
612
+ LANGUAGE sql
613
+ SECURITY DEFINER
614
+ SET search_path = public
615
+ AS $$
616
+ SELECT t.user_id, t.automation_id, t.id, a.target_id,
617
+ t.cron, t.timezone, t.next_run_at
618
+ FROM amalgm_automation_triggers t
619
+ JOIN amalgm_automations a
620
+ ON a.user_id = t.user_id AND a.id = t.automation_id
621
+ WHERE t.kind = 'schedule' AND t.next_run_at <= p_now
622
+ AND t.enabled AND a.enabled
623
+ ORDER BY t.next_run_at, t.user_id, t.automation_id, t.id;
624
+ $$;
625
+
626
+ CREATE FUNCTION enqueue_amalgm_run(
627
+ p_kind text,
628
+ p_user_id uuid,
629
+ p_automation_id text,
630
+ p_trigger_id text,
631
+ p_target_id text,
632
+ p_expected_next_run_at timestamptz,
633
+ p_next_run_at timestamptz,
634
+ p_input jsonb,
635
+ p_now timestamptz
636
+ )
637
+ RETURNS SETOF amalgm_automation_runs
638
+ LANGUAGE plpgsql
639
+ SECURITY DEFINER
640
+ SET search_path = public
641
+ AS $$
642
+ BEGIN
643
+ IF p_kind = 'cron' THEN
644
+ PERFORM 1
645
+ FROM amalgm_automation_triggers t
646
+ JOIN amalgm_automations a
647
+ ON a.user_id = t.user_id AND a.id = t.automation_id
648
+ WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
649
+ AND t.id = p_trigger_id AND a.target_id = p_target_id
650
+ AND t.kind = 'schedule' AND t.next_run_at = p_expected_next_run_at
651
+ AND t.enabled AND a.enabled
652
+ FOR UPDATE OF t;
653
+ ELSIF p_kind = 'event' THEN
654
+ PERFORM 1
655
+ FROM amalgm_automation_triggers t
656
+ JOIN amalgm_automations a
657
+ ON a.user_id = t.user_id AND a.id = t.automation_id
658
+ WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
659
+ AND t.id = p_trigger_id AND a.target_id = p_target_id
660
+ AND t.kind = 'webhook' AND t.enabled AND a.enabled;
661
+ ELSE
662
+ RAISE EXCEPTION 'Invalid automation trigger kind';
663
+ END IF;
664
+ IF NOT FOUND THEN RETURN; END IF;
665
+
666
+ RETURN QUERY
667
+ INSERT INTO amalgm_automation_runs (
668
+ user_id, target_id, automation_id, trigger_id, workflow_id,
669
+ automation_payload, input, status, created_at
670
+ )
671
+ SELECT
672
+ a.user_id,
673
+ a.target_id,
674
+ a.id,
675
+ t.id,
676
+ w.id,
677
+ jsonb_strip_nulls(jsonb_build_object(
678
+ 'id', a.id,
679
+ 'targetId', a.target_id,
680
+ 'name', a.name,
681
+ 'description', a.description,
682
+ 'enabled', a.enabled,
683
+ 'trigger', jsonb_strip_nulls(jsonb_build_object(
684
+ 'id', t.id,
685
+ 'kind', CASE WHEN t.kind = 'schedule' THEN 'cron' ELSE 'event' END,
686
+ 'enabled', t.enabled,
687
+ 'cron', t.cron,
688
+ 'timezone', t.timezone,
689
+ 'source', t.source,
690
+ 'event', t.event
691
+ )),
692
+ 'workflow', jsonb_strip_nulls(jsonb_build_object(
693
+ 'id', w.id,
694
+ 'name', w.name,
695
+ 'script', w.script,
696
+ 'compiled', w.compiled,
697
+ 'allowlist', w.allowlist,
698
+ 'limits', w.limits
699
+ ))
700
+ )),
701
+ p_input,
702
+ 'pending',
703
+ p_now
704
+ FROM amalgm_automations a
705
+ JOIN amalgm_automation_triggers t
706
+ ON t.user_id = a.user_id AND t.automation_id = a.id
707
+ JOIN amalgm_automation_workflows w
708
+ ON w.user_id = a.user_id AND w.automation_id = a.id
709
+ WHERE a.user_id = p_user_id AND a.id = p_automation_id
710
+ AND t.id = p_trigger_id
711
+ RETURNING *;
712
+
713
+ IF p_kind = 'cron' THEN
714
+ UPDATE amalgm_automation_triggers
715
+ SET next_run_at = p_next_run_at, updated_at = p_now
716
+ WHERE user_id = p_user_id AND automation_id = p_automation_id
717
+ AND id = p_trigger_id AND kind = 'schedule'
718
+ AND next_run_at = p_expected_next_run_at;
719
+ END IF;
720
+ END;
721
+ $$;
722
+
723
+ CREATE FUNCTION list_amalgm_pending_runs(p_user_id uuid, p_target_id text)
724
+ RETURNS SETOF amalgm_automation_runs
725
+ LANGUAGE sql
726
+ SECURITY DEFINER
727
+ SET search_path = public
728
+ AS $$
729
+ SELECT * FROM amalgm_automation_runs
730
+ WHERE user_id = p_user_id AND target_id = p_target_id AND status = 'pending'
731
+ ORDER BY created_at, id;
732
+ $$;
733
+
734
+ CREATE FUNCTION mark_amalgm_run_sent(
735
+ p_run_id uuid,
736
+ p_user_id uuid,
737
+ p_target_id text,
738
+ p_now timestamptz
739
+ )
740
+ RETURNS boolean
741
+ LANGUAGE plpgsql
742
+ SECURITY DEFINER
743
+ SET search_path = public
744
+ AS $$
745
+ BEGIN
746
+ UPDATE amalgm_automation_runs SET status = 'sent', sent_at = p_now
747
+ WHERE id = p_run_id AND user_id = p_user_id AND target_id = p_target_id
748
+ AND status = 'pending';
749
+ RETURN FOUND;
750
+ END;
751
+ $$;
752
+
753
+ CREATE FUNCTION update_amalgm_run(
754
+ p_run_id uuid,
755
+ p_user_id uuid,
756
+ p_target_id text,
757
+ p_update jsonb
758
+ )
759
+ RETURNS SETOF amalgm_automation_runs
760
+ LANGUAGE plpgsql
761
+ SECURITY DEFINER
762
+ SET search_path = public
763
+ AS $$
764
+ BEGIN
765
+ IF p_update->>'status' NOT IN ('running', 'completed', 'failed') THEN
766
+ RAISE EXCEPTION 'Invalid run status';
767
+ END IF;
768
+
769
+ RETURN QUERY
770
+ UPDATE amalgm_automation_runs SET
771
+ status = p_update->>'status',
772
+ started_at = coalesce((p_update->>'startedAt')::timestamptz, started_at),
773
+ finished_at = coalesce((p_update->>'finishedAt')::timestamptz, finished_at),
774
+ output = CASE WHEN p_update ? 'output' THEN p_update->'output' ELSE output END,
775
+ error = CASE WHEN p_update ? 'error' THEN p_update->>'error' ELSE error END
776
+ WHERE id = p_run_id AND user_id = p_user_id AND target_id = p_target_id
777
+ AND status IN ('sent', 'running')
778
+ RETURNING *;
779
+ END;
780
+ $$;
781
+
564
782
  REVOKE ALL ON FUNCTION amalgm_trigger_json(amalgm_automation_triggers) FROM PUBLIC;
565
783
  REVOKE ALL ON FUNCTION create_amalgm_automation(uuid, jsonb) FROM PUBLIC;
566
784
  REVOKE ALL ON FUNCTION list_amalgm_automations(uuid, text, boolean, integer, integer) FROM PUBLIC;
@@ -584,6 +802,12 @@ REVOKE ALL ON FUNCTION update_amalgm_workflow(uuid, text, jsonb) FROM PUBLIC;
584
802
  REVOKE ALL ON FUNCTION delete_amalgm_workflow(uuid, text) FROM PUBLIC;
585
803
  REVOKE ALL ON FUNCTION list_amalgm_runs(uuid, text, text, integer, integer) FROM PUBLIC;
586
804
  REVOKE ALL ON FUNCTION get_amalgm_run(uuid, text, text) FROM PUBLIC;
805
+ REVOKE ALL ON FUNCTION list_amalgm_event_triggers(uuid, text) FROM PUBLIC;
806
+ REVOKE ALL ON FUNCTION list_due_amalgm_crons(timestamptz) FROM PUBLIC;
807
+ REVOKE ALL ON FUNCTION enqueue_amalgm_run(text, uuid, text, text, text, timestamptz, timestamptz, jsonb, timestamptz) FROM PUBLIC;
808
+ REVOKE ALL ON FUNCTION list_amalgm_pending_runs(uuid, text) FROM PUBLIC;
809
+ REVOKE ALL ON FUNCTION mark_amalgm_run_sent(uuid, uuid, text, timestamptz) FROM PUBLIC;
810
+ REVOKE ALL ON FUNCTION update_amalgm_run(uuid, uuid, text, jsonb) FROM PUBLIC;
587
811
 
588
812
  GRANT EXECUTE ON FUNCTION create_amalgm_automation(uuid, jsonb) TO service_role;
589
813
  GRANT EXECUTE ON FUNCTION list_amalgm_automations(uuid, text, boolean, integer, integer) TO service_role;
@@ -607,3 +831,9 @@ GRANT EXECUTE ON FUNCTION update_amalgm_workflow(uuid, text, jsonb) TO service_r
607
831
  GRANT EXECUTE ON FUNCTION delete_amalgm_workflow(uuid, text) TO service_role;
608
832
  GRANT EXECUTE ON FUNCTION list_amalgm_runs(uuid, text, text, integer, integer) TO service_role;
609
833
  GRANT EXECUTE ON FUNCTION get_amalgm_run(uuid, text, text) TO service_role;
834
+ GRANT EXECUTE ON FUNCTION list_amalgm_event_triggers(uuid, text) TO service_role;
835
+ GRANT EXECUTE ON FUNCTION list_due_amalgm_crons(timestamptz) TO service_role;
836
+ GRANT EXECUTE ON FUNCTION enqueue_amalgm_run(text, uuid, text, text, text, timestamptz, timestamptz, jsonb, timestamptz) TO service_role;
837
+ GRANT EXECUTE ON FUNCTION list_amalgm_pending_runs(uuid, text) TO service_role;
838
+ GRANT EXECUTE ON FUNCTION mark_amalgm_run_sent(uuid, uuid, text, timestamptz) TO service_role;
839
+ GRANT EXECUTE ON FUNCTION update_amalgm_run(uuid, uuid, text, jsonb) TO service_role;