@objectstack/trigger-api 9.3.0
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/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +202 -0
- package/LICENSE.apache +202 -0
- package/dist/index.d.mts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +174 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
- package/src/api-trigger.test.ts +132 -0
- package/src/api-trigger.ts +216 -0
- package/src/index.ts +10 -0
- package/src/plugin.ts +105 -0
- package/tsconfig.json +18 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
2
|
+
import { AutomationContext } from '@objectstack/spec/contracts';
|
|
3
|
+
|
|
4
|
+
/** Mount point for inbound hooks on the host HTTP server. */
|
|
5
|
+
declare const HOOKS_PATH = "/api/v1/automation/hooks/:flowName/:hookId";
|
|
6
|
+
/**
|
|
7
|
+
* ApiTriggerPlugin (ADR-0041 Tier 1)
|
|
8
|
+
*
|
|
9
|
+
* Makes `type: 'api'` flows actually fire. The automation engine derives an
|
|
10
|
+
* `api` binding from the flow; this plugin provides the concrete trigger:
|
|
11
|
+
*
|
|
12
|
+
* - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host
|
|
13
|
+
* Hono app (resolved via the `http-server` service);
|
|
14
|
+
* - validates hookId + HMAC signature (`x-objectstack-signature`,
|
|
15
|
+
* GitHub/Stripe style, constant-time) against the flow's start-node
|
|
16
|
+
* config;
|
|
17
|
+
* - **enqueues** the payload (`queue` service) and ACKs 202 — flow
|
|
18
|
+
* execution happens on the queue consumer, never in-band with the
|
|
19
|
+
* inbound request (ADR-0041 §5: boundary triggers must not let a slow
|
|
20
|
+
* flow drop or block events). `x-idempotency-key` passes through to the
|
|
21
|
+
* queue's dedup window.
|
|
22
|
+
*
|
|
23
|
+
* Webhook payloads surface to the flow as the trigger record (`$record` /
|
|
24
|
+
* `record.*` / bare field references) — the same authoring surface
|
|
25
|
+
* record-change flows use.
|
|
26
|
+
*/
|
|
27
|
+
declare class ApiTriggerPlugin implements Plugin {
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
version: string;
|
|
31
|
+
dependencies: string[];
|
|
32
|
+
init(ctx: PluginContext): Promise<void>;
|
|
33
|
+
start(ctx: PluginContext): Promise<void>;
|
|
34
|
+
private resolveService;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Structural mirror of the automation engine's `FlowTriggerBinding` — same
|
|
39
|
+
* decoupling pattern as `trigger-schedule` / `trigger-record-change`: this
|
|
40
|
+
* package never imports `service-automation`.
|
|
41
|
+
*/
|
|
42
|
+
interface FlowTriggerBinding {
|
|
43
|
+
readonly flowName: string;
|
|
44
|
+
readonly object?: string;
|
|
45
|
+
readonly event?: string;
|
|
46
|
+
readonly condition?: string | {
|
|
47
|
+
dialect?: string;
|
|
48
|
+
source?: string;
|
|
49
|
+
ast?: unknown;
|
|
50
|
+
};
|
|
51
|
+
readonly schedule?: unknown;
|
|
52
|
+
readonly config?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/** Structural mirror of the engine's `FlowTrigger` extension point. */
|
|
55
|
+
interface FlowTrigger {
|
|
56
|
+
readonly type: string;
|
|
57
|
+
start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;
|
|
58
|
+
stop(flowName: string): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The slice of `IQueueService` this trigger needs. Boundary triggers MUST
|
|
62
|
+
* ingest through the queue (ADR-0041 §5): inbound HTTP is the first event
|
|
63
|
+
* source whose rate we don't control, and a slow flow execution may neither
|
|
64
|
+
* drop nor block inbound events. At-least-once delivery; flows should be
|
|
65
|
+
* authored idempotently (an `x-idempotency-key` header passes through to the
|
|
66
|
+
* queue's dedup window).
|
|
67
|
+
*/
|
|
68
|
+
interface QueueServiceSurface {
|
|
69
|
+
publish<T = unknown>(queue: string, data: T, options?: {
|
|
70
|
+
idempotencyKey?: string;
|
|
71
|
+
}): Promise<string>;
|
|
72
|
+
subscribe<T = unknown>(queue: string, handler: (message: {
|
|
73
|
+
data: T;
|
|
74
|
+
}) => Promise<void> | void): Promise<void>;
|
|
75
|
+
unsubscribe(queue: string): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/** Minimal logger surface (matches core's `ctx.logger`). */
|
|
78
|
+
interface TriggerLogger {
|
|
79
|
+
info(msg: string, ...args: unknown[]): void;
|
|
80
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
81
|
+
debug?(msg: string, ...args: unknown[]): void;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* GitHub/Stripe-style HMAC verification: the sender computes
|
|
85
|
+
* `sha256=<hex hmac of the raw body>` with the shared per-flow secret and
|
|
86
|
+
* sends it as `x-objectstack-signature`.
|
|
87
|
+
*/
|
|
88
|
+
declare function verifySignature(secret: string, rawBody: string, header: string | undefined): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* `api` flow trigger (ADR-0041 Tier 1) — inbound webhook/HTTP.
|
|
91
|
+
*
|
|
92
|
+
* The engine binds every `type: 'api'` flow to this trigger; `start()` arms a
|
|
93
|
+
* hook (URL path + optional HMAC secret from the start node's `config`) and
|
|
94
|
+
* subscribes a queue consumer that runs the flow. The HTTP side
|
|
95
|
+
* ({@link handleRequest}) validates and **enqueues** — it never executes the
|
|
96
|
+
* flow in-band:
|
|
97
|
+
*
|
|
98
|
+
* POST /api/v1/automation/hooks/:flowName/:hookId
|
|
99
|
+
* → 404 unknown flow / wrong hookId
|
|
100
|
+
* → 401 missing/bad HMAC signature (when the flow declares a `secret`)
|
|
101
|
+
* → 400 non-JSON body
|
|
102
|
+
* → 202 { accepted, messageId } — queued; a consumer executes the flow
|
|
103
|
+
*
|
|
104
|
+
* The JSON payload is exposed to the flow as the trigger record (`$record` /
|
|
105
|
+
* `record.*`, fields flattened to bare references) plus `params` — the same
|
|
106
|
+
* authoring surface record-change flows use.
|
|
107
|
+
*
|
|
108
|
+
* Start-node config keys:
|
|
109
|
+
* - `hookId` — URL path token (default `'default'`); rotate it to revoke
|
|
110
|
+
* old URLs without renaming the flow.
|
|
111
|
+
* - `secret` — HMAC-SHA256 shared secret. Strongly recommended; without it
|
|
112
|
+
* the endpoint accepts unsigned posts (the trigger logs a
|
|
113
|
+
* warning at arm time).
|
|
114
|
+
*/
|
|
115
|
+
declare class ApiTrigger implements FlowTrigger {
|
|
116
|
+
private readonly getQueue;
|
|
117
|
+
private readonly logger;
|
|
118
|
+
readonly type = "api";
|
|
119
|
+
private hooks;
|
|
120
|
+
constructor(getQueue: () => QueueServiceSurface | null, logger: TriggerLogger);
|
|
121
|
+
/** Currently armed hooks (for diagnostics/tests). */
|
|
122
|
+
listHooks(): Array<{
|
|
123
|
+
flowName: string;
|
|
124
|
+
hookId: string;
|
|
125
|
+
signed: boolean;
|
|
126
|
+
}>;
|
|
127
|
+
start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;
|
|
128
|
+
stop(flowName: string): void;
|
|
129
|
+
/**
|
|
130
|
+
* Handle one inbound request. Transport-agnostic: the plugin adapts this
|
|
131
|
+
* to the host HTTP server. Returns the response to send.
|
|
132
|
+
*/
|
|
133
|
+
handleRequest(input: {
|
|
134
|
+
flowName: string;
|
|
135
|
+
hookId: string;
|
|
136
|
+
rawBody: string;
|
|
137
|
+
signatureHeader?: string;
|
|
138
|
+
idempotencyKey?: string;
|
|
139
|
+
}): Promise<{
|
|
140
|
+
status: number;
|
|
141
|
+
body: Record<string, unknown>;
|
|
142
|
+
}>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { ApiTrigger, ApiTriggerPlugin, type FlowTrigger, type FlowTriggerBinding, HOOKS_PATH, type QueueServiceSurface, type TriggerLogger, verifySignature };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ApiTrigger: () => ApiTrigger,
|
|
24
|
+
ApiTriggerPlugin: () => ApiTriggerPlugin,
|
|
25
|
+
HOOKS_PATH: () => HOOKS_PATH,
|
|
26
|
+
verifySignature: () => verifySignature
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/api-trigger.ts
|
|
31
|
+
var import_node_crypto = require("crypto");
|
|
32
|
+
var QUEUE_PREFIX = "flow-api";
|
|
33
|
+
function safeEqual(a, b) {
|
|
34
|
+
const ab = Buffer.from(a, "utf8");
|
|
35
|
+
const bb = Buffer.from(b, "utf8");
|
|
36
|
+
if (ab.length !== bb.length) return false;
|
|
37
|
+
return (0, import_node_crypto.timingSafeEqual)(ab, bb);
|
|
38
|
+
}
|
|
39
|
+
function verifySignature(secret, rawBody, header) {
|
|
40
|
+
if (!header) return false;
|
|
41
|
+
const expected = "sha256=" + (0, import_node_crypto.createHmac)("sha256", secret).update(rawBody, "utf8").digest("hex");
|
|
42
|
+
return safeEqual(expected, header.trim());
|
|
43
|
+
}
|
|
44
|
+
var ApiTrigger = class {
|
|
45
|
+
constructor(getQueue, logger) {
|
|
46
|
+
this.getQueue = getQueue;
|
|
47
|
+
this.logger = logger;
|
|
48
|
+
this.type = "api";
|
|
49
|
+
this.hooks = /* @__PURE__ */ new Map();
|
|
50
|
+
}
|
|
51
|
+
/** Currently armed hooks (for diagnostics/tests). */
|
|
52
|
+
listHooks() {
|
|
53
|
+
return [...this.hooks.values()].map((h) => ({
|
|
54
|
+
flowName: h.flowName,
|
|
55
|
+
hookId: h.hookId,
|
|
56
|
+
signed: !!h.secret
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
start(binding, callback) {
|
|
60
|
+
const cfg = binding.config ?? {};
|
|
61
|
+
const hookId = typeof cfg.hookId === "string" && cfg.hookId.trim() ? cfg.hookId.trim() : "default";
|
|
62
|
+
const secret = typeof cfg.secret === "string" && cfg.secret.trim() ? cfg.secret.trim() : void 0;
|
|
63
|
+
const queue = `${QUEUE_PREFIX}:${binding.flowName}`;
|
|
64
|
+
const hook = { flowName: binding.flowName, hookId, secret, queue, callback };
|
|
65
|
+
this.hooks.set(binding.flowName, hook);
|
|
66
|
+
const q = this.getQueue();
|
|
67
|
+
if (!q) {
|
|
68
|
+
this.logger.warn(
|
|
69
|
+
`[trigger-api] no queue service \u2014 inbound posts for flow '${binding.flowName}' will be rejected until one is registered`
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
void q.subscribe(queue, async (message) => {
|
|
73
|
+
const payload = message?.data?.payload ?? {};
|
|
74
|
+
await hook.callback({
|
|
75
|
+
// The webhook body IS the trigger record: `$record`, `record.*`,
|
|
76
|
+
// and bare field references all resolve, matching how
|
|
77
|
+
// record-change flows are authored.
|
|
78
|
+
record: payload,
|
|
79
|
+
params: { ...payload },
|
|
80
|
+
event: "api"
|
|
81
|
+
});
|
|
82
|
+
}).catch((err) => {
|
|
83
|
+
this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (!secret) {
|
|
87
|
+
this.logger.warn(
|
|
88
|
+
`[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret \u2014 endpoint accepts unsigned posts`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
this.logger.info(
|
|
92
|
+
`[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? " (HMAC required)" : ""}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
stop(flowName) {
|
|
96
|
+
const hook = this.hooks.get(flowName);
|
|
97
|
+
if (!hook) return;
|
|
98
|
+
this.hooks.delete(flowName);
|
|
99
|
+
const q = this.getQueue();
|
|
100
|
+
if (q) void q.unsubscribe(hook.queue).catch(() => {
|
|
101
|
+
});
|
|
102
|
+
this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Handle one inbound request. Transport-agnostic: the plugin adapts this
|
|
106
|
+
* to the host HTTP server. Returns the response to send.
|
|
107
|
+
*/
|
|
108
|
+
async handleRequest(input) {
|
|
109
|
+
const hook = this.hooks.get(input.flowName);
|
|
110
|
+
if (!hook || !safeEqual(hook.hookId, input.hookId)) {
|
|
111
|
+
return { status: 404, body: { error: "not_found" } };
|
|
112
|
+
}
|
|
113
|
+
if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {
|
|
114
|
+
return { status: 401, body: { error: "invalid_signature" } };
|
|
115
|
+
}
|
|
116
|
+
let payload;
|
|
117
|
+
try {
|
|
118
|
+
const parsed = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};
|
|
119
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
120
|
+
return { status: 400, body: { error: "invalid_body", message: "Body must be a JSON object." } };
|
|
121
|
+
}
|
|
122
|
+
payload = parsed;
|
|
123
|
+
} catch {
|
|
124
|
+
return { status: 400, body: { error: "invalid_body", message: "Body must be valid JSON." } };
|
|
125
|
+
}
|
|
126
|
+
const q = this.getQueue();
|
|
127
|
+
if (!q) {
|
|
128
|
+
return { status: 503, body: { error: "queue_unavailable", message: "No queue service is registered." } };
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const messageId = await q.publish(hook.queue, { payload }, {
|
|
132
|
+
idempotencyKey: input.idempotencyKey
|
|
133
|
+
});
|
|
134
|
+
return { status: 202, body: { accepted: true, messageId } };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);
|
|
137
|
+
return { status: 503, body: { error: "enqueue_failed" } };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/plugin.ts
|
|
143
|
+
var HOOKS_PATH = "/api/v1/automation/hooks/:flowName/:hookId";
|
|
144
|
+
var ApiTriggerPlugin = class {
|
|
145
|
+
constructor() {
|
|
146
|
+
this.name = "com.objectstack.trigger.api";
|
|
147
|
+
this.type = "standard";
|
|
148
|
+
this.version = "1.0.0";
|
|
149
|
+
this.dependencies = ["com.objectstack.service.queue"];
|
|
150
|
+
}
|
|
151
|
+
async init(ctx) {
|
|
152
|
+
ctx.logger.info("API trigger plugin initialized");
|
|
153
|
+
}
|
|
154
|
+
async start(ctx) {
|
|
155
|
+
ctx.hook("kernel:ready", async () => {
|
|
156
|
+
const automation = this.resolveService(ctx, "automation");
|
|
157
|
+
if (!automation || typeof automation.registerTrigger !== "function") {
|
|
158
|
+
ctx.logger.warn("ApiTriggerPlugin: automation service not available \u2014 api trigger NOT installed");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!this.resolveService(ctx, "queue")) {
|
|
162
|
+
ctx.logger.warn("ApiTriggerPlugin: queue service not available \u2014 inbound posts will 503 until one is registered");
|
|
163
|
+
}
|
|
164
|
+
const trigger = new ApiTrigger(
|
|
165
|
+
() => this.resolveService(ctx, "queue"),
|
|
166
|
+
ctx.logger
|
|
167
|
+
);
|
|
168
|
+
automation.registerTrigger(trigger);
|
|
169
|
+
const http = this.resolveService(ctx, "http-server");
|
|
170
|
+
const rawApp = http && typeof http.getRawApp === "function" ? http.getRawApp() : null;
|
|
171
|
+
if (!rawApp) {
|
|
172
|
+
ctx.logger.warn("ApiTriggerPlugin: HTTP server not available \u2014 hooks endpoint not mounted");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
rawApp.post(HOOKS_PATH, async (c) => {
|
|
176
|
+
const rawBody = await c.req.text();
|
|
177
|
+
const out = await trigger.handleRequest({
|
|
178
|
+
flowName: c.req.param("flowName"),
|
|
179
|
+
hookId: c.req.param("hookId"),
|
|
180
|
+
rawBody,
|
|
181
|
+
signatureHeader: c.req.header("x-objectstack-signature"),
|
|
182
|
+
idempotencyKey: c.req.header("x-idempotency-key") || void 0
|
|
183
|
+
});
|
|
184
|
+
return c.json(out.body, out.status);
|
|
185
|
+
});
|
|
186
|
+
ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
resolveService(ctx, name) {
|
|
190
|
+
try {
|
|
191
|
+
return ctx.getService(name) ?? null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
198
|
+
0 && (module.exports = {
|
|
199
|
+
ApiTrigger,
|
|
200
|
+
ApiTriggerPlugin,
|
|
201
|
+
HOOKS_PATH,
|
|
202
|
+
verifySignature
|
|
203
|
+
});
|
|
204
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { ApiTriggerPlugin, HOOKS_PATH } from './plugin.js';\nexport { ApiTrigger, verifySignature } from './api-trigger.js';\nexport type {\n FlowTrigger,\n FlowTriggerBinding,\n QueueServiceSurface,\n TriggerLogger,\n} from './api-trigger.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { AutomationContext } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding` — same\n * decoupling pattern as `trigger-schedule` / `trigger-record-change`: this\n * package never imports `service-automation`.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/** Structural mirror of the engine's `FlowTrigger` extension point. */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IQueueService` this trigger needs. Boundary triggers MUST\n * ingest through the queue (ADR-0041 §5): inbound HTTP is the first event\n * source whose rate we don't control, and a slow flow execution may neither\n * drop nor block inbound events. At-least-once delivery; flows should be\n * authored idempotently (an `x-idempotency-key` header passes through to the\n * queue's dedup window).\n */\nexport interface QueueServiceSurface {\n publish<T = unknown>(queue: string, data: T, options?: { idempotencyKey?: string }): Promise<string>;\n subscribe<T = unknown>(queue: string, handler: (message: { data: T }) => Promise<void> | void): Promise<void>;\n unsubscribe(queue: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst QUEUE_PREFIX = 'flow-api';\n\n/** One armed inbound hook. */\ninterface ArmedHook {\n flowName: string;\n hookId: string;\n secret?: string;\n queue: string;\n callback: (ctx: AutomationContext) => Promise<void>;\n}\n\n/** Constant-time string compare (length leak only). */\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a, 'utf8');\n const bb = Buffer.from(b, 'utf8');\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\n/**\n * GitHub/Stripe-style HMAC verification: the sender computes\n * `sha256=<hex hmac of the raw body>` with the shared per-flow secret and\n * sends it as `x-objectstack-signature`.\n */\nexport function verifySignature(secret: string, rawBody: string, header: string | undefined): boolean {\n if (!header) return false;\n const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');\n return safeEqual(expected, header.trim());\n}\n\n/**\n * `api` flow trigger (ADR-0041 Tier 1) — inbound webhook/HTTP.\n *\n * The engine binds every `type: 'api'` flow to this trigger; `start()` arms a\n * hook (URL path + optional HMAC secret from the start node's `config`) and\n * subscribes a queue consumer that runs the flow. The HTTP side\n * ({@link handleRequest}) validates and **enqueues** — it never executes the\n * flow in-band:\n *\n * POST /api/v1/automation/hooks/:flowName/:hookId\n * → 404 unknown flow / wrong hookId\n * → 401 missing/bad HMAC signature (when the flow declares a `secret`)\n * → 400 non-JSON body\n * → 202 { accepted, messageId } — queued; a consumer executes the flow\n *\n * The JSON payload is exposed to the flow as the trigger record (`$record` /\n * `record.*`, fields flattened to bare references) plus `params` — the same\n * authoring surface record-change flows use.\n *\n * Start-node config keys:\n * - `hookId` — URL path token (default `'default'`); rotate it to revoke\n * old URLs without renaming the flow.\n * - `secret` — HMAC-SHA256 shared secret. Strongly recommended; without it\n * the endpoint accepts unsigned posts (the trigger logs a\n * warning at arm time).\n */\nexport class ApiTrigger implements FlowTrigger {\n readonly type = 'api';\n\n private hooks = new Map<string, ArmedHook>();\n\n constructor(\n private readonly getQueue: () => QueueServiceSurface | null,\n private readonly logger: TriggerLogger,\n ) {}\n\n /** Currently armed hooks (for diagnostics/tests). */\n listHooks(): Array<{ flowName: string; hookId: string; signed: boolean }> {\n return [...this.hooks.values()].map(h => ({\n flowName: h.flowName, hookId: h.hookId, signed: !!h.secret,\n }));\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const cfg = (binding.config ?? {}) as Record<string, unknown>;\n const hookId = typeof cfg.hookId === 'string' && cfg.hookId.trim() ? cfg.hookId.trim() : 'default';\n const secret = typeof cfg.secret === 'string' && cfg.secret.trim() ? cfg.secret.trim() : undefined;\n const queue = `${QUEUE_PREFIX}:${binding.flowName}`;\n\n const hook: ArmedHook = { flowName: binding.flowName, hookId, secret, queue, callback };\n this.hooks.set(binding.flowName, hook);\n\n const q = this.getQueue();\n if (!q) {\n this.logger.warn(\n `[trigger-api] no queue service — inbound posts for flow '${binding.flowName}' will be rejected until one is registered`,\n );\n } else {\n void q.subscribe<{ payload: Record<string, unknown> }>(queue, async (message) => {\n const payload = (message?.data as any)?.payload ?? {};\n await hook.callback({\n // The webhook body IS the trigger record: `$record`, `record.*`,\n // and bare field references all resolve, matching how\n // record-change flows are authored.\n record: payload,\n params: { ...payload },\n event: 'api',\n } as AutomationContext);\n }).catch((err: any) => {\n this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);\n });\n }\n\n if (!secret) {\n this.logger.warn(\n `[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret — endpoint accepts unsigned posts`,\n );\n }\n this.logger.info(\n `[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? ' (HMAC required)' : ''}`,\n );\n }\n\n stop(flowName: string): void {\n const hook = this.hooks.get(flowName);\n if (!hook) return;\n this.hooks.delete(flowName);\n const q = this.getQueue();\n if (q) void q.unsubscribe(hook.queue).catch(() => { /* already gone */ });\n this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);\n }\n\n /**\n * Handle one inbound request. Transport-agnostic: the plugin adapts this\n * to the host HTTP server. Returns the response to send.\n */\n async handleRequest(input: {\n flowName: string;\n hookId: string;\n rawBody: string;\n signatureHeader?: string;\n idempotencyKey?: string;\n }): Promise<{ status: number; body: Record<string, unknown> }> {\n const hook = this.hooks.get(input.flowName);\n // Unknown flow and wrong hookId answer identically — no oracle for\n // probing which flows exist.\n if (!hook || !safeEqual(hook.hookId, input.hookId)) {\n return { status: 404, body: { error: 'not_found' } };\n }\n if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {\n return { status: 401, body: { error: 'invalid_signature' } };\n }\n\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};\n if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { status: 400, body: { error: 'invalid_body', message: 'Body must be a JSON object.' } };\n }\n payload = parsed as Record<string, unknown>;\n } catch {\n return { status: 400, body: { error: 'invalid_body', message: 'Body must be valid JSON.' } };\n }\n\n const q = this.getQueue();\n if (!q) {\n return { status: 503, body: { error: 'queue_unavailable', message: 'No queue service is registered.' } };\n }\n try {\n const messageId = await q.publish(hook.queue, { payload }, {\n idempotencyKey: input.idempotencyKey,\n });\n return { status: 202, body: { accepted: true, messageId } };\n } catch (err: any) {\n this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);\n return { status: 503, body: { error: 'enqueue_failed' } };\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ApiTrigger } from './api-trigger.js';\nimport type { FlowTrigger, QueueServiceSurface } from './api-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs. Declared structurally\n * so the plugin does not take a build dependency on\n * `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/** The slice of the Hono host server this plugin needs. */\ninterface HttpServerSurface {\n getRawApp(): {\n post(path: string, handler: (c: any) => Promise<unknown> | unknown): void;\n } | null;\n}\n\n/** Mount point for inbound hooks on the host HTTP server. */\nexport const HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId';\n\n/**\n * ApiTriggerPlugin (ADR-0041 Tier 1)\n *\n * Makes `type: 'api'` flows actually fire. The automation engine derives an\n * `api` binding from the flow; this plugin provides the concrete trigger:\n *\n * - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host\n * Hono app (resolved via the `http-server` service);\n * - validates hookId + HMAC signature (`x-objectstack-signature`,\n * GitHub/Stripe style, constant-time) against the flow's start-node\n * config;\n * - **enqueues** the payload (`queue` service) and ACKs 202 — flow\n * execution happens on the queue consumer, never in-band with the\n * inbound request (ADR-0041 §5: boundary triggers must not let a slow\n * flow drop or block events). `x-idempotency-key` passes through to the\n * queue's dedup window.\n *\n * Webhook payloads surface to the flow as the trigger record (`$record` /\n * `record.*` / bare field references) — the same authoring surface\n * record-change flows use.\n */\nexport class ApiTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.api';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.service.queue'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('API trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Resolve at kernel:ready — the automation engine, queue service, and\n // HTTP server may all start after this plugin.\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn('ApiTriggerPlugin: automation service not available — api trigger NOT installed');\n return;\n }\n if (!this.resolveService<QueueServiceSurface>(ctx, 'queue')) {\n ctx.logger.warn('ApiTriggerPlugin: queue service not available — inbound posts will 503 until one is registered');\n }\n\n const trigger = new ApiTrigger(\n () => this.resolveService<QueueServiceSurface>(ctx, 'queue'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n\n const http = this.resolveService<HttpServerSurface>(ctx, 'http-server');\n const rawApp = http && typeof http.getRawApp === 'function' ? http.getRawApp() : null;\n if (!rawApp) {\n ctx.logger.warn('ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted');\n return;\n }\n rawApp.post(HOOKS_PATH, async (c: any) => {\n const rawBody = await c.req.text();\n const out = await trigger.handleRequest({\n flowName: c.req.param('flowName'),\n hookId: c.req.param('hookId'),\n rawBody,\n signatureHeader: c.req.header('x-objectstack-signature'),\n idempotencyKey: c.req.header('x-idempotency-key') || undefined,\n });\n return c.json(out.body, out.status);\n });\n ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/api-trigger.ts
|
|
2
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
3
|
+
var QUEUE_PREFIX = "flow-api";
|
|
4
|
+
function safeEqual(a, b) {
|
|
5
|
+
const ab = Buffer.from(a, "utf8");
|
|
6
|
+
const bb = Buffer.from(b, "utf8");
|
|
7
|
+
if (ab.length !== bb.length) return false;
|
|
8
|
+
return timingSafeEqual(ab, bb);
|
|
9
|
+
}
|
|
10
|
+
function verifySignature(secret, rawBody, header) {
|
|
11
|
+
if (!header) return false;
|
|
12
|
+
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
|
|
13
|
+
return safeEqual(expected, header.trim());
|
|
14
|
+
}
|
|
15
|
+
var ApiTrigger = class {
|
|
16
|
+
constructor(getQueue, logger) {
|
|
17
|
+
this.getQueue = getQueue;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
this.type = "api";
|
|
20
|
+
this.hooks = /* @__PURE__ */ new Map();
|
|
21
|
+
}
|
|
22
|
+
/** Currently armed hooks (for diagnostics/tests). */
|
|
23
|
+
listHooks() {
|
|
24
|
+
return [...this.hooks.values()].map((h) => ({
|
|
25
|
+
flowName: h.flowName,
|
|
26
|
+
hookId: h.hookId,
|
|
27
|
+
signed: !!h.secret
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
start(binding, callback) {
|
|
31
|
+
const cfg = binding.config ?? {};
|
|
32
|
+
const hookId = typeof cfg.hookId === "string" && cfg.hookId.trim() ? cfg.hookId.trim() : "default";
|
|
33
|
+
const secret = typeof cfg.secret === "string" && cfg.secret.trim() ? cfg.secret.trim() : void 0;
|
|
34
|
+
const queue = `${QUEUE_PREFIX}:${binding.flowName}`;
|
|
35
|
+
const hook = { flowName: binding.flowName, hookId, secret, queue, callback };
|
|
36
|
+
this.hooks.set(binding.flowName, hook);
|
|
37
|
+
const q = this.getQueue();
|
|
38
|
+
if (!q) {
|
|
39
|
+
this.logger.warn(
|
|
40
|
+
`[trigger-api] no queue service \u2014 inbound posts for flow '${binding.flowName}' will be rejected until one is registered`
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
void q.subscribe(queue, async (message) => {
|
|
44
|
+
const payload = message?.data?.payload ?? {};
|
|
45
|
+
await hook.callback({
|
|
46
|
+
// The webhook body IS the trigger record: `$record`, `record.*`,
|
|
47
|
+
// and bare field references all resolve, matching how
|
|
48
|
+
// record-change flows are authored.
|
|
49
|
+
record: payload,
|
|
50
|
+
params: { ...payload },
|
|
51
|
+
event: "api"
|
|
52
|
+
});
|
|
53
|
+
}).catch((err) => {
|
|
54
|
+
this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!secret) {
|
|
58
|
+
this.logger.warn(
|
|
59
|
+
`[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret \u2014 endpoint accepts unsigned posts`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
this.logger.info(
|
|
63
|
+
`[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? " (HMAC required)" : ""}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
stop(flowName) {
|
|
67
|
+
const hook = this.hooks.get(flowName);
|
|
68
|
+
if (!hook) return;
|
|
69
|
+
this.hooks.delete(flowName);
|
|
70
|
+
const q = this.getQueue();
|
|
71
|
+
if (q) void q.unsubscribe(hook.queue).catch(() => {
|
|
72
|
+
});
|
|
73
|
+
this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Handle one inbound request. Transport-agnostic: the plugin adapts this
|
|
77
|
+
* to the host HTTP server. Returns the response to send.
|
|
78
|
+
*/
|
|
79
|
+
async handleRequest(input) {
|
|
80
|
+
const hook = this.hooks.get(input.flowName);
|
|
81
|
+
if (!hook || !safeEqual(hook.hookId, input.hookId)) {
|
|
82
|
+
return { status: 404, body: { error: "not_found" } };
|
|
83
|
+
}
|
|
84
|
+
if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {
|
|
85
|
+
return { status: 401, body: { error: "invalid_signature" } };
|
|
86
|
+
}
|
|
87
|
+
let payload;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};
|
|
90
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
91
|
+
return { status: 400, body: { error: "invalid_body", message: "Body must be a JSON object." } };
|
|
92
|
+
}
|
|
93
|
+
payload = parsed;
|
|
94
|
+
} catch {
|
|
95
|
+
return { status: 400, body: { error: "invalid_body", message: "Body must be valid JSON." } };
|
|
96
|
+
}
|
|
97
|
+
const q = this.getQueue();
|
|
98
|
+
if (!q) {
|
|
99
|
+
return { status: 503, body: { error: "queue_unavailable", message: "No queue service is registered." } };
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const messageId = await q.publish(hook.queue, { payload }, {
|
|
103
|
+
idempotencyKey: input.idempotencyKey
|
|
104
|
+
});
|
|
105
|
+
return { status: 202, body: { accepted: true, messageId } };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);
|
|
108
|
+
return { status: 503, body: { error: "enqueue_failed" } };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/plugin.ts
|
|
114
|
+
var HOOKS_PATH = "/api/v1/automation/hooks/:flowName/:hookId";
|
|
115
|
+
var ApiTriggerPlugin = class {
|
|
116
|
+
constructor() {
|
|
117
|
+
this.name = "com.objectstack.trigger.api";
|
|
118
|
+
this.type = "standard";
|
|
119
|
+
this.version = "1.0.0";
|
|
120
|
+
this.dependencies = ["com.objectstack.service.queue"];
|
|
121
|
+
}
|
|
122
|
+
async init(ctx) {
|
|
123
|
+
ctx.logger.info("API trigger plugin initialized");
|
|
124
|
+
}
|
|
125
|
+
async start(ctx) {
|
|
126
|
+
ctx.hook("kernel:ready", async () => {
|
|
127
|
+
const automation = this.resolveService(ctx, "automation");
|
|
128
|
+
if (!automation || typeof automation.registerTrigger !== "function") {
|
|
129
|
+
ctx.logger.warn("ApiTriggerPlugin: automation service not available \u2014 api trigger NOT installed");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!this.resolveService(ctx, "queue")) {
|
|
133
|
+
ctx.logger.warn("ApiTriggerPlugin: queue service not available \u2014 inbound posts will 503 until one is registered");
|
|
134
|
+
}
|
|
135
|
+
const trigger = new ApiTrigger(
|
|
136
|
+
() => this.resolveService(ctx, "queue"),
|
|
137
|
+
ctx.logger
|
|
138
|
+
);
|
|
139
|
+
automation.registerTrigger(trigger);
|
|
140
|
+
const http = this.resolveService(ctx, "http-server");
|
|
141
|
+
const rawApp = http && typeof http.getRawApp === "function" ? http.getRawApp() : null;
|
|
142
|
+
if (!rawApp) {
|
|
143
|
+
ctx.logger.warn("ApiTriggerPlugin: HTTP server not available \u2014 hooks endpoint not mounted");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
rawApp.post(HOOKS_PATH, async (c) => {
|
|
147
|
+
const rawBody = await c.req.text();
|
|
148
|
+
const out = await trigger.handleRequest({
|
|
149
|
+
flowName: c.req.param("flowName"),
|
|
150
|
+
hookId: c.req.param("hookId"),
|
|
151
|
+
rawBody,
|
|
152
|
+
signatureHeader: c.req.header("x-objectstack-signature"),
|
|
153
|
+
idempotencyKey: c.req.header("x-idempotency-key") || void 0
|
|
154
|
+
});
|
|
155
|
+
return c.json(out.body, out.status);
|
|
156
|
+
});
|
|
157
|
+
ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
resolveService(ctx, name) {
|
|
161
|
+
try {
|
|
162
|
+
return ctx.getService(name) ?? null;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
export {
|
|
169
|
+
ApiTrigger,
|
|
170
|
+
ApiTriggerPlugin,
|
|
171
|
+
HOOKS_PATH,
|
|
172
|
+
verifySignature
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { AutomationContext } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding` — same\n * decoupling pattern as `trigger-schedule` / `trigger-record-change`: this\n * package never imports `service-automation`.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/** Structural mirror of the engine's `FlowTrigger` extension point. */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IQueueService` this trigger needs. Boundary triggers MUST\n * ingest through the queue (ADR-0041 §5): inbound HTTP is the first event\n * source whose rate we don't control, and a slow flow execution may neither\n * drop nor block inbound events. At-least-once delivery; flows should be\n * authored idempotently (an `x-idempotency-key` header passes through to the\n * queue's dedup window).\n */\nexport interface QueueServiceSurface {\n publish<T = unknown>(queue: string, data: T, options?: { idempotencyKey?: string }): Promise<string>;\n subscribe<T = unknown>(queue: string, handler: (message: { data: T }) => Promise<void> | void): Promise<void>;\n unsubscribe(queue: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst QUEUE_PREFIX = 'flow-api';\n\n/** One armed inbound hook. */\ninterface ArmedHook {\n flowName: string;\n hookId: string;\n secret?: string;\n queue: string;\n callback: (ctx: AutomationContext) => Promise<void>;\n}\n\n/** Constant-time string compare (length leak only). */\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a, 'utf8');\n const bb = Buffer.from(b, 'utf8');\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\n/**\n * GitHub/Stripe-style HMAC verification: the sender computes\n * `sha256=<hex hmac of the raw body>` with the shared per-flow secret and\n * sends it as `x-objectstack-signature`.\n */\nexport function verifySignature(secret: string, rawBody: string, header: string | undefined): boolean {\n if (!header) return false;\n const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');\n return safeEqual(expected, header.trim());\n}\n\n/**\n * `api` flow trigger (ADR-0041 Tier 1) — inbound webhook/HTTP.\n *\n * The engine binds every `type: 'api'` flow to this trigger; `start()` arms a\n * hook (URL path + optional HMAC secret from the start node's `config`) and\n * subscribes a queue consumer that runs the flow. The HTTP side\n * ({@link handleRequest}) validates and **enqueues** — it never executes the\n * flow in-band:\n *\n * POST /api/v1/automation/hooks/:flowName/:hookId\n * → 404 unknown flow / wrong hookId\n * → 401 missing/bad HMAC signature (when the flow declares a `secret`)\n * → 400 non-JSON body\n * → 202 { accepted, messageId } — queued; a consumer executes the flow\n *\n * The JSON payload is exposed to the flow as the trigger record (`$record` /\n * `record.*`, fields flattened to bare references) plus `params` — the same\n * authoring surface record-change flows use.\n *\n * Start-node config keys:\n * - `hookId` — URL path token (default `'default'`); rotate it to revoke\n * old URLs without renaming the flow.\n * - `secret` — HMAC-SHA256 shared secret. Strongly recommended; without it\n * the endpoint accepts unsigned posts (the trigger logs a\n * warning at arm time).\n */\nexport class ApiTrigger implements FlowTrigger {\n readonly type = 'api';\n\n private hooks = new Map<string, ArmedHook>();\n\n constructor(\n private readonly getQueue: () => QueueServiceSurface | null,\n private readonly logger: TriggerLogger,\n ) {}\n\n /** Currently armed hooks (for diagnostics/tests). */\n listHooks(): Array<{ flowName: string; hookId: string; signed: boolean }> {\n return [...this.hooks.values()].map(h => ({\n flowName: h.flowName, hookId: h.hookId, signed: !!h.secret,\n }));\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const cfg = (binding.config ?? {}) as Record<string, unknown>;\n const hookId = typeof cfg.hookId === 'string' && cfg.hookId.trim() ? cfg.hookId.trim() : 'default';\n const secret = typeof cfg.secret === 'string' && cfg.secret.trim() ? cfg.secret.trim() : undefined;\n const queue = `${QUEUE_PREFIX}:${binding.flowName}`;\n\n const hook: ArmedHook = { flowName: binding.flowName, hookId, secret, queue, callback };\n this.hooks.set(binding.flowName, hook);\n\n const q = this.getQueue();\n if (!q) {\n this.logger.warn(\n `[trigger-api] no queue service — inbound posts for flow '${binding.flowName}' will be rejected until one is registered`,\n );\n } else {\n void q.subscribe<{ payload: Record<string, unknown> }>(queue, async (message) => {\n const payload = (message?.data as any)?.payload ?? {};\n await hook.callback({\n // The webhook body IS the trigger record: `$record`, `record.*`,\n // and bare field references all resolve, matching how\n // record-change flows are authored.\n record: payload,\n params: { ...payload },\n event: 'api',\n } as AutomationContext);\n }).catch((err: any) => {\n this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);\n });\n }\n\n if (!secret) {\n this.logger.warn(\n `[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret — endpoint accepts unsigned posts`,\n );\n }\n this.logger.info(\n `[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? ' (HMAC required)' : ''}`,\n );\n }\n\n stop(flowName: string): void {\n const hook = this.hooks.get(flowName);\n if (!hook) return;\n this.hooks.delete(flowName);\n const q = this.getQueue();\n if (q) void q.unsubscribe(hook.queue).catch(() => { /* already gone */ });\n this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);\n }\n\n /**\n * Handle one inbound request. Transport-agnostic: the plugin adapts this\n * to the host HTTP server. Returns the response to send.\n */\n async handleRequest(input: {\n flowName: string;\n hookId: string;\n rawBody: string;\n signatureHeader?: string;\n idempotencyKey?: string;\n }): Promise<{ status: number; body: Record<string, unknown> }> {\n const hook = this.hooks.get(input.flowName);\n // Unknown flow and wrong hookId answer identically — no oracle for\n // probing which flows exist.\n if (!hook || !safeEqual(hook.hookId, input.hookId)) {\n return { status: 404, body: { error: 'not_found' } };\n }\n if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {\n return { status: 401, body: { error: 'invalid_signature' } };\n }\n\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};\n if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { status: 400, body: { error: 'invalid_body', message: 'Body must be a JSON object.' } };\n }\n payload = parsed as Record<string, unknown>;\n } catch {\n return { status: 400, body: { error: 'invalid_body', message: 'Body must be valid JSON.' } };\n }\n\n const q = this.getQueue();\n if (!q) {\n return { status: 503, body: { error: 'queue_unavailable', message: 'No queue service is registered.' } };\n }\n try {\n const messageId = await q.publish(hook.queue, { payload }, {\n idempotencyKey: input.idempotencyKey,\n });\n return { status: 202, body: { accepted: true, messageId } };\n } catch (err: any) {\n this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);\n return { status: 503, body: { error: 'enqueue_failed' } };\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ApiTrigger } from './api-trigger.js';\nimport type { FlowTrigger, QueueServiceSurface } from './api-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs. Declared structurally\n * so the plugin does not take a build dependency on\n * `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/** The slice of the Hono host server this plugin needs. */\ninterface HttpServerSurface {\n getRawApp(): {\n post(path: string, handler: (c: any) => Promise<unknown> | unknown): void;\n } | null;\n}\n\n/** Mount point for inbound hooks on the host HTTP server. */\nexport const HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId';\n\n/**\n * ApiTriggerPlugin (ADR-0041 Tier 1)\n *\n * Makes `type: 'api'` flows actually fire. The automation engine derives an\n * `api` binding from the flow; this plugin provides the concrete trigger:\n *\n * - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host\n * Hono app (resolved via the `http-server` service);\n * - validates hookId + HMAC signature (`x-objectstack-signature`,\n * GitHub/Stripe style, constant-time) against the flow's start-node\n * config;\n * - **enqueues** the payload (`queue` service) and ACKs 202 — flow\n * execution happens on the queue consumer, never in-band with the\n * inbound request (ADR-0041 §5: boundary triggers must not let a slow\n * flow drop or block events). `x-idempotency-key` passes through to the\n * queue's dedup window.\n *\n * Webhook payloads surface to the flow as the trigger record (`$record` /\n * `record.*` / bare field references) — the same authoring surface\n * record-change flows use.\n */\nexport class ApiTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.api';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.service.queue'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('API trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Resolve at kernel:ready — the automation engine, queue service, and\n // HTTP server may all start after this plugin.\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn('ApiTriggerPlugin: automation service not available — api trigger NOT installed');\n return;\n }\n if (!this.resolveService<QueueServiceSurface>(ctx, 'queue')) {\n ctx.logger.warn('ApiTriggerPlugin: queue service not available — inbound posts will 503 until one is registered');\n }\n\n const trigger = new ApiTrigger(\n () => this.resolveService<QueueServiceSurface>(ctx, 'queue'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n\n const http = this.resolveService<HttpServerSurface>(ctx, 'http-server');\n const rawApp = http && typeof http.getRawApp === 'function' ? http.getRawApp() : null;\n if (!rawApp) {\n ctx.logger.warn('ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted');\n return;\n }\n rawApp.post(HOOKS_PATH, async (c: any) => {\n const rawBody = await c.req.text();\n const out = await trigger.handleRequest({\n flowName: c.req.param('flowName'),\n hookId: c.req.param('hookId'),\n rawBody,\n signatureHeader: c.req.header('x-objectstack-signature'),\n idempotencyKey: c.req.header('x-idempotency-key') || undefined,\n });\n return c.json(out.body, out.status);\n });\n ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectstack/trigger-api",
|
|
3
|
+
"version": "9.3.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@objectstack/core": "9.3.0",
|
|
17
|
+
"@objectstack/spec": "9.3.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^25.9.2",
|
|
21
|
+
"typescript": "^6.0.3",
|
|
22
|
+
"vitest": "^4.1.8"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"objectstack",
|
|
26
|
+
"trigger",
|
|
27
|
+
"webhook",
|
|
28
|
+
"automation"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup --config ../../../tsup.config.ts",
|
|
32
|
+
"test": "vitest run --passWithNoTests"
|
|
33
|
+
}
|
|
34
|
+
}
|