@tangle-network/hub-sdk 0.2.2 → 0.4.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/README.md +98 -2
- package/dist/index.d.ts +701 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +780 -7
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SSEChunkParser } from "@tangle-network/sdk-telemetry";
|
|
1
2
|
//#region src/redaction.ts
|
|
2
3
|
const SECRET_KEY_PATTERN = /(api[_-]?key|authorization|capability[_-]?token|token|secret)/i;
|
|
3
4
|
const SECRET_VALUE_PATTERN = /\b(?:sk-tan[-_]|hubcap_|hct_|gh[pousr]_)[A-Za-z0-9_=-]+\b/g;
|
|
@@ -21,6 +22,160 @@ var HubSdkError = class extends Error {
|
|
|
21
22
|
this.status = options.status;
|
|
22
23
|
}
|
|
23
24
|
};
|
|
25
|
+
function bodySnippet(body, max = 200) {
|
|
26
|
+
const trimmed = body.trim();
|
|
27
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
|
|
28
|
+
}
|
|
29
|
+
function httpError(path, status, statusText, body, contentType) {
|
|
30
|
+
const suffix = statusText ? ` ${statusText}` : "";
|
|
31
|
+
return new HubSdkError({
|
|
32
|
+
code: `HUB_HTTP_${status}`,
|
|
33
|
+
message: `Hub request ${path} failed: HTTP ${status}${suffix}`,
|
|
34
|
+
details: {
|
|
35
|
+
status,
|
|
36
|
+
path,
|
|
37
|
+
contentType: contentType || null,
|
|
38
|
+
bodySnippet: bodySnippet(body)
|
|
39
|
+
}
|
|
40
|
+
}, { status });
|
|
41
|
+
}
|
|
42
|
+
function isHubErrorEnvelope(value) {
|
|
43
|
+
if (typeof value !== "object" || value === null) return false;
|
|
44
|
+
const candidate = value;
|
|
45
|
+
if (candidate.success !== false) return false;
|
|
46
|
+
const error = candidate.error;
|
|
47
|
+
return typeof error === "object" && error !== null && typeof error.code === "string";
|
|
48
|
+
}
|
|
49
|
+
function isHubSuccessEnvelope(value) {
|
|
50
|
+
return typeof value === "object" && value !== null && value.success === true;
|
|
51
|
+
}
|
|
52
|
+
function isRecord$1(value) {
|
|
53
|
+
return typeof value === "object" && value !== null;
|
|
54
|
+
}
|
|
55
|
+
function optString(value) {
|
|
56
|
+
return typeof value === "string" ? value : "";
|
|
57
|
+
}
|
|
58
|
+
/** An agent iteration's terminal status (`iteration.ended`) — only ever
|
|
59
|
+
* succeeded/failed; an iteration is never guard-skipped. */
|
|
60
|
+
function isIterationStatus(value) {
|
|
61
|
+
return value === "succeeded" || value === "failed";
|
|
62
|
+
}
|
|
63
|
+
/** An action's terminal status on `action.finished`. Beyond succeeded/failed a
|
|
64
|
+
* guarded action can end `skipped` (its `if` resolved false, so it never ran) —
|
|
65
|
+
* accepted here so a skipped event is surfaced, not dropped as malformed. */
|
|
66
|
+
function isActionFinishedStatus(value) {
|
|
67
|
+
return value === "succeeded" || value === "failed" || value === "skipped";
|
|
68
|
+
}
|
|
69
|
+
/** Terminal status of a whole run's `run.done` event. A run can end `cancelled`
|
|
70
|
+
* (a user cancel) — a value neither the action nor iteration status checkers
|
|
71
|
+
* accept — so `run.done` uses this rather than them, which would drop a
|
|
72
|
+
* cancelled terminal event and hang the stream. */
|
|
73
|
+
function isRunDoneStatus(value) {
|
|
74
|
+
return value === "succeeded" || value === "failed" || value === "cancelled";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Map one raw SSE event (its `event:` name + parsed JSON `data:`) to a typed
|
|
78
|
+
* run-stream event, validating the fields each variant declares rather than
|
|
79
|
+
* blindly casting the wire payload. A frame that doesn't match its event's
|
|
80
|
+
* shape (or an unknown event name) returns null so the consumer skips it — a
|
|
81
|
+
* malformed `token` can never reach a consumer as `{ delta: undefined }`, and
|
|
82
|
+
* new server event kinds don't break older clients.
|
|
83
|
+
*/
|
|
84
|
+
function toRunStreamEvent(eventType, data) {
|
|
85
|
+
if (eventType === "ping") return { type: "ping" };
|
|
86
|
+
if (!isRecord$1(data)) return null;
|
|
87
|
+
switch (eventType) {
|
|
88
|
+
case "snapshot": return typeof data.id === "string" && typeof data.workflowId === "string" && typeof data.status === "string" && Array.isArray(data.actionResults) ? {
|
|
89
|
+
type: "snapshot",
|
|
90
|
+
run: data
|
|
91
|
+
} : null;
|
|
92
|
+
case "token": return typeof data.delta === "string" && typeof data.actionIndex === "number" ? {
|
|
93
|
+
type: "token",
|
|
94
|
+
actionIndex: data.actionIndex,
|
|
95
|
+
delta: data.delta,
|
|
96
|
+
at: optString(data.at)
|
|
97
|
+
} : null;
|
|
98
|
+
case "run.done": return isRunDoneStatus(data.status) ? {
|
|
99
|
+
type: "run.done",
|
|
100
|
+
status: data.status,
|
|
101
|
+
error: typeof data.error === "string" ? data.error : null,
|
|
102
|
+
...typeof data.at === "string" ? { at: data.at } : {}
|
|
103
|
+
} : null;
|
|
104
|
+
case "run.waiting": return typeof data.decisionId === "string" && data.decisionId.length > 0 ? {
|
|
105
|
+
type: "run.waiting",
|
|
106
|
+
decisionId: data.decisionId,
|
|
107
|
+
...typeof data.at === "string" ? { at: data.at } : {}
|
|
108
|
+
} : null;
|
|
109
|
+
case "action.started": return typeof data.index === "number" && typeof data.kind === "string" && typeof data.at === "string" ? {
|
|
110
|
+
type: "action.started",
|
|
111
|
+
index: data.index,
|
|
112
|
+
kind: data.kind,
|
|
113
|
+
at: data.at
|
|
114
|
+
} : null;
|
|
115
|
+
case "action.finished": return typeof data.index === "number" && typeof data.kind === "string" && isActionFinishedStatus(data.status) && typeof data.at === "string" ? {
|
|
116
|
+
type: "action.finished",
|
|
117
|
+
index: data.index,
|
|
118
|
+
kind: data.kind,
|
|
119
|
+
status: data.status,
|
|
120
|
+
at: data.at,
|
|
121
|
+
...typeof data.costUsd === "number" ? { costUsd: data.costUsd } : {},
|
|
122
|
+
...typeof data.error === "string" ? { error: data.error } : {}
|
|
123
|
+
} : null;
|
|
124
|
+
case "iteration.started": return typeof data.actionIndex === "number" && typeof data.iterationIndex === "number" && typeof data.at === "string" ? {
|
|
125
|
+
type: "iteration.started",
|
|
126
|
+
actionIndex: data.actionIndex,
|
|
127
|
+
iterationIndex: data.iterationIndex,
|
|
128
|
+
at: data.at,
|
|
129
|
+
...typeof data.model === "string" ? { model: data.model } : {}
|
|
130
|
+
} : null;
|
|
131
|
+
case "iteration.ended": return typeof data.actionIndex === "number" && typeof data.iterationIndex === "number" && isIterationStatus(data.status) && typeof data.at === "string" ? {
|
|
132
|
+
type: "iteration.ended",
|
|
133
|
+
actionIndex: data.actionIndex,
|
|
134
|
+
iterationIndex: data.iterationIndex,
|
|
135
|
+
status: data.status,
|
|
136
|
+
at: data.at,
|
|
137
|
+
...typeof data.outputPreview === "string" ? { outputPreview: data.outputPreview } : {},
|
|
138
|
+
...typeof data.inputTokens === "number" ? { inputTokens: data.inputTokens } : {},
|
|
139
|
+
...typeof data.outputTokens === "number" ? { outputTokens: data.outputTokens } : {},
|
|
140
|
+
...typeof data.costUsd === "number" ? { costUsd: data.costUsd } : {}
|
|
141
|
+
} : null;
|
|
142
|
+
default: return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** True for an aborted/timed-out fetch (or any abort). fetch rejects with the
|
|
146
|
+
* signal's reason — a DOMException named `AbortError` (caller) or `TimeoutError`
|
|
147
|
+
* (AbortSignal.timeout). */
|
|
148
|
+
function isAbortError(err) {
|
|
149
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
150
|
+
}
|
|
151
|
+
/** A signal that aborts when the caller's signal aborts OR `timeoutMs` elapses.
|
|
152
|
+
* Returns undefined when neither bound applies (no per-request abort needed). */
|
|
153
|
+
function combineAbortSignals(caller, timeoutMs) {
|
|
154
|
+
const signals = [];
|
|
155
|
+
if (caller) signals.push(caller);
|
|
156
|
+
if (timeoutMs !== null) signals.push(AbortSignal.timeout(timeoutMs));
|
|
157
|
+
if (signals.length === 0) return void 0;
|
|
158
|
+
return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
159
|
+
}
|
|
160
|
+
/** Resolve after `ms`, or reject if `signal` aborts first. Clears its timer on
|
|
161
|
+
* abort so a cancelled wait leaves nothing pending. */
|
|
162
|
+
function delay(ms, signal) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
if (signal?.aborted) {
|
|
165
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("The operation was aborted"));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const onAbort = () => {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
reject(signal?.reason ?? /* @__PURE__ */ new Error("The operation was aborted"));
|
|
171
|
+
};
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
signal?.removeEventListener("abort", onAbort);
|
|
174
|
+
resolve();
|
|
175
|
+
}, ms);
|
|
176
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
24
179
|
const HUB_URL_ENV_VAR = "TANGLE_HUB_URL";
|
|
25
180
|
const HUB_API_KEY_ENV_VAR = "TANGLE_API_KEY";
|
|
26
181
|
const HUB_CAPABILITY_TOKEN_ENV_VAR = "TANGLE_HUB_CAPABILITY_TOKEN";
|
|
@@ -71,24 +226,30 @@ var HubClient = class HubClient {
|
|
|
71
226
|
apiKey;
|
|
72
227
|
authHeaders;
|
|
73
228
|
connections;
|
|
229
|
+
channels;
|
|
230
|
+
eventSubscriptions;
|
|
74
231
|
permissions;
|
|
75
232
|
tokens;
|
|
76
233
|
tools;
|
|
77
234
|
approvals;
|
|
78
235
|
audit;
|
|
79
236
|
githubApp;
|
|
237
|
+
workflows;
|
|
80
238
|
constructor(options) {
|
|
81
239
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
82
240
|
this.apiKey = options.apiKey;
|
|
83
241
|
this.authHeaders = options.authHeaders;
|
|
84
|
-
this.fetch = options.fetch ?? fetch;
|
|
242
|
+
this.fetch = options.fetch ?? fetch.bind(globalThis);
|
|
85
243
|
this.connections = new HubConnectionsClient((path, init) => this.request(path, init));
|
|
244
|
+
this.channels = new HubChannelsClient((path, init) => this.request(path, init));
|
|
245
|
+
this.eventSubscriptions = new HubEventSubscriptionsClient((path, init) => this.request(path, init));
|
|
86
246
|
this.permissions = new HubPermissionsClient((path, init) => this.request(path, init));
|
|
87
247
|
this.tokens = new HubTokensClient((path, init) => this.request(path, init));
|
|
88
248
|
this.tools = new HubToolsClient((path, init) => this.request(path, init));
|
|
89
249
|
this.approvals = new HubApprovalsClient((path, init) => this.request(path, init));
|
|
90
250
|
this.audit = new HubAuditClient((path, init) => this.request(path, init));
|
|
91
251
|
this.githubApp = new HubGithubAppClient((path, init) => this.request(path, init));
|
|
252
|
+
this.workflows = new HubWorkflowsClient((path, init) => this.request(path, init), (path, init) => this.stream(path, init));
|
|
92
253
|
}
|
|
93
254
|
static fromEnv(options = {}) {
|
|
94
255
|
const env = options.env ?? readProcessEnv();
|
|
@@ -107,13 +268,57 @@ var HubClient = class HubClient {
|
|
|
107
268
|
...init,
|
|
108
269
|
headers: await this.buildHeaders(init.headers)
|
|
109
270
|
});
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
271
|
+
const status = response.status;
|
|
272
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
273
|
+
const bodyText = await response.text();
|
|
274
|
+
if (!contentType.includes("application/json")) throw httpError(path, status, response.statusText, bodyText, contentType);
|
|
275
|
+
let parsed;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(bodyText);
|
|
278
|
+
} catch {
|
|
279
|
+
throw httpError(path, status, response.statusText, bodyText, contentType);
|
|
280
|
+
}
|
|
281
|
+
if (isHubErrorEnvelope(parsed)) throw new HubSdkError(parsed.error, { status });
|
|
282
|
+
if (isHubSuccessEnvelope(parsed)) return parsed.data;
|
|
283
|
+
throw httpError(path, status, response.statusText, bodyText, contentType);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Open a streaming (Server-Sent Events) response and return its raw body.
|
|
287
|
+
* Shares auth-header building + `fetch` with {@link request}, but does NOT
|
|
288
|
+
* buffer or JSON-parse the body — the caller consumes the stream.
|
|
289
|
+
*
|
|
290
|
+
* A successful stream comes back as `text/event-stream`. Anything else is an
|
|
291
|
+
* error envelope (404/429/401 JSON) or a transport failure (gateway/HTML
|
|
292
|
+
* page); it is surfaced the same way {@link request} surfaces errors — a
|
|
293
|
+
* typed `HubSdkError` from a `{success:false}` envelope, otherwise an
|
|
294
|
+
* `HUB_HTTP_<status>` transport error — rather than handed back as bogus SSE.
|
|
295
|
+
*/
|
|
296
|
+
async stream(path, init) {
|
|
297
|
+
const response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
298
|
+
...init,
|
|
299
|
+
headers: await this.buildHeaders(init.headers)
|
|
300
|
+
});
|
|
301
|
+
const status = response.status;
|
|
302
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
303
|
+
if (!response.ok || !contentType.includes("text/event-stream")) {
|
|
304
|
+
const bodyText = await response.text().catch(() => "");
|
|
305
|
+
if (contentType.includes("application/json")) {
|
|
306
|
+
let parsed;
|
|
307
|
+
try {
|
|
308
|
+
parsed = JSON.parse(bodyText);
|
|
309
|
+
} catch {
|
|
310
|
+
parsed = void 0;
|
|
311
|
+
}
|
|
312
|
+
if (isHubErrorEnvelope(parsed)) throw new HubSdkError(parsed.error, { status });
|
|
313
|
+
}
|
|
314
|
+
throw httpError(path, status, response.statusText, bodyText, contentType);
|
|
315
|
+
}
|
|
316
|
+
if (!response.body) throw httpError(path, status, response.statusText, "event-stream response had no body", contentType);
|
|
317
|
+
return response.body;
|
|
113
318
|
}
|
|
114
319
|
async buildHeaders(headers) {
|
|
115
320
|
const mergedHeaders = new Headers(headers);
|
|
116
|
-
mergedHeaders.set("Accept", "application/json");
|
|
321
|
+
if (!mergedHeaders.has("Accept")) mergedHeaders.set("Accept", "application/json");
|
|
117
322
|
if (this.apiKey) mergedHeaders.set("Authorization", `Bearer ${this.apiKey}`);
|
|
118
323
|
if (this.authHeaders) for (const [key, value] of new Headers(await this.authHeaders())) mergedHeaders.set(key, value);
|
|
119
324
|
for (const [key, value] of new Headers(headers)) mergedHeaders.set(key, value);
|
|
@@ -169,6 +374,33 @@ var HubPermissionsClient = class {
|
|
|
169
374
|
headers: { "Content-Type": "application/json" }
|
|
170
375
|
});
|
|
171
376
|
}
|
|
377
|
+
/** Reset an action to its default by deleting any stored override. */
|
|
378
|
+
async delete(input) {
|
|
379
|
+
return this.request("/v1/hub/policies", {
|
|
380
|
+
method: "DELETE",
|
|
381
|
+
body: JSON.stringify(input),
|
|
382
|
+
headers: { "Content-Type": "application/json" }
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/** Bulk-allow every write action of a connection's provider. Reads are
|
|
386
|
+
* already allowed for sandbox agents; destructive actions stay `ask`.
|
|
387
|
+
* Idempotent: actions with an existing policy are left untouched. */
|
|
388
|
+
async allowWrites(connectionId) {
|
|
389
|
+
return this.request("/v1/hub/policies/allow-writes", {
|
|
390
|
+
method: "POST",
|
|
391
|
+
body: JSON.stringify({ connectionId }),
|
|
392
|
+
headers: { "Content-Type": "application/json" }
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/** Inverse of `allowWrites`: delete only the rows it created for this
|
|
396
|
+
* connection. Manual per-action decisions are left intact. */
|
|
397
|
+
async revertWrites(connectionId) {
|
|
398
|
+
return this.request("/v1/hub/policies/revert-writes", {
|
|
399
|
+
method: "POST",
|
|
400
|
+
body: JSON.stringify({ connectionId }),
|
|
401
|
+
headers: { "Content-Type": "application/json" }
|
|
402
|
+
});
|
|
403
|
+
}
|
|
172
404
|
};
|
|
173
405
|
var HubToolsClient = class {
|
|
174
406
|
constructor(request) {
|
|
@@ -179,7 +411,8 @@ var HubToolsClient = class {
|
|
|
179
411
|
method: "POST",
|
|
180
412
|
body: JSON.stringify({
|
|
181
413
|
query,
|
|
182
|
-
provider: options.provider
|
|
414
|
+
provider: options.provider,
|
|
415
|
+
limit: options.limit
|
|
183
416
|
}),
|
|
184
417
|
headers: { "Content-Type": "application/json" }
|
|
185
418
|
});
|
|
@@ -275,6 +508,42 @@ function validateApprovedExecution(approved, path, connectionId) {
|
|
|
275
508
|
}
|
|
276
509
|
});
|
|
277
510
|
}
|
|
511
|
+
var HubChannelsClient = class {
|
|
512
|
+
constructor(request) {
|
|
513
|
+
this.request = request;
|
|
514
|
+
}
|
|
515
|
+
async list() {
|
|
516
|
+
return this.request("/v1/hub/channels", { method: "GET" });
|
|
517
|
+
}
|
|
518
|
+
async createEmail(input = {}) {
|
|
519
|
+
return this.request("/v1/hub/channels/email", {
|
|
520
|
+
method: "POST",
|
|
521
|
+
body: JSON.stringify(input),
|
|
522
|
+
headers: { "Content-Type": "application/json" }
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async delete(channelId) {
|
|
526
|
+
return this.request(`/v1/hub/channels/${encodeURIComponent(channelId)}`, { method: "DELETE" });
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
var HubEventSubscriptionsClient = class {
|
|
530
|
+
constructor(request) {
|
|
531
|
+
this.request = request;
|
|
532
|
+
}
|
|
533
|
+
async list() {
|
|
534
|
+
return this.request("/v1/hub/event-subscriptions", { method: "GET" });
|
|
535
|
+
}
|
|
536
|
+
async create(input) {
|
|
537
|
+
return this.request("/v1/hub/event-subscriptions", {
|
|
538
|
+
method: "POST",
|
|
539
|
+
body: JSON.stringify(input),
|
|
540
|
+
headers: { "Content-Type": "application/json" }
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
async delete(subscriptionId) {
|
|
544
|
+
return this.request(`/v1/hub/event-subscriptions/${encodeURIComponent(subscriptionId)}`, { method: "DELETE" });
|
|
545
|
+
}
|
|
546
|
+
};
|
|
278
547
|
var HubConnectionsClient = class {
|
|
279
548
|
constructor(request) {
|
|
280
549
|
this.request = request;
|
|
@@ -282,6 +551,12 @@ var HubConnectionsClient = class {
|
|
|
282
551
|
async list() {
|
|
283
552
|
return this.request("/v1/hub/connections", { method: "GET" });
|
|
284
553
|
}
|
|
554
|
+
/** Connector catalog: every provider the hub can expose, each flagged with
|
|
555
|
+
* `configured` (whether its OAuth app credentials are wired). Callers render
|
|
556
|
+
* this alongside `list()` to offer Connect for not-yet-connected providers. */
|
|
557
|
+
async providers() {
|
|
558
|
+
return this.request("/v1/hub/providers", { method: "GET" });
|
|
559
|
+
}
|
|
285
560
|
async start(provider, options = {}) {
|
|
286
561
|
return this.request(`/v1/hub/connections/${encodeURIComponent(provider)}/start`, {
|
|
287
562
|
method: "POST",
|
|
@@ -292,6 +567,31 @@ var HubConnectionsClient = class {
|
|
|
292
567
|
headers: { "Content-Type": "application/json" }
|
|
293
568
|
});
|
|
294
569
|
}
|
|
570
|
+
async startWhatsappEmbeddedSignup(options = {}) {
|
|
571
|
+
return this.request("/v1/hub/connections/whatsapp-business/embedded-signup/start", {
|
|
572
|
+
method: "POST",
|
|
573
|
+
body: JSON.stringify({ returnUrl: options.returnUrl }),
|
|
574
|
+
headers: { "Content-Type": "application/json" }
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
async completeWhatsappEmbeddedSignup(input) {
|
|
578
|
+
return this.request("/v1/hub/connections/whatsapp-business/embedded-signup/complete", {
|
|
579
|
+
method: "POST",
|
|
580
|
+
body: JSON.stringify(input),
|
|
581
|
+
headers: { "Content-Type": "application/json" }
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/** Connect a non-OAuth (api-key) connector by submitting the user's key.
|
|
585
|
+
* The server validates the key with a live probe before persisting and
|
|
586
|
+
* returns the created (or reconnected) connection — there is no redirect,
|
|
587
|
+
* unlike `start`. */
|
|
588
|
+
async connectApiKey(provider, apiKey) {
|
|
589
|
+
return this.request(`/v1/hub/connections/${encodeURIComponent(provider)}/connect-key`, {
|
|
590
|
+
method: "POST",
|
|
591
|
+
body: JSON.stringify({ apiKey }),
|
|
592
|
+
headers: { "Content-Type": "application/json" }
|
|
593
|
+
});
|
|
594
|
+
}
|
|
295
595
|
async revoke(connectionId) {
|
|
296
596
|
return this.request(`/v1/hub/connections/${encodeURIComponent(connectionId)}`, { method: "DELETE" });
|
|
297
597
|
}
|
|
@@ -355,7 +655,480 @@ var HubAuditClient = class {
|
|
|
355
655
|
return this.request(`/v1/hub/audit${query ? `?${query}` : ""}`, { method: "GET" });
|
|
356
656
|
}
|
|
357
657
|
};
|
|
658
|
+
var HubWorkflowsClient = class {
|
|
659
|
+
constructor(request, stream) {
|
|
660
|
+
this.request = request;
|
|
661
|
+
this.stream = stream;
|
|
662
|
+
}
|
|
663
|
+
async list() {
|
|
664
|
+
return this.request("/v1/workflows", { method: "GET" });
|
|
665
|
+
}
|
|
666
|
+
async get(id) {
|
|
667
|
+
return this.request(`/v1/workflows/${encodeURIComponent(id)}`, { method: "GET" });
|
|
668
|
+
}
|
|
669
|
+
async create(yaml) {
|
|
670
|
+
return this.request("/v1/workflows", {
|
|
671
|
+
method: "POST",
|
|
672
|
+
body: JSON.stringify({ yaml }),
|
|
673
|
+
headers: { "Content-Type": "application/json" }
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
async update(id, yaml) {
|
|
677
|
+
return this.request(`/v1/workflows/${encodeURIComponent(id)}`, {
|
|
678
|
+
method: "PUT",
|
|
679
|
+
body: JSON.stringify({ yaml }),
|
|
680
|
+
headers: { "Content-Type": "application/json" }
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
async delete(id) {
|
|
684
|
+
await this.request(`/v1/workflows/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Enable or disable a workflow without editing its YAML. Works even when the
|
|
688
|
+
* workflow's connection was revoked (a disabled workflow can't be recompiled
|
|
689
|
+
* via `update`, but it can still be paused/resumed here).
|
|
690
|
+
*/
|
|
691
|
+
async setEnabled(id, enabled) {
|
|
692
|
+
return this.request(`/v1/workflows/${encodeURIComponent(id)}`, {
|
|
693
|
+
method: "PATCH",
|
|
694
|
+
body: JSON.stringify({ enabled }),
|
|
695
|
+
headers: { "Content-Type": "application/json" }
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* One page of run history, newest first. Pass the previous page's
|
|
700
|
+
* `nextCursor` to fetch the next; a null `nextCursor` means the last page.
|
|
701
|
+
*/
|
|
702
|
+
async listRuns(id, opts = {}) {
|
|
703
|
+
const params = new URLSearchParams();
|
|
704
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
705
|
+
if (opts.cursor !== void 0) params.set("cursor", opts.cursor);
|
|
706
|
+
const query = params.toString();
|
|
707
|
+
return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs${query ? `?${query}` : ""}`, { method: "GET" });
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Enqueue an immediate ("Run now") run and return its `runId`. The run goes
|
|
711
|
+
* through the identical execution path a trigger delivery uses.
|
|
712
|
+
*
|
|
713
|
+
* `inputs` supplies the trigger fields the workflow reads (its
|
|
714
|
+
* {@link HubWorkflow.manualRunInputs}), as a flat `{ path: value }` map — e.g.
|
|
715
|
+
* `{ "pull_request.number": "123" }`. Omit it for a one-click run of a
|
|
716
|
+
* workflow that reads no trigger fields. Throws `HubSdkError`:
|
|
717
|
+
* `MISSING_RUN_INPUTS` when a required field is absent (`details.missing`
|
|
718
|
+
* names them), `WORKFLOW_DISABLED` when the workflow is paused, `NOT_FOUND`
|
|
719
|
+
* for an unknown/foreign id. Pass `opts.signal` to abort a slow request.
|
|
720
|
+
*/
|
|
721
|
+
async run(id, inputs, opts = {}) {
|
|
722
|
+
const hasInputs = inputs !== void 0 && Object.keys(inputs).length > 0;
|
|
723
|
+
return this.request(`/v1/workflows/${encodeURIComponent(id)}/run`, {
|
|
724
|
+
method: "POST",
|
|
725
|
+
body: JSON.stringify(hasInputs ? { inputs } : {}),
|
|
726
|
+
headers: { "Content-Type": "application/json" },
|
|
727
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* A single run's full detail: per-action `output` + `agentRun` execution
|
|
732
|
+
* detail (size-capped server-side) and the trigger context — the deep "why
|
|
733
|
+
* did this run do what it did" view. `NOT_FOUND` when the run is unknown or
|
|
734
|
+
* does not belong to both the caller and this workflow.
|
|
735
|
+
*/
|
|
736
|
+
async getRun(id, runId, opts = {}) {
|
|
737
|
+
return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}`, {
|
|
738
|
+
method: "GET",
|
|
739
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Stop a queued or in-flight run. A `queued` run is cancelled synchronously
|
|
744
|
+
* and comes back `{ status: "cancelled" }` (already terminal); a `running` run
|
|
745
|
+
* is signalled to abort and comes back `{ status: "cancelling", signalled }` —
|
|
746
|
+
* it settles `cancelled` a moment later as its current action tears down, which
|
|
747
|
+
* a {@link watchRun}/{@link getRun} observes. Throws `HubSdkError`:
|
|
748
|
+
* `RUN_NOT_CANCELLABLE` when the run already finished, `NOT_FOUND` for an
|
|
749
|
+
* unknown/foreign run id. Pass `opts.signal` to abort a slow request.
|
|
750
|
+
*/
|
|
751
|
+
async cancel(id, runId, opts = {}) {
|
|
752
|
+
return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Stream a run's live progress as an async iterable of typed events. The
|
|
759
|
+
* first event is always a `snapshot` of the current persisted state; then
|
|
760
|
+
* `action.*` / `iteration.*` / `token` ticks arrive as the run executes;
|
|
761
|
+
* `ping` is a keepalive; a terminal `run.done` ends the iteration. If the run
|
|
762
|
+
* is already finished when the stream opens, it yields the snapshot then
|
|
763
|
+
* `run.done` and completes.
|
|
764
|
+
*
|
|
765
|
+
* Live ticks require the worker executing the run to share the API process;
|
|
766
|
+
* across instances only `snapshot` + `run.done` (from the server's terminal
|
|
767
|
+
* poll) arrive — the persisted record, read via {@link getRun}, stays the
|
|
768
|
+
* source of truth. Pass `signal` to cancel; iterating to `run.done` (or
|
|
769
|
+
* breaking early) releases the connection.
|
|
770
|
+
*/
|
|
771
|
+
async *watchRun(id, runId, opts = {}) {
|
|
772
|
+
const openStream = this.stream;
|
|
773
|
+
if (!openStream) throw new HubSdkError({
|
|
774
|
+
code: "HUB_CONFIG_INVALID",
|
|
775
|
+
message: "watchRun needs a streaming transport; use HubClient (which wires it up) rather than constructing HubWorkflowsClient directly"
|
|
776
|
+
});
|
|
777
|
+
const reader = (await openStream(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/events`, {
|
|
778
|
+
method: "GET",
|
|
779
|
+
headers: { Accept: "text/event-stream" },
|
|
780
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
781
|
+
})).getReader();
|
|
782
|
+
const decoder = new TextDecoder();
|
|
783
|
+
const parser = new SSEChunkParser({ transform: (raw) => {
|
|
784
|
+
try {
|
|
785
|
+
return JSON.parse(raw);
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
} });
|
|
790
|
+
try {
|
|
791
|
+
for (;;) {
|
|
792
|
+
const { done, value } = await reader.read();
|
|
793
|
+
const text = done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
794
|
+
const parsedEvents = done ? [...parser.push(text), ...parser.flush()] : parser.push(text);
|
|
795
|
+
for (const parsed of parsedEvents) {
|
|
796
|
+
const event = toRunStreamEvent(parsed.eventType, parsed.data);
|
|
797
|
+
if (!event) continue;
|
|
798
|
+
yield event;
|
|
799
|
+
if (event.type === "run.done" || event.type === "run.waiting") return;
|
|
800
|
+
}
|
|
801
|
+
if (done) return;
|
|
802
|
+
}
|
|
803
|
+
} finally {
|
|
804
|
+
await reader.cancel().catch(() => {});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Poll {@link getRun} until the run reaches a terminal state (`succeeded` or
|
|
809
|
+
* `failed`) and return its detail. The scripting primitive behind "run and
|
|
810
|
+
* print the result": pairs with {@link run} for a one-call trigger-and-wait.
|
|
811
|
+
*
|
|
812
|
+
* Returns as soon as the run leaves the active set (`queued` / `running`) —
|
|
813
|
+
* i.e. on `succeeded`, `failed`, or any future non-active status — so a new
|
|
814
|
+
* terminal state can never hang the wait. Polls every `pollIntervalMs`
|
|
815
|
+
* (default 2000). With `timeoutMs` set, throws
|
|
816
|
+
* `HubSdkError(WORKFLOW_RUN_TIMEOUT)` — carrying the last observed status on
|
|
817
|
+
* `details.status` — only once the deadline has actually passed; the last
|
|
818
|
+
* wait is clamped to the remaining time so a `timeoutMs` shorter than the
|
|
819
|
+
* poll interval still waits the full requested window rather than giving up a
|
|
820
|
+
* poll early. Pass `signal` to cancel the wait between polls.
|
|
821
|
+
*/
|
|
822
|
+
async waitForRun(id, runId, opts = {}) {
|
|
823
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 2e3;
|
|
824
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) throw new HubSdkError({
|
|
825
|
+
code: "HUB_CONFIG_INVALID",
|
|
826
|
+
message: `waitForRun pollIntervalMs must be a positive number (got ${opts.pollIntervalMs})`
|
|
827
|
+
});
|
|
828
|
+
if (opts.timeoutMs !== void 0 && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs <= 0)) throw new HubSdkError({
|
|
829
|
+
code: "HUB_CONFIG_INVALID",
|
|
830
|
+
message: `waitForRun timeoutMs must be a positive number (got ${opts.timeoutMs})`
|
|
831
|
+
});
|
|
832
|
+
const deadline = opts.timeoutMs !== void 0 ? Date.now() + opts.timeoutMs : null;
|
|
833
|
+
const timeoutError = (status) => new HubSdkError({
|
|
834
|
+
code: "WORKFLOW_RUN_TIMEOUT",
|
|
835
|
+
message: `Workflow run ${runId} did not reach a terminal state within ${opts.timeoutMs}ms${status ? ` (last status: ${status})` : ""}`,
|
|
836
|
+
details: {
|
|
837
|
+
runId,
|
|
838
|
+
status,
|
|
839
|
+
timeoutMs: opts.timeoutMs
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
let lastStatus;
|
|
843
|
+
for (;;) {
|
|
844
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? /* @__PURE__ */ new Error("The operation was aborted");
|
|
845
|
+
const remainingMs = deadline !== null ? deadline - Date.now() : null;
|
|
846
|
+
if (remainingMs !== null && remainingMs <= 0) throw timeoutError(lastStatus);
|
|
847
|
+
const pollSignal = combineAbortSignals(opts.signal, remainingMs);
|
|
848
|
+
let run;
|
|
849
|
+
try {
|
|
850
|
+
run = await this.getRun(id, runId, pollSignal ? { signal: pollSignal } : {});
|
|
851
|
+
} catch (err) {
|
|
852
|
+
if (opts.signal?.aborted) throw err;
|
|
853
|
+
if (deadline !== null && isAbortError(err)) throw timeoutError(lastStatus);
|
|
854
|
+
throw err;
|
|
855
|
+
}
|
|
856
|
+
lastStatus = run.status;
|
|
857
|
+
if (run.status !== "queued" && run.status !== "running") return run;
|
|
858
|
+
const delayRemainingMs = deadline !== null ? deadline - Date.now() : null;
|
|
859
|
+
if (delayRemainingMs !== null && delayRemainingMs <= 0) throw timeoutError(lastStatus);
|
|
860
|
+
await delay(delayRemainingMs !== null ? Math.min(pollIntervalMs, delayRemainingMs) : pollIntervalMs, opts.signal);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async validate(yaml) {
|
|
864
|
+
return this.request("/v1/workflows/validate", {
|
|
865
|
+
method: "POST",
|
|
866
|
+
body: JSON.stringify({ yaml }),
|
|
867
|
+
headers: { "Content-Type": "application/json" }
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
async schema() {
|
|
871
|
+
return this.request("/v1/workflows/schema", { method: "GET" });
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/event-delivery.ts
|
|
876
|
+
const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
877
|
+
const MAX_TOLERANCE_SECONDS = 3600;
|
|
878
|
+
const MAX_SIGNATURE_HEADER_LENGTH = 1024;
|
|
879
|
+
const MAX_SIGNATURES = 5;
|
|
880
|
+
const SHA256_HEX_LENGTH = 64;
|
|
881
|
+
const MIN_CALLBACK_SECRET_BYTES = 32;
|
|
882
|
+
const MAX_SECRET_SCOPE_COMPONENT_BYTES = 512;
|
|
883
|
+
const DEFAULT_MAX_CALLBACK_BODY_BYTES = 5308416;
|
|
884
|
+
const MAX_CALLBACK_BODY_BYTES = 10 * 1024 * 1024;
|
|
885
|
+
var HubEventDeliveryError = class extends Error {
|
|
886
|
+
code;
|
|
887
|
+
constructor(code, message) {
|
|
888
|
+
super(message);
|
|
889
|
+
this.name = "HubEventDeliveryError";
|
|
890
|
+
this.code = code;
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
/**
|
|
894
|
+
* Derive one callback secret per product binding from a single server-held root
|
|
895
|
+
* secret. Products persist only the binding id; the derived secret can be
|
|
896
|
+
* reproduced for callback authentication without another secret table.
|
|
897
|
+
*/
|
|
898
|
+
async function deriveHubEventCallbackSecret(input) {
|
|
899
|
+
const encoder = new TextEncoder();
|
|
900
|
+
if (encoder.encode(input.rootSecret).byteLength < MIN_CALLBACK_SECRET_BYTES) throw new HubEventDeliveryError("INVALID_CALLBACK_SECRET", `Hub event root secret must contain at least ${MIN_CALLBACK_SECRET_BYTES} UTF-8 bytes`);
|
|
901
|
+
for (const [name, value] of [
|
|
902
|
+
["productId", input.productId],
|
|
903
|
+
["ownerId", input.ownerId],
|
|
904
|
+
["bindingId", input.bindingId]
|
|
905
|
+
]) {
|
|
906
|
+
const bytes = encoder.encode(value).byteLength;
|
|
907
|
+
if (bytes === 0 || bytes > MAX_SECRET_SCOPE_COMPONENT_BYTES) throw new HubEventDeliveryError("INVALID_SECRET_SCOPE", `${name} must contain 1-${MAX_SECRET_SCOPE_COMPONENT_BYTES} UTF-8 bytes`);
|
|
908
|
+
}
|
|
909
|
+
const subtle = requireSubtleCrypto();
|
|
910
|
+
const key = await subtle.importKey("raw", encoder.encode(input.rootSecret), {
|
|
911
|
+
name: "HMAC",
|
|
912
|
+
hash: "SHA-256"
|
|
913
|
+
}, false, ["sign"]);
|
|
914
|
+
const scope = JSON.stringify([
|
|
915
|
+
"tangle-hub-event-callback",
|
|
916
|
+
1,
|
|
917
|
+
input.productId,
|
|
918
|
+
input.ownerId,
|
|
919
|
+
input.bindingId
|
|
920
|
+
]);
|
|
921
|
+
return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", key, encoder.encode(scope))));
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Authenticate and parse the platform's callback as one operation. The exact
|
|
925
|
+
* raw body is read once with a size limit before its signature is checked.
|
|
926
|
+
*
|
|
927
|
+
* A successful callback can be retried. Use `delivery.runId` as the durable
|
|
928
|
+
* idempotency key before starting product work.
|
|
929
|
+
*/
|
|
930
|
+
async function authenticateHubEventRequest(input) {
|
|
931
|
+
if (input.request.method !== "POST") return requestFailure("METHOD_NOT_ALLOWED", 405, "method_not_allowed", { Allow: "POST" });
|
|
932
|
+
if (input.request.headers.get("x-tangle-event") !== "hub.event") return requestFailure("UNEXPECTED_EVENT", 400, "invalid_hub_event");
|
|
933
|
+
const contentType = input.request.headers.get("content-type") ?? "";
|
|
934
|
+
if (!/^application\/json(?:\s*;|$)/i.test(contentType)) return requestFailure("UNSUPPORTED_MEDIA_TYPE", 415, "unsupported_media_type");
|
|
935
|
+
const maxBodyBytes = input.maxBodyBytes ?? DEFAULT_MAX_CALLBACK_BODY_BYTES;
|
|
936
|
+
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes < 1 || maxBodyBytes > MAX_CALLBACK_BODY_BYTES) throw new HubEventDeliveryError("INVALID_BODY_LIMIT", `Hub event body limit must be an integer from 1 to ${MAX_CALLBACK_BODY_BYTES} bytes`);
|
|
937
|
+
if (new TextEncoder().encode(input.secret).byteLength < MIN_CALLBACK_SECRET_BYTES) throw new HubEventDeliveryError("INVALID_CALLBACK_SECRET", `Hub event callback secret must contain at least ${MIN_CALLBACK_SECRET_BYTES} UTF-8 bytes`);
|
|
938
|
+
const declaredLength = input.request.headers.get("content-length");
|
|
939
|
+
if (declaredLength !== null && /^\d+$/.test(declaredLength) && Number(declaredLength) > maxBodyBytes) return requestFailure("PAYLOAD_TOO_LARGE", 413, "payload_too_large");
|
|
940
|
+
const body = await readRequestBody(input.request, maxBodyBytes);
|
|
941
|
+
if (body === null) return requestFailure("PAYLOAD_TOO_LARGE", 413, "payload_too_large");
|
|
942
|
+
if (!(await verifyHubEventSignature({
|
|
943
|
+
body,
|
|
944
|
+
signature: input.request.headers.get("x-tangle-signature"),
|
|
945
|
+
secret: input.secret,
|
|
946
|
+
...input.toleranceSeconds !== void 0 ? { toleranceSeconds: input.toleranceSeconds } : {},
|
|
947
|
+
...input.now !== void 0 ? { now: input.now } : {}
|
|
948
|
+
})).valid) return requestFailure("INVALID_SIGNATURE", 401, "invalid_signature");
|
|
949
|
+
let delivery;
|
|
950
|
+
try {
|
|
951
|
+
delivery = parseHubEventDelivery(body);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
if (error instanceof HubEventDeliveryError) return requestFailure("INVALID_DELIVERY", 400, "invalid_delivery");
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
if (input.request.headers.get("x-tangle-delivery-id") !== delivery.runId) return requestFailure("DELIVERY_ID_MISMATCH", 400, "invalid_delivery");
|
|
957
|
+
return {
|
|
958
|
+
ok: true,
|
|
959
|
+
delivery
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Authenticate a Hub event callback against its exact raw request body.
|
|
964
|
+
* The timestamp window rejects captured-request replay, and byte-wise
|
|
965
|
+
* comparison avoids secret-dependent string comparison behavior.
|
|
966
|
+
*/
|
|
967
|
+
async function verifyHubEventSignature(input) {
|
|
968
|
+
if (!input.signature) return {
|
|
969
|
+
valid: false,
|
|
970
|
+
reason: "missing"
|
|
971
|
+
};
|
|
972
|
+
if (input.signature.length > MAX_SIGNATURE_HEADER_LENGTH) return {
|
|
973
|
+
valid: false,
|
|
974
|
+
reason: "malformed"
|
|
975
|
+
};
|
|
976
|
+
const parsed = parseSignatureHeader(input.signature);
|
|
977
|
+
if (!parsed) return {
|
|
978
|
+
valid: false,
|
|
979
|
+
reason: "malformed"
|
|
980
|
+
};
|
|
981
|
+
const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
982
|
+
if (!Number.isFinite(tolerance) || !Number.isInteger(tolerance) || tolerance < 0 || tolerance > MAX_TOLERANCE_SECONDS) throw new HubEventDeliveryError("INVALID_TOLERANCE", `Signature tolerance must be an integer from 0 to ${MAX_TOLERANCE_SECONDS} seconds`);
|
|
983
|
+
const nowMs = input.now instanceof Date ? input.now.getTime() : typeof input.now === "number" ? input.now : Date.now();
|
|
984
|
+
if (!Number.isFinite(nowMs) || Math.abs(Math.floor(nowMs / 1e3) - parsed.timestamp) > tolerance) return {
|
|
985
|
+
valid: false,
|
|
986
|
+
reason: "stale"
|
|
987
|
+
};
|
|
988
|
+
const subtle = requireSubtleCrypto();
|
|
989
|
+
const encoder = new TextEncoder();
|
|
990
|
+
const key = await subtle.importKey("raw", encoder.encode(input.secret), {
|
|
991
|
+
name: "HMAC",
|
|
992
|
+
hash: "SHA-256"
|
|
993
|
+
}, false, ["sign"]);
|
|
994
|
+
const expected = new Uint8Array(await subtle.sign("HMAC", key, encoder.encode(`${parsed.timestamp}.${input.body}`)));
|
|
995
|
+
let matched = 0;
|
|
996
|
+
for (const signature of parsed.signatures) matched |= constantTimeEqual(expected, signature);
|
|
997
|
+
return matched === 1 ? {
|
|
998
|
+
valid: true,
|
|
999
|
+
timestamp: parsed.timestamp
|
|
1000
|
+
} : {
|
|
1001
|
+
valid: false,
|
|
1002
|
+
reason: "mismatch"
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function parseHubEventDelivery(input) {
|
|
1006
|
+
let value = input;
|
|
1007
|
+
if (typeof input === "string") try {
|
|
1008
|
+
value = JSON.parse(input);
|
|
1009
|
+
} catch {
|
|
1010
|
+
throw new HubEventDeliveryError("INVALID_JSON", "Hub event callback body is not valid JSON");
|
|
1011
|
+
}
|
|
1012
|
+
if (!isRecord(value)) return invalidDelivery();
|
|
1013
|
+
const source = value.source;
|
|
1014
|
+
const providerEvent = value.providerEvent;
|
|
1015
|
+
if (!nonEmptyString(value.subscriptionId) || !nonEmptyString(value.workflowId) || !nonEmptyString(value.runId) || !isRecord(source) || source.kind !== "channel" && source.kind !== "connection" || !nonEmptyString(source.id) || !nonEmptyString(source.provider) || !nonEmptyString(source.event) || !isRecord(providerEvent) || !nonEmptyString(providerEvent.provider) || !nonEmptyString(providerEvent.connectionId) || !nonEmptyString(providerEvent.type) || providerEvent.provider !== source.provider || providerEvent.type !== source.event || !Object.hasOwn(providerEvent, "payload") || !optionalString(providerEvent.action) || !optionalString(providerEvent.repo) || !optionalString(providerEvent.deliveryId) || !nonEmptyString(value.firedAt) || !Number.isFinite(Date.parse(value.firedAt))) return invalidDelivery();
|
|
1016
|
+
return {
|
|
1017
|
+
subscriptionId: value.subscriptionId,
|
|
1018
|
+
workflowId: value.workflowId,
|
|
1019
|
+
runId: value.runId,
|
|
1020
|
+
source: {
|
|
1021
|
+
kind: source.kind,
|
|
1022
|
+
id: source.id,
|
|
1023
|
+
provider: source.provider,
|
|
1024
|
+
event: source.event
|
|
1025
|
+
},
|
|
1026
|
+
providerEvent: {
|
|
1027
|
+
provider: providerEvent.provider,
|
|
1028
|
+
connectionId: providerEvent.connectionId,
|
|
1029
|
+
type: providerEvent.type,
|
|
1030
|
+
...typeof providerEvent.action === "string" ? { action: providerEvent.action } : {},
|
|
1031
|
+
...typeof providerEvent.repo === "string" ? { repo: providerEvent.repo } : {},
|
|
1032
|
+
...typeof providerEvent.deliveryId === "string" ? { deliveryId: providerEvent.deliveryId } : {},
|
|
1033
|
+
payload: providerEvent.payload
|
|
1034
|
+
},
|
|
1035
|
+
firedAt: value.firedAt
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function parseSignatureHeader(header) {
|
|
1039
|
+
let timestamp;
|
|
1040
|
+
const signatures = [];
|
|
1041
|
+
for (const segment of header.split(",")) {
|
|
1042
|
+
const separator = segment.indexOf("=");
|
|
1043
|
+
if (separator <= 0) return null;
|
|
1044
|
+
const key = segment.slice(0, separator).trim();
|
|
1045
|
+
const value = segment.slice(separator + 1).trim();
|
|
1046
|
+
if (key === "t") {
|
|
1047
|
+
if (timestamp !== void 0 || !/^\d+$/.test(value)) return null;
|
|
1048
|
+
const candidate = Number(value);
|
|
1049
|
+
if (!Number.isSafeInteger(candidate) || candidate <= 0) return null;
|
|
1050
|
+
timestamp = candidate;
|
|
1051
|
+
} else if (key === "v1") {
|
|
1052
|
+
if (signatures.length >= MAX_SIGNATURES || value.length !== SHA256_HEX_LENGTH || !/^[0-9a-f]+$/i.test(value)) return null;
|
|
1053
|
+
signatures.push(hexToBytes(value));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return timestamp !== void 0 && signatures.length > 0 ? {
|
|
1057
|
+
timestamp,
|
|
1058
|
+
signatures
|
|
1059
|
+
} : null;
|
|
1060
|
+
}
|
|
1061
|
+
function hexToBytes(hex) {
|
|
1062
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
1063
|
+
for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
1064
|
+
return bytes;
|
|
1065
|
+
}
|
|
1066
|
+
function constantTimeEqual(expected, actual) {
|
|
1067
|
+
let difference = expected.length ^ actual.length;
|
|
1068
|
+
const length = Math.max(expected.length, actual.length);
|
|
1069
|
+
for (let index = 0; index < length; index += 1) difference |= (expected[index % expected.length] ?? 0) ^ (actual[index % actual.length] ?? 0);
|
|
1070
|
+
return difference === 0 ? 1 : 0;
|
|
1071
|
+
}
|
|
1072
|
+
function nonEmptyString(value) {
|
|
1073
|
+
return typeof value === "string" && value.length > 0;
|
|
1074
|
+
}
|
|
1075
|
+
function optionalString(value) {
|
|
1076
|
+
return value === void 0 || nonEmptyString(value);
|
|
1077
|
+
}
|
|
1078
|
+
function isRecord(value) {
|
|
1079
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1080
|
+
}
|
|
1081
|
+
function invalidDelivery() {
|
|
1082
|
+
throw new HubEventDeliveryError("INVALID_PAYLOAD", "Hub event callback body does not match the delivery contract");
|
|
1083
|
+
}
|
|
1084
|
+
function requireSubtleCrypto() {
|
|
1085
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1086
|
+
if (!subtle) throw new HubEventDeliveryError("CRYPTO_UNAVAILABLE", "Web Crypto is required for Hub event callback authentication");
|
|
1087
|
+
return subtle;
|
|
1088
|
+
}
|
|
1089
|
+
function bytesToBase64Url(bytes) {
|
|
1090
|
+
let binary = "";
|
|
1091
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1092
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
1093
|
+
}
|
|
1094
|
+
async function readRequestBody(request, maxBytes) {
|
|
1095
|
+
const reader = request.body?.getReader();
|
|
1096
|
+
if (!reader) return "";
|
|
1097
|
+
const chunks = [];
|
|
1098
|
+
let total = 0;
|
|
1099
|
+
try {
|
|
1100
|
+
while (true) {
|
|
1101
|
+
const next = await reader.read();
|
|
1102
|
+
if (next.done) break;
|
|
1103
|
+
total += next.value.byteLength;
|
|
1104
|
+
if (total > maxBytes) return null;
|
|
1105
|
+
chunks.push(next.value);
|
|
1106
|
+
}
|
|
1107
|
+
} finally {
|
|
1108
|
+
await reader.cancel().catch(() => {});
|
|
1109
|
+
}
|
|
1110
|
+
const body = new Uint8Array(total);
|
|
1111
|
+
let offset = 0;
|
|
1112
|
+
for (const chunk of chunks) {
|
|
1113
|
+
body.set(chunk, offset);
|
|
1114
|
+
offset += chunk.byteLength;
|
|
1115
|
+
}
|
|
1116
|
+
return new TextDecoder().decode(body);
|
|
1117
|
+
}
|
|
1118
|
+
function requestFailure(code, status, error, headers) {
|
|
1119
|
+
return {
|
|
1120
|
+
ok: false,
|
|
1121
|
+
code,
|
|
1122
|
+
response: Response.json({ error }, {
|
|
1123
|
+
status,
|
|
1124
|
+
headers: {
|
|
1125
|
+
"cache-control": "no-store",
|
|
1126
|
+
...headers ? Object.fromEntries(new Headers(headers)) : {}
|
|
1127
|
+
}
|
|
1128
|
+
})
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
358
1131
|
//#endregion
|
|
359
|
-
export { HUB_API_KEY_ENV_VAR, HUB_CAPABILITY_TOKEN_ENV_VAR, HUB_URL_ENV_VAR, HubApprovalsClient, HubAuditClient, HubClient, HubConnectionsClient, HubGithubAppClient, HubPermissionsClient, HubSdkError, HubTokensClient, HubToolsClient, redactHubValue, resolveHubAuth, resolveHubBaseUrl };
|
|
1132
|
+
export { HUB_API_KEY_ENV_VAR, HUB_CAPABILITY_TOKEN_ENV_VAR, HUB_URL_ENV_VAR, HubApprovalsClient, HubAuditClient, HubChannelsClient, HubClient, HubConnectionsClient, HubEventDeliveryError, HubEventSubscriptionsClient, HubGithubAppClient, HubPermissionsClient, HubSdkError, HubTokensClient, HubToolsClient, HubWorkflowsClient, authenticateHubEventRequest, deriveHubEventCallbackSecret, parseHubEventDelivery, redactHubValue, resolveHubAuth, resolveHubBaseUrl, verifyHubEventSignature };
|
|
360
1133
|
|
|
361
1134
|
//# sourceMappingURL=index.js.map
|