From 35c9043f7c0350c9fdb20571e4de811e9d88a2b5 Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Mon, 1 Jun 2026 23:58:00 +0530 Subject: [PATCH 1/6] feat: explicit httpUrl in constructor, remove hardcoded port computation --- packages/scrawn/src/core/scrawn.ts | 32 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/scrawn/src/core/scrawn.ts b/packages/scrawn/src/core/scrawn.ts index 687d2be..793c8f4 100644 --- a/packages/scrawn/src/core/scrawn.ts +++ b/packages/scrawn/src/core/scrawn.ts @@ -114,6 +114,7 @@ const log = new ScrawnLogger("Scrawn"); * const biller = scrawn({ * apiKey: process.env.SCRAWN_KEY, * baseURL: 'http://localhost:8069', + * httpUrl: 'http://localhost:8070', * tags: ["PREMIUM_CALL", "EXTRA_FEE"] as const, * }); * @@ -205,13 +206,15 @@ export class Scrawn< * * @param config - Configuration object * @param config.apiKey - Your Scrawn API key for authentication - * @param config.baseURL - Base URL for the Scrawn API (e.g., 'https://api.scrawn.dev') + * @param config.baseURL - Base URL for the Scrawn gRPC API (e.g., 'http://localhost:8069') + * @param config.httpUrl - HTTP URL for the Scrawn HTTP API (e.g., 'http://localhost:8070') * * @example * ```typescript * const scrawn = new Scrawn({ * apiKey: 'sk_test_...', - * baseURL: 'https://api.scrawn.dev' + * baseURL: 'http://localhost:8069', + * httpUrl: 'http://localhost:8070', * }); * await scrawn.init(); * ``` @@ -219,6 +222,7 @@ export class Scrawn< constructor(config: { apiKey: AllCredentials["apiKey"]; baseURL: string; + httpUrl: string; secure?: boolean; credentials?: import("@grpc/grpc-js").ChannelCredentials; retryCount?: number; @@ -243,9 +247,18 @@ export class Scrawn< ); } + if (!config.httpUrl || typeof config.httpUrl !== "string") { + throw new ScrawnConfigError( + "httpUrl is required and must be a string", + { + details: { provided: typeof config.httpUrl }, + } + ); + } + this.apiKey = config.apiKey; this.retryCount = config.retryCount ?? 2; - this.httpUrl = this.buildHttpUrl(config.baseURL); + this.httpUrl = config.httpUrl; this.grpcClient = new GrpcClient(this.parseURLToTarget(config.baseURL), { secure: config.secure ?? true, credentials: config.credentials, @@ -268,16 +281,6 @@ 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. * @@ -1307,6 +1310,7 @@ export class Scrawn< export interface ScrawnInitConfig { apiKey: string; baseURL: string; + httpUrl: string; secure?: boolean; credentials?: import("@grpc/grpc-js").ChannelCredentials; tags?: readonly string[]; @@ -1333,6 +1337,7 @@ export interface ScrawnInitConfig { * const biller = scrawn({ * apiKey: process.env.SCRAWN_KEY, * baseURL: process.env.SCRAWN_BASE_URL, + * httpUrl: process.env.SCRAWN_HTTP_URL, * tags: ["PREMIUM_CALL", "EXTRA_FEE"] as const, * expressions: ["MY_EXPR"] as const, * }); @@ -1363,6 +1368,7 @@ export function scrawn( return new Scrawn({ apiKey: config.apiKey as AllCredentials["apiKey"], baseURL: config.baseURL, + httpUrl: config.httpUrl, secure: config.secure, credentials: config.credentials, retryCount: config.retryCount, From b855795aba277e1f83acab1e395151438db8f561 Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Tue, 2 Jun 2026 00:00:09 +0530 Subject: [PATCH 2/6] fix: add httpUrl to example biller config --- examples/scrawn/biller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/scrawn/biller.ts b/examples/scrawn/biller.ts index 19a8c6d..d315376 100644 --- a/examples/scrawn/biller.ts +++ b/examples/scrawn/biller.ts @@ -4,6 +4,7 @@ import { TAGS, EXPRESSIONS } from "./pricerefs.ts"; export const biller = scrawn({ apiKey: process.env.SCRAWN_KEY as string, baseURL: process.env.SCRAWN_BASE_URL as string, + httpUrl: process.env.SCRAWN_HTTP_URL as string, secure: process.env.SCRAWN_BASE_URL?.startsWith("https") ?? false, tags: TAGS, expressions: EXPRESSIONS, From 36135ea264e9fb5f87806e4c020cc146c4e52b1f Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Tue, 2 Jun 2026 00:02:32 +0530 Subject: [PATCH 3/6] fix: add httpUrl to all test scrawn() calls --- packages/scrawn/tests/unit/scrawn/middleware.test.ts | 5 +++++ packages/scrawn/tests/unit/scrawn/scrawn.test.ts | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/scrawn/tests/unit/scrawn/middleware.test.ts b/packages/scrawn/tests/unit/scrawn/middleware.test.ts index 6207201..420132e 100644 --- a/packages/scrawn/tests/unit/scrawn/middleware.test.ts +++ b/packages/scrawn/tests/unit/scrawn/middleware.test.ts @@ -54,6 +54,7 @@ describe("middlewareEventConsumer", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); const middleware = biller.middlewareEventConsumer({ @@ -73,6 +74,7 @@ describe("middlewareEventConsumer", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); const middleware = biller.middlewareEventConsumer({ @@ -92,6 +94,7 @@ describe("middlewareEventConsumer", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); const middleware = biller.middlewareEventConsumer({ @@ -110,6 +113,7 @@ describe("middlewareEventConsumer", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", retryCount: 0, }); attachMockClient(biller); @@ -135,6 +139,7 @@ describe("middlewareEventConsumer", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); const onError = vi.fn(); diff --git a/packages/scrawn/tests/unit/scrawn/scrawn.test.ts b/packages/scrawn/tests/unit/scrawn/scrawn.test.ts index 690703d..b141966 100644 --- a/packages/scrawn/tests/unit/scrawn/scrawn.test.ts +++ b/packages/scrawn/tests/unit/scrawn/scrawn.test.ts @@ -59,6 +59,9 @@ describe("Scrawn", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", + httpUrl: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); @@ -77,6 +80,8 @@ describe("Scrawn", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); @@ -93,6 +98,8 @@ describe("Scrawn", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); const link = await biller.collectPayment("user_1"); @@ -112,6 +119,8 @@ describe("Scrawn", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", + httpUrl: "https://api.example", }); attachMockClient(biller); @@ -124,6 +133,8 @@ describe("Scrawn", () => { const biller = scrawn({ apiKey: validKey, baseURL: "https://api.example", + httpUrl: "https://api.example", + httpUrl: "https://api.example", retryCount: 0, }); const onError = vi.fn(); From 6b05e258ef2fb913ece85978c3ec51dc60f0fbd3 Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Tue, 2 Jun 2026 00:03:26 +0530 Subject: [PATCH 4/6] fix: export isValidApiKey and validateApiKey from apiKeyAuth --- packages/scrawn/src/core/auth/apiKeyAuth.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/scrawn/src/core/auth/apiKeyAuth.ts b/packages/scrawn/src/core/auth/apiKeyAuth.ts index f766d24..9e0697e 100644 --- a/packages/scrawn/src/core/auth/apiKeyAuth.ts +++ b/packages/scrawn/src/core/auth/apiKeyAuth.ts @@ -16,15 +16,11 @@ const API_KEY_REGEX = /^scrn_(dash|live|test)_[a-zA-Z0-9]{32}$/; /** * Type guard to validate API key format */ -function isValidApiKey(key: string): key is ApiKeyFormat { +export 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 - */ -function validateApiKey(key: string): ApiKeyFormat { +export function validateApiKey(key: string): ApiKeyFormat { if (!isValidApiKey(key)) { log.error(`Invalid API key format: "${key}".`); throw new ScrawnValidationError( From ed9dfbb5c5cda836351d269a254e24babec4e6e3 Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Tue, 2 Jun 2026 00:19:12 +0530 Subject: [PATCH 5/6] feat: optional webhookPublicKey in constructor to skip backend fetch --- examples/scrawn/biller.ts | 1 + packages/scrawn/src/core/scrawn.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/examples/scrawn/biller.ts b/examples/scrawn/biller.ts index d315376..3784d4d 100644 --- a/examples/scrawn/biller.ts +++ b/examples/scrawn/biller.ts @@ -8,4 +8,5 @@ export const biller = scrawn({ secure: process.env.SCRAWN_BASE_URL?.startsWith("https") ?? false, tags: TAGS, expressions: EXPRESSIONS, + webhookPublicKey: process.env.SCRAWN_WEBHOOK_PUBLIC_KEY, }); diff --git a/packages/scrawn/src/core/scrawn.ts b/packages/scrawn/src/core/scrawn.ts index 793c8f4..d1ca92c 100644 --- a/packages/scrawn/src/core/scrawn.ts +++ b/packages/scrawn/src/core/scrawn.ts @@ -226,6 +226,7 @@ export class Scrawn< secure?: boolean; credentials?: import("@grpc/grpc-js").ChannelCredentials; retryCount?: number; + webhookPublicKey?: string; }) { try { // Validate configuration @@ -259,6 +260,9 @@ export class Scrawn< this.apiKey = config.apiKey; this.retryCount = config.retryCount ?? 2; this.httpUrl = config.httpUrl; + if (config.webhookPublicKey) { + this.cachedPublicKey = config.webhookPublicKey; + } this.grpcClient = new GrpcClient(this.parseURLToTarget(config.baseURL), { secure: config.secure ?? true, credentials: config.credentials, @@ -1321,6 +1325,12 @@ export interface ScrawnInitConfig { * Each event also gets a manual `.retry()` context in the onError callback. */ retryCount?: number; + /** + * Optional webhook public key to skip fetching it from the backend. + * The dashboard displays this key — paste it here to avoid an extra HTTP + * call on every cold start of biller.webhook(). + */ + webhookPublicKey?: string; } /** @@ -1372,5 +1382,6 @@ export function scrawn( secure: config.secure, credentials: config.credentials, retryCount: config.retryCount, + webhookPublicKey: config.webhookPublicKey, }); } From 5321d91106427a0eda4dd8738b25fc7736a1555f Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Tue, 2 Jun 2026 00:25:22 +0530 Subject: [PATCH 6/6] chore: release Signed-off-by: Devyash Saini --- .changeset/ready-horses-punch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ready-horses-punch.md diff --git a/.changeset/ready-horses-punch.md b/.changeset/ready-horses-punch.md new file mode 100644 index 0000000..7e880cf --- /dev/null +++ b/.changeset/ready-horses-punch.md @@ -0,0 +1,5 @@ +--- +"@scrawn/core": patch +--- + +feat: pass in httpurl and webhook public key to constructor