@tangle-network/hub-sdk 0.3.0 → 0.5.1

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/dist/index.js CHANGED
@@ -49,7 +49,7 @@ function isHubErrorEnvelope(value) {
49
49
  function isHubSuccessEnvelope(value) {
50
50
  return typeof value === "object" && value !== null && value.success === true;
51
51
  }
52
- function isRecord(value) {
52
+ function isRecord$1(value) {
53
53
  return typeof value === "object" && value !== null;
54
54
  }
55
55
  function optString(value) {
@@ -83,7 +83,7 @@ function isRunDoneStatus(value) {
83
83
  */
84
84
  function toRunStreamEvent(eventType, data) {
85
85
  if (eventType === "ping") return { type: "ping" };
86
- if (!isRecord(data)) return null;
86
+ if (!isRecord$1(data)) return null;
87
87
  switch (eventType) {
88
88
  case "snapshot": return typeof data.id === "string" && typeof data.workflowId === "string" && typeof data.status === "string" && Array.isArray(data.actionResults) ? {
89
89
  type: "snapshot",
@@ -226,6 +226,8 @@ var HubClient = class HubClient {
226
226
  apiKey;
227
227
  authHeaders;
228
228
  connections;
229
+ channels;
230
+ eventSubscriptions;
229
231
  permissions;
230
232
  tokens;
231
233
  tools;
@@ -239,13 +241,15 @@ var HubClient = class HubClient {
239
241
  this.authHeaders = options.authHeaders;
240
242
  this.fetch = options.fetch ?? fetch.bind(globalThis);
241
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));
242
246
  this.permissions = new HubPermissionsClient((path, init) => this.request(path, init));
243
247
  this.tokens = new HubTokensClient((path, init) => this.request(path, init));
244
248
  this.tools = new HubToolsClient((path, init) => this.request(path, init));
245
249
  this.approvals = new HubApprovalsClient((path, init) => this.request(path, init));
246
250
  this.audit = new HubAuditClient((path, init) => this.request(path, init));
247
251
  this.githubApp = new HubGithubAppClient((path, init) => this.request(path, init));
248
- this.workflows = new HubWorkflowsClient((path, init) => this.request(path, init), (path, init) => this.stream(path, init));
252
+ this.workflows = new HubWorkflowsClient((path, init) => this.request(path, init), (path, init) => this.stream(path, init), (path, init) => this.requestBytes(path, init));
249
253
  }
250
254
  static fromEnv(options = {}) {
251
255
  const env = options.env ?? readProcessEnv();
@@ -312,6 +316,38 @@ var HubClient = class HubClient {
312
316
  if (!response.body) throw httpError(path, status, response.statusText, "event-stream response had no body", contentType);
313
317
  return response.body;
314
318
  }
319
+ /**
320
+ * Fetch raw (non-envelope) bytes — the artifact download route, whose 200 is
321
+ * the artifact's own content type, not JSON. Error responses on such routes
322
+ * are still the standard envelope, so a failure surfaces exactly like
323
+ * {@link request}: a typed `HubSdkError` from a `{success:false}` body,
324
+ * otherwise an `HUB_HTTP_<status>` transport error.
325
+ */
326
+ async requestBytes(path, init) {
327
+ const response = await this.fetch(`${this.baseUrl}${path}`, {
328
+ ...init,
329
+ headers: await this.buildHeaders(init.headers)
330
+ });
331
+ const status = response.status;
332
+ const contentType = response.headers.get("content-type") ?? "";
333
+ if (!response.ok) {
334
+ const bodyText = await response.text().catch(() => "");
335
+ if (contentType.includes("application/json")) {
336
+ let parsed;
337
+ try {
338
+ parsed = JSON.parse(bodyText);
339
+ } catch {
340
+ parsed = void 0;
341
+ }
342
+ if (isHubErrorEnvelope(parsed)) throw new HubSdkError(parsed.error, { status });
343
+ }
344
+ throw httpError(path, status, response.statusText, bodyText, contentType);
345
+ }
346
+ return {
347
+ bytes: new Uint8Array(await response.arrayBuffer()),
348
+ contentType
349
+ };
350
+ }
315
351
  async buildHeaders(headers) {
316
352
  const mergedHeaders = new Headers(headers);
317
353
  if (!mergedHeaders.has("Accept")) mergedHeaders.set("Accept", "application/json");
@@ -504,6 +540,42 @@ function validateApprovedExecution(approved, path, connectionId) {
504
540
  }
505
541
  });
506
542
  }
543
+ var HubChannelsClient = class {
544
+ constructor(request) {
545
+ this.request = request;
546
+ }
547
+ async list() {
548
+ return this.request("/v1/hub/channels", { method: "GET" });
549
+ }
550
+ async createEmail(input = {}) {
551
+ return this.request("/v1/hub/channels/email", {
552
+ method: "POST",
553
+ body: JSON.stringify(input),
554
+ headers: { "Content-Type": "application/json" }
555
+ });
556
+ }
557
+ async delete(channelId) {
558
+ return this.request(`/v1/hub/channels/${encodeURIComponent(channelId)}`, { method: "DELETE" });
559
+ }
560
+ };
561
+ var HubEventSubscriptionsClient = class {
562
+ constructor(request) {
563
+ this.request = request;
564
+ }
565
+ async list() {
566
+ return this.request("/v1/hub/event-subscriptions", { method: "GET" });
567
+ }
568
+ async create(input) {
569
+ return this.request("/v1/hub/event-subscriptions", {
570
+ method: "POST",
571
+ body: JSON.stringify(input),
572
+ headers: { "Content-Type": "application/json" }
573
+ });
574
+ }
575
+ async delete(subscriptionId) {
576
+ return this.request(`/v1/hub/event-subscriptions/${encodeURIComponent(subscriptionId)}`, { method: "DELETE" });
577
+ }
578
+ };
507
579
  var HubConnectionsClient = class {
508
580
  constructor(request) {
509
581
  this.request = request;
@@ -616,13 +688,24 @@ var HubAuditClient = class {
616
688
  }
617
689
  };
618
690
  var HubWorkflowsClient = class {
619
- constructor(request, stream) {
691
+ constructor(request, stream, download) {
620
692
  this.request = request;
621
693
  this.stream = stream;
694
+ this.download = download;
622
695
  }
623
696
  async list() {
624
697
  return this.request("/v1/workflows", { method: "GET" });
625
698
  }
699
+ /**
700
+ * Owner-wide fleet overview: one row per workflow with its run tallies over
701
+ * a trailing window (default 7d), sorted failing-first. Answers "how are ALL
702
+ * my workflows doing" in one call — the per-workflow insights endpoint is
703
+ * the drill-down, not a way to build this row set client-side.
704
+ */
705
+ async fleetInsights(opts = {}) {
706
+ const query = opts.window ? `?window=${encodeURIComponent(opts.window)}` : "";
707
+ return this.request(`/v1/workflows/insights/fleet${query}`, { method: "GET" });
708
+ }
626
709
  async get(id) {
627
710
  return this.request(`/v1/workflows/${encodeURIComponent(id)}`, { method: "GET" });
628
711
  }
@@ -633,10 +716,21 @@ var HubWorkflowsClient = class {
633
716
  headers: { "Content-Type": "application/json" }
634
717
  });
635
718
  }
636
- async update(id, yaml) {
719
+ /**
720
+ * Replace the workflow's definition from YAML (recompiled server-side,
721
+ * triggers reconciled). Every landed update appends a revision;
722
+ * `opts.note` rides onto that revision row (e.g. "why this change"), visible
723
+ * in {@link listRevisions}. Throws `HubSdkError`: `WORKFLOW_INVALID` when the
724
+ * YAML doesn't compile or its connections aren't met, `CONFLICT` on a
725
+ * concurrent modification, `NOT_FOUND` for an unknown/foreign id.
726
+ */
727
+ async update(id, yaml, opts = {}) {
637
728
  return this.request(`/v1/workflows/${encodeURIComponent(id)}`, {
638
729
  method: "PUT",
639
- body: JSON.stringify({ yaml }),
730
+ body: JSON.stringify({
731
+ yaml,
732
+ ...opts.note !== void 0 ? { note: opts.note } : {}
733
+ }),
640
734
  headers: { "Content-Type": "application/json" }
641
735
  });
642
736
  }
@@ -715,6 +809,24 @@ var HubWorkflowsClient = class {
715
809
  });
716
810
  }
717
811
  /**
812
+ * Re-run a terminal failed/cancelled run: enqueues a FRESH run of the same
813
+ * workflow with the SAME trigger context the original ran against, linked
814
+ * back to it via `retriedFromRunId`. The original row is never mutated —
815
+ * execution is at-most-once, so a retry is always a new run, never a
816
+ * requeue. The response carries the NEW run id (and the run it retried);
817
+ * follow it with {@link getRun}/{@link watchRun}/{@link waitForRun} like any
818
+ * other run. Throws `HubSdkError`: `RUN_NOT_RETRYABLE` when the run is not
819
+ * terminal-failed/cancelled (queued/running/waiting, or succeeded),
820
+ * `WORKFLOW_DISABLED` when the workflow has since been disabled, `NOT_FOUND`
821
+ * for an unknown/foreign run id. Pass `opts.signal` to abort a slow request.
822
+ */
823
+ async retryRun(id, runId, opts = {}) {
824
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/retry`, {
825
+ method: "POST",
826
+ ...opts.signal ? { signal: opts.signal } : {}
827
+ });
828
+ }
829
+ /**
718
830
  * Stream a run's live progress as an async iterable of typed events. The
719
831
  * first event is always a `snapshot` of the current persisted state; then
720
832
  * `action.*` / `iteration.*` / `token` ticks arrive as the run executes;
@@ -820,10 +932,262 @@ var HubWorkflowsClient = class {
820
932
  await delay(delayRemainingMs !== null ? Math.min(pollIntervalMs, delayRemainingMs) : pollIntervalMs, opts.signal);
821
933
  }
822
934
  }
823
- async validate(yaml) {
935
+ /**
936
+ * The workflow's definition history, newest rev first — metadata only (rev,
937
+ * attribution, note); fetch the YAML per-rev with {@link getRevision}. Empty
938
+ * for a pre-versioning workflow until its next update.
939
+ */
940
+ async listRevisions(id) {
941
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/revisions`, { method: "GET" });
942
+ }
943
+ /**
944
+ * One revision WITH its YAML (and the actions compiled alongside it), for
945
+ * diff/inspect views. Throws `HubSdkError(REVISION_NOT_FOUND)` for an
946
+ * unknown rev, `NOT_FOUND` for an unknown/foreign workflow.
947
+ */
948
+ async getRevision(id, rev) {
949
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/revisions/${encodeURIComponent(String(rev))}`, { method: "GET" });
950
+ }
951
+ /**
952
+ * Restore a prior rev's YAML as a NEW head revision (history is append-only)
953
+ * through the same compile path as {@link update}, so the restored definition
954
+ * is re-validated against today's connections/models. Throws `HubSdkError`:
955
+ * `REVISION_NOT_FOUND` for an unknown rev, `WORKFLOW_INVALID` when the old
956
+ * YAML no longer compiles, `CONFLICT` on a concurrent modification.
957
+ */
958
+ async rollback(id, rev) {
959
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/rollback`, {
960
+ method: "POST",
961
+ body: JSON.stringify({ rev }),
962
+ headers: { "Content-Type": "application/json" }
963
+ });
964
+ }
965
+ /**
966
+ * Deterministic health stats over the workflow's last N terminal runs
967
+ * (default 20, max 100): per-node visits/failures/cost/duration, failure
968
+ * clusters by normalized error signature, and distillation CANDIDATES
969
+ * (agent.run nodes with near-static outputs — labeled with their
970
+ * measurements, never as verdicts). No model in this path.
971
+ */
972
+ async health(id, opts = {}) {
973
+ const query = opts.runs !== void 0 ? `?runs=${opts.runs}` : "";
974
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/health${query}`, { method: "GET" });
975
+ }
976
+ /**
977
+ * Propose a definition change — the intelligence path. SERVICE-TOKEN
978
+ * callers only (user principals get `HubSdkError(SERVICE_TOKEN_REQUIRED)`,
979
+ * 403). The YAML is compile-validated server-side (`WORKFLOW_INVALID` on
980
+ * failure) and appended as a `proposed` revision; the head NEVER advances
981
+ * until an owner approves.
982
+ */
983
+ async propose(id, input) {
984
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/proposals`, {
985
+ method: "POST",
986
+ body: JSON.stringify(input),
987
+ headers: { "Content-Type": "application/json" }
988
+ });
989
+ }
990
+ /**
991
+ * Pending (proposed) and declined (rejected) revisions, newest first, with
992
+ * their evidence — the approval surface's input.
993
+ */
994
+ async listProposals(id) {
995
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/proposals`, { method: "GET" });
996
+ }
997
+ /**
998
+ * Accept a proposal: its YAML lands as a NEW applied head revision through
999
+ * the same compile + advance path as {@link rollback} (re-validated against
1000
+ * today's connections/models), and the proposal settles to `applied`.
1001
+ * Throws `HubSdkError`: `PROPOSAL_NOT_PENDING` (409) when the rev is not a
1002
+ * pending proposal, `WORKFLOW_INVALID` when it no longer compiles,
1003
+ * `CONFLICT` on a concurrent modification.
1004
+ */
1005
+ async approveProposal(id, rev) {
1006
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/proposals/${encodeURIComponent(String(rev))}/approve`, { method: "POST" });
1007
+ }
1008
+ /**
1009
+ * Decline a proposal: the revision settles to `rejected` (head untouched);
1010
+ * an optional reason is merged into its evidence as `rejectionReason`.
1011
+ * Throws `HubSdkError(PROPOSAL_NOT_PENDING)` (409) when the rev is not a
1012
+ * pending proposal.
1013
+ */
1014
+ async rejectProposal(id, rev, opts = {}) {
1015
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/proposals/${encodeURIComponent(String(rev))}/reject`, {
1016
+ method: "POST",
1017
+ body: JSON.stringify(opts.reason !== void 0 ? { reason: opts.reason } : {}),
1018
+ headers: { "Content-Type": "application/json" }
1019
+ });
1020
+ }
1021
+ /**
1022
+ * Mint a fresh webhook hook token, retiring the current one immediately.
1023
+ * The plaintext token is returned exactly once (only the hash is stored).
1024
+ * Requires an active webhook trigger — otherwise `HubSdkError` with
1025
+ * `NO_WEBHOOK_TRIGGER` (409).
1026
+ */
1027
+ async rotateHookToken(id) {
1028
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/hook-token/rotate`, { method: "POST" });
1029
+ }
1030
+ /**
1031
+ * One agent round's execution timeline, paged. `action` is the action's
1032
+ * `${steps[N]}` position and `iteration` the round's position in that
1033
+ * action's `iterations` — both exactly as the run detail serializes them.
1034
+ * Pass the previous page's `nextCursor` as `after` for the next page; a null
1035
+ * `nextCursor` means the round is fully read.
1036
+ */
1037
+ async listRunSteps(id, runId, input) {
1038
+ const params = new URLSearchParams({
1039
+ action: String(input.action),
1040
+ iteration: String(input.iteration)
1041
+ });
1042
+ if (input.after !== void 0) params.set("after", String(input.after));
1043
+ if (input.limit !== void 0) params.set("limit", String(input.limit));
1044
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/steps?${params.toString()}`, { method: "GET" });
1045
+ }
1046
+ /**
1047
+ * Store one artifact for a run (raw bytes) and return the opaque ref — the
1048
+ * value a script.run node returns as its step output. `contentType` defaults
1049
+ * to `application/octet-stream`; `nodeId` attributes the write to a graph
1050
+ * node (owner credential only — a run-scoped token binds its own node).
1051
+ * Throws `HubSdkError(ARTIFACT_TOO_LARGE)` past the server cap.
1052
+ */
1053
+ async uploadArtifact(id, runId, input) {
1054
+ const headers = {
1055
+ "Content-Type": input.contentType ?? "application/octet-stream",
1056
+ "x-wf-artifact-name": input.name
1057
+ };
1058
+ if (input.nodeId !== void 0) headers["x-wf-artifact-node"] = input.nodeId;
1059
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/artifacts`, {
1060
+ method: "POST",
1061
+ body: input.bytes,
1062
+ headers
1063
+ });
1064
+ }
1065
+ /** Metadata for every artifact of the run (never the bytes), oldest first. */
1066
+ async listArtifacts(id, runId) {
1067
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/artifacts`, { method: "GET" });
1068
+ }
1069
+ /**
1070
+ * The artifact's raw bytes with their stored content type — the route every
1071
+ * `downloadUrl` attached to an `$artifact` ref in a run detail points at.
1072
+ */
1073
+ async downloadArtifact(id, runId, artifactId) {
1074
+ const downloadBytes = this.download;
1075
+ if (!downloadBytes) throw new HubSdkError({
1076
+ code: "HUB_CONFIG_INVALID",
1077
+ message: "downloadArtifact needs a byte transport; use HubClient (which wires it up) rather than constructing HubWorkflowsClient directly"
1078
+ });
1079
+ return downloadBytes(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}`, { method: "GET" });
1080
+ }
1081
+ /**
1082
+ * The workflow's whole keyspace as metadata (key, version, updatedAt — never
1083
+ * values), key-ordered. Bounded by the server's per-workflow key cap.
1084
+ */
1085
+ async listKv(id) {
1086
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/kv`, { method: "GET" });
1087
+ }
1088
+ /** One KV entry (value + version); `HubSdkError(NOT_FOUND)` when absent. */
1089
+ async getKv(id, key) {
1090
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/kv/${encodeURIComponent(key)}`, { method: "GET" });
1091
+ }
1092
+ /**
1093
+ * Upsert a key; returns the entry with its NEW version (increments on every
1094
+ * write). Throws `HubSdkError(KV_VALUE_TOO_LARGE)` past the 64KiB value cap
1095
+ * or `KV_KEY_LIMIT` past the per-workflow key cap.
1096
+ */
1097
+ async putKv(id, key, value) {
1098
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/kv/${encodeURIComponent(key)}`, {
1099
+ method: "PUT",
1100
+ body: JSON.stringify({ value }),
1101
+ headers: { "Content-Type": "application/json" }
1102
+ });
1103
+ }
1104
+ /**
1105
+ * Compare-and-swap: the write lands only when the stored version still
1106
+ * equals `expectedVersion`; otherwise `HubSdkError(KV_VERSION_CONFLICT)`
1107
+ * (409) — re-read and retry. Creation is {@link putKv}'s job.
1108
+ */
1109
+ async casKv(id, key, expectedVersion, value) {
1110
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/kv/${encodeURIComponent(key)}/cas`, {
1111
+ method: "POST",
1112
+ body: JSON.stringify({
1113
+ expectedVersion,
1114
+ value
1115
+ }),
1116
+ headers: { "Content-Type": "application/json" }
1117
+ });
1118
+ }
1119
+ /** Delete a key; idempotent — `deleted` reports whether a row was removed. */
1120
+ async deleteKv(id, key) {
1121
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/kv/${encodeURIComponent(key)}`, { method: "DELETE" });
1122
+ }
1123
+ /** The curated starter gallery runnable on THIS deployment: metadata,
1124
+ * parameters, referenced providers, and a concrete `previewYaml` per card. */
1125
+ async listTemplates() {
1126
+ return this.request("/v1/workflows/templates", { method: "GET" });
1127
+ }
1128
+ /**
1129
+ * One-click create from a gallery template: substitutes `parameters` into
1130
+ * the template server-side, then runs the SAME create path as
1131
+ * {@link create}. A `{ created: false }` result is NOT an error — an
1132
+ * actionable continuation listing the connections to satisfy before
1133
+ * retrying. Invalid parameters or a compile failure throw `HubSdkError`
1134
+ * (400); an unknown template id throws `NOT_FOUND`.
1135
+ */
1136
+ async instantiateTemplate(templateId, parameters) {
1137
+ return this.request(`/v1/workflows/templates/${encodeURIComponent(templateId)}/instantiate`, {
1138
+ method: "POST",
1139
+ body: JSON.stringify(parameters ? { parameters } : {}),
1140
+ headers: { "Content-Type": "application/json" }
1141
+ });
1142
+ }
1143
+ /**
1144
+ * The caller's open decisions across all workflows — the always-available
1145
+ * "pending approvals" surface. Each entry carries its workflow's name (null
1146
+ * when the workflow was deleted while the run was parked).
1147
+ */
1148
+ async listPendingDecisions() {
1149
+ return this.request("/v1/workflows/decisions/pending", { method: "GET" });
1150
+ }
1151
+ /**
1152
+ * The pending decision a run is parked on. Throws `HubSdkError(NOT_FOUND)`
1153
+ * when the run is unknown/foreign or holds no pending decision.
1154
+ */
1155
+ async getRunDecision(id, runId) {
1156
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/decision`, { method: "GET" });
1157
+ }
1158
+ /**
1159
+ * Answer the decision a run is parked on and resume it from the next action.
1160
+ * `decisionId` is REQUIRED: a run can resolve one decision and re-park on the
1161
+ * next, so an answer addressed only by run could silently land on a question
1162
+ * the caller never saw — a mismatch throws `HubSdkError(DECISION_SUPERSEDED)`.
1163
+ * Other typed failures: `INVALID_CHOICE` (400), `DECISION_ALREADY_RESOLVED` /
1164
+ * `DECISION_EXPIRED` (409), `DECISION_NOT_FOUND` (404), `RUN_NOT_WAITING`
1165
+ * (409 — answered, timed out, or cancelled first).
1166
+ */
1167
+ async resolveRunDecision(id, runId, input) {
1168
+ return this.request(`/v1/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/decision/resolve`, {
1169
+ method: "POST",
1170
+ body: JSON.stringify(input),
1171
+ headers: { "Content-Type": "application/json" }
1172
+ });
1173
+ }
1174
+ /**
1175
+ * Dry-run a YAML definition against the platform compiler without saving it.
1176
+ *
1177
+ * `opts.structural` selects the CI mode: the platform skips its owner-scoped
1178
+ * connection/skill/profile resolution, so the verdict depends only on the
1179
+ * definition and the deployment's capabilities — identical no matter which
1180
+ * providers the calling account has connected. Use it in pipelines that
1181
+ * assert "this YAML is well-formed"; leave it off in an editor, where naming
1182
+ * the unconnected providers is the point.
1183
+ */
1184
+ async validate(yaml, opts = {}) {
824
1185
  return this.request("/v1/workflows/validate", {
825
1186
  method: "POST",
826
- body: JSON.stringify({ yaml }),
1187
+ body: JSON.stringify({
1188
+ yaml,
1189
+ ...opts.structural ? { structural: true } : {}
1190
+ }),
827
1191
  headers: { "Content-Type": "application/json" }
828
1192
  });
829
1193
  }
