Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@polar-sh/sdk": "^0.47.0",
"effect": "^3.17.7",
"eventsource": "^4.1.0",
"lossless-json": "^4.3.0",
"open": "^10.2.0"
},
"devDependencies": {
Expand Down
49 changes: 49 additions & 0 deletions src/commands/listen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Effect, Fiber } from "effect";
import {
type CreateEventSource,
type ListenEventSource,
signedWebhookBody,
startListening,
} from "./listen";

Expand Down Expand Up @@ -34,6 +35,11 @@ class FakeEventSource implements ListenEventSource {
emit(data: unknown) {
this.onmessage?.({ data: JSON.stringify(data) } as MessageEvent);
}

/** Simulate the server pushing a message frame with exact raw bytes. */
emitRaw(data: string) {
this.onmessage?.({ data } as MessageEvent);
}
}

/** Returns the event source at `index`, asserting it exists. */
Expand Down Expand Up @@ -77,6 +83,32 @@ afterEach(async () => {
FakeEventSource.instances = [];
});

describe("signedWebhookBody", () => {
test("rebuilds the compact body from a spaced frame, keeping number literals", () => {
const frame =
'{"key": "webhook.created", "payload": {"webhook_event_id": "whid_1", "payload": {"type": "customer.state_changed", "data": {"consumed_units": 0.0, "credited_units": 10.50, "balance": 1e3, "count": 42}}}}';

expect(signedWebhookBody(frame)).toBe(
'{"type":"customer.state_changed","data":{"consumed_units":0.0,"credited_units":10.50,"balance":1e3,"count":42}}',
);
});

test("preserves key order and nested structure", () => {
const frame =
'{"payload": {"payload": {"b": [1.0, {"z": 2.50}], "a": null, "ok": true}}}';

expect(signedWebhookBody(frame)).toBe(
'{"b":[1.0,{"z":2.50}],"a":null,"ok":true}',
);
});

test("returns undefined when the frame carries no body", () => {
expect(signedWebhookBody('{"key": "connected", "secret": "whsec"}')).toBeUndefined();
expect(signedWebhookBody('{"payload": {}}')).toBeUndefined();
expect(signedWebhookBody('"just a string"')).toBeUndefined();
});
});

describe("startListening", () => {
test("opens a single event source for the listen url", () => {
run();
Expand Down Expand Up @@ -140,6 +172,23 @@ describe("startListening", () => {
);
});

test("forwards the signed bytes for usage metering payloads", () => {
const forward = mock(okResponse) as unknown as typeof fetch;
run({ forward });

instanceAt(0).emitRaw(
'{"id": "evt_1", "key": "webhook", "payload": {"webhook_event_id": "whid_1", "payload": {"type": "customer.state_changed", "timestamp": "2026-01-01T00:00:00Z", "data": {"active_meters": [{"consumed_units": 0.0, "credited_units": 10.50, "balance": 1e3}]}}}, "headers": {"user-agent": "polar.sh webhooks", "content-type": "application/json", "webhook-id": "wh_1", "webhook-timestamp": "12345", "webhook-signature": "sig"}}',
);

expect(forward).toHaveBeenCalledWith(
"http://localhost:3000/webhook",
expect.objectContaining({
method: "POST",
body: '{"type":"customer.state_changed","timestamp":"2026-01-01T00:00:00Z","data":{"active_meters":[{"consumed_units":0.0,"credited_units":10.50,"balance":1e3}]}}',
}),
);
});

test("reconnects when the server sends a reconnect event", () => {
run();

Expand Down
23 changes: 21 additions & 2 deletions src/commands/listen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
EventSource,
type EventSourceInit,
} from "eventsource";
import { parse, stringify } from "lossless-json";
import { environmentPrompt } from "../prompts/environment";
import { organizationLoginPrompt } from "../prompts/organizations";
import {
Expand Down Expand Up @@ -56,6 +57,23 @@ export interface StartListeningOptions {
const defaultCreateEventSource: CreateEventSource = (listenUrl, init) =>
new EventSource(listenUrl, init);

const hasPayload = (value: unknown): value is { payload: unknown } =>
typeof value === "object" && value !== null && "payload" in value;

/**
* Rebuilds the exact bytes Polar signed for the webhook body embedded in a
* listen stream frame, or `undefined` when the frame carries no body.
*/
export const signedWebhookBody = (frame: string): string | undefined => {
const parsed = parse(frame);

if (!hasPayload(parsed) || !hasPayload(parsed.payload)) {
return undefined;
}

return stringify(parsed.payload.payload);
};

/**
* Opens the CLI listen stream and forwards incoming webhook events to the local
* server. When the server emits a `reconnect` event the current connection is
Expand Down Expand Up @@ -130,16 +148,17 @@ export const startListening = ({

const webhookEvent =
Schema.decodeUnknownEither(ListenWebhookEvent)(json);
const body = signedWebhookBody(event.data);

if (Either.isLeft(webhookEvent)) {
if (Either.isLeft(webhookEvent) || body === undefined) {
console.error(">> Failed to decode event");
return;
}

forward(forwardUrl, {
method: "POST",
headers: webhookEvent.right.headers,
body: JSON.stringify(webhookEvent.right.payload.payload),
body,
})
.then((res) => {
const cyan = "\x1b[36m";
Expand Down