@superwall/server 0.1.5 → 0.2.1

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.
@@ -0,0 +1,17 @@
1
+ import type { CacheAdapter, CacheEntry } from "./types.js";
2
+ /**
3
+ * In-memory LRU cache. Entries are evicted (a) on TTL expiry when read,
4
+ * and (b) when the map exceeds `maxEntries` (oldest-insertion-order first).
5
+ *
6
+ * Single-process only. For multi-instance deployments, supply a Redis or
7
+ * KV adapter via `cache.storage`.
8
+ */
9
+ export declare class InMemoryCache implements CacheAdapter {
10
+ #private;
11
+ constructor(maxEntries: number);
12
+ get(key: string): CacheEntry | null;
13
+ set(key: string, value: CacheEntry): void;
14
+ delete(key: string): void;
15
+ clear(): void;
16
+ get size(): number;
17
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * In-memory LRU cache. Entries are evicted (a) on TTL expiry when read,
3
+ * and (b) when the map exceeds `maxEntries` (oldest-insertion-order first).
4
+ *
5
+ * Single-process only. For multi-instance deployments, supply a Redis or
6
+ * KV adapter via `cache.storage`.
7
+ */
8
+ export class InMemoryCache {
9
+ #map = new Map();
10
+ #maxEntries;
11
+ constructor(maxEntries) {
12
+ this.#maxEntries = maxEntries;
13
+ }
14
+ get(key) {
15
+ const entry = this.#map.get(key);
16
+ if (!entry)
17
+ return null;
18
+ if (entry.expiresAt <= Date.now()) {
19
+ this.#map.delete(key);
20
+ return null;
21
+ }
22
+ // Refresh LRU position — re-insert moves to the end.
23
+ this.#map.delete(key);
24
+ this.#map.set(key, entry);
25
+ return entry;
26
+ }
27
+ set(key, value) {
28
+ if (this.#map.has(key))
29
+ this.#map.delete(key);
30
+ this.#map.set(key, value);
31
+ while (this.#map.size > this.#maxEntries) {
32
+ const oldest = this.#map.keys().next().value;
33
+ if (oldest === undefined)
34
+ break;
35
+ this.#map.delete(oldest);
36
+ }
37
+ }
38
+ delete(key) {
39
+ this.#map.delete(key);
40
+ }
41
+ clear() {
42
+ this.#map.clear();
43
+ }
44
+ // Test helper — current size.
45
+ get size() {
46
+ return this.#map.size;
47
+ }
48
+ }
@@ -0,0 +1,13 @@
1
+ import { type Entitlements, type NetworkEnvironment } from "@superwall/core";
2
+ export interface FetcherConfig {
3
+ readonly apiKey: string;
4
+ readonly environment: NetworkEnvironment;
5
+ readonly timeoutMs: number;
6
+ }
7
+ /**
8
+ * GET /subscriptions-api/public/v1/users/{userId}/entitlements
9
+ *
10
+ * Maps status codes to typed error classes. Returns the parsed
11
+ * Entitlements bucket on success.
12
+ */
13
+ export declare const fetchEntitlements: (cfg: FetcherConfig, userId: string) => Promise<Entitlements>;
@@ -0,0 +1,67 @@
1
+ import { parseEntitlements, resolveHosts, SuperwallAuthError, SuperwallDecodingError, SuperwallNetworkError, SuperwallNotFoundError, SuperwallTimeoutError, } from "@superwall/core";
2
+ const SDK_VERSION = "0.0.0";
3
+ const requireFetch = () => {
4
+ if (typeof globalThis !== "undefined" && "fetch" in globalThis) {
5
+ return globalThis.fetch.bind(globalThis);
6
+ }
7
+ throw new SuperwallNetworkError("No fetch implementation available — Superwall needs `globalThis.fetch` (Node 18+, Bun, Deno, Workers).");
8
+ };
9
+ /**
10
+ * GET /subscriptions-api/public/v1/users/{userId}/entitlements
11
+ *
12
+ * Maps status codes to typed error classes. Returns the parsed
13
+ * Entitlements bucket on success.
14
+ */
15
+ export const fetchEntitlements = async (cfg, userId) => {
16
+ const hosts = resolveHosts(cfg.environment);
17
+ const url = `https://${hosts.subscriptions}/subscriptions-api/public/v1/users/${encodeURIComponent(userId)}/entitlements`;
18
+ const headers = {
19
+ Authorization: `Bearer ${cfg.apiKey}`,
20
+ "Content-Type": "application/json",
21
+ "X-Platform": "Server",
22
+ "X-Platform-Environment": "SDK",
23
+ "X-Platform-Wrapper": "Server",
24
+ "X-SDK-Version": SDK_VERSION,
25
+ "X-App-User-ID": userId,
26
+ };
27
+ const controller = new AbortController();
28
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
29
+ let response;
30
+ try {
31
+ response = await requireFetch()(url, {
32
+ method: "GET",
33
+ headers,
34
+ signal: controller.signal,
35
+ });
36
+ }
37
+ catch (cause) {
38
+ clearTimeout(timer);
39
+ if (cause instanceof Error && cause.name === "AbortError") {
40
+ throw new SuperwallTimeoutError(`Entitlements request timed out after ${cfg.timeoutMs}ms`, { url, timeoutMs: cfg.timeoutMs });
41
+ }
42
+ throw new SuperwallNetworkError(`Entitlements network error: ${describe(cause)}`, { url, cause });
43
+ }
44
+ clearTimeout(timer);
45
+ if (response.status === 401 || response.status === 403) {
46
+ throw new SuperwallAuthError(`Entitlements auth failed (status ${response.status}). Check SUPERWALL_API_KEY.`, { url });
47
+ }
48
+ if (response.status === 404) {
49
+ throw new SuperwallNotFoundError(`Entitlements lookup returned 404 for user ${userId}.`, { url });
50
+ }
51
+ if (!response.ok) {
52
+ throw new SuperwallNetworkError(`Entitlements returned ${response.status}`, { url, status: response.status });
53
+ }
54
+ let body;
55
+ try {
56
+ body = (await response.json());
57
+ }
58
+ catch (cause) {
59
+ throw new SuperwallDecodingError(`Entitlements JSON decode failed: ${describe(cause)}`, { url, cause });
60
+ }
61
+ return parseEntitlements(body);
62
+ };
63
+ const describe = (cause) => cause instanceof Error
64
+ ? cause.message
65
+ : typeof cause === "string"
66
+ ? cause
67
+ : JSON.stringify(cause);
@@ -0,0 +1,4 @@
1
+ export { Superwall } from "./superwall.js";
2
+ export type { SuperwallOptions, SuperwallInstance, EntitlementSpec, RequiresOptions, UserIdExtractor, CacheAdapter, CacheOptions, RequestInfo as OnRequestInfo, UnauthorizedContext, ConnectStyleRequest, ConnectStyleResponse, ConnectStyleNext, } from "./types.js";
3
+ export type { Entitlement, Entitlements, SubscriptionStatus, NetworkEnvironment, CustomEnvironmentHosts, } from "@superwall/core";
4
+ export { SuperwallError, SuperwallNetworkError, SuperwallAuthError, SuperwallNotFoundError, SuperwallTimeoutError, SuperwallDecodingError, } from "@superwall/core";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ // @superwall/server — server-side entitlement enforcement for the Superwall web SDK.
2
+ //
3
+ // Quickstart:
4
+ //
5
+ // import { Superwall } from "@superwall/server"
6
+ //
7
+ // const sw = Superwall({
8
+ // apiKey: process.env.SUPERWALL_API_KEY!,
9
+ // userId: (req) => req.session?.userId ?? null,
10
+ // })
11
+ //
12
+ // app.get("/api/export", sw.requires("pro"), exportHandler)
13
+ //
14
+ // The browser SDK's local subscription state is writable from DevTools. This
15
+ // package gates routes server-to-server against Superwall's `/entitlements`
16
+ // endpoint so tampering with the client cannot grant access to real resources.
17
+ export { Superwall } from "./superwall.js";
18
+ export { SuperwallError, SuperwallNetworkError, SuperwallAuthError, SuperwallNotFoundError, SuperwallTimeoutError, SuperwallDecodingError, } from "@superwall/core";
@@ -0,0 +1,18 @@
1
+ import type { Entitlements } from "@superwall/core";
2
+ import type { ConnectStyleNext, ConnectStyleResponse, EntitlementSpec, RequestInfo, RequiresOptions, UserIdExtractor } from "./types.js";
3
+ interface RequiresFactoryDeps<TReq> {
4
+ defaultUserIdExtractor: UserIdExtractor<TReq> | undefined;
5
+ loadEntitlements: (userId: string) => Promise<{
6
+ entitlements: Entitlements;
7
+ cacheHit: boolean;
8
+ }>;
9
+ emitRequest: (info: RequestInfo) => void;
10
+ }
11
+ /**
12
+ * Build the `sw.requires(spec, options?)` factory bound to the instance's
13
+ * cache + extractor + telemetry. Returned middleware is connect-style
14
+ * `(req, res, next)`, duck-typed so it works with Express directly and is
15
+ * trivially adaptable to Hono / Bun.serve / Next via a thin wrapper.
16
+ */
17
+ export declare const makeRequires: <TReq>(deps: RequiresFactoryDeps<TReq>) => (spec: EntitlementSpec, options?: RequiresOptions<TReq>) => (req: TReq, res: ConnectStyleResponse, next: ConnectStyleNext) => Promise<void>;
18
+ export {};
@@ -0,0 +1,80 @@
1
+ import { findMissing, normalizeSpec } from "./spec.js";
2
+ /**
3
+ * Build the `sw.requires(spec, options?)` factory bound to the instance's
4
+ * cache + extractor + telemetry. Returned middleware is connect-style
5
+ * `(req, res, next)`, duck-typed so it works with Express directly and is
6
+ * trivially adaptable to Hono / Bun.serve / Next via a thin wrapper.
7
+ */
8
+ export const makeRequires = (deps) => {
9
+ return (spec, options = {}) => {
10
+ // Normalize once at registration time; validation errors surface at
11
+ // app boot, not at first request.
12
+ const normalized = normalizeSpec(spec);
13
+ const extractor = options.userId ?? deps.defaultUserIdExtractor;
14
+ const allowAnonymous = options.allowAnonymous ?? false;
15
+ return async (req, res, next) => {
16
+ const start = Date.now();
17
+ let userId = null;
18
+ if (extractor) {
19
+ const extracted = await extractor(req);
20
+ if (typeof extracted === "string" && extracted.length > 0) {
21
+ userId = extracted;
22
+ }
23
+ }
24
+ if (!userId) {
25
+ if (allowAnonymous) {
26
+ next();
27
+ return;
28
+ }
29
+ await rejectUnauthorized(req, res, options, {
30
+ userId: null,
31
+ entitlement: normalized.entitlements[0] ?? "",
32
+ missing: normalized.entitlements,
33
+ reason: "no_user_id",
34
+ });
35
+ return;
36
+ }
37
+ let ents;
38
+ let cacheHit;
39
+ try {
40
+ const loaded = await deps.loadEntitlements(userId);
41
+ ents = loaded.entitlements;
42
+ cacheHit = loaded.cacheHit;
43
+ }
44
+ catch (err) {
45
+ // Surfaced to the framework's error handler. Default Express
46
+ // behavior is a 500; consumers can intercept via their own
47
+ // error middleware.
48
+ next(err);
49
+ return;
50
+ }
51
+ const missing = findMissing(normalized, ents);
52
+ deps.emitRequest({
53
+ userId,
54
+ entitlements: ents.active.map((e) => e.id),
55
+ cacheHit,
56
+ durationMs: Date.now() - start,
57
+ });
58
+ if (missing.length === 0) {
59
+ next();
60
+ return;
61
+ }
62
+ await rejectUnauthorized(req, res, options, {
63
+ userId,
64
+ entitlement: missing[0] ?? normalized.entitlements[0] ?? "",
65
+ missing,
66
+ reason: "not_entitled",
67
+ });
68
+ };
69
+ };
70
+ };
71
+ const rejectUnauthorized = async (req, res, options, ctx) => {
72
+ if (options.onUnauthorized) {
73
+ await options.onUnauthorized(req, res, ctx);
74
+ return;
75
+ }
76
+ res.status(403).json({
77
+ error: "entitlement_required",
78
+ entitlement: ctx.entitlement,
79
+ });
80
+ };
package/dist/spec.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Entitlements } from "@superwall/core";
2
+ import type { EntitlementSpec } from "./types.js";
3
+ export interface NormalizedSpec {
4
+ readonly mode: "all" | "any";
5
+ readonly entitlements: ReadonlyArray<string>;
6
+ }
7
+ /**
8
+ * Normalize any accepted spec shape into `{ mode, entitlements }`.
9
+ * Throws on empty / malformed input — fail loud at config time, not
10
+ * at request time.
11
+ */
12
+ export declare const normalizeSpec: (spec: EntitlementSpec) => NormalizedSpec;
13
+ /**
14
+ * Returns the entitlement IDs from `spec` that are NOT active on `ents`.
15
+ * If `mode === "all"`, this is the set of unmet entitlements. If
16
+ * `mode === "any"`, returns empty when at least one is met, otherwise
17
+ * returns all listed entitlements (none satisfied).
18
+ */
19
+ export declare const findMissing: (spec: NormalizedSpec, ents: Entitlements) => ReadonlyArray<string>;
package/dist/spec.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Normalize any accepted spec shape into `{ mode, entitlements }`.
3
+ * Throws on empty / malformed input — fail loud at config time, not
4
+ * at request time.
5
+ */
6
+ export const normalizeSpec = (spec) => {
7
+ if (typeof spec === "string") {
8
+ if (spec.length === 0) {
9
+ throw new TypeError("Entitlement spec cannot be an empty string.");
10
+ }
11
+ return { mode: "all", entitlements: [spec] };
12
+ }
13
+ if (Array.isArray(spec)) {
14
+ if (spec.length === 0) {
15
+ throw new TypeError("Entitlement spec array cannot be empty.");
16
+ }
17
+ return { mode: "all", entitlements: spec };
18
+ }
19
+ if (typeof spec === "object" && spec !== null) {
20
+ if ("all" in spec) {
21
+ if (!Array.isArray(spec.all) || spec.all.length === 0) {
22
+ throw new TypeError("`{ all: [...] }` must be a non-empty string array.");
23
+ }
24
+ return { mode: "all", entitlements: spec.all };
25
+ }
26
+ if ("any" in spec) {
27
+ if (!Array.isArray(spec.any) || spec.any.length === 0) {
28
+ throw new TypeError("`{ any: [...] }` must be a non-empty string array.");
29
+ }
30
+ return { mode: "any", entitlements: spec.any };
31
+ }
32
+ }
33
+ throw new TypeError(`Unrecognized entitlement spec: ${JSON.stringify(spec)}. Expected string, string[], { all: string[] }, or { any: string[] }.`);
34
+ };
35
+ /**
36
+ * Returns the entitlement IDs from `spec` that are NOT active on `ents`.
37
+ * If `mode === "all"`, this is the set of unmet entitlements. If
38
+ * `mode === "any"`, returns empty when at least one is met, otherwise
39
+ * returns all listed entitlements (none satisfied).
40
+ */
41
+ export const findMissing = (spec, ents) => {
42
+ const activeIds = new Set(ents.active.map((e) => e.id));
43
+ if (spec.mode === "all") {
44
+ return spec.entitlements.filter((id) => !activeIds.has(id));
45
+ }
46
+ // any
47
+ const anyMet = spec.entitlements.some((id) => activeIds.has(id));
48
+ return anyMet ? [] : spec.entitlements;
49
+ };
@@ -0,0 +1,16 @@
1
+ import { type FetcherConfig } from "./fetcher.js";
2
+ import type { SuperwallInstance, SuperwallOptions } from "./types.js";
3
+ /**
4
+ * Construct a Superwall server instance. One per process — internally
5
+ * shares the cache and outbound HTTP config across all middleware and
6
+ * direct calls.
7
+ *
8
+ * ```ts
9
+ * const sw = Superwall({
10
+ * apiKey: process.env.SUPERWALL_API_KEY!,
11
+ * userId: (req) => req.session?.userId ?? null,
12
+ * })
13
+ * ```
14
+ */
15
+ export declare const Superwall: <TReq = unknown>(options: SuperwallOptions<TReq>) => SuperwallInstance<TReq>;
16
+ export type { FetcherConfig };
@@ -0,0 +1,91 @@
1
+ import { InMemoryCache } from "./cache.js";
2
+ import { fetchEntitlements } from "./fetcher.js";
3
+ import { findMissing, normalizeSpec } from "./spec.js";
4
+ import { makeRequires } from "./requires.js";
5
+ const DEFAULT_CACHE_TTL_MS = 60_000;
6
+ const DEFAULT_CACHE_MAX = 10_000;
7
+ const DEFAULT_TIMEOUT_MS = 5_000;
8
+ /**
9
+ * Construct a Superwall server instance. One per process — internally
10
+ * shares the cache and outbound HTTP config across all middleware and
11
+ * direct calls.
12
+ *
13
+ * ```ts
14
+ * const sw = Superwall({
15
+ * apiKey: process.env.SUPERWALL_API_KEY!,
16
+ * userId: (req) => req.session?.userId ?? null,
17
+ * })
18
+ * ```
19
+ */
20
+ export const Superwall = (options) => {
21
+ if (!options.apiKey || typeof options.apiKey !== "string") {
22
+ throw new TypeError("Superwall: `apiKey` is required.");
23
+ }
24
+ const cacheOpts = options.cache ?? {};
25
+ const ttlMs = cacheOpts.ttlMs ?? DEFAULT_CACHE_TTL_MS;
26
+ const maxEntries = cacheOpts.maxEntries ?? DEFAULT_CACHE_MAX;
27
+ const cache = cacheOpts.storage === undefined || cacheOpts.storage === "memory"
28
+ ? new InMemoryCache(maxEntries)
29
+ : cacheOpts.storage;
30
+ const fetcherConfig = {
31
+ apiKey: options.apiKey,
32
+ environment: options.environment ?? "release",
33
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
34
+ };
35
+ /** Fetch + cache for one userId. Inflight de-dup is intentionally
36
+ * omitted for v0 — callers rarely concurrently lookup the same user
37
+ * thousands of times, and the trade-off (extra closure per request)
38
+ * isn't worth it before we measure. */
39
+ const loadEntitlements = async (userId) => {
40
+ const cached = await cache.get(userId);
41
+ if (cached)
42
+ return { entitlements: cached.value, cacheHit: true };
43
+ const ents = await fetchEntitlements(fetcherConfig, userId);
44
+ await cache.set(userId, { value: ents, expiresAt: Date.now() + ttlMs });
45
+ return { entitlements: ents, cacheHit: false };
46
+ };
47
+ const emitRequest = (info) => {
48
+ if (!options.onRequest)
49
+ return;
50
+ try {
51
+ options.onRequest(info);
52
+ }
53
+ catch {
54
+ // Telemetry hooks never break the request.
55
+ }
56
+ };
57
+ const getEntitlements = async (userId) => {
58
+ const start = Date.now();
59
+ const { entitlements, cacheHit } = await loadEntitlements(userId);
60
+ emitRequest({
61
+ userId,
62
+ entitlements: entitlements.active.map((e) => e.id),
63
+ cacheHit,
64
+ durationMs: Date.now() - start,
65
+ });
66
+ return entitlements;
67
+ };
68
+ const userHas = async (userId, spec) => {
69
+ const normalized = normalizeSpec(spec);
70
+ const ents = await getEntitlements(userId);
71
+ return findMissing(normalized, ents).length === 0;
72
+ };
73
+ const invalidate = async (userId) => {
74
+ await cache.delete(userId);
75
+ };
76
+ const invalidateAll = async () => {
77
+ await cache.clear();
78
+ };
79
+ const requires = makeRequires({
80
+ defaultUserIdExtractor: options.userId,
81
+ loadEntitlements,
82
+ emitRequest,
83
+ });
84
+ return {
85
+ requires,
86
+ userHas,
87
+ getEntitlements,
88
+ invalidate,
89
+ invalidateAll,
90
+ };
91
+ };
@@ -0,0 +1,106 @@
1
+ import type { Entitlements, NetworkEnvironment } from "@superwall/core";
2
+ /**
3
+ * Entitlement spec accepted by `sw.requires()` and `sw.userHas()`.
4
+ *
5
+ * - `"pro"` — single entitlement (must be active)
6
+ * - `["pro", "team"]` — multiple entitlements, ALL required
7
+ * - `{ all: [...] }` — explicit AND
8
+ * - `{ any: [...] }` — OR (any of the listed must be active)
9
+ */
10
+ export type EntitlementSpec = string | ReadonlyArray<string> | {
11
+ readonly all: ReadonlyArray<string>;
12
+ } | {
13
+ readonly any: ReadonlyArray<string>;
14
+ };
15
+ /**
16
+ * Cache adapter shape. The built-in `"memory"` storage implements this.
17
+ * Plug in Redis, KV, or any async store with the same surface.
18
+ *
19
+ * Keys are app-user IDs. Values are the parsed Entitlements bucket plus
20
+ * the timestamp it expires at.
21
+ */
22
+ export interface CacheAdapter {
23
+ get(key: string): Promise<CacheEntry | null> | CacheEntry | null;
24
+ set(key: string, value: CacheEntry): Promise<void> | void;
25
+ delete(key: string): Promise<void> | void;
26
+ clear(): Promise<void> | void;
27
+ }
28
+ export interface CacheEntry {
29
+ readonly value: Entitlements;
30
+ readonly expiresAt: number;
31
+ }
32
+ export interface CacheOptions {
33
+ /** Time-to-live in milliseconds. Default 60_000 (60s). */
34
+ ttlMs?: number;
35
+ /** LRU cap; oldest evicted first. Default 10_000. */
36
+ maxEntries?: number;
37
+ /** `"memory"` or a custom adapter. Default `"memory"`. */
38
+ storage?: "memory" | CacheAdapter;
39
+ }
40
+ /**
41
+ * Extracts the user identifier from whatever request shape your framework
42
+ * uses. Safety-critical knob: read from authenticated session, never from
43
+ * request body / query string / unverified header.
44
+ */
45
+ export type UserIdExtractor<TReq = unknown> = (req: TReq) => string | null | undefined | Promise<string | null | undefined>;
46
+ export interface RequestInfo {
47
+ readonly userId: string;
48
+ readonly entitlements: ReadonlyArray<string>;
49
+ readonly cacheHit: boolean;
50
+ readonly durationMs: number;
51
+ }
52
+ export interface SuperwallOptions<TReq = unknown> {
53
+ /** Superwall API key. Read from `process.env.SUPERWALL_API_KEY`. */
54
+ apiKey: string;
55
+ /** Environment selector. Default `"release"`. */
56
+ environment?: NetworkEnvironment;
57
+ cache?: CacheOptions;
58
+ /** Default userId extractor; can be overridden per-call on `requires()`. */
59
+ userId?: UserIdExtractor<TReq>;
60
+ /** Hook invoked after every entitlement check. Use for tracing. */
61
+ onRequest?: (info: RequestInfo) => void;
62
+ /** Request timeout in milliseconds for outbound calls to Superwall. Default 5_000. */
63
+ timeoutMs?: number;
64
+ }
65
+ /**
66
+ * Narrow internal `fetch` surface. Not exported as a public option — server
67
+ * runtimes (Node 18+, Bun, Deno, Workers) all carry `globalThis.fetch`
68
+ * natively, and unlike the browser there's no CORS proxy use case. Tests
69
+ * stub `globalThis.fetch` directly.
70
+ */
71
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
72
+ export interface ConnectStyleResponse {
73
+ status(code: number): ConnectStyleResponse;
74
+ json(body: unknown): unknown;
75
+ setHeader?(name: string, value: string): unknown;
76
+ }
77
+ export type ConnectStyleNext = (err?: unknown) => void;
78
+ export type ConnectStyleRequest = Record<string, unknown>;
79
+ export interface UnauthorizedContext {
80
+ readonly userId: string | null;
81
+ readonly entitlement: string;
82
+ readonly missing: ReadonlyArray<string>;
83
+ readonly reason: "no_user_id" | "not_entitled";
84
+ }
85
+ export interface RequiresOptions<TReq = unknown> {
86
+ /** Per-route userId override (rare). */
87
+ userId?: UserIdExtractor<TReq>;
88
+ /**
89
+ * Custom rejection handler. Default: respond `403 { error:
90
+ * "entitlement_required", entitlement }`.
91
+ */
92
+ onUnauthorized?: (req: TReq, res: ConnectStyleResponse, ctx: UnauthorizedContext) => void | Promise<void>;
93
+ /**
94
+ * Allow anonymous requests (no userId extracted) to fall through to the
95
+ * route handler. Defaults to `false` — fail closed. Set `true` when the
96
+ * route handler itself decides what to render for guests vs. entitled users.
97
+ */
98
+ allowAnonymous?: boolean;
99
+ }
100
+ export interface SuperwallInstance<TReq = unknown> {
101
+ requires(spec: EntitlementSpec, options?: RequiresOptions<TReq>): (req: TReq, res: ConnectStyleResponse, next: ConnectStyleNext) => Promise<void>;
102
+ userHas(userId: string, spec: EntitlementSpec): Promise<boolean>;
103
+ getEntitlements(userId: string): Promise<Entitlements>;
104
+ invalidate(userId: string): Promise<void>;
105
+ invalidateAll(): Promise<void>;
106
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from "@superwall/verify";
package/package.json CHANGED
@@ -1,27 +1,41 @@
1
1
  {
2
2
  "name": "@superwall/server",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "sideEffects": false,
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "files": [
14
+ "dist"
15
+ ],
6
16
  "exports": {
7
17
  ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
18
+ "@superwall/source": "./src/index.ts",
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "default": "./dist/index.js"
10
22
  },
11
23
  "./verify": {
12
- "types": "./src/verify.ts",
13
- "default": "./src/verify.ts"
24
+ "@superwall/source": "./src/verify.ts",
25
+ "types": "./dist/verify.d.ts",
26
+ "import": "./dist/verify.js",
27
+ "default": "./dist/verify.js"
14
28
  }
15
29
  },
16
30
  "scripts": {
17
31
  "test": "bun test",
18
32
  "typecheck": "tsc --noEmit",
19
- "build": "echo 'no-op for v0; consumers import .ts directly via Bun/Vite/Next ESM'",
20
- "clean": "rm -rf node_modules .turbo *.tsbuildinfo"
33
+ "build": "bun run ../../scripts/build-package.ts",
34
+ "clean": "rm -rf dist node_modules .turbo *.tsbuildinfo"
21
35
  },
22
36
  "dependencies": {
23
- "@superwall/core": "^0.1.3",
24
- "@superwall/verify": "^0.1.3"
37
+ "@superwall/core": "^0.2.0",
38
+ "@superwall/verify": "^0.2.0"
25
39
  },
26
40
  "devDependencies": {
27
41
  "@types/bun": "latest",
@@ -1,3 +0,0 @@
1
-
2
- $ echo 'no-op for v0; consumers import .ts directly via Bun/Vite/Next ESM'
3
- no-op for v0; consumers import .ts directly via Bun/Vite/Next ESM