From b35f28cb6697b7344b7684849b38e77d86ee08e7 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:34 +0400 Subject: [PATCH 01/14] Add types.ts --- types.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 types.ts diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..1438c5d --- /dev/null +++ b/types.ts @@ -0,0 +1,86 @@ +/** + * Webhook notification service types + */ + +/** Supported contract event types */ +export type EventType = + | "group.created" + | "group.joined" + | "group.contribution" + | "group.payout" + | "group.completed" + | "group.cancelled" + | "member.added" + | "member.removed"; + +/** Registered webhook configuration */ +export interface Webhook { + /** Unique identifier for the webhook */ + id: string; + /** The URL to POST event data to */ + url: string; + /** Event types this webhook is subscribed to */ + events: EventType[]; + /** HMAC secret used to sign payloads */ + secret: string; + /** Whether the webhook is active */ + active: boolean; + /** ISO timestamp of when the webhook was created */ + createdAt: string; + /** Optional description */ + description?: string; +} + +/** Input for registering a new webhook */ +export interface RegisterWebhookInput { + /** The URL to POST event data to */ + url: string; + /** Event types to subscribe to */ + events: EventType[]; + /** HMAC secret for payload signing */ + secret: string; + /** Optional description */ + description?: string; +} + +/** Payload sent to webhook endpoints */ +export interface WebhookPayload { + /** The webhook ID that triggered this delivery */ + webhookId: string; + /** The event type */ + event: EventType; + /** ISO timestamp of when the event occurred */ + timestamp: string; + /** Unique delivery ID for idempotency */ + deliveryId: string; + /** Event-specific data */ + data: Record; +} + +/** Result of a webhook delivery attempt */ +export interface DeliveryResult { + /** Delivery attempt ID */ + deliveryId: string; + /** Webhook ID */ + webhookId: string; + /** Whether the delivery succeeded */ + success: boolean; + /** HTTP status code, if available */ + statusCode?: number; + /** Number of attempts made */ + attempts: number; + /** ISO timestamp of final attempt */ + completedAt: string; + /** Error message, if delivery failed */ + error?: string; +} + +/** Options for the WebhookService */ +export interface WebhookServiceOptions { + /** Maximum number of retry attempts (default: 3) */ + maxRetries?: number; + /** Base delay in ms between retries (default: 1000) */ + retryDelay?: number; + /** Request timeout in ms (default: 5000) */ + timeout?: number; +} From 411e4d1f69f3767c28452512647b9e660fb80bae Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:36 +0400 Subject: [PATCH 02/14] Add store.ts --- store.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 store.ts diff --git a/store.ts b/store.ts new file mode 100644 index 0000000..87700f8 --- /dev/null +++ b/store.ts @@ -0,0 +1,82 @@ +import { Webhook, RegisterWebhookInput, EventType } from "./types"; +import { randomUUID } from "crypto"; + +/** + * In-memory store for webhook registrations. + * Can be extended to use a persistent backend. + */ +export class WebhookStore { + private webhooks: Map = new Map(); + + /** + * Register a new webhook. + * @param input - Webhook registration input + * @returns The created webhook + */ + register(input: RegisterWebhookInput): Webhook { + const webhook: Webhook = { + id: randomUUID(), + url: input.url, + events: input.events, + secret: input.secret, + active: true, + createdAt: new Date().toISOString(), + description: input.description, + }; + this.webhooks.set(webhook.id, webhook); + return webhook; + } + + /** + * List all registered webhooks, optionally filtered by event type. + * @param event - Optional event type filter + * @returns Array of matching webhooks + */ + list(event?: EventType): Webhook[] { + const all = Array.from(this.webhooks.values()); + if (!event) return all; + return all.filter((wh) => wh.active && wh.events.includes(event)); + } + + /** + * Get a webhook by ID. + * @param id - Webhook ID + * @returns The webhook or undefined + */ + get(id: string): Webhook | undefined { + return this.webhooks.get(id); + } + + /** + * Delete a webhook by ID. + * @param id - Webhook ID + * @returns true if deleted, false if not found + */ + delete(id: string): boolean { + return this.webhooks.delete(id); + } + + /** + * Deactivate a webhook without deleting it. + * @param id - Webhook ID + * @returns true if deactivated, false if not found + */ + deactivate(id: string): boolean { + const webhook = this.webhooks.get(id); + if (!webhook) return false; + webhook.active = false; + return true; + } + + /** + * Activate a previously deactivated webhook. + * @param id - Webhook ID + * @returns true if activated, false if not found + */ + activate(id: string): boolean { + const webhook = this.webhooks.get(id); + if (!webhook) return false; + webhook.active = true; + return true; + } +} From c428d87520280f12bddbedaaf6787c2128ab05f7 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:37 +0400 Subject: [PATCH 03/14] Add signature.ts --- signature.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 signature.ts diff --git a/signature.ts b/signature.ts new file mode 100644 index 0000000..0a6d212 --- /dev/null +++ b/signature.ts @@ -0,0 +1,44 @@ +import { createHmac } from "crypto"; + +/** + * Generates an HMAC-SHA256 signature for a webhook payload. + * + * The signature is computed over the raw JSON string of the payload + * and provided as a hex digest, prefixed with `sha256=`. + * + * @param payload - The raw JSON payload string + * @param secret - The webhook's HMAC secret + * @returns Signature string in the form `sha256=` + */ +export function generateSignature(payload: string, secret: string): string { + const hmac = createHmac("sha256", secret); + hmac.update(payload, "utf8"); + return `sha256=${hmac.digest("hex")}`; +} + +/** + * Verifies an HMAC-SHA256 signature against a payload. + * + * Uses a timing-safe comparison to prevent timing attacks. + * + * @param payload - The raw JSON payload string + * @param secret - The webhook's HMAC secret + * @param signature - The signature to verify (in `sha256=` format) + * @returns true if the signature is valid, false otherwise + */ +export function verifySignature( + payload: string, + secret: string, + signature: string +): boolean { + const expected = generateSignature(payload, secret); + // Timing-safe comparison + if (expected.length !== signature.length) return false; + const a = Buffer.from(expected, "utf8"); + const b = Buffer.from(signature, "utf8"); + let diff = 0; + for (let i = 0; i < a.length; i++) { + diff |= a[i] ^ b[i]; + } + return diff === 0; +} From 8f43364f87ca25b75be479e7198aaf1039b3c5d1 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:39 +0400 Subject: [PATCH 04/14] Add delivery.ts --- delivery.ts | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 delivery.ts diff --git a/delivery.ts b/delivery.ts new file mode 100644 index 0000000..26e15a5 --- /dev/null +++ b/delivery.ts @@ -0,0 +1,128 @@ +import { randomUUID } from "crypto"; +import { Webhook, WebhookPayload, DeliveryResult, EventType, WebhookServiceOptions } from "./types"; +import { generateSignature } from "./signature"; + +const DEFAULT_MAX_RETRIES = 3; +const DEFAULT_RETRY_DELAY_MS = 1000; +const DEFAULT_TIMEOUT_MS = 5000; + +/** + * Handles the delivery of webhook payloads to registered URLs, + * including retry logic and HMAC signing. + */ +export class WebhookDelivery { + private readonly maxRetries: number; + private readonly retryDelay: number; + private readonly timeout: number; + + constructor(options: WebhookServiceOptions = {}) { + this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; + this.retryDelay = options.retryDelay ?? DEFAULT_RETRY_DELAY_MS; + this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS; + } + + /** + * Deliver an event payload to a single webhook endpoint. + * Retries up to `maxRetries` times on failure with exponential back-off. + * + * @param webhook - The target webhook + * @param event - The event type being delivered + * @param data - The event-specific data + * @returns A DeliveryResult describing the outcome + */ + async deliver( + webhook: Webhook, + event: EventType, + data: Record + ): Promise { + const deliveryId = randomUUID(); + const payload: WebhookPayload = { + webhookId: webhook.id, + event, + timestamp: new Date().toISOString(), + deliveryId, + data, + }; + + const body = JSON.stringify(payload); + const signature = generateSignature(body, webhook.secret); + + let lastError: string | undefined; + let lastStatusCode: number | undefined; + + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + try { + const result = await this.attemptDelivery(webhook.url, body, signature, deliveryId); + lastStatusCode = result.statusCode; + + if (result.ok) { + return { + deliveryId, + webhookId: webhook.id, + success: true, + statusCode: result.statusCode, + attempts: attempt, + completedAt: new Date().toISOString(), + }; + } + + lastError = `HTTP ${result.statusCode}`; + } catch (err: unknown) { + lastError = err instanceof Error ? err.message : String(err); + } + + // Wait before retrying (exponential back-off, skip wait on last attempt) + if (attempt < this.maxRetries) { + await this.sleep(this.retryDelay * Math.pow(2, attempt - 1)); + } + } + + return { + deliveryId, + webhookId: webhook.id, + success: false, + statusCode: lastStatusCode, + attempts: this.maxRetries, + completedAt: new Date().toISOString(), + error: lastError, + }; + } + + /** + * Perform a single HTTP POST attempt. + */ + private async attemptDelivery( + url: string, + body: string, + signature: string, + deliveryId: string + ): Promise<{ ok: boolean; statusCode: number }> { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeout); + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-SoroSave-Signature": signature, + "X-SoroSave-Delivery": deliveryId, + "User-Agent": "SoroSave-Webhook/1.0", + }, + body, + signal: controller.signal, + }); + + return { ok: response.ok, statusCode: response.status }; + } finally { + clearTimeout(timer); + } + } + + /** + * Sleep for the specified number of milliseconds. + */ + private sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} From e7ef03f953ec5d4be652ab53cd565c1f51281c5a Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:41 +0400 Subject: [PATCH 05/14] Add service.ts --- service.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 service.ts diff --git a/service.ts b/service.ts new file mode 100644 index 0000000..a47181d --- /dev/null +++ b/service.ts @@ -0,0 +1,20 @@ +import { + Webhook, + RegisterWebhookInput, + DeliveryResult, + EventType, + WebhookServiceOptions, +} from "./types"; +import { WebhookStore } from "./store"; +import { WebhookDelivery } from "./delivery"; + +/** + * WebhookService — the main entry point for the webhook notification system. + * + * Responsibilities: + * - Register, list, and delete webhook endpoints + * - Dispatch events to all subscribed, active webhooks + * - Handle retry logic via WebhookDelivery + * + * @example + * \ No newline at end of file From b64e52f252c751c671317583ae402ca2789a8caf Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:42 +0400 Subject: [PATCH 06/14] Add server.ts --- server.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 server.ts diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..6b1c537 --- /dev/null +++ b/server.ts @@ -0,0 +1,21 @@ +import { createServer, IncomingMessage, ServerResponse } from "http"; +import { + WebhookService, +} from "./service"; +import { RegisterWebhookInput, EventType } from "./types"; +import { verifySignature } from "./signature"; + +/** + * Lightweight HTTP management API for the WebhookService. + * + * Routes: + * POST /webhooks — Register a webhook + * GET /webhooks — List all webhooks + * GET /webhooks/:id — Get a single webhook + * DELETE /webhooks/:id — Delete a webhook + * POST /webhooks/:id/activate — Activate webhook + * POST /webhooks/:id/deactivate — Deactivate webhook + * POST /webhooks/test — Dispatch a test event + * + * @example + * \ No newline at end of file From 46dd254142f96b4310360495b8d80894b37120de Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:44 +0400 Subject: [PATCH 07/14] Add index.ts --- index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 index.ts diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..3c5da28 --- /dev/null +++ b/index.ts @@ -0,0 +1,10 @@ +/** + * @module webhook + * + * SoroSave Webhook Notification Service + * + * Provides webhook registration, management, and event dispatching + * with HMAC-SHA256 signature verification and retry logic. + * + * @example + * \ No newline at end of file From 41a764925910ae087f6b89b6d6cdb3f6cc20caa6 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:46 +0400 Subject: [PATCH 08/14] Update package.json --- package.json | 75 +++++++++++++--------------------------------------- 1 file changed, 19 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index d035dd5..303f2f2 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,26 @@ { - "name": "@sorosave/sdk", - "version": "0.1.0", - "description": "TypeScript SDK for SoroSave — Decentralized Group Savings Protocol on Soroban", + "name": "@sorosave/webhook", + "version": "1.0.0", + "description": "Webhook notification service for SoroSave contract events", "main": "dist/index.js", "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./react": { - "types": "./dist/react/index.d.ts", - "default": "./dist/react/index.js" - } - }, + "files": [ + "dist" + ], "scripts": { - "build": "tsc", - "test": "vitest run", - "test:watch": "vitest", - "lint": "eslint src/" - }, - "bin": { - "sorosave": "./dist/cli.js" - }, - "dependencies": { - "@stellar/stellar-sdk": "^13.0.0", - "commander": "^11.0.0" - }, - "peerDependencies": { - "react": ">=17.0.0", - "@stellar/stellar-sdk": "^13.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - }, - "devDependencies": { - "@types/node": "^20.0.0", - "@types/react": "^18.0.0", - "react": "^18.0.0", - "typescript": "^5.5.0", - "vitest": "^2.0.0" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/big14way/sorosave.git", - "directory": "sdk" + "build": "tsc --project tsconfig.json", + "test": "jest --testPathPattern=webhook", + "lint": "eslint . --ext .ts" }, "keywords": [ - "soroban", + "sorosave", + "webhook", "stellar", - "defi", - "savings", - "ajo", - "susu", - "chit-fund", - "react", - "hooks" - ] -} \ No newline at end of file + "soroban", + "notifications" + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } +} From 4bfffa1c160530adf29bd2e238b13bab2a1422cc Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:47 +0400 Subject: [PATCH 09/14] Add __tests__\signature.test.ts --- "__tests__\\signature.test.ts" | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 "__tests__\\signature.test.ts" diff --git "a/__tests__\\signature.test.ts" "b/__tests__\\signature.test.ts" new file mode 100644 index 0000000..5a6e7bb --- /dev/null +++ "b/__tests__\\signature.test.ts" @@ -0,0 +1,44 @@ +import { generateSignature, verifySignature } from "../signature"; + +describe("generateSignature", () => { + it("returns a sha256= prefixed hex string", () => { + const sig = generateSignature('{"hello":"world"}', "mysecret"); + expect(sig).toMatch(/^sha256=[0-9a-f]{64}$/); + }); + + it("produces consistent output for the same input", () => { + const payload = JSON.stringify({ event: "group.created" }); + const sig1 = generateSignature(payload, "secret123"); + const sig2 = generateSignature(payload, "secret123"); + expect(sig1).toBe(sig2); + }); + + it("produces different output for different secrets", () => { + const payload = JSON.stringify({ event: "group.created" }); + const sig1 = generateSignature(payload, "secret-a"); + const sig2 = generateSignature(payload, "secret-b"); + expect(sig1).not.toBe(sig2); + }); +}); + +describe("verifySignature", () => { + it("returns true for a valid signature", () => { + const payload = JSON.stringify({ event: "group.created" }); + const secret = "test-secret-key"; + const sig = generateSignature(payload, secret); + expect(verifySignature(payload, secret, sig)).toBe(true); + }); + + it("returns false for an invalid signature", () => { + const payload = JSON.stringify({ event: "group.created" }); + expect(verifySignature(payload, "test-secret", "sha256=invalidsig")).toBe(false); + }); + + it("returns false when payload is tampered with", () => { + const secret = "test-secret-key"; + const original = JSON.stringify({ event: "group.created", groupId: 1 }); + const sig = generateSignature(original, secret); + const tampered = JSON.stringify({ event: "group.created", groupId: 999 }); + expect(verifySignature(tampered, secret, sig)).toBe(false); + }); +}); From 820fb6a6ea969d7f1af9c65b6535a74800368695 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:49 +0400 Subject: [PATCH 10/14] Add __tests__\store.test.ts --- "__tests__\\store.test.ts" | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 "__tests__\\store.test.ts" diff --git "a/__tests__\\store.test.ts" "b/__tests__\\store.test.ts" new file mode 100644 index 0000000..5ca71ff --- /dev/null +++ "b/__tests__\\store.test.ts" @@ -0,0 +1,60 @@ +import { WebhookStore } from "../store"; + +const sampleInput = { + url: "https://example.com/hook", + events: ["group.created" as const], + secret: "supersecretkey", +}; + +describe("WebhookStore", () => { + let store: WebhookStore; + + beforeEach(() => { + store = new WebhookStore(); + }); + + it("registers a webhook and returns it with a generated ID", () => { + const wh = store.register(sampleInput); + expect(wh.id).toBeDefined(); + expect(wh.url).toBe(sampleInput.url); + expect(wh.active).toBe(true); + expect(wh.createdAt).toBeDefined(); + }); + + it("lists all webhooks", () => { + store.register(sampleInput); + store.register({ ...sampleInput, url: "https://example.com/hook2" }); + expect(store.list()).toHaveLength(2); + }); + + it("filters by event type", () => { + store.register(sampleInput); + store.register({ + ...sampleInput, + url: "https://other.com/hook", + events: ["group.payout"], + }); + const filtered = store.list("group.created"); + expect(filtered).toHaveLength(1); + expect(filtered[0].url).toBe(sampleInput.url); + }); + + it("deletes a webhook by ID", () => { + const wh = store.register(sampleInput); + expect(store.delete(wh.id)).toBe(true); + expect(store.get(wh.id)).toBeUndefined(); + }); + + it("returns false when deleting non-existent webhook", () => { + expect(store.delete("nonexistent")).toBe(false); + }); + + it("deactivates and reactivates a webhook", () => { + const wh = store.register(sampleInput); + expect(store.deactivate(wh.id)).toBe(true); + expect(store.list("group.created")).toHaveLength(0); // filtered out + + expect(store.activate(wh.id)).toBe(true); + expect(store.list("group.created")).toHaveLength(1); + }); +}); From ccd5b210d87f4821906a361c043bb81d6e609370 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:51 +0400 Subject: [PATCH 11/14] Add __tests__\service.test.ts --- "__tests__\\service.test.ts" | 110 +++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 "__tests__\\service.test.ts" diff --git "a/__tests__\\service.test.ts" "b/__tests__\\service.test.ts" new file mode 100644 index 0000000..0e45ec6 --- /dev/null +++ "b/__tests__\\service.test.ts" @@ -0,0 +1,110 @@ +import { WebhookService } from "../service"; + +describe("WebhookService", () => { + let service: WebhookService; + + beforeEach(() => { + service = new WebhookService({ maxRetries: 1, retryDelay: 10 }); + }); + + describe("register", () => { + it("registers a valid webhook", () => { + const wh = service.register({ + url: "https://example.com/hook", + events: ["group.created"], + secret: "supersecretkey", + }); + expect(wh.id).toBeDefined(); + expect(wh.active).toBe(true); + }); + + it("throws for an invalid URL", () => { + expect(() => + service.register({ + url: "not-a-url", + events: ["group.created"], + secret: "supersecretkey", + }) + ).toThrow("Invalid webhook URL"); + }); + + it("throws for empty events array", () => { + expect(() => + service.register({ + url: "https://example.com/hook", + events: [], + secret: "supersecretkey", + }) + ).toThrow("At least one event type"); + }); + + it("throws for a secret shorter than 8 characters", () => { + expect(() => + service.register({ + url: "https://example.com/hook", + events: ["group.created"], + secret: "short", + }) + ).toThrow("Secret must be at least 8 characters"); + }); + }); + + describe("list / get / delete", () => { + it("lists registered webhooks", () => { + service.register({ + url: "https://example.com/hook", + events: ["group.created"], + secret: "supersecretkey", + }); + expect(service.list()).toHaveLength(1); + }); + + it("gets a webhook by ID", () => { + const wh = service.register({ + url: "https://example.com/hook", + events: ["group.created"], + secret: "supersecretkey", + }); + expect(service.get(wh.id)).toEqual(wh); + }); + + it("deletes a webhook", () => { + const wh = service.register({ + url: "https://example.com/hook", + events: ["group.created"], + secret: "supersecretkey", + }); + expect(service.delete(wh.id)).toBe(true); + expect(service.get(wh.id)).toBeUndefined(); + }); + }); + + describe("dispatch", () => { + it("returns empty array when no webhooks are registered", async () => { + const results = await service.dispatch("group.created", { groupId: 1 }); + expect(results).toHaveLength(0); + }); + + it("delivers to matching webhooks and records failure for unreachable URLs", async () => { + service.register({ + url: "http://localhost:19999/unreachable", + events: ["group.created"], + secret: "supersecretkey", + }); + const results = await service.dispatch("group.created", { groupId: 1 }); + expect(results).toHaveLength(1); + expect(results[0].success).toBe(false); + expect(results[0].attempts).toBeGreaterThanOrEqual(1); + }); + + it("does not deliver to webhooks not subscribed to the event", async () => { + service.register({ + url: "https://example.com/hook", + events: ["group.payout"], + secret: "supersecretkey", + }); + const results = await service.dispatch("group.created", { groupId: 1 }); + expect(results).toHaveLength(0); + }); + }); +}); From 4ad177ce4460a59a9484acba3a2f5ebb5e1e0070 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:52 +0400 Subject: [PATCH 12/14] Add __tests__\delivery.test.ts --- "__tests__\\delivery.test.ts" | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 "__tests__\\delivery.test.ts" diff --git "a/__tests__\\delivery.test.ts" "b/__tests__\\delivery.test.ts" new file mode 100644 index 0000000..fc10428 --- /dev/null +++ "b/__tests__\\delivery.test.ts" @@ -0,0 +1,32 @@ +import { WebhookDelivery } from "../delivery"; +import { Webhook } from "../types"; + +const mockWebhook: Webhook = { + id: "test-webhook-id", + url: "http://localhost:19998/hook", + events: ["group.created"], + secret: "supersecretkey", + active: true, + createdAt: new Date().toISOString(), +}; + +describe("WebhookDelivery", () => { + it("returns a failed DeliveryResult when endpoint is unreachable", async () => { + const delivery = new WebhookDelivery({ maxRetries: 2, retryDelay: 10, timeout: 200 }); + const result = await delivery.deliver(mockWebhook, "group.created", { groupId: 1 }); + + expect(result.success).toBe(false); + expect(result.webhookId).toBe(mockWebhook.id); + expect(result.attempts).toBe(2); + expect(result.error).toBeDefined(); + expect(result.deliveryId).toBeDefined(); + expect(result.completedAt).toBeDefined(); + }); + + it("includes a deliveryId in results", async () => { + const delivery = new WebhookDelivery({ maxRetries: 1, retryDelay: 10, timeout: 200 }); + const result = await delivery.deliver(mockWebhook, "group.created", {}); + expect(typeof result.deliveryId).toBe("string"); + expect(result.deliveryId.length).toBeGreaterThan(0); + }); +}); From 84e923b67b56aee27e973ca5ecc99723d813ce51 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:54 +0400 Subject: [PATCH 13/14] Update package.json --- package.json | 60 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 303f2f2..781d4a7 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,64 @@ { - "name": "@sorosave/webhook", + "name": "@sorosave/sdk", "version": "1.0.0", - "description": "Webhook notification service for SoroSave contract events", + "description": "TypeScript SDK for interacting with the SoroSave smart contracts on Soroban", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./webhook": { + "import": "./webhook/dist/index.js", + "require": "./webhook/dist/index.js", + "types": "./webhook/dist/index.d.ts" + }, + "./react": { + "import": "./dist/react/index.js", + "require": "./dist/react/index.js", + "types": "./dist/react/index.d.ts" + } + }, "files": [ - "dist" + "dist", + "webhook/dist", + "generated" ], "scripts": { - "build": "tsc --project tsconfig.json", - "test": "jest --testPathPattern=webhook", - "lint": "eslint . --ext .ts" + "build": "tsc && npm run build:webhook", + "build:webhook": "tsc --project webhook/tsconfig.json", + "test": "jest", + "test:webhook": "jest --testPathPattern=webhook", + "lint": "eslint . --ext .ts", + "codegen": "ts-node scripts/codegen.ts" }, "keywords": [ - "sorosave", - "webhook", "stellar", "soroban", - "notifications" + "sorosave", + "sdk", + "blockchain" ], "license": "MIT", - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + }, + "devDependencies": { + "@types/jest": "^29.0.0", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.0.0", + "jest": "^29.0.0", + "ts-jest": "^29.0.0", + "ts-node": "^10.0.0", + "typescript": "^5.0.0" } } From 955e6777e7669e109d8b070f7401dde867ee8583 Mon Sep 17 00:00:00 2001 From: obsofficer-ctrl Date: Tue, 17 Mar 2026 01:12:56 +0400 Subject: [PATCH 14/14] Update tsconfig.json --- tsconfig.json | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 5a12f91..a2974fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,27 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2020", "DOM"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", + "module": "CommonJS", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": ".", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", "resolveJsonModule": true, - "jsx": "react-jsx" + "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] -} \ No newline at end of file + "include": [ + "*.ts", + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/__tests__/**" + ] +}