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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 });
Expand Down
4 changes: 4 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/event-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
137 changes: 137 additions & 0 deletions src/taxonomy.ts
Original file line number Diff line number Diff line change
@@ -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];
132 changes: 132 additions & 0 deletions test/taxonomy.test.ts
Original file line number Diff line number Diff line change
@@ -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']);
});
});
5 changes: 3 additions & 2 deletions test/webhooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading