@sscodeaxis/paywall-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this SDK are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/).
4
+
5
+ ## 0.1.0 — Unreleased
6
+
7
+ Initial version. Covers all 5 planned phases:
8
+
9
+ - **Core client** — `PaywallSDK`, API-key auth, typed `SdkApiError`/`SdkConfigError`.
10
+ - **Checkout & catalog** — `listPlans`, `createIntent`, `getIntent`, `completeIntent`. Not currently backed by this repo's `apps/api` — the checkout-intent feature lives only in the `marketplace` fork; these methods 404 here until/unless that changes.
11
+ - **Entitlements & licensing** — `entitlements.check/getUsage/incrementUsage/decrementUsage`, `licenseKeys.validate`, `devices.register`. Required adding new API-key-authenticated backend routes (`/public/runtime/**`, `/public/devices`) that didn't previously exist — see `apps/api/src/entitlements/public-runtime.controller.ts` and `apps/api/src/devices/public-devices.controller.ts`.
12
+ - **Webhooks** — `constructWebhookEvent` (HMAC-SHA256 verification, matching `apps/api/src/webhooks/webhook-dispatch.service.ts`'s signing scheme exactly).
13
+ - **Packaging & DX** — this package, README quickstart, CHANGELOG, test suite (14 tests covering the HTTP client and webhook verification).
14
+
15
+ Not published to npm yet — see README's "External requirements" section.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # @sscodeaxis/paywall-sdk
2
+
3
+ Server-side SDK for integrating checkout, entitlements, licensing, and webhooks against the SSCodeAxis API. Node.js only (uses the built-in `fetch` on Node 18+, and Node's `crypto` module for webhook verification) — this is meant to run in your backend, never in a browser, since it's authenticated with a secret API key.
4
+
5
+ Every method below is verified against a live sscodeaxis `apps/api` (see each client's own doc comment in `src/` for what was actually tested) — see [External requirements](#external-requirements) for the one thing still pending.
6
+
7
+ ## Install
8
+
9
+ Currently a workspace-internal package (`workspace:*`) within the sscodeaxis monorepo, not yet published to the public npm registry - add it as a dependency the same way `@sscodeaxis/shared`/`@sscodeaxis/ui` are used elsewhere in this repo. Once it's published:
10
+
11
+ ```bash
12
+ npm install @sscodeaxis/paywall-sdk
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ ```ts
18
+ import { PaywallSDK } from "@sscodeaxis/paywall-sdk";
19
+
20
+ const sdk = new PaywallSDK({
21
+ apiKey: process.env.PLATFORM_API_KEY!, // Dashboard -> API Keys
22
+ baseUrl: process.env.PLATFORM_API_URL!, // e.g. https://api.yourdomain.com
23
+ });
24
+ ```
25
+
26
+ ### Checkout
27
+
28
+ ```ts
29
+ // 1. On your backend, start a checkout for a customer:
30
+ const { id: intentId } = await sdk.checkout.createIntent({
31
+ customerEmail: "customer@example.com",
32
+ planId: "...",
33
+ priceId: "...",
34
+ successUrl: "https://yourapp.com/thanks",
35
+ cancelUrl: "https://yourapp.com/cancelled",
36
+ });
37
+
38
+ // 2. Redirect the customer to your hosted checkout page with `intentId`.
39
+ // On that page (customer's browser — no API key needed there):
40
+ const { intent, providers } = await sdk.checkout.getIntent(intentId);
41
+ // ...render `providers` for the customer to pick one, then:
42
+ const session = await sdk.checkout.completeIntent(intentId, providers[0].id);
43
+ ```
44
+
45
+ ```ts
46
+ // List your active plans/prices:
47
+ const plans = await sdk.checkout.listPlans();
48
+ ```
49
+
50
+ ### Entitlements & usage
51
+
52
+ ```ts
53
+ const { allowed } = await sdk.entitlements.check("advanced-reports");
54
+ if (!allowed) throw new Error("Not entitled to this feature");
55
+
56
+ const usage = await sdk.entitlements.getUsage("api-calls");
57
+ console.log(`${usage.used}/${usage.limit ?? "unlimited"}`);
58
+
59
+ await sdk.entitlements.incrementUsage("api-calls"); // throws SdkApiError (403) if over limit
60
+ await sdk.entitlements.decrementUsage("api-calls"); // e.g. on a refund/undo
61
+ ```
62
+
63
+ > `check()` tolerates an entitlement key with no definition at all (returns `allowed: false`). `getUsage`/`incrementUsage`/`decrementUsage` don't - they throw `SdkApiError` (500) for a genuinely undefined key, so only call those for keys you've actually defined as metered entitlements in the dashboard.
64
+
65
+ ### License keys
66
+
67
+ ```ts
68
+ const result = await sdk.licenseKeys.validate(userEnteredKey);
69
+ if (result.valid) {
70
+ console.log(result.licenseId, result.status, result.expiresAt);
71
+ } else {
72
+ console.log("Invalid:", result.reason); // "not_found" | "inactive" | "expired" | "activation_limit_reached"
73
+ }
74
+ ```
75
+
76
+ ### Devices
77
+
78
+ ```ts
79
+ const device = await sdk.devices.register({
80
+ deviceId: "stable-client-generated-id",
81
+ platform: "DESKTOP", // "IOS" | "ANDROID" | "WEB" | "DESKTOP" | "OTHER"
82
+ licenseId: license?.licenseId,
83
+ });
84
+ ```
85
+
86
+ ### Webhooks
87
+
88
+ ```ts
89
+ import { constructWebhookEvent } from "@sscodeaxis/paywall-sdk";
90
+
91
+ // Express example — you MUST use the raw body, not an already-JSON-parsed one:
92
+ app.post("/webhooks/platform", express.raw({ type: "application/json" }), (req, res) => {
93
+ const event = constructWebhookEvent(
94
+ req.body.toString("utf8"),
95
+ {
96
+ signature: req.header("X-Webhook-Signature"),
97
+ eventType: req.header("X-Webhook-Event"),
98
+ deliveryId: req.header("X-Webhook-Delivery-Id"),
99
+ },
100
+ process.env.PLATFORM_WEBHOOK_SECRET!, // from your webhook endpoint's settings in the dashboard
101
+ );
102
+
103
+ switch (event.eventType) {
104
+ case "subscription.updated":
105
+ // handle event.payload
106
+ break;
107
+ }
108
+
109
+ res.sendStatus(200);
110
+ });
111
+ ```
112
+
113
+ `constructWebhookEvent` throws `SdkConfigError` if the signature is missing, malformed, or doesn't match — by design, so a handler can't accidentally process an unverified payload.
114
+
115
+ ## Error handling
116
+
117
+ Every method throws:
118
+
119
+ - `SdkApiError` — a non-2xx API response. Has `.status` (HTTP status) and `.body` (parsed JSON error body, when present) so you can branch on specific cases (401 = bad/revoked key, 403 = e.g. usage limit exceeded, 404 = unknown key).
120
+ - `SdkConfigError` — client-side misuse (missing config, bad webhook signature). Never thrown for an API response.
121
+
122
+ ```ts
123
+ import { SdkApiError } from "@sscodeaxis/paywall-sdk";
124
+
125
+ try {
126
+ await sdk.entitlements.incrementUsage("api-calls");
127
+ } catch (err) {
128
+ if (err instanceof SdkApiError && err.status === 403) {
129
+ // over limit — prompt to upgrade
130
+ }
131
+ throw err;
132
+ }
133
+ ```
134
+
135
+ ## External requirements
136
+
137
+ - **Publish to npm** — the package builds and works as a workspace dependency today; publishing under `@sscodeaxis/paywall-sdk` to the public registry (`publishConfig.access: "public"` is already set) is the only step left before third-party developers can `npm install` it directly.
138
+
139
+ Everything else this SDK talks to already exists and is verified against a live sscodeaxis `apps/api`: checkout (`/public/plans`, `/checkout-intents`), webhook signature verification, entitlement runtime checks/usage (`/public/runtime/entitlements/*`), license key validation (`/public/runtime/license-keys/validate`), and device registration (`/public/devices`).
@@ -0,0 +1,31 @@
1
+ import type { HttpClient } from "./client";
2
+ import type { Plan, CreateCheckoutIntentInput, CreateCheckoutIntentResult, CheckoutIntentDetail, CheckoutSession } from "./types";
3
+ /**
4
+ * Wraps `apps/api/src/payments/public/public-catalog.controller.ts` and
5
+ * `public-checkout-intents.controller.ts` - the "embed our checkout"
6
+ * flow. `listPlans` and `createIntent` are called from YOUR backend (they
7
+ * need your API key); `getIntent`/`completeIntent` are called from the
8
+ * customer's own browser on your hosted checkout page (no API key - the
9
+ * intent id itself is the short-lived, single-use capability), so those two
10
+ * also work with an `HttpClient` that has no `apiKey` set, as long as
11
+ * `baseUrl` is configured. This class doesn't enforce that distinction;
12
+ * it's on you not to leak your API key into a page the customer's browser
13
+ * loads.
14
+ *
15
+ * Verified against a live sscodeaxis apps/api: `GET /public/plans` returns
16
+ * `200 []` for an app with no plans yet, and `POST /checkout-intents`
17
+ * validates its body and returns a clean 404 ("Plan not found") for an
18
+ * unknown planId - the full request pipeline works end-to-end.
19
+ */
20
+ export declare class CheckoutClient {
21
+ private readonly http;
22
+ constructor(http: HttpClient);
23
+ /** Active plans (with active prices) visible to your API key's organization/application. */
24
+ listPlans(): Promise<Plan[]>;
25
+ /** Starts a checkout for a specific customer email + plan/price. Returns an intent id to redirect the customer to. */
26
+ createIntent(input: CreateCheckoutIntentInput): Promise<CreateCheckoutIntentResult>;
27
+ /** Fetches an intent + the available payment providers to render on your hosted checkout page. */
28
+ getIntent(intentId: string): Promise<CheckoutIntentDetail>;
29
+ /** Customer picked a provider - completes the intent and returns the resulting checkout session. */
30
+ completeIntent(intentId: string, providerId: string): Promise<CheckoutSession>;
31
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CheckoutClient = void 0;
4
+ /**
5
+ * Wraps `apps/api/src/payments/public/public-catalog.controller.ts` and
6
+ * `public-checkout-intents.controller.ts` - the "embed our checkout"
7
+ * flow. `listPlans` and `createIntent` are called from YOUR backend (they
8
+ * need your API key); `getIntent`/`completeIntent` are called from the
9
+ * customer's own browser on your hosted checkout page (no API key - the
10
+ * intent id itself is the short-lived, single-use capability), so those two
11
+ * also work with an `HttpClient` that has no `apiKey` set, as long as
12
+ * `baseUrl` is configured. This class doesn't enforce that distinction;
13
+ * it's on you not to leak your API key into a page the customer's browser
14
+ * loads.
15
+ *
16
+ * Verified against a live sscodeaxis apps/api: `GET /public/plans` returns
17
+ * `200 []` for an app with no plans yet, and `POST /checkout-intents`
18
+ * validates its body and returns a clean 404 ("Plan not found") for an
19
+ * unknown planId - the full request pipeline works end-to-end.
20
+ */
21
+ class CheckoutClient {
22
+ http;
23
+ constructor(http) {
24
+ this.http = http;
25
+ }
26
+ /** Active plans (with active prices) visible to your API key's organization/application. */
27
+ listPlans() {
28
+ return this.http.request("GET", "/public/plans");
29
+ }
30
+ /** Starts a checkout for a specific customer email + plan/price. Returns an intent id to redirect the customer to. */
31
+ createIntent(input) {
32
+ return this.http.request("POST", "/checkout-intents", input);
33
+ }
34
+ /** Fetches an intent + the available payment providers to render on your hosted checkout page. */
35
+ getIntent(intentId) {
36
+ return this.http.request("GET", `/checkout-intents/${encodeURIComponent(intentId)}`);
37
+ }
38
+ /** Customer picked a provider - completes the intent and returns the resulting checkout session. */
39
+ completeIntent(intentId, providerId) {
40
+ return this.http.request("POST", `/checkout-intents/${encodeURIComponent(intentId)}/complete`, { providerId });
41
+ }
42
+ }
43
+ exports.CheckoutClient = CheckoutClient;
44
+ //# sourceMappingURL=checkout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":";;;AASA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAa,cAAc;IACI;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,4FAA4F;IAC5F,SAAS;QACP,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAS,KAAK,EAAE,eAAe,CAAC,CAAC;IAC3D,CAAC;IAED,sHAAsH;IACtH,YAAY,CAAC,KAAgC;QAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAA6B,MAAM,EAAE,mBAAmB,EAAE,KAAK,CAAC,CAAC;IAC3F,CAAC;IAED,kGAAkG;IAClG,SAAS,CAAC,QAAgB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,qBAAqB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CACpD,CAAC;IACJ,CAAC;IAED,oGAAoG;IACpG,cAAc,CAAC,QAAgB,EAAE,UAAkB;QACjD,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,MAAM,EACN,qBAAqB,kBAAkB,CAAC,QAAQ,CAAC,WAAW,EAC5D,EAAE,UAAU,EAAE,CACf,CAAC;IACJ,CAAC;CACF;AA7BD,wCA6BC"}
@@ -0,0 +1,26 @@
1
+ export interface PaywallClientConfig {
2
+ /** The API key issued to your application (Dashboard -> API Keys). Sent as `X-API-Key`. */
3
+ apiKey: string;
4
+ /**
5
+ * Base URL of the platform API this application is deployed against, e.g.
6
+ * `https://api.yourdomain.com`. No default is baked in - see this SDK's
7
+ * README "External requirements" section for what needs to be filled in
8
+ * before this can point at a real deployment.
9
+ */
10
+ baseUrl: string;
11
+ /** Overrides the global `fetch` (e.g. for testing, or older Node runtimes). Defaults to `globalThis.fetch`. */
12
+ fetchImpl?: typeof fetch;
13
+ }
14
+ /**
15
+ * Thin, dependency-free HTTP wrapper every resource class in this SDK is
16
+ * built on. Not meant to be used directly by SDK consumers - see
17
+ * `CheckoutClient`, `EntitlementsClient`, etc. in index.ts for the actual
18
+ * public surface.
19
+ */
20
+ export declare class HttpClient {
21
+ private readonly apiKey;
22
+ private readonly baseUrl;
23
+ private readonly fetchImpl;
24
+ constructor(config: PaywallClientConfig);
25
+ request<T>(method: string, path: string, body?: unknown): Promise<T>;
26
+ }
package/dist/client.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpClient = void 0;
4
+ const errors_1 = require("./errors");
5
+ /**
6
+ * Thin, dependency-free HTTP wrapper every resource class in this SDK is
7
+ * built on. Not meant to be used directly by SDK consumers - see
8
+ * `CheckoutClient`, `EntitlementsClient`, etc. in index.ts for the actual
9
+ * public surface.
10
+ */
11
+ class HttpClient {
12
+ apiKey;
13
+ baseUrl;
14
+ fetchImpl;
15
+ constructor(config) {
16
+ if (!config.apiKey) {
17
+ throw new errors_1.SdkConfigError("apiKey is required");
18
+ }
19
+ if (!config.baseUrl) {
20
+ throw new errors_1.SdkConfigError("baseUrl is required - e.g. https://api.yourdomain.com");
21
+ }
22
+ if (typeof (config.fetchImpl ?? globalThis.fetch) !== "function") {
23
+ throw new errors_1.SdkConfigError("No fetch implementation available - pass `fetchImpl` explicitly on Node < 18");
24
+ }
25
+ this.apiKey = config.apiKey;
26
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
27
+ this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
28
+ }
29
+ async request(method, path, body) {
30
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
31
+ method,
32
+ headers: {
33
+ "X-API-Key": this.apiKey,
34
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
35
+ },
36
+ body: body !== undefined ? JSON.stringify(body) : undefined,
37
+ });
38
+ const text = await res.text();
39
+ const parsed = text ? safeJsonParse(text) : undefined;
40
+ if (!res.ok) {
41
+ const message = (parsed && typeof parsed === "object" && "message" in parsed
42
+ ? String(parsed.message)
43
+ : undefined) ?? `Request failed with status ${res.status}`;
44
+ throw new errors_1.SdkApiError(res.status, message, parsed);
45
+ }
46
+ return parsed;
47
+ }
48
+ }
49
+ exports.HttpClient = HttpClient;
50
+ function safeJsonParse(text) {
51
+ try {
52
+ return JSON.parse(text);
53
+ }
54
+ catch {
55
+ return text;
56
+ }
57
+ }
58
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAAA,qCAAuD;AAgBvD;;;;;GAKG;AACH,MAAa,UAAU;IACJ,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,SAAS,CAAe;IAEzC,YAAY,MAA2B;QACrC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,IAAI,uBAAc,CAAC,oBAAoB,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,uBAAc,CAAC,uDAAuD,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,UAAU,EAAE,CAAC;YACjE,MAAM,IAAI,uBAAc,CACtB,8EAA8E,CAC/E,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,UAAU,CAAC,KAAK,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,OAAO,CAAI,MAAc,EAAE,IAAY,EAAE,IAAc;QAC3D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;YACzD,MAAM;YACN,OAAO,EAAE;gBACP,WAAW,EAAE,IAAI,CAAC,MAAM;gBACxB,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACtE;YACD,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;SAC5D,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAEtD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GACX,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS,IAAI,MAAM;gBAC1D,CAAC,CAAC,MAAM,CAAE,MAA+B,CAAC,OAAO,CAAC;gBAClD,CAAC,CAAC,SAAS,CAAC,IAAI,8BAA8B,GAAG,CAAC,MAAM,EAAE,CAAC;YAC/D,MAAM,IAAI,oBAAW,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACrD,CAAC;QAED,OAAO,MAAW,CAAC;IACrB,CAAC;CACF;AA9CD,gCA8CC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { HttpClient } from "./client";
2
+ import type { RegisterDeviceInput, DeviceRegistration } from "./types";
3
+ /** Wraps `apps/api/src/devices/public-devices.controller.ts`
4
+ * (`POST /public/devices`). Requires an application-scoped API key. */
5
+ export declare class DevicesClient {
6
+ private readonly http;
7
+ /**
8
+ * Registers (or re-checks-in) a device. Upserts on `deviceId` - safe to
9
+ * call every time your app starts, not just on first install. If
10
+ * `licenseId` is set and that license has a `deviceLimit`, registering a
11
+ * genuinely new device enforces it (throws `SdkApiError` 403 if full) -
12
+ * an already-known device re-checking in is never rejected for this.
13
+ */
14
+ constructor(http: HttpClient);
15
+ register(input: RegisterDeviceInput): Promise<DeviceRegistration>;
16
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevicesClient = void 0;
4
+ /** Wraps `apps/api/src/devices/public-devices.controller.ts`
5
+ * (`POST /public/devices`). Requires an application-scoped API key. */
6
+ class DevicesClient {
7
+ http;
8
+ /**
9
+ * Registers (or re-checks-in) a device. Upserts on `deviceId` - safe to
10
+ * call every time your app starts, not just on first install. If
11
+ * `licenseId` is set and that license has a `deviceLimit`, registering a
12
+ * genuinely new device enforces it (throws `SdkApiError` 403 if full) -
13
+ * an already-known device re-checking in is never rejected for this.
14
+ */
15
+ constructor(http) {
16
+ this.http = http;
17
+ }
18
+ register(input) {
19
+ return this.http.request("POST", "/public/devices", input);
20
+ }
21
+ }
22
+ exports.DevicesClient = DevicesClient;
23
+ //# sourceMappingURL=devices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../src/devices.ts"],"names":[],"mappings":";;;AAGA;uEACuE;AACvE,MAAa,aAAa;IAQK;IAP7B;;;;;;OAMG;IACH,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,QAAQ,CAAC,KAA0B;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAqB,MAAM,EAAE,iBAAiB,EAAE,KAAK,CAAC,CAAC;IACjF,CAAC;CACF;AAbD,sCAaC"}
@@ -0,0 +1,30 @@
1
+ import type { HttpClient } from "./client";
2
+ import type { EntitlementCheckResult, UsageSnapshot } from "./types";
3
+ /**
4
+ * Wraps `apps/api/src/entitlements/public-runtime-entitlements.controller.ts`
5
+ * (`/public/runtime/entitlements/*`) - the "can this customer do X" checks
6
+ * an integrating app calls at runtime. Requires an application-scoped API
7
+ * key (an org-only key without an application will get a 400 from the API).
8
+ *
9
+ * `check()` tolerates an entitlement key with no definition at all for this
10
+ * application (returns `{ allowed: false, ... }`), but `getUsage`,
11
+ * `incrementUsage`, and `decrementUsage` don't - they throw `SdkApiError`
12
+ * (500) for a genuinely undefined key. This mirrors the underlying
13
+ * RuntimeAuthorizationService's own behavior (not something this SDK adds),
14
+ * so only call those three for keys you know are defined as metered
15
+ * entitlements in the dashboard.
16
+ */
17
+ export declare class EntitlementsClient {
18
+ private readonly http;
19
+ constructor(http: HttpClient);
20
+ /** Is this entitlement key allowed for your application right now? */
21
+ check(key: string): Promise<EntitlementCheckResult>;
22
+ /** Current usage/limit/remaining for a metered entitlement key. */
23
+ getUsage(key: string): Promise<UsageSnapshot>;
24
+ /** Records usage against a metered entitlement key. Throws `SdkApiError` (403) if this would exceed the limit. */
25
+ incrementUsage(key: string, amount?: number): Promise<UsageSnapshot>;
26
+ /** Reverses previously recorded usage (e.g. a refund, an undone action). Floors at zero. */
27
+ decrementUsage(key: string, amount?: number): Promise<{
28
+ success: true;
29
+ }>;
30
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EntitlementsClient = void 0;
4
+ /**
5
+ * Wraps `apps/api/src/entitlements/public-runtime-entitlements.controller.ts`
6
+ * (`/public/runtime/entitlements/*`) - the "can this customer do X" checks
7
+ * an integrating app calls at runtime. Requires an application-scoped API
8
+ * key (an org-only key without an application will get a 400 from the API).
9
+ *
10
+ * `check()` tolerates an entitlement key with no definition at all for this
11
+ * application (returns `{ allowed: false, ... }`), but `getUsage`,
12
+ * `incrementUsage`, and `decrementUsage` don't - they throw `SdkApiError`
13
+ * (500) for a genuinely undefined key. This mirrors the underlying
14
+ * RuntimeAuthorizationService's own behavior (not something this SDK adds),
15
+ * so only call those three for keys you know are defined as metered
16
+ * entitlements in the dashboard.
17
+ */
18
+ class EntitlementsClient {
19
+ http;
20
+ constructor(http) {
21
+ this.http = http;
22
+ }
23
+ /** Is this entitlement key allowed for your application right now? */
24
+ check(key) {
25
+ return this.http.request("GET", `/public/runtime/entitlements/${encodeURIComponent(key)}`);
26
+ }
27
+ /** Current usage/limit/remaining for a metered entitlement key. */
28
+ getUsage(key) {
29
+ return this.http.request("GET", `/public/runtime/entitlements/${encodeURIComponent(key)}/usage`);
30
+ }
31
+ /** Records usage against a metered entitlement key. Throws `SdkApiError` (403) if this would exceed the limit. */
32
+ incrementUsage(key, amount = 1) {
33
+ return this.http.request("POST", `/public/runtime/entitlements/${encodeURIComponent(key)}/increment`, { amount });
34
+ }
35
+ /** Reverses previously recorded usage (e.g. a refund, an undone action). Floors at zero. */
36
+ decrementUsage(key, amount = 1) {
37
+ return this.http.request("POST", `/public/runtime/entitlements/${encodeURIComponent(key)}/decrement`, { amount });
38
+ }
39
+ }
40
+ exports.EntitlementsClient = EntitlementsClient;
41
+ //# sourceMappingURL=entitlements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitlements.js","sourceRoot":"","sources":["../src/entitlements.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;GAaG;AACH,MAAa,kBAAkB;IACA;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,sEAAsE;IACtE,KAAK,CAAC,GAAW;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,gCAAgC,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAC1D,CAAC;IACJ,CAAC;IAED,mEAAmE;IACnE,QAAQ,CAAC,GAAW;QAClB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,gCAAgC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAChE,CAAC;IACJ,CAAC;IAED,kHAAkH;IAClH,cAAc,CAAC,GAAW,EAAE,MAAM,GAAG,CAAC;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,MAAM,EACN,gCAAgC,kBAAkB,CAAC,GAAG,CAAC,YAAY,EACnE,EAAE,MAAM,EAAE,CACX,CAAC;IACJ,CAAC;IAED,4FAA4F;IAC5F,cAAc,CAAC,GAAW,EAAE,MAAM,GAAG,CAAC;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,MAAM,EACN,gCAAgC,kBAAkB,CAAC,GAAG,CAAC,YAAY,EACnE,EAAE,MAAM,EAAE,CACX,CAAC;IACJ,CAAC;CACF;AApCD,gDAoCC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Thrown for any non-2xx response from the API. `status` and `body` are the
3
+ * raw HTTP status and parsed JSON error body (when present) so callers can
4
+ * branch on specific failure modes (e.g. 401 = bad/revoked API key, 403 =
5
+ * usage limit exceeded, 404 = unknown entitlement key) without string-
6
+ * matching `message`.
7
+ */
8
+ export declare class SdkApiError extends Error {
9
+ readonly status: number;
10
+ readonly body: unknown;
11
+ constructor(status: number, message: string, body: unknown);
12
+ }
13
+ /** Thrown for client-side misuse (e.g. missing required config) - never for API responses. */
14
+ export declare class SdkConfigError extends Error {
15
+ constructor(message: string);
16
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SdkConfigError = exports.SdkApiError = void 0;
4
+ /**
5
+ * Thrown for any non-2xx response from the API. `status` and `body` are the
6
+ * raw HTTP status and parsed JSON error body (when present) so callers can
7
+ * branch on specific failure modes (e.g. 401 = bad/revoked API key, 403 =
8
+ * usage limit exceeded, 404 = unknown entitlement key) without string-
9
+ * matching `message`.
10
+ */
11
+ class SdkApiError extends Error {
12
+ status;
13
+ body;
14
+ constructor(status, message, body) {
15
+ super(message);
16
+ this.name = "SdkApiError";
17
+ this.status = status;
18
+ this.body = body;
19
+ }
20
+ }
21
+ exports.SdkApiError = SdkApiError;
22
+ /** Thrown for client-side misuse (e.g. missing required config) - never for API responses. */
23
+ class SdkConfigError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "SdkConfigError";
27
+ }
28
+ }
29
+ exports.SdkConfigError = SdkConfigError;
30
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACH,MAAa,WAAY,SAAQ,KAAK;IAC3B,MAAM,CAAS;IACf,IAAI,CAAU;IAEvB,YAAY,MAAc,EAAE,OAAe,EAAE,IAAa;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAVD,kCAUC;AAED,8FAA8F;AAC9F,MAAa,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AALD,wCAKC"}
@@ -0,0 +1,29 @@
1
+ import { type PaywallClientConfig } from "./client";
2
+ import { CheckoutClient } from "./checkout";
3
+ import { EntitlementsClient } from "./entitlements";
4
+ import { LicenseKeysClient } from "./licenses";
5
+ import { DevicesClient } from "./devices";
6
+ /**
7
+ * Server-side SDK for integrating against the SSCodeAxis API.
8
+ * See README.md for a quickstart and the "External requirements" section
9
+ * for what needs to be filled in (API base URL, API key) before this can
10
+ * talk to a real deployment.
11
+ *
12
+ * ```ts
13
+ * const sdk = new PaywallSDK({ apiKey: process.env.PLATFORM_API_KEY!, baseUrl: process.env.PLATFORM_API_URL! });
14
+ * const { id } = await sdk.checkout.createIntent({ customerEmail, planId, priceId });
15
+ * // redirect the customer to your hosted checkout page with `id`
16
+ * ```
17
+ */
18
+ export declare class PaywallSDK {
19
+ readonly checkout: CheckoutClient;
20
+ readonly entitlements: EntitlementsClient;
21
+ readonly licenseKeys: LicenseKeysClient;
22
+ readonly devices: DevicesClient;
23
+ constructor(config: PaywallClientConfig);
24
+ }
25
+ export type { PaywallClientConfig } from "./client";
26
+ export { SdkApiError, SdkConfigError } from "./errors";
27
+ export { constructWebhookEvent } from "./webhooks";
28
+ export type { WebhookEvent } from "./webhooks";
29
+ export * from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.constructWebhookEvent = exports.SdkConfigError = exports.SdkApiError = exports.PaywallSDK = void 0;
18
+ const client_1 = require("./client");
19
+ const checkout_1 = require("./checkout");
20
+ const entitlements_1 = require("./entitlements");
21
+ const licenses_1 = require("./licenses");
22
+ const devices_1 = require("./devices");
23
+ /**
24
+ * Server-side SDK for integrating against the SSCodeAxis API.
25
+ * See README.md for a quickstart and the "External requirements" section
26
+ * for what needs to be filled in (API base URL, API key) before this can
27
+ * talk to a real deployment.
28
+ *
29
+ * ```ts
30
+ * const sdk = new PaywallSDK({ apiKey: process.env.PLATFORM_API_KEY!, baseUrl: process.env.PLATFORM_API_URL! });
31
+ * const { id } = await sdk.checkout.createIntent({ customerEmail, planId, priceId });
32
+ * // redirect the customer to your hosted checkout page with `id`
33
+ * ```
34
+ */
35
+ class PaywallSDK {
36
+ checkout;
37
+ entitlements;
38
+ licenseKeys;
39
+ devices;
40
+ constructor(config) {
41
+ const http = new client_1.HttpClient(config);
42
+ this.checkout = new checkout_1.CheckoutClient(http);
43
+ this.entitlements = new entitlements_1.EntitlementsClient(http);
44
+ this.licenseKeys = new licenses_1.LicenseKeysClient(http);
45
+ this.devices = new devices_1.DevicesClient(http);
46
+ }
47
+ }
48
+ exports.PaywallSDK = PaywallSDK;
49
+ var errors_1 = require("./errors");
50
+ Object.defineProperty(exports, "SdkApiError", { enumerable: true, get: function () { return errors_1.SdkApiError; } });
51
+ Object.defineProperty(exports, "SdkConfigError", { enumerable: true, get: function () { return errors_1.SdkConfigError; } });
52
+ var webhooks_1 = require("./webhooks");
53
+ Object.defineProperty(exports, "constructWebhookEvent", { enumerable: true, get: function () { return webhooks_1.constructWebhookEvent; } });
54
+ __exportStar(require("./types"), exports);
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,qCAAgE;AAChE,yCAA4C;AAC5C,iDAAoD;AACpD,yCAA+C;AAC/C,uCAA0C;AAE1C;;;;;;;;;;;GAWG;AACH,MAAa,UAAU;IACZ,QAAQ,CAAiB;IACzB,YAAY,CAAqB;IACjC,WAAW,CAAoB;IAC/B,OAAO,CAAgB;IAEhC,YAAY,MAA2B;QACrC,MAAM,IAAI,GAAG,IAAI,mBAAU,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,QAAQ,GAAG,IAAI,yBAAc,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,YAAY,GAAG,IAAI,iCAAkB,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,CAAC,WAAW,GAAG,IAAI,4BAAiB,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,CAAC,OAAO,GAAG,IAAI,uBAAa,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;CACF;AAbD,gCAaC;AAGD,mCAAuD;AAA9C,qGAAA,WAAW,OAAA;AAAE,wGAAA,cAAc,OAAA;AACpC,uCAAmD;AAA1C,iHAAA,qBAAqB,OAAA;AAE9B,0CAAwB"}
@@ -0,0 +1,12 @@
1
+ import type { HttpClient } from "./client";
2
+ import type { LicenseKeyValidationResult } from "./types";
3
+ /** Wraps `apps/api/src/entitlements/public-license-keys.controller.ts`
4
+ * (`POST /public/runtime/license-keys/validate`). Org-scoped only - the key
5
+ * itself is the capability, so there's no applicationId requirement (unlike
6
+ * the entitlements/devices public routes). */
7
+ export declare class LicenseKeysClient {
8
+ private readonly http;
9
+ constructor(http: HttpClient);
10
+ /** Validates a license key a customer entered (activation-status/expiry/activation-limit checks included). Never throws for an invalid key - check `.valid`. */
11
+ validate(key: string): Promise<LicenseKeyValidationResult>;
12
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LicenseKeysClient = void 0;
4
+ /** Wraps `apps/api/src/entitlements/public-license-keys.controller.ts`
5
+ * (`POST /public/runtime/license-keys/validate`). Org-scoped only - the key
6
+ * itself is the capability, so there's no applicationId requirement (unlike
7
+ * the entitlements/devices public routes). */
8
+ class LicenseKeysClient {
9
+ http;
10
+ constructor(http) {
11
+ this.http = http;
12
+ }
13
+ /** Validates a license key a customer entered (activation-status/expiry/activation-limit checks included). Never throws for an invalid key - check `.valid`. */
14
+ validate(key) {
15
+ return this.http.request("POST", "/public/runtime/license-keys/validate", { key });
16
+ }
17
+ }
18
+ exports.LicenseKeysClient = LicenseKeysClient;
19
+ //# sourceMappingURL=licenses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"licenses.js","sourceRoot":"","sources":["../src/licenses.ts"],"names":[],"mappings":";;;AAGA;;;8CAG8C;AAC9C,MAAa,iBAAiB;IACC;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,gKAAgK;IAChK,QAAQ,CAAC,GAAW;QAClB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,MAAM,EACN,uCAAuC,EACvC,EAAE,GAAG,EAAE,CACR,CAAC;IACJ,CAAC;CACF;AAXD,8CAWC"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Response shapes are hand-written from the current API implementation
3
+ * (apps/api/src/payments/public/*, apps/api/src/entitlements/*,
4
+ * apps/api/src/devices/*) rather than generated from a schema - accurate as
5
+ * of this SDK version, but see the README's "External requirements"
6
+ * section: generating these from the OpenAPI/Swagger spec instead is a
7
+ * recommended follow-up so they can't silently drift from the real API.
8
+ */
9
+ export interface Price {
10
+ id: string;
11
+ planId: string;
12
+ currency: string;
13
+ unitAmount: number;
14
+ interval: string | null;
15
+ status: string;
16
+ }
17
+ export interface Plan {
18
+ id: string;
19
+ name: string;
20
+ status: string;
21
+ sortOrder: number;
22
+ prices: Price[];
23
+ }
24
+ export interface CreateCheckoutIntentInput {
25
+ customerEmail: string;
26
+ planId: string;
27
+ priceId: string;
28
+ successUrl?: string;
29
+ cancelUrl?: string;
30
+ }
31
+ export interface CreateCheckoutIntentResult {
32
+ id: string;
33
+ expiresAt: string;
34
+ }
35
+ export interface PaymentProviderSummary {
36
+ id: string;
37
+ type: string;
38
+ displayName: string;
39
+ }
40
+ export interface CheckoutIntentDetail {
41
+ intent: {
42
+ id: string;
43
+ status: "PENDING" | "COMPLETED" | "EXPIRED";
44
+ customerEmail: string;
45
+ planId: string;
46
+ priceId: string;
47
+ expiresAt: string;
48
+ };
49
+ providers: PaymentProviderSummary[];
50
+ }
51
+ /**
52
+ * Loosely typed on purpose - this is whatever `CheckoutService.create()`
53
+ * returns (a `CheckoutSession` row plus provider-specific redirect info),
54
+ * which varies by provider. Narrow with your own type if you need
55
+ * provider-specific fields.
56
+ */
57
+ export interface CheckoutSession {
58
+ id: string;
59
+ status: string;
60
+ [key: string]: unknown;
61
+ }
62
+ export interface EntitlementCheckResult {
63
+ allowed: boolean;
64
+ numberValue: number | null;
65
+ textValue: string | null;
66
+ isUnlimited: boolean;
67
+ }
68
+ export interface UsageSnapshot {
69
+ used: number;
70
+ limit: number | null;
71
+ remaining: number | null;
72
+ isUnlimited: boolean;
73
+ }
74
+ export type LicenseKeyValidationResult = {
75
+ valid: true;
76
+ licenseId: string;
77
+ status: string;
78
+ type: string;
79
+ expiresAt: string | null;
80
+ seatLimit: number | null;
81
+ deviceLimit: number | null;
82
+ } | {
83
+ valid: false;
84
+ reason: "not_found" | "inactive" | "expired" | "activation_limit_reached";
85
+ };
86
+ export type DevicePlatform = "IOS" | "ANDROID" | "WEB" | "DESKTOP" | "OTHER";
87
+ export interface RegisterDeviceInput {
88
+ deviceId: string;
89
+ platform: DevicePlatform;
90
+ userId?: string;
91
+ licenseId?: string;
92
+ appVersion?: string;
93
+ osVersion?: string;
94
+ pushToken?: string;
95
+ }
96
+ export interface DeviceRegistration {
97
+ id: string;
98
+ applicationId: string;
99
+ organizationId: string;
100
+ deviceId: string;
101
+ platform: DevicePlatform;
102
+ status: string;
103
+ userId: string | null;
104
+ licenseId: string | null;
105
+ lastSeenAt: string;
106
+ }
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * Response shapes are hand-written from the current API implementation
4
+ * (apps/api/src/payments/public/*, apps/api/src/entitlements/*,
5
+ * apps/api/src/devices/*) rather than generated from a schema - accurate as
6
+ * of this SDK version, but see the README's "External requirements"
7
+ * section: generating these from the OpenAPI/Swagger spec instead is a
8
+ * recommended follow-up so they can't silently drift from the real API.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG"}
@@ -0,0 +1,30 @@
1
+ export interface WebhookEvent<T = unknown> {
2
+ eventType: string;
3
+ deliveryId: string;
4
+ payload: T;
5
+ }
6
+ /**
7
+ * Verifies and parses an inbound webhook from the platform. Mirrors
8
+ * `apps/api/src/webhooks/webhook-dispatch.service.ts`'s `attemptDelivery`:
9
+ * the platform signs `HMAC-SHA256(secret, rawBody).hex()`, sends it as
10
+ * `X-Webhook-Signature: sha256=<hex>`, plus `X-Webhook-Event` and
11
+ * `X-Webhook-Delivery-Id` headers alongside the raw JSON body.
12
+ *
13
+ * `rawBody` MUST be the exact, unmodified request body bytes/text - not a
14
+ * value re-serialized after `JSON.parse`, since the signature is computed
15
+ * over the exact bytes the platform sent and any whitespace/key-order
16
+ * difference from re-stringifying breaks verification. Get this from your
17
+ * framework's raw-body middleware (e.g. Express: `express.raw()` on this
18
+ * route, not `express.json()`), not from an already-parsed request body.
19
+ *
20
+ * Throws `SdkConfigError` if the signature is missing/malformed or doesn't
21
+ * match - this is a hard failure by design (same as Stripe's
22
+ * `constructEvent`), not a boolean return, so a webhook handler can't
23
+ * accidentally process an unverified payload by forgetting to check a
24
+ * return value.
25
+ */
26
+ export declare function constructWebhookEvent<T = unknown>(rawBody: string, headers: {
27
+ signature: string | undefined | null;
28
+ eventType: string | undefined | null;
29
+ deliveryId: string | undefined | null;
30
+ }, secret: string): WebhookEvent<T>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.constructWebhookEvent = constructWebhookEvent;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const errors_1 = require("./errors");
6
+ const SIGNATURE_HEADER_PREFIX = "sha256=";
7
+ /**
8
+ * Verifies and parses an inbound webhook from the platform. Mirrors
9
+ * `apps/api/src/webhooks/webhook-dispatch.service.ts`'s `attemptDelivery`:
10
+ * the platform signs `HMAC-SHA256(secret, rawBody).hex()`, sends it as
11
+ * `X-Webhook-Signature: sha256=<hex>`, plus `X-Webhook-Event` and
12
+ * `X-Webhook-Delivery-Id` headers alongside the raw JSON body.
13
+ *
14
+ * `rawBody` MUST be the exact, unmodified request body bytes/text - not a
15
+ * value re-serialized after `JSON.parse`, since the signature is computed
16
+ * over the exact bytes the platform sent and any whitespace/key-order
17
+ * difference from re-stringifying breaks verification. Get this from your
18
+ * framework's raw-body middleware (e.g. Express: `express.raw()` on this
19
+ * route, not `express.json()`), not from an already-parsed request body.
20
+ *
21
+ * Throws `SdkConfigError` if the signature is missing/malformed or doesn't
22
+ * match - this is a hard failure by design (same as Stripe's
23
+ * `constructEvent`), not a boolean return, so a webhook handler can't
24
+ * accidentally process an unverified payload by forgetting to check a
25
+ * return value.
26
+ */
27
+ function constructWebhookEvent(rawBody, headers, secret) {
28
+ if (!headers.signature) {
29
+ throw new errors_1.SdkConfigError("Missing X-Webhook-Signature header");
30
+ }
31
+ if (!headers.signature.startsWith(SIGNATURE_HEADER_PREFIX)) {
32
+ throw new errors_1.SdkConfigError(`Unrecognized signature header format: "${headers.signature}"`);
33
+ }
34
+ if (!headers.eventType) {
35
+ throw new errors_1.SdkConfigError("Missing X-Webhook-Event header");
36
+ }
37
+ if (!headers.deliveryId) {
38
+ throw new errors_1.SdkConfigError("Missing X-Webhook-Delivery-Id header");
39
+ }
40
+ const provided = headers.signature.slice(SIGNATURE_HEADER_PREFIX.length);
41
+ const expected = (0, node_crypto_1.createHmac)("sha256", secret).update(rawBody).digest("hex");
42
+ const providedBuf = Buffer.from(provided, "hex");
43
+ const expectedBuf = Buffer.from(expected, "hex");
44
+ const signatureValid = providedBuf.length === expectedBuf.length && (0, node_crypto_1.timingSafeEqual)(providedBuf, expectedBuf);
45
+ if (!signatureValid) {
46
+ throw new errors_1.SdkConfigError("Webhook signature verification failed");
47
+ }
48
+ let payload;
49
+ try {
50
+ payload = JSON.parse(rawBody);
51
+ }
52
+ catch {
53
+ throw new errors_1.SdkConfigError("Webhook body is not valid JSON");
54
+ }
55
+ return { eventType: headers.eventType, deliveryId: headers.deliveryId, payload };
56
+ }
57
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":";;AA+BA,sDA0CC;AAzED,6CAA0D;AAC1D,qCAA0C;AAE1C,MAAM,uBAAuB,GAAG,SAAS,CAAC;AAQ1C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,qBAAqB,CACnC,OAAe,EACf,OAIC,EACD,MAAc;IAEd,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,uBAAc,CAAC,oCAAoC,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,uBAAuB,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,uBAAc,CAAC,0CAA0C,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3F,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,uBAAc,CAAC,gCAAgC,CAAC,CAAC;IAC7D,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,uBAAc,CAAC,sCAAsC,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAA,wBAAU,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE5E,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACjD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACjD,MAAM,cAAc,GAClB,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,IAAI,IAAA,6BAAe,EAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAEzF,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,uBAAc,CAAC,uCAAuC,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,OAAU,CAAC;IACf,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAM,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,uBAAc,CAAC,gCAAgC,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;AACnF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@sscodeaxis/paywall-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Server-side SDK for integrating checkout, entitlements, licensing, and webhooks against the SSCodeAxis API.",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "lint": "eslint .",
24
+ "type-check": "tsc --noEmit",
25
+ "test": "VITE_CJS_IGNORE_WARNING=true vitest run",
26
+ "clean": "rm -rf dist"
27
+ },
28
+ "keywords": [
29
+ "checkout",
30
+ "entitlements",
31
+ "licensing",
32
+ "webhooks",
33
+ "sdk"
34
+ ],
35
+ "license": "UNLICENSED",
36
+ "devDependencies": {
37
+ "@types/node": "catalog:",
38
+ "typescript": "catalog:",
39
+ "vitest": "catalog:"
40
+ }
41
+ }