From 0acf0b107220970b4d60df8957c756ed10066e7c Mon Sep 17 00:00:00 2001 From: Alex Standiford Date: Sun, 19 Jul 2026 21:25:02 -0400 Subject: [PATCH] feat(taxonomy): export Siren's full typed taxonomy; add missing webhook events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Siren owns its domain vocabulary (global-taxonomy-and-sdk-doctrine); this SDK is the typed carrier of it so integrations never hand-roll magic strings. - Add EventSlug (built-in ingestion slugs) and status vocabularies: ConversionStatus, TransactionStatus, ObligationStatus, PayoutStatus, FulfillmentStatus, OpportunityStatus, ApiKeyStatus, WebhookSubscriptionStatus (src/taxonomy.ts, exported from the root). - WebhookEventType: add credit.issued, credit.redeemed, currency.created, currency.deleted — dispatched by the service but missing from the SDK and openapi.yaml. - Pin the canonical vocabularies in test/taxonomy.test.ts (mirrors the service's dispatchers and REST validations). - README: use WebhookEventType constants in the webhook example instead of raw strings; document the taxonomy exports. Charter 761 (SDKs speak the taxonomy). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0138jQKyzwyDfEbbAeMmYNnL --- CHANGELOG.md | 11 ++++ README.md | 16 ++++- openapi.yaml | 4 ++ src/event-types.ts | 4 ++ src/index.ts | 11 ++++ src/taxonomy.ts | 137 ++++++++++++++++++++++++++++++++++++++++++ test/taxonomy.test.ts | 132 ++++++++++++++++++++++++++++++++++++++++ test/webhooks.test.ts | 5 +- 8 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 src/taxonomy.ts create mode 100644 test/taxonomy.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index af6847a..6e55b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Typed taxonomy exports so integrations use SDK types instead of magic strings: + `EventSlug` (built-in ingestion slugs) and the status vocabularies + `ConversionStatus`, `TransactionStatus`, `ObligationStatus`, `PayoutStatus`, + `FulfillmentStatus`, `OpportunityStatus`, `ApiKeyStatus`, and + `WebhookSubscriptionStatus`. +- `WebhookEventType`: added the missing `credit.issued`, `credit.redeemed`, + `currency.created`, and `currency.deleted` events (also added to + `openapi.yaml`), matching the full set Siren dispatches. + ## [0.1.0] - 2026-07-11 Initial public release of the Siren SDK for Node.js and TypeScript. diff --git a/README.md b/README.md index a4459b0..2d1b039 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ request body keyed by your subscription's signing secret. `constructEvent` verif ```ts import express from 'express'; -import { Siren, SignatureVerificationError } from '@novatorius/siren'; +import { Siren, SignatureVerificationError, WebhookEventType } from '@novatorius/siren'; const siren = new Siren({ apiKey: process.env.SIREN_API_KEY! }); const app = express(); @@ -110,10 +110,10 @@ app.post('/webhooks/siren', express.raw({ type: 'application/json' }), (req, res } switch (event.type) { - case 'conversion.approved': + case WebhookEventType.ConversionApproved: // ... break; - case 'payout.paid': + case WebhookEventType.PayoutPaid: // ... break; } @@ -149,6 +149,16 @@ await saveSecretSomewhereSafe(sub.signingSecret); `transactions`, `obligations`, and `payouts`. - **Typed errors** — every failure throws a typed subclass of `SirenError` carrying `message`, `code`, and `statusCode` (`NotFoundError`, `RateLimitError`, `ValidationError`, and more). +- **Typed taxonomy** — Siren's domain vocabulary as constants, so no magic strings cross the + boundary: `WebhookEventType`, `EventSlug`, and the status vocabularies (`ConversionStatus`, + `TransactionStatus`, `ObligationStatus`, `PayoutStatus`, `FulfillmentStatus`, + `OpportunityStatus`, `ApiKeyStatus`, `WebhookSubscriptionStatus`). + +```ts +import { ConversionStatus } from '@novatorius/siren'; + +const approved = await siren.conversions.list({ status: ConversionStatus.Approved }); +``` ```ts const conversions = await siren.conversions.list({ page: 1, perPage: 50 }); diff --git a/openapi.yaml b/openapi.yaml index 7476aab..117ab6e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -403,6 +403,10 @@ components: - conversion.rejected - conversion.renewed - coupon.applied + - credit.issued + - credit.redeemed + - currency.created + - currency.deleted - distribution.completed - engagement.awarded - engagement.completed diff --git a/src/event-types.ts b/src/event-types.ts index 3c474c3..e5d9051 100644 --- a/src/event-types.ts +++ b/src/event-types.ts @@ -21,6 +21,10 @@ export const WebhookEventType = { ConversionRejected: 'conversion.rejected', ConversionRenewed: 'conversion.renewed', CouponApplied: 'coupon.applied', + CreditIssued: 'credit.issued', + CreditRedeemed: 'credit.redeemed', + CurrencyCreated: 'currency.created', + CurrencyDeleted: 'currency.deleted', DistributionCompleted: 'distribution.completed', EngagementAwarded: 'engagement.awarded', EngagementCompleted: 'engagement.completed', diff --git a/src/index.ts b/src/index.ts index 62f990a..b18bdee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,16 @@ export { Siren } from './client'; export { WebhookEventType } from './event-types'; +export { + ApiKeyStatus, + ConversionStatus, + EventSlug, + FulfillmentStatus, + ObligationStatus, + OpportunityStatus, + PayoutStatus, + TransactionStatus, + WebhookSubscriptionStatus, +} from './taxonomy'; export { SirenError, BadRequestError, diff --git a/src/taxonomy.ts b/src/taxonomy.ts new file mode 100644 index 0000000..61a24ce --- /dev/null +++ b/src/taxonomy.ts @@ -0,0 +1,137 @@ +/** + * Siren's domain taxonomy — the canonical vocabularies the API speaks. + * + * Siren owns these terms (statuses, event slugs); this module is the typed + * source for them so integrations never hand-roll magic strings. Use the + * constants when filtering reconciliation readers or dispatching on webhook + * payload fields: + * + * ```ts + * const approved = await siren.conversions.list({ status: ConversionStatus.Approved }); + * ``` + */ + +/** + * URL slugs for the built-in ingestion event types (`POST /event/{slug}`). + * + * `siren.events.sale()` / `.refund()` / `.siteVisited()` already use these + * internally; the constants exist for code that routes slugs dynamically + * (e.g. wrapping `siren.events.ingest`). + */ +export const EventSlug = { + Sale: 'sale', + Refund: 'refund', + SiteVisited: 'site-visited', +} as const; + +/** Union of built-in ingestion event slugs. */ +export type EventSlug = (typeof EventSlug)[keyof typeof EventSlug]; + +/** + * Statuses a conversion can hold (`siren.conversions` records and + * `conversion.*` webhook payloads). + */ +export const ConversionStatus = { + Pending: 'pending', + Approved: 'approved', + Rejected: 'rejected', + Expired: 'expired', + /** Soft-delete bucket written by the bulk delete action. */ + Deleted: 'deleted', +} as const; + +/** Union of conversion status strings. */ +export type ConversionStatus = (typeof ConversionStatus)[keyof typeof ConversionStatus]; + +/** + * Statuses a transaction can hold (`siren.transactions` records and + * `transaction.*` webhook payloads). + */ +export const TransactionStatus = { + Complete: 'complete', + Cancelled: 'cancelled', + Refunded: 'refunded', +} as const; + +/** Union of transaction status strings. */ +export type TransactionStatus = (typeof TransactionStatus)[keyof typeof TransactionStatus]; + +/** + * Statuses an obligation can hold (`siren.obligations` records and + * `obligation.*` webhook payloads). + * + * Note: Siren's machine paths (fulfillment generation, bulk actions) write + * `complete`, while its management REST surface accepts `fulfilled` — both + * appear in the wild, so both are listed here. + */ +export const ObligationStatus = { + Pending: 'pending', + Complete: 'complete', + Fulfilled: 'fulfilled', + Cancelled: 'cancelled', +} as const; + +/** Union of obligation status strings. */ +export type ObligationStatus = (typeof ObligationStatus)[keyof typeof ObligationStatus]; + +/** + * Statuses a payout can hold (`siren.payouts` records and `payout.*` + * webhook payloads). + */ +export const PayoutStatus = { + Unpaid: 'unpaid', + Processing: 'processing', + Paid: 'paid', + Failed: 'failed', +} as const; + +/** Union of payout status strings. */ +export type PayoutStatus = (typeof PayoutStatus)[keyof typeof PayoutStatus]; + +/** + * Statuses a fulfillment can hold (`fulfillment.created` / + * `fulfillment.updated` webhook payloads). + */ +export const FulfillmentStatus = { + Pending: 'pending', + Processing: 'processing', + Complete: 'complete', + Failed: 'failed', +} as const; + +/** Union of fulfillment status strings. */ +export type FulfillmentStatus = (typeof FulfillmentStatus)[keyof typeof FulfillmentStatus]; + +/** + * Statuses an opportunity can hold (`opportunity.created` / + * `opportunity.invalidated` webhook payloads; `trackingId` on sales refers + * to an opportunity). + */ +export const OpportunityStatus = { + Active: 'active', + Inactive: 'inactive', + /** Set by Siren's invalidation service; never operator-settable. */ + Invalid: 'invalid', +} as const; + +/** Union of opportunity status strings. */ +export type OpportunityStatus = (typeof OpportunityStatus)[keyof typeof OpportunityStatus]; + +/** Statuses an API key can hold (`siren.apiKeys` records). */ +export const ApiKeyStatus = { + Active: 'active', + Revoked: 'revoked', +} as const; + +/** Union of API key status strings. */ +export type ApiKeyStatus = (typeof ApiKeyStatus)[keyof typeof ApiKeyStatus]; + +/** Statuses a webhook subscription can hold (`siren.webhooks.subscriptions`). */ +export const WebhookSubscriptionStatus = { + Active: 'active', + Paused: 'paused', +} as const; + +/** Union of webhook subscription status strings. */ +export type WebhookSubscriptionStatus = + (typeof WebhookSubscriptionStatus)[keyof typeof WebhookSubscriptionStatus]; diff --git a/test/taxonomy.test.ts b/test/taxonomy.test.ts new file mode 100644 index 0000000..dd349e4 --- /dev/null +++ b/test/taxonomy.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest'; + +import { + ApiKeyStatus, + ConversionStatus, + EventSlug, + FulfillmentStatus, + ObligationStatus, + OpportunityStatus, + PayoutStatus, + TransactionStatus, + WebhookEventType, + WebhookSubscriptionStatus, +} from '../src/index'; + +/** + * These tests pin the SDK's taxonomy to Siren's canonical vocabulary. If one + * fails, either the SDK drifted or the Siren service changed its domain + * language — reconcile against the service (the taxonomy owner), not by + * editing the expectation to match the code. + */ + +describe('WebhookEventType', () => { + it('matches the canonical set of subscribable events', () => { + expect(Object.values(WebhookEventType).sort()).toEqual( + [ + '*', + 'allocation.completed', + 'collaborator.created', + 'collaborator.registered', + 'conversion.approved', + 'conversion.created', + 'conversion.rejected', + 'conversion.renewed', + 'coupon.applied', + 'credit.issued', + 'credit.redeemed', + 'currency.created', + 'currency.deleted', + 'distribution.completed', + 'engagement.awarded', + 'engagement.completed', + 'engagement.created', + 'fulfillment.created', + 'fulfillment.updated', + 'lead.created', + 'metrics.updated', + 'obligation.completed', + 'obligation.created', + 'opportunity.created', + 'opportunity.invalidated', + 'payout.created', + 'payout.paid', + 'refund.created', + 'renewal.created', + 'sale.created', + 'transaction.completed', + 'transaction.created', + ].sort(), + ); + }); + + it('has no duplicate values', () => { + const values = Object.values(WebhookEventType); + expect(new Set(values).size).toBe(values.length); + }); +}); + +describe('EventSlug', () => { + it('matches the built-in ingestion slugs', () => { + expect(Object.values(EventSlug).sort()).toEqual(['refund', 'sale', 'site-visited']); + }); +}); + +describe('status vocabularies', () => { + it('ConversionStatus matches the canonical set', () => { + expect(Object.values(ConversionStatus).sort()).toEqual([ + 'approved', + 'deleted', + 'expired', + 'pending', + 'rejected', + ]); + }); + + it('TransactionStatus matches the canonical set', () => { + expect(Object.values(TransactionStatus).sort()).toEqual([ + 'cancelled', + 'complete', + 'refunded', + ]); + }); + + it('ObligationStatus matches the canonical set', () => { + expect(Object.values(ObligationStatus).sort()).toEqual([ + 'cancelled', + 'complete', + 'fulfilled', + 'pending', + ]); + }); + + it('PayoutStatus matches the canonical set', () => { + expect(Object.values(PayoutStatus).sort()).toEqual([ + 'failed', + 'paid', + 'processing', + 'unpaid', + ]); + }); + + it('FulfillmentStatus matches the canonical set', () => { + expect(Object.values(FulfillmentStatus).sort()).toEqual([ + 'complete', + 'failed', + 'pending', + 'processing', + ]); + }); + + it('OpportunityStatus matches the canonical set', () => { + expect(Object.values(OpportunityStatus).sort()).toEqual(['active', 'inactive', 'invalid']); + }); + + it('ApiKeyStatus matches the canonical set', () => { + expect(Object.values(ApiKeyStatus).sort()).toEqual(['active', 'revoked']); + }); + + it('WebhookSubscriptionStatus matches the canonical set', () => { + expect(Object.values(WebhookSubscriptionStatus).sort()).toEqual(['active', 'paused']); + }); +}); diff --git a/test/webhooks.test.ts b/test/webhooks.test.ts index 5457470..ab5fb30 100644 --- a/test/webhooks.test.ts +++ b/test/webhooks.test.ts @@ -101,9 +101,10 @@ describe('webhooks.constructEvent', () => { }); describe('WebhookEventType', () => { - it('contains all 27 event types plus "*"', () => { + it('contains all 31 event types plus "*"', () => { + // The full canonical set is pinned in test/taxonomy.test.ts. const values = Object.values(WebhookEventType); - expect(values).toHaveLength(28); + expect(values).toHaveLength(32); expect(values).toContain('*'); expect(values).toContain('conversion.approved'); expect(values).toContain('transaction.created');