@superwall/server 0.2.0 → 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.
package/src/superwall.ts DELETED
@@ -1,121 +0,0 @@
1
- import type { Entitlements } from "@superwall/core";
2
- import { InMemoryCache } from "./cache.ts";
3
- import { fetchEntitlements, type FetcherConfig } from "./fetcher.ts";
4
- import { findMissing, normalizeSpec } from "./spec.ts";
5
- import { makeRequires } from "./requires.ts";
6
- import type {
7
- CacheAdapter,
8
- EntitlementSpec,
9
- RequestInfo,
10
- RequiresOptions,
11
- SuperwallInstance,
12
- SuperwallOptions,
13
- } from "./types.ts";
14
-
15
- const DEFAULT_CACHE_TTL_MS = 60_000;
16
- const DEFAULT_CACHE_MAX = 10_000;
17
- const DEFAULT_TIMEOUT_MS = 5_000;
18
-
19
- /**
20
- * Construct a Superwall server instance. One per process — internally
21
- * shares the cache and outbound HTTP config across all middleware and
22
- * direct calls.
23
- *
24
- * ```ts
25
- * const sw = Superwall({
26
- * apiKey: process.env.SUPERWALL_API_KEY!,
27
- * userId: (req) => req.session?.userId ?? null,
28
- * })
29
- * ```
30
- */
31
- export const Superwall = <TReq = unknown>(
32
- options: SuperwallOptions<TReq>,
33
- ): SuperwallInstance<TReq> => {
34
- if (!options.apiKey || typeof options.apiKey !== "string") {
35
- throw new TypeError("Superwall: `apiKey` is required.");
36
- }
37
-
38
- const cacheOpts = options.cache ?? {};
39
- const ttlMs = cacheOpts.ttlMs ?? DEFAULT_CACHE_TTL_MS;
40
- const maxEntries = cacheOpts.maxEntries ?? DEFAULT_CACHE_MAX;
41
- const cache: CacheAdapter =
42
- cacheOpts.storage === undefined || cacheOpts.storage === "memory"
43
- ? new InMemoryCache(maxEntries)
44
- : cacheOpts.storage;
45
-
46
- const fetcherConfig: FetcherConfig = {
47
- apiKey: options.apiKey,
48
- environment: options.environment ?? "release",
49
- timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
50
- };
51
-
52
- /** Fetch + cache for one userId. Inflight de-dup is intentionally
53
- * omitted for v0 — callers rarely concurrently lookup the same user
54
- * thousands of times, and the trade-off (extra closure per request)
55
- * isn't worth it before we measure. */
56
- const loadEntitlements = async (userId: string): Promise<{
57
- entitlements: Entitlements;
58
- cacheHit: boolean;
59
- }> => {
60
- const cached = await cache.get(userId);
61
- if (cached) return { entitlements: cached.value, cacheHit: true };
62
- const ents = await fetchEntitlements(fetcherConfig, userId);
63
- await cache.set(userId, { value: ents, expiresAt: Date.now() + ttlMs });
64
- return { entitlements: ents, cacheHit: false };
65
- };
66
-
67
- const emitRequest = (info: RequestInfo): void => {
68
- if (!options.onRequest) return;
69
- try {
70
- options.onRequest(info);
71
- } catch {
72
- // Telemetry hooks never break the request.
73
- }
74
- };
75
-
76
- const getEntitlements = async (userId: string): Promise<Entitlements> => {
77
- const start = Date.now();
78
- const { entitlements, cacheHit } = await loadEntitlements(userId);
79
- emitRequest({
80
- userId,
81
- entitlements: entitlements.active.map((e) => e.id),
82
- cacheHit,
83
- durationMs: Date.now() - start,
84
- });
85
- return entitlements;
86
- };
87
-
88
- const userHas = async (
89
- userId: string,
90
- spec: EntitlementSpec,
91
- ): Promise<boolean> => {
92
- const normalized = normalizeSpec(spec);
93
- const ents = await getEntitlements(userId);
94
- return findMissing(normalized, ents).length === 0;
95
- };
96
-
97
- const invalidate = async (userId: string): Promise<void> => {
98
- await cache.delete(userId);
99
- };
100
-
101
- const invalidateAll = async (): Promise<void> => {
102
- await cache.clear();
103
- };
104
-
105
- const requires = makeRequires<TReq>({
106
- defaultUserIdExtractor: options.userId,
107
- loadEntitlements,
108
- emitRequest,
109
- });
110
-
111
- return {
112
- requires,
113
- userHas,
114
- getEntitlements,
115
- invalidate,
116
- invalidateAll,
117
- };
118
- };
119
-
120
- // Re-export for tests that need to construct fetcher config directly.
121
- export type { FetcherConfig };
package/src/types.ts DELETED
@@ -1,137 +0,0 @@
1
- import type { Entitlements, NetworkEnvironment } from "@superwall/core";
2
-
3
- /**
4
- * Entitlement spec accepted by `sw.requires()` and `sw.userHas()`.
5
- *
6
- * - `"pro"` — single entitlement (must be active)
7
- * - `["pro", "team"]` — multiple entitlements, ALL required
8
- * - `{ all: [...] }` — explicit AND
9
- * - `{ any: [...] }` — OR (any of the listed must be active)
10
- */
11
- export type EntitlementSpec =
12
- | string
13
- | ReadonlyArray<string>
14
- | { readonly all: ReadonlyArray<string> }
15
- | { readonly any: ReadonlyArray<string> };
16
-
17
- /**
18
- * Cache adapter shape. The built-in `"memory"` storage implements this.
19
- * Plug in Redis, KV, or any async store with the same surface.
20
- *
21
- * Keys are app-user IDs. Values are the parsed Entitlements bucket plus
22
- * the timestamp it expires at.
23
- */
24
- export interface CacheAdapter {
25
- get(key: string): Promise<CacheEntry | null> | CacheEntry | null;
26
- set(key: string, value: CacheEntry): Promise<void> | void;
27
- delete(key: string): Promise<void> | void;
28
- clear(): Promise<void> | void;
29
- }
30
-
31
- export interface CacheEntry {
32
- readonly value: Entitlements;
33
- readonly expiresAt: number;
34
- }
35
-
36
- export interface CacheOptions {
37
- /** Time-to-live in milliseconds. Default 60_000 (60s). */
38
- ttlMs?: number;
39
- /** LRU cap; oldest evicted first. Default 10_000. */
40
- maxEntries?: number;
41
- /** `"memory"` or a custom adapter. Default `"memory"`. */
42
- storage?: "memory" | CacheAdapter;
43
- }
44
-
45
- /**
46
- * Extracts the user identifier from whatever request shape your framework
47
- * uses. Safety-critical knob: read from authenticated session, never from
48
- * request body / query string / unverified header.
49
- */
50
- export type UserIdExtractor<TReq = unknown> = (
51
- req: TReq,
52
- ) => string | null | undefined | Promise<string | null | undefined>;
53
-
54
- export interface RequestInfo {
55
- readonly userId: string;
56
- readonly entitlements: ReadonlyArray<string>;
57
- readonly cacheHit: boolean;
58
- readonly durationMs: number;
59
- }
60
-
61
- export interface SuperwallOptions<TReq = unknown> {
62
- /** Superwall API key. Read from `process.env.SUPERWALL_API_KEY`. */
63
- apiKey: string;
64
- /** Environment selector. Default `"release"`. */
65
- environment?: NetworkEnvironment;
66
- cache?: CacheOptions;
67
- /** Default userId extractor; can be overridden per-call on `requires()`. */
68
- userId?: UserIdExtractor<TReq>;
69
- /** Hook invoked after every entitlement check. Use for tracing. */
70
- onRequest?: (info: RequestInfo) => void;
71
- /** Request timeout in milliseconds for outbound calls to Superwall. Default 5_000. */
72
- timeoutMs?: number;
73
- }
74
-
75
- /**
76
- * Narrow internal `fetch` surface. Not exported as a public option — server
77
- * runtimes (Node 18+, Bun, Deno, Workers) all carry `globalThis.fetch`
78
- * natively, and unlike the browser there's no CORS proxy use case. Tests
79
- * stub `globalThis.fetch` directly.
80
- */
81
- export type FetchLike = (
82
- input: string | URL | Request,
83
- init?: RequestInit,
84
- ) => Promise<Response>;
85
-
86
- // Connect-style middleware signature — req/res/next, duck-typed. Compatible
87
- // with Express, Connect, and shimmable from Bun.serve / Hono / Next.
88
-
89
- export interface ConnectStyleResponse {
90
- status(code: number): ConnectStyleResponse;
91
- json(body: unknown): unknown;
92
- setHeader?(name: string, value: string): unknown;
93
- }
94
-
95
- export type ConnectStyleNext = (err?: unknown) => void;
96
-
97
- export type ConnectStyleRequest = Record<string, unknown>;
98
-
99
- export interface UnauthorizedContext {
100
- readonly userId: string | null;
101
- readonly entitlement: string;
102
- readonly missing: ReadonlyArray<string>;
103
- readonly reason: "no_user_id" | "not_entitled";
104
- }
105
-
106
- export interface RequiresOptions<TReq = unknown> {
107
- /** Per-route userId override (rare). */
108
- userId?: UserIdExtractor<TReq>;
109
- /**
110
- * Custom rejection handler. Default: respond `403 { error:
111
- * "entitlement_required", entitlement }`.
112
- */
113
- onUnauthorized?: (
114
- req: TReq,
115
- res: ConnectStyleResponse,
116
- ctx: UnauthorizedContext,
117
- ) => void | Promise<void>;
118
- /**
119
- * Allow anonymous requests (no userId extracted) to fall through to the
120
- * route handler. Defaults to `false` — fail closed. Set `true` when the
121
- * route handler itself decides what to render for guests vs. entitled users.
122
- */
123
- allowAnonymous?: boolean;
124
- }
125
-
126
- // The object returned by Superwall(options). Methods bind back to the
127
- // shared cache + fetcher so a single instance fronts the entire process.
128
- export interface SuperwallInstance<TReq = unknown> {
129
- requires(
130
- spec: EntitlementSpec,
131
- options?: RequiresOptions<TReq>,
132
- ): (req: TReq, res: ConnectStyleResponse, next: ConnectStyleNext) => Promise<void>;
133
- userHas(userId: string, spec: EntitlementSpec): Promise<boolean>;
134
- getEntitlements(userId: string): Promise<Entitlements>;
135
- invalidate(userId: string): Promise<void>;
136
- invalidateAll(): Promise<void>;
137
- }
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "rootDir": "src"
5
- },
6
- "include": ["src/**/*"]
7
- }
File without changes