@amalgm/automations 0.1.1 → 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
@@ -35,3 +35,6 @@
35
35
  firing instant that was claimed.
36
36
  14. Legacy local automation storage is not a compatibility authority. Engine
37
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
@@ -25,6 +25,11 @@ The product has two composable halves over that one state:
25
25
  and started by the existing host runtime. Execution itself stays on the
26
26
  machine.
27
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.
32
+
28
33
  Amalgm supplies a resolved authenticated principal from its user session or
29
34
  HMAC-refresh flow; future API keys resolve to the same principal capability.
30
35
  Core provides authenticated user identity, opaque machine identity,
package/README.md CHANGED
@@ -98,6 +98,20 @@ await automations.updateRun({ userId, targetId }, runId, {
98
98
  const history = await sdk.runs.list(automationId);
99
99
  ```
100
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);
113
+ ```
114
+
101
115
  Configuration is written only through the auth-bound SDK shown above. The
102
116
  delivery rail exposes `receiveEvent`, `fireDueCrons`, `targetOnline`, and
103
117
  `updateRun`; it has no parallel save, delete, or history API. The surrounding
@@ -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,6 +7,7 @@ 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';
package/dist/src/index.js CHANGED
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/automations",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Amalgm's cloud automation SDK: Supabase-backed configuration, trigger admission, and run delivery.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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",