@vonpay/checkout-cli 0.3.3 → 0.4.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/README.md +4 -1
- package/dist/commands/checkout.d.ts.map +1 -1
- package/dist/commands/checkout.js +2 -0
- package/dist/commands/checkout.js.map +1 -1
- package/dist/commands/listen.d.ts +19 -0
- package/dist/commands/listen.d.ts.map +1 -0
- package/dist/commands/listen.js +236 -0
- package/dist/commands/listen.js.map +1 -0
- package/dist/commands/trigger.d.ts +20 -0
- package/dist/commands/trigger.d.ts.map +1 -1
- package/dist/commands/trigger.js +28 -11
- package/dist/commands/trigger.js.map +1 -1
- package/dist/utils/forward-to.d.ts +34 -0
- package/dist/utils/forward-to.d.ts.map +1 -0
- package/dist/utils/forward-to.js +88 -0
- package/dist/utils/forward-to.js.map +1 -0
- package/dist/utils/listen-forward.d.ts +49 -0
- package/dist/utils/listen-forward.d.ts.map +1 -0
- package/dist/utils/listen-forward.js +113 -0
- package/dist/utils/listen-forward.js.map +1 -0
- package/dist/utils/listen-output.d.ts +52 -0
- package/dist/utils/listen-output.d.ts.map +1 -0
- package/dist/utils/listen-output.js +85 -0
- package/dist/utils/listen-output.js.map +1 -0
- package/dist/utils/sse-consumer.d.ts +81 -0
- package/dist/utils/sse-consumer.d.ts.map +1 -0
- package/dist/utils/sse-consumer.js +172 -0
- package/dist/utils/sse-consumer.js.map +1 -0
- package/dist/utils/sse-parser.d.ts +63 -0
- package/dist/utils/sse-parser.d.ts.map +1 -0
- package/dist/utils/sse-parser.js +149 -0
- package/dist/utils/sse-parser.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-forward.d.ts","sourceRoot":"","sources":["../../src/utils/listen-forward.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,UAAU,cAAc;IACtB,wCAAwC;IACxC,GAAG,EAAE,GAAG,CAAC;IACT;;;;6EAIyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,MAAM,EAAE,WAAW,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,cAAc,CAAC,CAsEzB"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Sign + POST a JSON payload to the local URL. Returns an outcome
|
|
4
|
+
* shape that the output formatters render — never throws (errors
|
|
5
|
+
* surface via `outcome.error`).
|
|
6
|
+
*/
|
|
7
|
+
export async function forwardPayload(payload, opts) {
|
|
8
|
+
const body = JSON.stringify(payload);
|
|
9
|
+
// Stamp `t` at the moment we forward — NOT the server's original
|
|
10
|
+
// delivery-attempt time. The CLI is the *re-sender* for this local
|
|
11
|
+
// delivery: the dev's handler calls `webhooks.constructEvent`, which
|
|
12
|
+
// rejects any `t` outside the freshness window (>5 min old / >30 sec
|
|
13
|
+
// future). The server's original timestamp (carried on the SSE
|
|
14
|
+
// `webhook.attempt` frame) is routinely minutes old — retries,
|
|
15
|
+
// processing lag, a transcript the dev scrolled back through — so
|
|
16
|
+
// replaying it would trip the window. A fresh `t` here matches Stripe
|
|
17
|
+
// CLI's re-sign posture. Unlike the old bare-hex shape, `t` IS folded
|
|
18
|
+
// into the HMAC per the v1 contract (`${t}.${body}`).
|
|
19
|
+
const t = Math.floor(Date.now() / 1000);
|
|
20
|
+
// v1 signature contract (vonpay-checkout/docs/webhook-signature-v1.md):
|
|
21
|
+
// ONE header `x-vonpay-signature: t=<unix-seconds>,v1=<hex>`, HMAC-SHA256
|
|
22
|
+
// over `${t}.${body}`. There is NO separate timestamp header. Rotation
|
|
23
|
+
// (multiple `v1=` entries) does not apply to this synthetic local loop —
|
|
24
|
+
// we sign with a single secret (the API key). When the server ships a
|
|
25
|
+
// per-subscription signing secret on the SSE `webhook.attempt` frame,
|
|
26
|
+
// sign with that here instead of the API key. (The 0.0.0.0 loopback
|
|
27
|
+
// warning already shipped in forward-to.ts.)
|
|
28
|
+
const v1 = createHmac("sha256", opts.signingSecret)
|
|
29
|
+
.update(`${t}.${body}`)
|
|
30
|
+
.digest("hex");
|
|
31
|
+
const signatureHeader = `t=${t},v1=${v1}`;
|
|
32
|
+
const timeoutMs = opts.timeoutMs ?? 5_000;
|
|
33
|
+
const localController = new AbortController();
|
|
34
|
+
const timeoutId = setTimeout(() => localController.abort(), timeoutMs);
|
|
35
|
+
const combinedSignal = anySignal([opts.signal, localController.signal]);
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(opts.url.toString(), {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"x-vonpay-signature": signatureHeader,
|
|
43
|
+
"User-Agent": "vonpay-cli-listen",
|
|
44
|
+
},
|
|
45
|
+
body,
|
|
46
|
+
signal: combinedSignal,
|
|
47
|
+
});
|
|
48
|
+
// Drain response body so the connection can be reused — best practice
|
|
49
|
+
// for repeated POSTs to the same local endpoint.
|
|
50
|
+
try {
|
|
51
|
+
await res.arrayBuffer();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore drain errors; not relevant to forward semantics
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
attempted: true,
|
|
58
|
+
status: res.status,
|
|
59
|
+
latencyMs: Date.now() - start,
|
|
60
|
+
error: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (opts.signal.aborted) {
|
|
65
|
+
return { attempted: true, status: null, latencyMs: null, error: "aborted" };
|
|
66
|
+
}
|
|
67
|
+
if (localController.signal.aborted) {
|
|
68
|
+
return { attempted: true, status: null, latencyMs: null, error: `timeout after ${timeoutMs}ms` };
|
|
69
|
+
}
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
return { attempted: true, status: null, latencyMs: null, error: message };
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
clearTimeout(timeoutId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Combine multiple AbortSignals into one. The returned signal is
|
|
79
|
+
* aborted when any input signal aborts. Polyfill for `AbortSignal.any`
|
|
80
|
+
* which is Node 20.5+ only — guard for older 20.x deployments.
|
|
81
|
+
*
|
|
82
|
+
* The polyfill tracks every listener it registers and removes them all
|
|
83
|
+
* once any input fires — without this, the long-lived parent
|
|
84
|
+
* `opts.signal` accumulates one orphaned listener per forward call,
|
|
85
|
+
* which on a busy listen stream is a measurable leak.
|
|
86
|
+
*/
|
|
87
|
+
function anySignal(signals) {
|
|
88
|
+
if (typeof AbortSignal.any === "function") {
|
|
89
|
+
return AbortSignal.any(signals);
|
|
90
|
+
}
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const tracked = [];
|
|
93
|
+
const onAbort = () => {
|
|
94
|
+
for (const sig of tracked) {
|
|
95
|
+
sig.removeEventListener("abort", onAbort);
|
|
96
|
+
}
|
|
97
|
+
controller.abort();
|
|
98
|
+
};
|
|
99
|
+
for (const signal of signals) {
|
|
100
|
+
if (signal.aborted) {
|
|
101
|
+
controller.abort();
|
|
102
|
+
// Remove already-added listeners — none yet, but defensive.
|
|
103
|
+
for (const sig of tracked) {
|
|
104
|
+
sig.removeEventListener("abort", onAbort);
|
|
105
|
+
}
|
|
106
|
+
return controller.signal;
|
|
107
|
+
}
|
|
108
|
+
signal.addEventListener("abort", onAbort);
|
|
109
|
+
tracked.push(signal);
|
|
110
|
+
}
|
|
111
|
+
return controller.signal;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=listen-forward.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-forward.js","sourceRoot":"","sources":["../../src/utils/listen-forward.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatters for `vonpay checkout listen`.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - "human" (default) — colored transcript, one line per attempt.
|
|
6
|
+
* Optimized for the dev sitting at a terminal watching events
|
|
7
|
+
* flow in real time.
|
|
8
|
+
* - "json" — one JSON object per line on stdout. Optimized for
|
|
9
|
+
* piping to `jq`, `grep`, or log aggregators.
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle messages (connecting, reconnecting, etc.) ALWAYS go to
|
|
12
|
+
* stderr in human mode so stdout stays parseable in `--json` mode
|
|
13
|
+
* even when interleaved with reconnects.
|
|
14
|
+
*/
|
|
15
|
+
export type OutputMode = "human" | "json";
|
|
16
|
+
/**
|
|
17
|
+
* Wire shape of the SSE `webhook.attempt` payload. Mirrors
|
|
18
|
+
* vonpay-checkout's WebhookListenAttempt (snake_case on the wire).
|
|
19
|
+
*
|
|
20
|
+
* Duplicated here rather than importing from @vonpay/checkout-node
|
|
21
|
+
* because the SDK does not currently export an SSE-event type — the
|
|
22
|
+
* listen surface is CLI-only. If the SDK adds a streaming consumer in
|
|
23
|
+
* a future minor, this type should move to the SDK and be re-imported.
|
|
24
|
+
*/
|
|
25
|
+
export interface WebhookAttemptPayload {
|
|
26
|
+
type: "webhook.attempt";
|
|
27
|
+
subscription_id: string;
|
|
28
|
+
event_id: string;
|
|
29
|
+
event_type: string;
|
|
30
|
+
attempt: number;
|
|
31
|
+
succeeded: boolean;
|
|
32
|
+
response_status: number | null;
|
|
33
|
+
latency_ms: number;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
livemode: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ForwardOutcome {
|
|
38
|
+
attempted: boolean;
|
|
39
|
+
/** Local-URL response status, or null when no attempt was made (no --forward-to). */
|
|
40
|
+
status: number | null;
|
|
41
|
+
/** Latency ms of the local-URL POST. */
|
|
42
|
+
latencyMs: number | null;
|
|
43
|
+
/** Error message if the forward attempt failed at the network layer. */
|
|
44
|
+
error: string | null;
|
|
45
|
+
}
|
|
46
|
+
export declare function formatAttemptHuman(payload: WebhookAttemptPayload, forward: ForwardOutcome | null): string;
|
|
47
|
+
export declare function formatAttemptJson(payload: WebhookAttemptPayload, forward: ForwardOutcome | null): string;
|
|
48
|
+
/** Lifecycle line for human mode (stderr). */
|
|
49
|
+
export declare function formatLifecycleHuman(message: string, kind: "info" | "warn" | "error"): string;
|
|
50
|
+
/** Lifecycle line for JSON mode (stderr). */
|
|
51
|
+
export declare function formatLifecycleJson(kind: "connecting" | "connected" | "disconnected" | "auth_failed" | "fatal" | "ready" | "shutdown" | "warning", extra?: Record<string, unknown>): string;
|
|
52
|
+
//# sourceMappingURL=listen-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-output.d.ts","sourceRoot":"","sources":["../../src/utils/listen-output.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,qFAAqF;IACrF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,wCAAwC;IACxC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,wEAAwE;IACxE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GAAG,IAAI,GAC7B,MAAM,CAwBR;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GAAG,IAAI,GAC7B,MAAM,CAqBR;AAED,8CAA8C;AAC9C,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAQ7F;AAED,6CAA6C;AAC7C,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,aAAa,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,EAC9G,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,MAAM,CAER"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export function formatAttemptHuman(payload, forward) {
|
|
3
|
+
const ts = formatTimestamp(payload.timestamp);
|
|
4
|
+
const eventType = chalk.cyan(payload.event_type.padEnd(24));
|
|
5
|
+
const eventId = chalk.gray(payload.event_id.padEnd(28));
|
|
6
|
+
const attempt = chalk.gray(`attempt ${payload.attempt}`);
|
|
7
|
+
const arrow = chalk.gray("→");
|
|
8
|
+
const upstream = formatUpstream(payload);
|
|
9
|
+
const latency = chalk.gray(`${payload.latency_ms}ms`);
|
|
10
|
+
const liveBadge = payload.livemode ? chalk.red(" [LIVE]") : "";
|
|
11
|
+
let line = `[${ts}] ${eventType} ${eventId} ${attempt} ${arrow} ${upstream} ${latency}${liveBadge}`;
|
|
12
|
+
if (forward && forward.attempted) {
|
|
13
|
+
const fwdArrow = chalk.gray("→ local");
|
|
14
|
+
if (forward.error) {
|
|
15
|
+
line += ` ${fwdArrow} ${chalk.red(forward.error)}`;
|
|
16
|
+
}
|
|
17
|
+
else if (forward.status !== null) {
|
|
18
|
+
const fwdStatus = formatStatus(forward.status);
|
|
19
|
+
const fwdLatency = chalk.gray(`${forward.latencyMs}ms`);
|
|
20
|
+
line += ` ${fwdArrow} ${fwdStatus} ${fwdLatency}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return line;
|
|
24
|
+
}
|
|
25
|
+
export function formatAttemptJson(payload, forward) {
|
|
26
|
+
const body = {
|
|
27
|
+
type: "webhook.attempt",
|
|
28
|
+
event_id: payload.event_id,
|
|
29
|
+
event_type: payload.event_type,
|
|
30
|
+
subscription_id: payload.subscription_id,
|
|
31
|
+
attempt: payload.attempt,
|
|
32
|
+
succeeded: payload.succeeded,
|
|
33
|
+
response_status: payload.response_status,
|
|
34
|
+
latency_ms: payload.latency_ms,
|
|
35
|
+
timestamp: payload.timestamp,
|
|
36
|
+
livemode: payload.livemode,
|
|
37
|
+
};
|
|
38
|
+
if (forward && forward.attempted) {
|
|
39
|
+
body.forward = {
|
|
40
|
+
status: forward.status,
|
|
41
|
+
latency_ms: forward.latencyMs,
|
|
42
|
+
error: forward.error,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return JSON.stringify(body);
|
|
46
|
+
}
|
|
47
|
+
/** Lifecycle line for human mode (stderr). */
|
|
48
|
+
export function formatLifecycleHuman(message, kind) {
|
|
49
|
+
const tag = kind === "info"
|
|
50
|
+
? chalk.cyan("[vonpay]")
|
|
51
|
+
: kind === "warn"
|
|
52
|
+
? chalk.yellow("[vonpay]")
|
|
53
|
+
: chalk.red("[vonpay]");
|
|
54
|
+
return `${tag} ${message}`;
|
|
55
|
+
}
|
|
56
|
+
/** Lifecycle line for JSON mode (stderr). */
|
|
57
|
+
export function formatLifecycleJson(kind, extra = {}) {
|
|
58
|
+
return JSON.stringify({ type: `cli.${kind}`, ...extra, timestamp: new Date().toISOString() });
|
|
59
|
+
}
|
|
60
|
+
function formatStatus(status) {
|
|
61
|
+
if (status >= 200 && status < 300)
|
|
62
|
+
return chalk.green(String(status));
|
|
63
|
+
if (status >= 400 && status < 500)
|
|
64
|
+
return chalk.yellow(String(status));
|
|
65
|
+
if (status >= 500)
|
|
66
|
+
return chalk.red(String(status));
|
|
67
|
+
return chalk.gray(String(status));
|
|
68
|
+
}
|
|
69
|
+
function formatUpstream(payload) {
|
|
70
|
+
if (payload.response_status === null) {
|
|
71
|
+
return chalk.red("timeout");
|
|
72
|
+
}
|
|
73
|
+
return formatStatus(payload.response_status);
|
|
74
|
+
}
|
|
75
|
+
/** Local-time `HH:mm:ss` for the human stream. */
|
|
76
|
+
function formatTimestamp(iso) {
|
|
77
|
+
const date = new Date(iso);
|
|
78
|
+
if (isNaN(date.getTime()))
|
|
79
|
+
return iso.slice(0, 19);
|
|
80
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
81
|
+
const m = String(date.getMinutes()).padStart(2, "0");
|
|
82
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
83
|
+
return `${h}:${m}:${s}`;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=listen-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen-output.js","sourceRoot":"","sources":["../../src/utils/listen-output.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type SseEvent } from "./sse-parser.js";
|
|
2
|
+
/**
|
|
3
|
+
* Long-lived SSE consumer with WHATWG-spec reconnect semantics.
|
|
4
|
+
*
|
|
5
|
+
* Wraps native `fetch()` against a streaming endpoint, parses incoming
|
|
6
|
+
* bytes via {@link SseParser}, yields events to the caller, and
|
|
7
|
+
* transparently reconnects on disconnect with exponential backoff.
|
|
8
|
+
*
|
|
9
|
+
* The `Last-Event-ID` header is sent on every reconnect after the first
|
|
10
|
+
* event. The server emits sticky `id: <event_id>:<attempt>` per frame,
|
|
11
|
+
* so a clean reconnect with the header in place skips duplicate events
|
|
12
|
+
* the consumer has already surfaced — at-least-once → effectively
|
|
13
|
+
* exactly-once for SSE.
|
|
14
|
+
*
|
|
15
|
+
* Cancellation: pass an `AbortSignal`. The consumer terminates cleanly
|
|
16
|
+
* (closes the response body reader, no more events, no more reconnects)
|
|
17
|
+
* when the signal aborts. This is the SIGINT / SIGTERM path.
|
|
18
|
+
*
|
|
19
|
+
* Backoff is exponential 1s → 2s → 4s → 8s → 30s cap with infinite
|
|
20
|
+
* retries — matches Stripe CLI / kubectl / aws CLI patterns for
|
|
21
|
+
* long-lived streaming connections. Reset to 1s after a successful
|
|
22
|
+
* read of at least one byte.
|
|
23
|
+
*/
|
|
24
|
+
export interface SseConsumerOptions {
|
|
25
|
+
/** Full URL of the SSE endpoint. */
|
|
26
|
+
url: string;
|
|
27
|
+
/** Headers to send on every connection. Authorization / Accept set automatically. */
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
/** AbortSignal for graceful shutdown. */
|
|
30
|
+
signal: AbortSignal;
|
|
31
|
+
/**
|
|
32
|
+
* Logger for connection-lifecycle events (connect, disconnect, reconnect).
|
|
33
|
+
* The consumer never logs to stdout directly — the CLI command owns
|
|
34
|
+
* output formatting. Default is a no-op.
|
|
35
|
+
*/
|
|
36
|
+
onLifecycle?: (event: LifecycleEvent) => void;
|
|
37
|
+
/** Max backoff in ms. Default 30000 (30s). */
|
|
38
|
+
maxBackoffMs?: number;
|
|
39
|
+
/** Initial backoff in ms. Default 1000 (1s). */
|
|
40
|
+
initialBackoffMs?: number;
|
|
41
|
+
}
|
|
42
|
+
export type LifecycleEvent = {
|
|
43
|
+
kind: "connecting";
|
|
44
|
+
lastEventId: string | undefined;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "connected";
|
|
47
|
+
} | {
|
|
48
|
+
kind: "disconnected";
|
|
49
|
+
reason: string;
|
|
50
|
+
willRetryInMs: number;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "auth_failed";
|
|
53
|
+
status: number;
|
|
54
|
+
body: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "fatal";
|
|
57
|
+
error: Error;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Returns an async iterator of SseEvent. Iterates until the signal
|
|
61
|
+
* aborts OR an unrecoverable error occurs (e.g. 401 / 403 auth failure
|
|
62
|
+
* — those throw because retrying with the same key won't help).
|
|
63
|
+
*
|
|
64
|
+
* The 1h server ceiling fires as `event: timeout` with
|
|
65
|
+
* `data: {"reason":"server-ceiling"}` (per Phase 3d event-format.ts).
|
|
66
|
+
* The consumer surfaces the timeout event to the caller AND triggers
|
|
67
|
+
* an immediate reconnect — same as the upstream-disconnect path,
|
|
68
|
+
* but without the backoff delay.
|
|
69
|
+
*/
|
|
70
|
+
export declare function consumeSse(opts: SseConsumerOptions): AsyncGenerator<SseEvent, void, void>;
|
|
71
|
+
/**
|
|
72
|
+
* Thrown when the server responds 401 or 403. Auth errors do not get
|
|
73
|
+
* retried — the caller is responsible for surfacing the error to the
|
|
74
|
+
* user and exiting non-zero.
|
|
75
|
+
*/
|
|
76
|
+
export declare class SseAuthError extends Error {
|
|
77
|
+
readonly status: number;
|
|
78
|
+
readonly body: string;
|
|
79
|
+
constructor(status: number, body: string);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=sse-consumer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-consumer.d.ts","sourceRoot":"","sources":["../../src/utils/sse-consumer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,yCAAyC;IACzC,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAuB,UAAU,CAC/B,IAAI,EAAE,kBAAkB,GACvB,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,CAsHtC;AAED;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAEnB,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAO/B"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { SseParser } from "./sse-parser.js";
|
|
2
|
+
/**
|
|
3
|
+
* Returns an async iterator of SseEvent. Iterates until the signal
|
|
4
|
+
* aborts OR an unrecoverable error occurs (e.g. 401 / 403 auth failure
|
|
5
|
+
* — those throw because retrying with the same key won't help).
|
|
6
|
+
*
|
|
7
|
+
* The 1h server ceiling fires as `event: timeout` with
|
|
8
|
+
* `data: {"reason":"server-ceiling"}` (per Phase 3d event-format.ts).
|
|
9
|
+
* The consumer surfaces the timeout event to the caller AND triggers
|
|
10
|
+
* an immediate reconnect — same as the upstream-disconnect path,
|
|
11
|
+
* but without the backoff delay.
|
|
12
|
+
*/
|
|
13
|
+
export async function* consumeSse(opts) {
|
|
14
|
+
const initialBackoffMs = opts.initialBackoffMs ?? 1000;
|
|
15
|
+
const maxBackoffMs = opts.maxBackoffMs ?? 30_000;
|
|
16
|
+
const parser = new SseParser();
|
|
17
|
+
let backoffMs = initialBackoffMs;
|
|
18
|
+
const onLifecycle = opts.onLifecycle ?? noop;
|
|
19
|
+
while (!opts.signal.aborted) {
|
|
20
|
+
const headers = {
|
|
21
|
+
Accept: "text/event-stream",
|
|
22
|
+
"Cache-Control": "no-cache",
|
|
23
|
+
...opts.headers,
|
|
24
|
+
};
|
|
25
|
+
const lastEventId = parser.getLastEventId();
|
|
26
|
+
if (lastEventId !== undefined) {
|
|
27
|
+
headers["Last-Event-ID"] = lastEventId;
|
|
28
|
+
}
|
|
29
|
+
onLifecycle({ kind: "connecting", lastEventId });
|
|
30
|
+
let response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetch(opts.url, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
headers,
|
|
35
|
+
signal: opts.signal,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (opts.signal.aborted)
|
|
40
|
+
return;
|
|
41
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
42
|
+
onLifecycle({ kind: "disconnected", reason, willRetryInMs: backoffMs });
|
|
43
|
+
await sleep(backoffMs, opts.signal);
|
|
44
|
+
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (response.status === 401 || response.status === 403) {
|
|
48
|
+
const body = await safeReadText(response);
|
|
49
|
+
onLifecycle({ kind: "auth_failed", status: response.status, body });
|
|
50
|
+
throw new SseAuthError(response.status, body);
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const body = await safeReadText(response);
|
|
54
|
+
onLifecycle({
|
|
55
|
+
kind: "disconnected",
|
|
56
|
+
reason: `HTTP ${response.status}: ${truncate(body, 200)}`,
|
|
57
|
+
willRetryInMs: backoffMs,
|
|
58
|
+
});
|
|
59
|
+
await sleep(backoffMs, opts.signal);
|
|
60
|
+
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!response.body) {
|
|
64
|
+
onLifecycle({
|
|
65
|
+
kind: "disconnected",
|
|
66
|
+
reason: "response has no body",
|
|
67
|
+
willRetryInMs: backoffMs,
|
|
68
|
+
});
|
|
69
|
+
await sleep(backoffMs, opts.signal);
|
|
70
|
+
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
onLifecycle({ kind: "connected" });
|
|
74
|
+
backoffMs = initialBackoffMs; // reset after a successful connect
|
|
75
|
+
const reader = response.body.getReader();
|
|
76
|
+
const decoder = new TextDecoder("utf-8");
|
|
77
|
+
let serverInitiatedReconnect = false;
|
|
78
|
+
try {
|
|
79
|
+
while (!opts.signal.aborted) {
|
|
80
|
+
const { value, done } = await reader.read();
|
|
81
|
+
if (done)
|
|
82
|
+
break;
|
|
83
|
+
const events = parser.feed(decoder.decode(value, { stream: true }));
|
|
84
|
+
for (const ev of events) {
|
|
85
|
+
// Server-ceiling timeout: surface to caller AND mark for
|
|
86
|
+
// immediate reconnect without backoff. Per Phase 3d
|
|
87
|
+
// event-format.ts the server sends this right before
|
|
88
|
+
// closing the connection.
|
|
89
|
+
if (ev.type === "timeout") {
|
|
90
|
+
yield ev;
|
|
91
|
+
serverInitiatedReconnect = true;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
yield ev;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (opts.signal.aborted)
|
|
101
|
+
return;
|
|
102
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
103
|
+
onLifecycle({ kind: "disconnected", reason, willRetryInMs: backoffMs });
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
// Best-effort cancel — fetch already releases on abort but be defensive.
|
|
107
|
+
try {
|
|
108
|
+
await reader.cancel();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (opts.signal.aborted)
|
|
115
|
+
return;
|
|
116
|
+
if (serverInitiatedReconnect) {
|
|
117
|
+
// Skip the backoff sleep; server politely told us to reconnect.
|
|
118
|
+
// backoffMs already at initial because we successfully connected.
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
onLifecycle({
|
|
122
|
+
kind: "disconnected",
|
|
123
|
+
reason: "stream ended",
|
|
124
|
+
willRetryInMs: backoffMs,
|
|
125
|
+
});
|
|
126
|
+
await sleep(backoffMs, opts.signal);
|
|
127
|
+
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Thrown when the server responds 401 or 403. Auth errors do not get
|
|
132
|
+
* retried — the caller is responsible for surfacing the error to the
|
|
133
|
+
* user and exiting non-zero.
|
|
134
|
+
*/
|
|
135
|
+
export class SseAuthError extends Error {
|
|
136
|
+
status;
|
|
137
|
+
body;
|
|
138
|
+
constructor(status, body) {
|
|
139
|
+
super(`SSE auth failed: HTTP ${status}${body ? ` — ${truncate(body, 200)}` : ""}`);
|
|
140
|
+
this.status = status;
|
|
141
|
+
this.body = body;
|
|
142
|
+
this.name = "SseAuthError";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function sleep(ms, signal) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
signal.removeEventListener("abort", onAbort);
|
|
149
|
+
resolve();
|
|
150
|
+
}, ms);
|
|
151
|
+
const onAbort = () => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
resolve();
|
|
154
|
+
};
|
|
155
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async function safeReadText(response) {
|
|
159
|
+
try {
|
|
160
|
+
return await response.text();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function truncate(s, n) {
|
|
167
|
+
return s.length <= n ? s : `${s.slice(0, n - 3)}...`;
|
|
168
|
+
}
|
|
169
|
+
function noop() {
|
|
170
|
+
/* no-op */
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=sse-consumer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-consumer.js","sourceRoot":"","sources":["../../src/utils/sse-consumer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure incremental parser for the Server-Sent Events wire format
|
|
3
|
+
* (WHATWG HTML §9.2.6 "Parsing an event stream").
|
|
4
|
+
*
|
|
5
|
+
* Decoupled from I/O so the consumer can drive it from any byte source
|
|
6
|
+
* (fetch ReadableStream, Node Readable, in-memory buffer in tests).
|
|
7
|
+
*
|
|
8
|
+
* Spec details that are easy to get wrong:
|
|
9
|
+
* - Frame separator is one blank line — any of `\n\n`, `\r\n\r\n`, `\r\r`.
|
|
10
|
+
* - A line starting with `:` is a comment — ignored (used as heartbeat).
|
|
11
|
+
* - `field: value` — the single space after `:` is stripped if present.
|
|
12
|
+
* - Multiple `data:` lines within a frame concatenate with `\n` between them.
|
|
13
|
+
* - `id:` sets the parser's lastEventId for the NEXT and subsequent
|
|
14
|
+
* dispatched events (sticky across frames until overridden).
|
|
15
|
+
* - `event:` defaults to `"message"` when absent.
|
|
16
|
+
* - `retry:` is reported on the event but the consumer decides whether
|
|
17
|
+
* to honor it (we expose it; consumer caps with its own backoff).
|
|
18
|
+
* - Unknown fields are ignored — additive evolution is safe.
|
|
19
|
+
*
|
|
20
|
+
* Stateful only in `lastEventId` (per spec). Caller drives by repeatedly
|
|
21
|
+
* calling feed(chunk) on incoming bytes; events come back per call.
|
|
22
|
+
*/
|
|
23
|
+
export interface SseEvent {
|
|
24
|
+
/** Event type from `event:` field. Defaults to `"message"`. */
|
|
25
|
+
type: string;
|
|
26
|
+
/** Joined data payload — `data:` lines joined with `\n`. */
|
|
27
|
+
data: string;
|
|
28
|
+
/** Sticky id from `id:`. Undefined until the stream emits one. */
|
|
29
|
+
lastEventId: string | undefined;
|
|
30
|
+
/** Reconnect-time hint from `retry:` (ms). Undefined when absent. */
|
|
31
|
+
retry: number | undefined;
|
|
32
|
+
}
|
|
33
|
+
export declare class SseBufferOverflowError extends Error {
|
|
34
|
+
readonly cap: number;
|
|
35
|
+
constructor(cap: number);
|
|
36
|
+
}
|
|
37
|
+
export interface SseParserOptions {
|
|
38
|
+
/** Hard cap on in-flight bytes before {@link SseBufferOverflowError}. */
|
|
39
|
+
maxBufferBytes?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare class SseParser {
|
|
42
|
+
private buffer;
|
|
43
|
+
private lastEventId;
|
|
44
|
+
private readonly maxBufferBytes;
|
|
45
|
+
constructor(options?: SseParserOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Feed a chunk of decoded text. Returns any events that completed within
|
|
48
|
+
* this chunk. The remainder (partial frame) is held internally and
|
|
49
|
+
* concatenated with the next chunk.
|
|
50
|
+
*
|
|
51
|
+
* Throws {@link SseBufferOverflowError} when the in-flight buffer
|
|
52
|
+
* exceeds `maxBufferBytes` — the consumer's reconnect-on-error loop
|
|
53
|
+
* recovers by discarding the buffer and re-establishing the
|
|
54
|
+
* connection.
|
|
55
|
+
*/
|
|
56
|
+
feed(chunk: string): SseEvent[];
|
|
57
|
+
/** Drain any in-flight buffer as a final event. Used when the source closes mid-frame and the consumer wants to surface the partial. Returns at most one event. Most callers don't need this — for a well-behaved server-side stream the parser never holds a complete trailing event without a separator. */
|
|
58
|
+
flush(): SseEvent | undefined;
|
|
59
|
+
private parseFrame;
|
|
60
|
+
/** Current sticky id. Consumer uses this for the Last-Event-ID header on reconnect. */
|
|
61
|
+
getLastEventId(): string | undefined;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=sse-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/utils/sse-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,QAAQ;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,qEAAqE;IACrE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAeD,qBAAa,sBAAuB,SAAQ,KAAK;aACnB,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM;CAIxC;AAED,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,OAAO,GAAE,gBAAqB;IAI1C;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE;IAoB/B,8SAA8S;IAC9S,KAAK,IAAI,QAAQ,GAAG,SAAS;IAO7B,OAAO,CAAC,UAAU;IA+DlB,uFAAuF;IACvF,cAAc,IAAI,MAAM,GAAG,SAAS;CAGrC"}
|