@@ -832,6 +1196,263 @@ var HubWorkflowsClient = class {
832
1196
  }
833
1197
  };
834
1198
  //#endregion
835
- export { HUB_API_KEY_ENV_VAR, HUB_CAPABILITY_TOKEN_ENV_VAR, HUB_URL_ENV_VAR, HubApprovalsClient, HubAuditClient, HubClient, HubConnectionsClient, HubGithubAppClient, HubPermissionsClient, HubSdkError, HubTokensClient, HubToolsClient, HubWorkflowsClient, redactHubValue, resolveHubAuth, resolveHubBaseUrl };
1199
+ //#region src/event-delivery.ts
1200
+ const DEFAULT_TOLERANCE_SECONDS = 300;
1201
+ const MAX_TOLERANCE_SECONDS = 3600;
1202
+ const MAX_SIGNATURE_HEADER_LENGTH = 1024;
1203
+ const MAX_SIGNATURES = 5;
1204
+ const SHA256_HEX_LENGTH = 64;
1205
+ const MIN_CALLBACK_SECRET_BYTES = 32;
1206
+ const MAX_SECRET_SCOPE_COMPONENT_BYTES = 512;
1207
+ const DEFAULT_MAX_CALLBACK_BODY_BYTES = 5308416;
1208
+ const MAX_CALLBACK_BODY_BYTES = 10 * 1024 * 1024;
1209
+ var HubEventDeliveryError = class extends Error {
1210
+ code;
1211
+ constructor(code, message) {
1212
+ super(message);
1213
+ this.name = "HubEventDeliveryError";
1214
+ this.code = code;
1215
+ }
1216
+ };
1217
+ /**
1218
+ * Derive one callback secret per product binding from a single server-held root
1219
+ * secret. Products persist only the binding id; the derived secret can be
1220
+ * reproduced for callback authentication without another secret table.
1221
+ */
1222
+ async function deriveHubEventCallbackSecret(input) {
1223
+ const encoder = new TextEncoder();
1224
+ 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`);
1225
+ for (const [name, value] of [
1226
+ ["productId", input.productId],
1227
+ ["ownerId", input.ownerId],
1228
+ ["bindingId", input.bindingId]
1229
+ ]) {
1230
+ const bytes = encoder.encode(value).byteLength;
1231
+ 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`);
1232
+ }
1233
+ const subtle = requireSubtleCrypto();
1234
+ const key = await subtle.importKey("raw", encoder.encode(input.rootSecret), {
1235
+ name: "HMAC",
1236
+ hash: "SHA-256"
1237
+ }, false, ["sign"]);
1238
+ const scope = JSON.stringify([
1239
+ "tangle-hub-event-callback",
1240
+ 1,
1241
+ input.productId,
1242
+ input.ownerId,
1243
+ input.bindingId
1244
+ ]);
1245
+ return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", key, encoder.encode(scope))));
1246
+ }
1247
+ /**
1248
+ * Authenticate and parse the platform's callback as one operation. The exact
1249
+ * raw body is read once with a size limit before its signature is checked.
1250
+ *
1251
+ * A successful callback can be retried. Use `delivery.runId` as the durable
1252
+ * idempotency key before starting product work.
1253
+ */
1254
+ async function authenticateHubEventRequest(input) {
1255
+ if (input.request.method !== "POST") return requestFailure("METHOD_NOT_ALLOWED", 405, "method_not_allowed", { Allow: "POST" });
1256
+ if (input.request.headers.get("x-tangle-event") !== "hub.event") return requestFailure("UNEXPECTED_EVENT", 400, "invalid_hub_event");
1257
+ const contentType = input.request.headers.get("content-type") ?? "";
1258
+ if (!/^application\/json(?:\s*;|$)/i.test(contentType)) return requestFailure("UNSUPPORTED_MEDIA_TYPE", 415, "unsupported_media_type");
1259
+ const maxBodyBytes = input.maxBodyBytes ?? DEFAULT_MAX_CALLBACK_BODY_BYTES;
1260
+ 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`);
1261
+ 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`);
1262
+ const declaredLength = input.request.headers.get("content-length");
1263
+ if (declaredLength !== null && /^\d+$/.test(declaredLength) && Number(declaredLength) > maxBodyBytes) return requestFailure("PAYLOAD_TOO_LARGE", 413, "payload_too_large");
1264
+ const body = await readRequestBody(input.request, maxBodyBytes);
1265
+ if (body === null) return requestFailure("PAYLOAD_TOO_LARGE", 413, "payload_too_large");
1266
+ if (!(await verifyHubEventSignature({
1267
+ body,
1268
+ signature: input.request.headers.get("x-tangle-signature"),
1269
+ secret: input.secret,
1270
+ ...input.toleranceSeconds !== void 0 ? { toleranceSeconds: input.toleranceSeconds } : {},
1271
+ ...input.now !== void 0 ? { now: input.now } : {}
1272
+ })).valid) return requestFailure("INVALID_SIGNATURE", 401, "invalid_signature");
1273
+ let delivery;
1274
+ try {
1275
+ delivery = parseHubEventDelivery(body);
1276
+ } catch (error) {
1277
+ if (error instanceof HubEventDeliveryError) return requestFailure("INVALID_DELIVERY", 400, "invalid_delivery");
1278
+ throw error;
1279
+ }
1280
+ if (input.request.headers.get("x-tangle-delivery-id") !== delivery.runId) return requestFailure("DELIVERY_ID_MISMATCH", 400, "invalid_delivery");
1281
+ return {
1282
+ ok: true,
1283
+ delivery
1284
+ };
1285
+ }
1286
+ /**
1287
+ * Authenticate a Hub event callback against its exact raw request body.
1288
+ * The timestamp window rejects captured-request replay, and byte-wise
1289
+ * comparison avoids secret-dependent string comparison behavior.
1290
+ */
1291
+ async function verifyHubEventSignature(input) {
1292
+ if (!input.signature) return {
1293
+ valid: false,
1294
+ reason: "missing"
1295
+ };
1296
+ if (input.signature.length > MAX_SIGNATURE_HEADER_LENGTH) return {
1297
+ valid: false,
1298
+ reason: "malformed"
1299
+ };
1300
+ const parsed = parseSignatureHeader(input.signature);
1301
+ if (!parsed) return {
1302
+ valid: false,
1303
+ reason: "malformed"
1304
+ };
1305
+ const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
1306
+ 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`);
1307
+ const nowMs = input.now instanceof Date ? input.now.getTime() : typeof input.now === "number" ? input.now : Date.now();
1308
+ if (!Number.isFinite(nowMs) || Math.abs(Math.floor(nowMs / 1e3) - parsed.timestamp) > tolerance) return {
1309
+ valid: false,
1310
+ reason: "stale"
1311
+ };
1312
+ const subtle = requireSubtleCrypto();
1313
+ const encoder = new TextEncoder();
1314
+ const key = await subtle.importKey("raw", encoder.encode(input.secret), {
1315
+ name: "HMAC",
1316
+ hash: "SHA-256"
1317
+ }, false, ["sign"]);
1318
+ const expected = new Uint8Array(await subtle.sign("HMAC", key, encoder.encode(`${parsed.timestamp}.${input.body}`)));
1319
+ let matched = 0;
1320
+ for (const signature of parsed.signatures) matched |= constantTimeEqual(expected, signature);
1321
+ return matched === 1 ? {
1322
+ valid: true,
1323
+ timestamp: parsed.timestamp
1324
+ } : {
1325
+ valid: false,
1326
+ reason: "mismatch"
1327
+ };
1328
+ }
1329
+ function parseHubEventDelivery(input) {
1330
+ let value = input;
1331
+ if (typeof input === "string") try {
1332
+ value = JSON.parse(input);
1333
+ } catch {
1334
+ throw new HubEventDeliveryError("INVALID_JSON", "Hub event callback body is not valid JSON");
1335
+ }
1336
+ if (!isRecord(value)) return invalidDelivery();
1337
+ const source = value.source;
1338
+ const providerEvent = value.providerEvent;
1339
+ 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();
1340
+ return {
1341
+ subscriptionId: value.subscriptionId,
1342
+ workflowId: value.workflowId,
1343
+ runId: value.runId,
1344
+ source: {
1345
+ kind: source.kind,
1346
+ id: source.id,
1347
+ provider: source.provider,
1348
+ event: source.event
1349
+ },
1350
+ providerEvent: {
1351
+ provider: providerEvent.provider,
1352
+ connectionId: providerEvent.connectionId,
1353
+ type: providerEvent.type,
1354
+ ...typeof providerEvent.action === "string" ? { action: providerEvent.action } : {},
1355
+ ...typeof providerEvent.repo === "string" ? { repo: providerEvent.repo } : {},
1356
+ ...typeof providerEvent.deliveryId === "string" ? { deliveryId: providerEvent.deliveryId } : {},
1357
+ payload: providerEvent.payload
1358
+ },
1359
+ firedAt: value.firedAt
1360
+ };
1361
+ }
1362
+ function parseSignatureHeader(header) {
1363
+ let timestamp;
1364
+ const signatures = [];
1365
+ for (const segment of header.split(",")) {
1366
+ const separator = segment.indexOf("=");
1367
+ if (separator <= 0) return null;
1368
+ const key = segment.slice(0, separator).trim();
1369
+ const value = segment.slice(separator + 1).trim();
1370
+ if (key === "t") {
1371
+ if (timestamp !== void 0 || !/^\d+$/.test(value)) return null;
1372
+ const candidate = Number(value);
1373
+ if (!Number.isSafeInteger(candidate) || candidate <= 0) return null;
1374
+ timestamp = candidate;
1375
+ } else if (key === "v1") {
1376
+ if (signatures.length >= MAX_SIGNATURES || value.length !== SHA256_HEX_LENGTH || !/^[0-9a-f]+$/i.test(value)) return null;
1377
+ signatures.push(hexToBytes(value));
1378
+ }
1379
+ }
1380
+ return timestamp !== void 0 && signatures.length > 0 ? {
1381
+ timestamp,
1382
+ signatures
1383
+ } : null;
1384
+ }
1385
+ function hexToBytes(hex) {
1386
+ const bytes = new Uint8Array(hex.length / 2);
1387
+ for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
1388
+ return bytes;
1389
+ }
1390
+ function constantTimeEqual(expected, actual) {
1391
+ let difference = expected.length ^ actual.length;
1392
+ const length = Math.max(expected.length, actual.length);
1393
+ for (let index = 0; index < length; index += 1) difference |= (expected[index % expected.length] ?? 0) ^ (actual[index % actual.length] ?? 0);
1394
+ return difference === 0 ? 1 : 0;
1395
+ }
1396
+ function nonEmptyString(value) {
1397
+ return typeof value === "string" && value.length > 0;
1398
+ }
1399
+ function optionalString(value) {
1400
+ return value === void 0 || nonEmptyString(value);
1401
+ }
1402
+ function isRecord(value) {
1403
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1404
+ }
1405
+ function invalidDelivery() {
1406
+ throw new HubEventDeliveryError("INVALID_PAYLOAD", "Hub event callback body does not match the delivery contract");
1407
+ }
1408
+ function requireSubtleCrypto() {
1409
+ const subtle = globalThis.crypto?.subtle;
1410
+ if (!subtle) throw new HubEventDeliveryError("CRYPTO_UNAVAILABLE", "Web Crypto is required for Hub event callback authentication");
1411
+ return subtle;
1412
+ }
1413
+ function bytesToBase64Url(bytes) {
1414
+ let binary = "";
1415
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1416
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1417
+ }
1418
+ async function readRequestBody(request, maxBytes) {
1419
+ const reader = request.body?.getReader();
1420
+ if (!reader) return "";
1421
+ const chunks = [];
1422
+ let total = 0;
1423
+ try {
1424
+ while (true) {
1425
+ const next = await reader.read();
1426
+ if (next.done) break;
1427
+ total += next.value.byteLength;
1428
+ if (total > maxBytes) return null;
1429
+ chunks.push(next.value);
1430
+ }
1431
+ } finally {
1432
+ await reader.cancel().catch(() => {});
1433
+ }
1434
+ const body = new Uint8Array(total);
1435
+ let offset = 0;
1436
+ for (const chunk of chunks) {
1437
+ body.set(chunk, offset);
1438
+ offset += chunk.byteLength;
1439
+ }
1440
+ return new TextDecoder().decode(body);
1441
+ }
1442
+ function requestFailure(code, status, error, headers) {
1443
+ return {
1444
+ ok: false,
1445
+ code,
1446
+ response: Response.json({ error }, {
1447
+ status,
1448
+ headers: {
1449
+ "cache-control": "no-store",
1450
+ ...headers ? Object.fromEntries(new Headers(headers)) : {}
1451
+ }
1452
+ })
1453
+ };
1454
+ }
1455
+ //#endregion
1456
+ 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 };
836
1457
 
837
1458
  //# sourceMappingURL=index.js.map