Skip to content
Merged
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
2 changes: 1 addition & 1 deletion examples/ai-token-stream-expr-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ config({ path: ".env.local" });
async function* tokenUsageFromAIStream(): AsyncGenerator<
AITokenUsagePayload<"PREMIUM_CALL" | "EXTRA_FEE">
> {
const userId = "c0971bcb-b901-4c3e-a191-c9a97871c39f";
const userId = "c0971bcb-b901-4c3e-a191-c9a97871c36f";

yield {
userId,
Expand Down
9 changes: 0 additions & 9 deletions examples/create-checkout.ts

This file was deleted.

49 changes: 0 additions & 49 deletions examples/webhook-server.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/scrawn/src/core/auth/apiKeyAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ const API_KEY_REGEX = /^scrn_(dash|live|test)_[a-zA-Z0-9]{32}$/;
/**
* Type guard to validate API key format
*/
export function isValidApiKey(key: string): key is ApiKeyFormat {
function isValidApiKey(key: string): key is ApiKeyFormat {
return API_KEY_REGEX.test(key);
}

/**
* Validates and returns a properly typed API key
* @throws Error if the API key format is invalid
*/
export function validateApiKey(key: string): ApiKeyFormat {
function validateApiKey(key: string): ApiKeyFormat {
if (!isValidApiKey(key)) {
log.error(`Invalid API key format: "${key}".`);
throw new ScrawnValidationError(
Expand Down
16 changes: 15 additions & 1 deletion packages/scrawn/src/core/scrawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export class Scrawn<
return this.apiKey;
}

/** Base URL for the HTTP API (derived from baseURL config) */
private httpUrl: string;

private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Expand Down Expand Up @@ -223,6 +226,7 @@ export class Scrawn<

this.apiKey = config.apiKey;
this.retryCount = config.retryCount ?? 2;
this.httpUrl = this.buildHttpUrl(config.baseURL);
this.grpcClient = new GrpcClient(this.parseURLToTarget(config.baseURL), {
secure: config.secure ?? true,
credentials: config.credentials,
Expand All @@ -245,6 +249,16 @@ export class Scrawn<
: `${baseURL}:${ScrawnConfig.grpc.defaultPort}`;
}

private buildHttpUrl(baseURL: string): string {
if (baseURL.includes("://")) {
const url = new URL(baseURL);
return `http://${url.hostname}:8070`;
}

const host = baseURL.includes(":") ? baseURL.split(":")[0] : baseURL;
return `http://${host}:8070`;
}

/**
* Create a type-safe tag reference.
*
Expand Down Expand Up @@ -1289,7 +1303,7 @@ export class Scrawn<
async webhook(request: Request): Promise<WebhookEvent> {
if (!this.cachedPublicKey) {
const response = await fetch(
"http://localhost:8070/api/v1/internals/webhook-endpoint/public-key",
`${this.httpUrl}/api/v1/internals/webhook-endpoint/public-key`,
{ headers: { Authorization: `Bearer ${this.apiKey}` } }
);
if (!response.ok)
Expand Down
27 changes: 19 additions & 8 deletions packages/scrawn/src/core/webhook/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createVerify } from "node:crypto";
import { createPublicKey, verify } from "node:crypto";
import { parseEventType } from "./types.js";
import type { WebhookEvent } from "./types.js";

Expand Down Expand Up @@ -50,10 +50,13 @@ function verifyEd25519(
publicKeyPem: string
): boolean {
try {
const verifier = createVerify("ed25519");
verifier.update(payload);
verifier.end();
return verifier.verify(publicKeyPem, signatureBase64, "base64");
const publicKey = createPublicKey(publicKeyPem);
return verify(
null,
Buffer.from(payload),
publicKey,
Buffer.from(signatureBase64, "base64")
);
} catch {
return false;
}
Expand Down Expand Up @@ -111,7 +114,11 @@ export async function verifyWebhook(
300
);

let parsed: { type: string; data: Record<string, unknown> };
let parsed: {
type: string;
data: Record<string, unknown>;
raw_data?: Record<string, unknown>;
};
try {
parsed = JSON.parse(rawBody);
} catch {
Expand All @@ -131,22 +138,26 @@ export async function verifyWebhook(
);
}

const eventData = parsed.raw_data
? { ...parsed.data, raw_data: parsed.raw_data }
: parsed.data;

switch (parsed.type) {
case "payment.succeeded":
return {
id,
timestamp,
resource: "payment" as const,
action: "succeeded" as const,
data: parsed.data,
data: eventData,
} as unknown as WebhookEvent;
case "payment.failed":
return {
id,
timestamp,
resource: "payment" as const,
action: "failed" as const,
data: parsed.data,
data: eventData,
} as unknown as WebhookEvent;
default:
throw new WebhookVerificationError(`Unknown event type: ${parsed.type}`);
Expand Down
91 changes: 78 additions & 13 deletions packages/scrawn/src/core/webhook/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,93 @@
export interface BillingAddress {
country: string;
city?: string | null;
state?: string | null;
street?: string | null;
zipcode?: string | null;
}

export interface CustomerLimitedDetails {
customer_id: string;
email: string;
name: string;
phone_number?: string | null;
metadata?: { [key: string]: string };
}

export type IntentStatus =
| "succeeded"
| "failed"
| "cancelled"
| "processing"
| "requires_customer_action"
| "requires_merchant_action"
| "requires_payment_method"
| "requires_confirmation"
| "requires_capture"
| "partially_captured"
| "partially_captured_and_capturable";

export interface DodoPaymentData {
billing: BillingAddress;
brand_id: string;
business_id: string;
created_at: string;
currency: string;
customer: CustomerLimitedDetails;
digital_products_delivered: boolean;
metadata: { [key: string]: string };
payment_id: string;
total_amount: number;
status?: IntentStatus | null;
subscription_id?: string | null;
tax?: number | null;
updated_at?: string | null;
invoice_id?: string | null;
invoice_url?: string | null;
payment_method?: string | null;
payment_method_type?: string | null;
discount_id?: string | null;
discount_ids?: string[] | null;
product_cart?: Array<{
product_id: string;
quantity: number;
}>;
refund_status?: string | null;
}

export interface DodoPaymentSucceededEvent {
business_id: string;
data: DodoPaymentData;
timestamp: string;
type: "payment.succeeded";
}

export interface DodoPaymentFailedEvent {
business_id: string;
data: Record<string, unknown>;
timestamp: string;
type: "payment.failed";
}

export interface PaymentSucceededData {
paymentId: string;
checkoutSessionId: string;
userId: string;
amount: number;
currency: string;
mode: "test" | "production";
billed_upto: string;
createdAt: string;
raw_data: DodoPaymentSucceededEvent;
}

export interface PaymentFailedData {
paymentId: string;
checkoutSessionId: string;
userId: string;
mode: "test" | "production";
createdAt: string;
raw_data: DodoPaymentFailedEvent;
}

export type WebhookEvent =
Expand All @@ -28,19 +106,6 @@ export type WebhookEvent =
data: PaymentFailedData;
};

export type WebhookEventMap = {
"payment.succeeded": {
resource: "payment";
action: "succeeded";
data: PaymentSucceededData;
};
"payment.failed": {
resource: "payment";
action: "failed";
data: PaymentFailedData;
};
};

export function parseEventType(
type: string
): { resource: string; action: string } | null {
Expand Down
Loading
Loading