@amalgm/automations 0.2.2 → 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.
- package/AXIOMS.md +29 -23
- package/PURPOSE.md +15 -10
- package/README.md +28 -2
- package/dist/host/config.d.ts +2 -0
- package/dist/host/config.js +8 -0
- package/dist/host/main.js +4 -10
- package/dist/host/server.js +1 -1
- package/dist/src/automations.d.ts +12 -3
- package/dist/src/automations.js +21 -12
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +5 -2
- package/dist/src/client.js +1 -0
- package/dist/src/contract.d.ts +11 -4
- package/dist/src/crud/context.d.ts +3 -1
- package/dist/src/crud/context.js +2 -1
- package/dist/src/crud/repository.d.ts +17 -6
- package/dist/src/crud/runs.d.ts +1 -1
- package/dist/src/crud/runs.js +24 -1
- package/dist/src/crud/triggers.js +18 -9
- package/dist/src/crud.d.ts +4 -2
- package/dist/src/crud.js +5 -2
- package/dist/src/events-http.d.ts +3 -5
- package/dist/src/events-http.js +18 -26
- package/dist/src/http.js +4 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +2 -2
- package/dist/src/mcp.js +9 -1
- package/dist/src/run-contract.d.ts +6 -0
- package/dist/src/schema.d.ts +29 -13
- package/dist/src/schema.js +10 -2
- package/dist/src/supabase-crud/mappers.d.ts +3 -2
- package/dist/src/supabase-crud/mappers.js +3 -0
- package/dist/src/supabase-crud/rows.d.ts +1 -0
- package/dist/src/supabase-crud/workflow-runs.d.ts +1 -1
- package/dist/src/supabase-crud/workflow-runs.js +10 -0
- package/dist/src/supabase-store.d.ts +3 -3
- package/dist/src/supabase-store.js +13 -10
- package/dist/src/tool-surface.d.ts +1 -0
- package/dist/src/tool-surface.js +1 -0
- package/dist/src/types.d.ts +7 -8
- package/dist/src/webhook.d.ts +15 -1
- package/dist/src/webhook.js +86 -0
- package/package.json +1 -1
- package/skills/automations/SKILL.md +12 -4
- package/supabase/migrations/20260831010000_public_webhook_endpoints.sql +287 -0
- package/supabase/migrations/20260831020000_manual_run_admission.sql +68 -0
package/AXIOMS.md
CHANGED
|
@@ -24,49 +24,55 @@
|
|
|
24
24
|
9. Supabase is authoritative for automations, triggers, workflow source, and
|
|
25
25
|
permanent run history. A definition edit or deletion never rewrites prior
|
|
26
26
|
runs.
|
|
27
|
-
10.
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
10. A webhook URL is a rotatable bearer capability for exactly one trigger. Its
|
|
28
|
+
random locator is not sufficient without the hosted service's HMAC, and
|
|
29
|
+
caller input never supplies an owner or target.
|
|
30
|
+
11. Optional provider signing secrets are persisted but write-only: normal
|
|
31
|
+
reads reveal only that a secret is configured.
|
|
32
|
+
12. Run history is read-only in the control plane and always scoped to its
|
|
30
33
|
automation's owner.
|
|
31
|
-
|
|
34
|
+
13. Storage records, Supabase RPCs, HTTP details, and MCP protocol details are
|
|
32
35
|
implementation concerns, not SDK concepts.
|
|
33
|
-
|
|
36
|
+
14. The control-plane SDK is the only configuration write path. The delivery
|
|
34
37
|
rail reads that configuration and writes only schedule clocks and run
|
|
35
38
|
state; it never owns a second automation-definition API.
|
|
36
|
-
|
|
39
|
+
15. Admitting a run atomically verifies the current enabled configuration,
|
|
37
40
|
stores a secret-free snapshot, and, for a schedule, advances exactly the
|
|
38
41
|
firing instant that was claimed.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
16. Legacy local automation storage is not a compatibility authority. Engine
|
|
43
|
+
is deprecated read-only evidence; active callers use this SDK and its
|
|
44
|
+
standalone service, and no old store may run beside them.
|
|
45
|
+
17. Event ingress returns success only after its admitted run is durable in
|
|
46
|
+
Supabase. A repeated explicit delivery id resolves to the same run.
|
|
47
|
+
18. A finite schedule decrements its durable remaining occurrence count in the
|
|
45
48
|
same transaction that admits a run; zero disables the trigger.
|
|
46
|
-
|
|
49
|
+
19. A machine receives work only by exclusively leasing runs whose persisted
|
|
47
50
|
target equals the `computer_id` in its DPoP-bound access token.
|
|
48
|
-
|
|
51
|
+
20. The configured public origin, never forwarding headers, reconstructs the
|
|
49
52
|
DPoP request URL behind the Fly proxy.
|
|
50
|
-
|
|
53
|
+
21. Machine execution consumes the immutable workflow snapshot stored on the
|
|
51
54
|
run. It never rediscovers or silently updates the automation definition.
|
|
52
|
-
|
|
55
|
+
22. A compiled workflow is a small declarative sequence of one or more tool actions. The
|
|
53
56
|
executor receives tool calling as a host capability and never embeds a
|
|
54
57
|
Channels, Shell, CLI, or provider special case.
|
|
55
|
-
|
|
58
|
+
23. Automations is a standalone hosted service. Gateway owns none of its API,
|
|
56
59
|
scheduling, claim, execution, or persistence path.
|
|
57
|
-
|
|
60
|
+
24. The durable run ledger is the offline queue. The platform never keeps a
|
|
58
61
|
second online-machine delivery buffer and never drops an unclaimed run.
|
|
59
|
-
|
|
62
|
+
25. A run advances through its immutable plan in order. Every step transition
|
|
60
63
|
commits under the run's current lease before execution advances, and a
|
|
61
64
|
completed step is never intentionally invoked again.
|
|
62
|
-
|
|
65
|
+
26. A machine may begin an action only after the service confirms its current
|
|
63
66
|
lease. It renews that lease while the action is running and stops advancing
|
|
64
67
|
when renewal fails.
|
|
65
|
-
|
|
68
|
+
27. One run-step pair has one stable idempotency key. Step ids are unique in a
|
|
66
69
|
plan, and every action host receives that key and a cancellation signal.
|
|
67
|
-
|
|
70
|
+
28. Only transient transport failures retry. They release the same run with a
|
|
68
71
|
bounded future retry time and retain completed step output; invalid plans,
|
|
69
72
|
missing actions, authorization failures, and other deterministic errors
|
|
70
73
|
are terminal.
|
|
71
|
-
|
|
74
|
+
29. The selected target executes every action effect. The hosted service owns
|
|
72
75
|
only configuration, admission, leases, the step journal, and run history.
|
|
76
|
+
30. Run Now is triggerless manual admission. It atomically snapshots the
|
|
77
|
+
current enabled automation and compiled workflow into the pending ledger;
|
|
78
|
+
it never executes inline or changes a trigger's schedule state.
|
package/PURPOSE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
5
|
Automations is the sole owner of Amalgm automation behavior and data. It lets an
|
|
6
|
-
authenticated user create, inspect, change, and
|
|
6
|
+
authenticated user create, inspect, change, delete, and manually run automation
|
|
7
7
|
configuration — an automation belongs to one user and one target, may own any
|
|
8
8
|
number of scheduled and webhook triggers, and may own one workflow script — and
|
|
9
9
|
have each trigger occurrence run on the selected existing Amalgm machine.
|
|
@@ -13,7 +13,7 @@ complete workflow, and permanent run history.
|
|
|
13
13
|
The product has two composable halves over that one state:
|
|
14
14
|
|
|
15
15
|
- **The configuration control plane** — one ergonomic, auth-bound SDK contract
|
|
16
|
-
for automation CRUD and run-history reads. The HTTP API, CLI, MCP server, and
|
|
16
|
+
for automation CRUD, manual run admission, and run-history reads. The HTTP API, CLI, MCP server, and
|
|
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
|
|
@@ -42,10 +42,14 @@ occurrence count in durable state, so “every minute for ten minutes” means t
|
|
|
42
42
|
admitted runs and then an automatically disabled trigger — not a timer that a
|
|
43
43
|
machine must remember.
|
|
44
44
|
|
|
45
|
-
The event HTTP adapter is one thin door over that delivery rail.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
The event HTTP adapter is one thin door over that delivery rail. Every webhook
|
|
46
|
+
trigger owns one opaque URL under `automations.amalgm.ai`; the URL resolves the
|
|
47
|
+
trigger, owner, and target, so a caller never supplies routing identity. The
|
|
48
|
+
adapter bounds and parses the body, optionally verifies the provider's signing
|
|
49
|
+
secret, deduplicates explicit provider delivery ids, and returns only after the
|
|
50
|
+
run has been stored in Supabase. The signed URL can be rotated without changing
|
|
51
|
+
the automation, and its usable bearer token cannot be reconstructed from the
|
|
52
|
+
database alone.
|
|
49
53
|
|
|
50
54
|
Amalgm supplies a resolved authenticated principal from its user session or
|
|
51
55
|
HMAC-refresh flow; future API keys resolve to the same principal capability.
|
|
@@ -57,7 +61,8 @@ connectivity, and narrow host capabilities. The product receives identity and
|
|
|
57
61
|
scopes — never raw credentials or Core storage — and neither side reaches into
|
|
58
62
|
the other's storage or reimplements the other's decisions.
|
|
59
63
|
|
|
60
|
-
This
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
This repository and its standalone Fly host are the active Automations
|
|
65
|
+
authority. Shell calls the published machine-claim and execution contracts
|
|
66
|
+
directly; the UI calls the hosted control plane through the published client.
|
|
67
|
+
Any remaining Engine automation code is deprecated migration/parity evidence,
|
|
68
|
+
not a cutover dependency or compatibility authority.
|
package/README.md
CHANGED
|
@@ -25,12 +25,19 @@ await sdk.triggers.schedule.create(automation.id, {
|
|
|
25
25
|
timezone: 'America/Los_Angeles',
|
|
26
26
|
maxOccurrences: 10,
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
const run = await sdk.runs.runNow(automation.id, {
|
|
30
|
+
input: { requestedFrom: 'automation-page' },
|
|
31
|
+
idempotencyKey: crypto.randomUUID(),
|
|
32
|
+
});
|
|
28
33
|
```
|
|
29
34
|
|
|
30
35
|
That schedule admits exactly ten durable runs, then disables itself. If the
|
|
31
36
|
machine is offline, the runs remain pending. Shell later claims only work whose
|
|
32
37
|
target matches its DPoP `computer_id`, executes the immutable tool-action plan,
|
|
33
|
-
and commits the result.
|
|
38
|
+
and commits the result. Run Now returns the newly admitted `pending` run; it
|
|
39
|
+
uses that same machine execution path and never executes workflow effects in
|
|
40
|
+
the API request.
|
|
34
41
|
|
|
35
42
|
## Surfaces
|
|
36
43
|
|
|
@@ -49,7 +56,8 @@ or depend on Amalgm Gateway.
|
|
|
49
56
|
An automation belongs to one user and target. It owns any number of schedule
|
|
50
57
|
or webhook triggers and zero or one workflow. Supabase owns definitions,
|
|
51
58
|
schedule clocks, immutable run snapshots, machine leases, and permanent run
|
|
52
|
-
history.
|
|
59
|
+
history. Every webhook trigger has one rotatable URL; an optional provider
|
|
60
|
+
signing secret is write-only.
|
|
53
61
|
|
|
54
62
|
Executable workflows use one small, non-empty format:
|
|
55
63
|
|
|
@@ -71,11 +79,29 @@ one action-calling capability, and each step receives the stable idempotency key
|
|
|
71
79
|
|
|
72
80
|
The control API lives under `/v1/automations`. The machine execution API is:
|
|
73
81
|
|
|
82
|
+
```text
|
|
83
|
+
POST /v1/automations/:automationId/runs
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The optional JSON body contains `input` and an `idempotencyKey`. Reusing the
|
|
87
|
+
key for the same automation returns the same admitted run.
|
|
88
|
+
|
|
89
|
+
The machine execution API is:
|
|
90
|
+
|
|
74
91
|
```text
|
|
75
92
|
POST /v1/machine/runs/claim
|
|
76
93
|
PATCH /v1/machine/runs/:runId
|
|
77
94
|
```
|
|
78
95
|
|
|
96
|
+
Public webhook admission uses the URL returned on each webhook trigger:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
POST https://automations.amalgm.ai/e/<opaque-capability>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`Idempotency-Key` and common provider delivery-id headers deduplicate retries
|
|
103
|
+
against the same trigger. The caller never supplies a user or target id.
|
|
104
|
+
|
|
79
105
|
The target is never accepted in a machine request. It comes from the verified
|
|
80
106
|
access token. Run leases expire and are safely reclaimable; terminal updates
|
|
81
107
|
must present the active lease token.
|
package/dist/host/config.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface AutomationsHostConfig {
|
|
2
2
|
readonly port: number;
|
|
3
3
|
readonly publicOrigin: string;
|
|
4
|
+
readonly webhookOrigin: string;
|
|
5
|
+
readonly webhookTokenKey: string;
|
|
4
6
|
readonly supabaseUrl: string;
|
|
5
7
|
readonly supabaseServiceRoleKey: string;
|
|
6
8
|
readonly authorizationIssuer: string;
|
package/dist/host/config.js
CHANGED
|
@@ -2,6 +2,8 @@ export function automationsHostConfig(env = process.env) {
|
|
|
2
2
|
return Object.freeze({
|
|
3
3
|
port: integer(env.PORT, 8080, 1, 65_535),
|
|
4
4
|
publicOrigin: url(env.AMALGAM_PUBLIC_ORIGIN, 'AMALGAM_PUBLIC_ORIGIN'),
|
|
5
|
+
webhookOrigin: url(env.AUTOMATIONS_WEBHOOK_ORIGIN, 'AUTOMATIONS_WEBHOOK_ORIGIN'),
|
|
6
|
+
webhookTokenKey: secret(env.AUTOMATIONS_WEBHOOK_TOKEN_KEY, 'AUTOMATIONS_WEBHOOK_TOKEN_KEY'),
|
|
5
7
|
supabaseUrl: url(env.SUPABASE_URL ?? env.NEXT_PUBLIC_SUPABASE_URL, 'SUPABASE_URL'),
|
|
6
8
|
supabaseServiceRoleKey: required(env.SUPABASE_SERVICE_ROLE_KEY, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
7
9
|
authorizationIssuer: url(env.AMALGM_AUTHORIZATION_ISSUER, 'AMALGM_AUTHORIZATION_ISSUER'),
|
|
@@ -13,6 +15,12 @@ function required(value, name) {
|
|
|
13
15
|
throw new Error(`${name} is required`);
|
|
14
16
|
return value.trim();
|
|
15
17
|
}
|
|
18
|
+
function secret(value, name) {
|
|
19
|
+
const result = required(value, name);
|
|
20
|
+
if (Buffer.byteLength(result) < 32)
|
|
21
|
+
throw new Error(`${name} must contain at least 32 bytes`);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
16
24
|
function url(value, name) {
|
|
17
25
|
const parsed = new URL(required(value, name));
|
|
18
26
|
const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
|
package/dist/host/main.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createMachineRunsApi } from '../src/machine-http.js';
|
|
|
8
8
|
import { SupabaseAutomationCrudRepository } from '../src/supabase-crud.js';
|
|
9
9
|
import { SupabaseMachineRunRepository } from '../src/supabase-machine.js';
|
|
10
10
|
import { SupabaseStore } from '../src/supabase-store.js';
|
|
11
|
+
import { WebhookEndpoints } from '../src/webhook.js';
|
|
11
12
|
import { createAutomationsAuthenticators } from './auth.js';
|
|
12
13
|
import { automationsHostConfig } from './config.js';
|
|
13
14
|
import { createAutomationsHost } from './server.js';
|
|
@@ -19,9 +20,10 @@ const authentication = createAutomationsAuthenticators({
|
|
|
19
20
|
issuer: config.authorizationIssuer,
|
|
20
21
|
supabase,
|
|
21
22
|
});
|
|
23
|
+
const webhooks = new WebhookEndpoints(config.webhookOrigin, config.webhookTokenKey);
|
|
22
24
|
const delivery = new Automations(new SupabaseStore(supabase), (event, details) => log(event, details));
|
|
23
25
|
const controlApi = createAutomationApi({
|
|
24
|
-
service: new AutomationCrudService(new SupabaseAutomationCrudRepository(supabase)),
|
|
26
|
+
service: new AutomationCrudService(new SupabaseAutomationCrudRepository(supabase), () => new Date(), webhooks),
|
|
25
27
|
authenticate: authentication.control,
|
|
26
28
|
});
|
|
27
29
|
const machineRepository = new SupabaseMachineRunRepository(supabase);
|
|
@@ -31,7 +33,7 @@ const machineApi = createMachineRunsApi({
|
|
|
31
33
|
});
|
|
32
34
|
const eventsApi = createAutomationEventsApi({
|
|
33
35
|
delivery,
|
|
34
|
-
|
|
36
|
+
endpoints: webhooks,
|
|
35
37
|
});
|
|
36
38
|
const host = createAutomationsHost({
|
|
37
39
|
publicOrigin: config.publicOrigin,
|
|
@@ -49,11 +51,3 @@ for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
|
49
51
|
function log(event, details = {}) {
|
|
50
52
|
console.log(JSON.stringify({ service: 'amalgm-automations', event, ...details }));
|
|
51
53
|
}
|
|
52
|
-
function eventTarget(request) {
|
|
53
|
-
const userId = request.headers.get('x-amalgm-user-id')?.trim() ?? '';
|
|
54
|
-
const targetId = request.headers.get('x-amalgm-target-id')?.trim() ?? '';
|
|
55
|
-
if (!/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(userId) || !targetId) {
|
|
56
|
-
throw Object.assign(new Error('Webhook routing headers are required'), { status: 400 });
|
|
57
|
-
}
|
|
58
|
-
return { userId, targetId };
|
|
59
|
-
}
|
package/dist/host/server.js
CHANGED
|
@@ -19,7 +19,7 @@ export function createAutomationsHost(options) {
|
|
|
19
19
|
return send(outgoing, Response.json({ ok: true }));
|
|
20
20
|
const request = await webRequest(incoming, options.publicOrigin, options.maxRequestBodyBytes ?? 2 * 1024 * 1024);
|
|
21
21
|
const pathname = new URL(request.url).pathname;
|
|
22
|
-
const api = pathname
|
|
22
|
+
const api = pathname.startsWith('/e/') && options.eventsApi
|
|
23
23
|
? options.eventsApi
|
|
24
24
|
: pathname.startsWith('/v1/machine/') ? options.machineApi : options.controlApi;
|
|
25
25
|
await send(outgoing, await api(request));
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import type { AutomationLog, AutomationRun, AutomationStore,
|
|
1
|
+
import type { AutomationLog, AutomationRun, AutomationStore, Json } from './types.js';
|
|
2
2
|
export declare class EventRejectedError extends Error {
|
|
3
3
|
constructor();
|
|
4
4
|
}
|
|
5
|
+
export declare class EventEndpointNotFoundError extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
export interface EventAdmission {
|
|
9
|
+
run: AutomationRun | null;
|
|
10
|
+
source: string;
|
|
11
|
+
event: string;
|
|
12
|
+
}
|
|
5
13
|
/** Admits trigger occurrences to the durable run ledger. It never delivers them. */
|
|
6
14
|
export declare class Automations {
|
|
7
15
|
#private;
|
|
@@ -9,13 +17,14 @@ export declare class Automations {
|
|
|
9
17
|
readonly log: AutomationLog;
|
|
10
18
|
constructor(store: AutomationStore, log?: AutomationLog);
|
|
11
19
|
receiveEvent(input: {
|
|
12
|
-
|
|
20
|
+
endpointId: string;
|
|
13
21
|
headers: Record<string, string>;
|
|
14
22
|
body: Buffer;
|
|
15
23
|
payload: Json;
|
|
24
|
+
deliveryKey: string | null;
|
|
16
25
|
source?: string;
|
|
17
26
|
event?: string;
|
|
18
27
|
now?: Date;
|
|
19
|
-
}): Promise<
|
|
28
|
+
}): Promise<EventAdmission>;
|
|
20
29
|
fireDueCrons(now?: Date, maximumRuns?: number): Promise<AutomationRun[]>;
|
|
21
30
|
}
|
package/dist/src/automations.js
CHANGED
|
@@ -7,6 +7,12 @@ export class EventRejectedError extends Error {
|
|
|
7
7
|
this.name = 'EventRejectedError';
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
export class EventEndpointNotFoundError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('Webhook endpoint was not found');
|
|
13
|
+
this.name = 'EventEndpointNotFoundError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
10
16
|
/** Admits trigger occurrences to the durable run ledger. It never delivers them. */
|
|
11
17
|
export class Automations {
|
|
12
18
|
store;
|
|
@@ -17,28 +23,31 @@ export class Automations {
|
|
|
17
23
|
}
|
|
18
24
|
async receiveEvent(input) {
|
|
19
25
|
assertJson(input.payload, 'Event payload');
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
const trigger = await this.store.eventTrigger(input.endpointId);
|
|
27
|
+
if (!trigger)
|
|
28
|
+
throw new EventEndpointNotFoundError();
|
|
29
|
+
if (!verifyEventSecret(trigger.secret, input.headers, input.body)) {
|
|
30
|
+
this.log('event.rejected', { triggerId: trigger.triggerId });
|
|
24
31
|
throw new EventRejectedError();
|
|
25
32
|
}
|
|
26
33
|
const references = input.source && input.event
|
|
27
34
|
? { primary: { source: input.source, event: input.event } }
|
|
28
35
|
: eventReferences(input.headers, input.payload);
|
|
29
36
|
let reference = references.primary;
|
|
30
|
-
let
|
|
31
|
-
if (
|
|
37
|
+
let matches = matchesEvent(trigger, reference.source, reference.event);
|
|
38
|
+
if (!matches && references.fallback) {
|
|
32
39
|
reference = references.fallback;
|
|
33
|
-
|
|
40
|
+
matches = matchesEvent(trigger, reference.source, reference.event);
|
|
34
41
|
}
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
42
|
+
if (!matches)
|
|
43
|
+
return { run: null, ...reference };
|
|
37
44
|
const now = input.now || new Date();
|
|
38
45
|
const runInput = { kind: 'event', ...reference, payload: input.payload };
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const run = await this.store.enqueueEvent(trigger, runInput, now, input.deliveryKey);
|
|
47
|
+
if (!run)
|
|
48
|
+
return { run: null, ...reference };
|
|
49
|
+
this.#logPending([run]);
|
|
50
|
+
return { run, ...reference };
|
|
42
51
|
}
|
|
43
52
|
async fireDueCrons(now = new Date(), maximumRuns = 1_000) {
|
|
44
53
|
if (!Number.isInteger(maximumRuns) || maximumRuns < 1)
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AutomationCrud } from './contract.js';
|
|
|
2
2
|
interface Output {
|
|
3
3
|
write(chunk: string): unknown;
|
|
4
4
|
}
|
|
5
|
-
export declare const automationCliHelp = "Usage: amalgm-automations <resource> <action>\n\nautomations create|list|get|update|delete\ntriggers list | schedule <create|list|get|update|delete> | webhook <create|list|get|update|delete>\nworkflow create|get|update|delete\nruns list|get\n\nList filters:\n automations list [--target-id ID] [--enabled true|false] [--limit N] [--offset N]\n triggers schedule|webhook list AUTOMATION_ID [--limit N] [--offset N]\n runs list AUTOMATION_ID [--status STATUS] [--limit N] [--offset N]\n";
|
|
5
|
+
export declare const automationCliHelp = "Usage: amalgm-automations <resource> <action>\n\nautomations create|list|get|update|delete\ntriggers list | schedule <create|list|get|update|delete> | webhook <create|list|get|update|delete>\nworkflow create|get|update|delete\nruns run-now|list|get\n\nList filters:\n automations list [--target-id ID] [--enabled true|false] [--limit N] [--offset N]\n triggers schedule|webhook list AUTOMATION_ID [--limit N] [--offset N]\n runs list AUTOMATION_ID [--status STATUS] [--limit N] [--offset N]\n";
|
|
6
6
|
export declare function runAutomationCli(argv: string[], sdk: AutomationCrud, output?: {
|
|
7
7
|
stdout?: Output;
|
|
8
8
|
stderr?: Output;
|
package/dist/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ export const automationCliHelp = `Usage: amalgm-automations <resource> <action>
|
|
|
4
4
|
automations create|list|get|update|delete
|
|
5
5
|
triggers list | schedule <create|list|get|update|delete> | webhook <create|list|get|update|delete>
|
|
6
6
|
workflow create|get|update|delete
|
|
7
|
-
runs list|get
|
|
7
|
+
runs run-now|list|get
|
|
8
8
|
|
|
9
9
|
List filters:
|
|
10
10
|
automations list [--target-id ID] [--enabled true|false] [--limit N] [--offset N]
|
|
@@ -109,11 +109,14 @@ async function workflowCommand(action, args, sdk) {
|
|
|
109
109
|
}
|
|
110
110
|
async function runsCommand(action, args, sdk) {
|
|
111
111
|
const automationId = required(args, 0, 'automation id');
|
|
112
|
+
if (action === 'run-now') {
|
|
113
|
+
return sdk.runs.runNow(automationId, args[1] ? await jsonFile(args[1]) : {});
|
|
114
|
+
}
|
|
112
115
|
if (action === 'list')
|
|
113
116
|
return sdk.runs.list(automationId, runListOptions(args.slice(1)));
|
|
114
117
|
if (action === 'get')
|
|
115
118
|
return sdk.runs.get(automationId, required(args, 1, 'run id'));
|
|
116
|
-
throw new Error('Usage: runs <list|get>');
|
|
119
|
+
throw new Error('Usage: runs <run-now|list|get>');
|
|
117
120
|
}
|
|
118
121
|
function automationListOptions(args) {
|
|
119
122
|
const options = readOptions(args, new Set(['--target-id', '--enabled', '--limit', '--offset']));
|
package/dist/src/client.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createAutomationClient(options) {
|
|
|
34
34
|
delete: (automationId) => request(workflowPath(automationId), 'DELETE'),
|
|
35
35
|
},
|
|
36
36
|
runs: {
|
|
37
|
+
runNow: (automationId, input = {}) => request(runsPath(automationId), 'POST', input),
|
|
37
38
|
list: (automationId, query = {}) => request(`${runsPath(automationId)}${queryString(query)}`, 'GET'),
|
|
38
39
|
get: (automationId, runId) => request(`${runsPath(automationId)}/${part(runId)}`, 'GET', undefined, true),
|
|
39
40
|
},
|
package/dist/src/contract.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type Json = null | boolean | number | string | Json[] | {
|
|
2
2
|
[key: string]: Json;
|
|
3
3
|
};
|
|
4
|
-
import type { AutomationRun, ListRuns } from './run-contract.js';
|
|
5
|
-
export type { AutomationPlan, AutomationRun, AutomationRunJournal, AutomationStepRun, AutomationStepStatus, ListRuns, RunStatus, ToolActionStep, } from './run-contract.js';
|
|
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';
|
|
6
6
|
export type AutomationScope = 'automations:read' | 'automations:write' | 'runs:read' | 'runs:execute' | '*';
|
|
7
7
|
/** Identity resolved by Amalgm before the SDK is bound to a caller. */
|
|
8
8
|
export interface AutomationPrincipal {
|
|
@@ -68,6 +68,8 @@ export interface WebhookTrigger extends TriggerBase {
|
|
|
68
68
|
kind: 'webhook';
|
|
69
69
|
source: string;
|
|
70
70
|
event: string;
|
|
71
|
+
/** Bearer-capability URL. Treat it as a credential and rotate it if exposed. */
|
|
72
|
+
webhookUrl: string;
|
|
71
73
|
secretConfigured: boolean;
|
|
72
74
|
}
|
|
73
75
|
export type Trigger = ScheduleTrigger | WebhookTrigger;
|
|
@@ -90,13 +92,17 @@ export interface CreateWebhookTrigger {
|
|
|
90
92
|
id?: string;
|
|
91
93
|
source?: string;
|
|
92
94
|
event?: string;
|
|
93
|
-
secret
|
|
95
|
+
/** Optional provider signing secret; the generated webhook URL authenticates admission. */
|
|
96
|
+
secret?: string;
|
|
94
97
|
enabled?: boolean;
|
|
95
98
|
}
|
|
96
99
|
export interface UpdateWebhookTrigger {
|
|
97
100
|
source?: string;
|
|
98
101
|
event?: string;
|
|
99
|
-
|
|
102
|
+
/** Set null to remove provider-signature verification. */
|
|
103
|
+
secret?: string | null;
|
|
104
|
+
/** Invalidates the existing URL and returns its replacement. */
|
|
105
|
+
rotateUrl?: boolean;
|
|
100
106
|
enabled?: boolean;
|
|
101
107
|
}
|
|
102
108
|
export interface Workflow {
|
|
@@ -157,6 +163,7 @@ export interface AutomationCrud {
|
|
|
157
163
|
delete(automationId: string): Promise<void>;
|
|
158
164
|
};
|
|
159
165
|
readonly runs: {
|
|
166
|
+
runNow(automationId: string, input?: RunAutomationNow): Promise<AutomationRun>;
|
|
160
167
|
list(automationId: string, query?: ListRuns): Promise<Page<AutomationRun>>;
|
|
161
168
|
get(automationId: string, runId: string): Promise<AutomationRun | null>;
|
|
162
169
|
};
|
|
@@ -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;
|
package/dist/src/crud/context.js
CHANGED
|
@@ -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,
|
|
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:
|
|
24
|
-
listWebhookTriggers(userId: string, automationId: string, query: Required<PageQuery>): Promise<Page<
|
|
25
|
-
getWebhookTrigger(userId: string, automationId: string, triggerId: string): Promise<
|
|
26
|
-
updateWebhookTrigger(userId: string, automationId: string, triggerId: string, patch:
|
|
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<
|
|
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
|
}
|
package/dist/src/crud/runs.d.ts
CHANGED
package/dist/src/crud/runs.js
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
|
-
import {
|
|
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);
|