@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.
@@ -1,219 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { Superwall } from "./superwall.ts";
3
- import type {
4
- ConnectStyleNext,
5
- ConnectStyleResponse,
6
- FetchLike,
7
- } from "./types.ts";
8
-
9
- interface FakeRequest {
10
- session?: { userId?: string };
11
- params?: Record<string, string>;
12
- }
13
-
14
- interface ResponseLog {
15
- statusCode: number | null;
16
- body: unknown;
17
- }
18
-
19
- const realFetch = globalThis.fetch;
20
- const setFetch = (impl: FetchLike): void => {
21
- (globalThis as unknown as { fetch: FetchLike }).fetch = impl;
22
- };
23
-
24
- afterEach(() => {
25
- (globalThis as unknown as { fetch: typeof realFetch }).fetch = realFetch;
26
- });
27
-
28
- const makeRes = (): { res: ConnectStyleResponse; log: ResponseLog } => {
29
- const log: ResponseLog = { statusCode: null, body: null };
30
- const res: ConnectStyleResponse = {
31
- status(code) {
32
- log.statusCode = code;
33
- return res;
34
- },
35
- json(body) {
36
- log.body = body;
37
- return res;
38
- },
39
- };
40
- return { res, log };
41
- };
42
-
43
- const makeNext = (): {
44
- next: ConnectStyleNext;
45
- calls: Array<{ err?: unknown }>;
46
- } => {
47
- const calls: Array<{ err?: unknown }> = [];
48
- const next: ConnectStyleNext = (err) => {
49
- calls.push(err === undefined ? {} : { err });
50
- };
51
- return { next, calls };
52
- };
53
-
54
- const okResponse = (
55
- entitlements: Array<{ id: string; isActive: boolean }>,
56
- ): Response =>
57
- new Response(JSON.stringify({ entitlements }), {
58
- status: 200,
59
- headers: { "content-type": "application/json" },
60
- });
61
-
62
- const urlOf = (input: string | URL | Request): string =>
63
- typeof input === "string"
64
- ? input
65
- : input instanceof URL
66
- ? input.toString()
67
- : input.url;
68
-
69
- describe("sw.requires()", () => {
70
- test("calls next() when entitled", async () => {
71
- setFetch(async () => okResponse([{ id: "pro", isActive: true }]));
72
- const sw = Superwall<FakeRequest>({
73
- apiKey: "k",
74
- userId: (r) => r.session?.userId ?? null,
75
- });
76
- const { res, log } = makeRes();
77
- const { next, calls } = makeNext();
78
- await sw.requires("pro")({ session: { userId: "u1" } }, res, next);
79
- expect(calls).toHaveLength(1);
80
- expect(calls[0]).toEqual({});
81
- expect(log.statusCode).toBeNull();
82
- });
83
-
84
- test("default 403 when not entitled", async () => {
85
- setFetch(async () => okResponse([{ id: "team", isActive: true }]));
86
- const sw = Superwall<FakeRequest>({
87
- apiKey: "k",
88
- userId: (r) => r.session?.userId ?? null,
89
- });
90
- const { res, log } = makeRes();
91
- const { next, calls } = makeNext();
92
- await sw.requires("pro")({ session: { userId: "u1" } }, res, next);
93
- expect(calls).toHaveLength(0);
94
- expect(log.statusCode).toBe(403);
95
- expect(log.body).toEqual({
96
- error: "entitlement_required",
97
- entitlement: "pro",
98
- });
99
- });
100
-
101
- test("fail-closed when no userId can be extracted", async () => {
102
- setFetch(async () => {
103
- throw new Error("should not be called — extractor returned null");
104
- });
105
- const sw = Superwall<FakeRequest>({
106
- apiKey: "k",
107
- userId: (r) => r.session?.userId ?? null,
108
- });
109
- const { res, log } = makeRes();
110
- const { next, calls } = makeNext();
111
- await sw.requires("pro")({}, res, next);
112
- expect(calls).toHaveLength(0);
113
- expect(log.statusCode).toBe(403);
114
- });
115
-
116
- test("allowAnonymous lets the handler decide", async () => {
117
- setFetch(async () => okResponse([]));
118
- const sw = Superwall<FakeRequest>({
119
- apiKey: "k",
120
- userId: (r) => r.session?.userId ?? null,
121
- });
122
- const { res, log } = makeRes();
123
- const { next, calls } = makeNext();
124
- await sw.requires("pro", { allowAnonymous: true })({}, res, next);
125
- expect(calls).toEqual([{}]);
126
- expect(log.statusCode).toBeNull();
127
- });
128
-
129
- test("onUnauthorized override", async () => {
130
- setFetch(async () => okResponse([]));
131
- const seen: Array<{ reason: string; missing: ReadonlyArray<string> }> = [];
132
- const sw = Superwall<FakeRequest>({
133
- apiKey: "k",
134
- userId: (r) => r.session?.userId ?? null,
135
- });
136
- const { res, log } = makeRes();
137
- const { next, calls } = makeNext();
138
- await sw.requires("pro", {
139
- onUnauthorized: (_req, r, ctx) => {
140
- seen.push({ reason: ctx.reason, missing: ctx.missing });
141
- r.status(402).json({ paywall: ctx.entitlement });
142
- },
143
- })({ session: { userId: "u1" } }, res, next);
144
- expect(calls).toHaveLength(0);
145
- expect(log.statusCode).toBe(402);
146
- expect(log.body).toEqual({ paywall: "pro" });
147
- expect(seen).toEqual([{ reason: "not_entitled", missing: ["pro"] }]);
148
- });
149
-
150
- test("per-route userId override wins", async () => {
151
- const observed: { queriedId: string | null } = { queriedId: null };
152
- setFetch(async (input) => {
153
- const m = urlOf(input).match(/\/users\/([^/]+)\/entitlements/);
154
- observed.queriedId = m ? decodeURIComponent(m[1]!) : null;
155
- return okResponse([{ id: "pro", isActive: true }]);
156
- });
157
- const sw = Superwall<FakeRequest>({
158
- apiKey: "k",
159
- userId: () => "default-user",
160
- });
161
- const { res } = makeRes();
162
- const { next, calls } = makeNext();
163
- await sw.requires("pro", {
164
- userId: (r) => r.params?.userId ?? null,
165
- })({ params: { userId: "explicit-user" } }, res, next);
166
- expect(calls).toEqual([{}]);
167
- expect(observed.queriedId).toBe("explicit-user");
168
- });
169
-
170
- test("network errors pass to next(err)", async () => {
171
- setFetch(async () => new Response("oops", { status: 500 }));
172
- const sw = Superwall<FakeRequest>({
173
- apiKey: "k",
174
- userId: () => "u1",
175
- });
176
- const { res, log } = makeRes();
177
- const { next, calls } = makeNext();
178
- await sw.requires("pro")({}, res, next);
179
- expect(calls).toHaveLength(1);
180
- expect(calls[0]?.err).toBeDefined();
181
- expect(log.statusCode).toBeNull();
182
- });
183
-
184
- test("multiple-AND spec rejects when any missing", async () => {
185
- setFetch(async () => okResponse([{ id: "pro", isActive: true }]));
186
- const sw = Superwall<FakeRequest>({
187
- apiKey: "k",
188
- userId: () => "u1",
189
- });
190
- const { res, log } = makeRes();
191
- const { next, calls } = makeNext();
192
- await sw.requires(["pro", "team"])({}, res, next);
193
- expect(calls).toHaveLength(0);
194
- expect(log.statusCode).toBe(403);
195
- expect((log.body as { entitlement: string }).entitlement).toBe("team");
196
- });
197
-
198
- test("ANY spec passes when one active", async () => {
199
- setFetch(async () => okResponse([{ id: "team", isActive: true }]));
200
- const sw = Superwall<FakeRequest>({
201
- apiKey: "k",
202
- userId: () => "u1",
203
- });
204
- const { res } = makeRes();
205
- const { next, calls } = makeNext();
206
- await sw.requires({ any: ["pro", "team"] })({}, res, next);
207
- expect(calls).toEqual([{}]);
208
- });
209
-
210
- test("normalizes spec at registration time (fails loud)", () => {
211
- setFetch(async () => okResponse([]));
212
- const sw = Superwall<FakeRequest>({
213
- apiKey: "k",
214
- userId: () => "u1",
215
- });
216
- expect(() => sw.requires("")).toThrow(TypeError);
217
- expect(() => sw.requires([])).toThrow(TypeError);
218
- });
219
- });
package/src/requires.ts DELETED
@@ -1,118 +0,0 @@
1
- import type { Entitlements } from "@superwall/core";
2
- import { findMissing, normalizeSpec } from "./spec.ts";
3
- import type {
4
- ConnectStyleNext,
5
- ConnectStyleResponse,
6
- EntitlementSpec,
7
- RequestInfo,
8
- RequiresOptions,
9
- UserIdExtractor,
10
- } from "./types.ts";
11
-
12
- interface RequiresFactoryDeps<TReq> {
13
- defaultUserIdExtractor: UserIdExtractor<TReq> | undefined;
14
- loadEntitlements: (
15
- userId: string,
16
- ) => Promise<{ entitlements: Entitlements; cacheHit: boolean }>;
17
- emitRequest: (info: RequestInfo) => void;
18
- }
19
-
20
- /**
21
- * Build the `sw.requires(spec, options?)` factory bound to the instance's
22
- * cache + extractor + telemetry. Returned middleware is connect-style
23
- * `(req, res, next)`, duck-typed so it works with Express directly and is
24
- * trivially adaptable to Hono / Bun.serve / Next via a thin wrapper.
25
- */
26
- export const makeRequires = <TReq>(deps: RequiresFactoryDeps<TReq>) => {
27
- return (spec: EntitlementSpec, options: RequiresOptions<TReq> = {}) => {
28
- // Normalize once at registration time; validation errors surface at
29
- // app boot, not at first request.
30
- const normalized = normalizeSpec(spec);
31
- const extractor = options.userId ?? deps.defaultUserIdExtractor;
32
- const allowAnonymous = options.allowAnonymous ?? false;
33
-
34
- return async (
35
- req: TReq,
36
- res: ConnectStyleResponse,
37
- next: ConnectStyleNext,
38
- ): Promise<void> => {
39
- const start = Date.now();
40
- let userId: string | null = null;
41
- if (extractor) {
42
- const extracted = await extractor(req);
43
- if (typeof extracted === "string" && extracted.length > 0) {
44
- userId = extracted;
45
- }
46
- }
47
-
48
- if (!userId) {
49
- if (allowAnonymous) {
50
- next();
51
- return;
52
- }
53
- await rejectUnauthorized(req, res, options, {
54
- userId: null,
55
- entitlement: normalized.entitlements[0] ?? "",
56
- missing: normalized.entitlements,
57
- reason: "no_user_id",
58
- });
59
- return;
60
- }
61
-
62
- let ents: Entitlements;
63
- let cacheHit: boolean;
64
- try {
65
- const loaded = await deps.loadEntitlements(userId);
66
- ents = loaded.entitlements;
67
- cacheHit = loaded.cacheHit;
68
- } catch (err) {
69
- // Surfaced to the framework's error handler. Default Express
70
- // behavior is a 500; consumers can intercept via their own
71
- // error middleware.
72
- next(err);
73
- return;
74
- }
75
-
76
- const missing = findMissing(normalized, ents);
77
- deps.emitRequest({
78
- userId,
79
- entitlements: ents.active.map((e) => e.id),
80
- cacheHit,
81
- durationMs: Date.now() - start,
82
- });
83
-
84
- if (missing.length === 0) {
85
- next();
86
- return;
87
- }
88
-
89
- await rejectUnauthorized(req, res, options, {
90
- userId,
91
- entitlement: missing[0] ?? normalized.entitlements[0] ?? "",
92
- missing,
93
- reason: "not_entitled",
94
- });
95
- };
96
- };
97
- };
98
-
99
- const rejectUnauthorized = async <TReq>(
100
- req: TReq,
101
- res: ConnectStyleResponse,
102
- options: RequiresOptions<TReq>,
103
- ctx: {
104
- userId: string | null;
105
- entitlement: string;
106
- missing: ReadonlyArray<string>;
107
- reason: "no_user_id" | "not_entitled";
108
- },
109
- ): Promise<void> => {
110
- if (options.onUnauthorized) {
111
- await options.onUnauthorized(req, res, ctx);
112
- return;
113
- }
114
- res.status(403).json({
115
- error: "entitlement_required",
116
- entitlement: ctx.entitlement,
117
- });
118
- };
package/src/spec.test.ts DELETED
@@ -1,121 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { Entitlement, Entitlements } from "@superwall/core";
3
- import { findMissing, normalizeSpec } from "./spec.ts";
4
-
5
- const ent = (id: string, isActive = true): Entitlement => ({
6
- id,
7
- type: "SERVICE_LEVEL",
8
- isActive,
9
- productIds: [],
10
- });
11
-
12
- const ents = (...active: string[]): Entitlements => {
13
- const items = active.map((id) => ent(id, true));
14
- return { active: items, inactive: [], all: items };
15
- };
16
-
17
- describe("normalizeSpec", () => {
18
- test("string", () => {
19
- expect(normalizeSpec("pro")).toEqual({ mode: "all", entitlements: ["pro"] });
20
- });
21
-
22
- test("array → all", () => {
23
- expect(normalizeSpec(["pro", "team"])).toEqual({
24
- mode: "all",
25
- entitlements: ["pro", "team"],
26
- });
27
- });
28
-
29
- test("{ all }", () => {
30
- expect(normalizeSpec({ all: ["pro", "team"] })).toEqual({
31
- mode: "all",
32
- entitlements: ["pro", "team"],
33
- });
34
- });
35
-
36
- test("{ any }", () => {
37
- expect(normalizeSpec({ any: ["pro", "team"] })).toEqual({
38
- mode: "any",
39
- entitlements: ["pro", "team"],
40
- });
41
- });
42
-
43
- test("rejects empty string", () => {
44
- expect(() => normalizeSpec("")).toThrow(TypeError);
45
- });
46
-
47
- test("rejects empty array", () => {
48
- expect(() => normalizeSpec([])).toThrow(TypeError);
49
- });
50
-
51
- test("rejects empty all", () => {
52
- expect(() => normalizeSpec({ all: [] })).toThrow(TypeError);
53
- });
54
-
55
- test("rejects empty any", () => {
56
- expect(() => normalizeSpec({ any: [] })).toThrow(TypeError);
57
- });
58
-
59
- test("rejects malformed object", () => {
60
- expect(() =>
61
- normalizeSpec({ foo: ["pro"] } as unknown as Parameters<typeof normalizeSpec>[0]),
62
- ).toThrow(TypeError);
63
- });
64
- });
65
-
66
- describe("findMissing (mode: all)", () => {
67
- test("returns empty when all entitlements active", () => {
68
- const missing = findMissing(
69
- { mode: "all", entitlements: ["pro", "team"] },
70
- ents("pro", "team"),
71
- );
72
- expect(missing).toEqual([]);
73
- });
74
-
75
- test("returns the missing entitlement", () => {
76
- const missing = findMissing(
77
- { mode: "all", entitlements: ["pro", "team"] },
78
- ents("pro"),
79
- );
80
- expect(missing).toEqual(["team"]);
81
- });
82
-
83
- test("ignores extra active entitlements", () => {
84
- const missing = findMissing(
85
- { mode: "all", entitlements: ["pro"] },
86
- ents("pro", "extra"),
87
- );
88
- expect(missing).toEqual([]);
89
- });
90
-
91
- test("inactive entitlements are not counted", () => {
92
- const proInactive: Entitlements = {
93
- active: [],
94
- inactive: [ent("pro", false)],
95
- all: [ent("pro", false)],
96
- };
97
- const missing = findMissing(
98
- { mode: "all", entitlements: ["pro"] },
99
- proInactive,
100
- );
101
- expect(missing).toEqual(["pro"]);
102
- });
103
- });
104
-
105
- describe("findMissing (mode: any)", () => {
106
- test("returns empty when any one is active", () => {
107
- const missing = findMissing(
108
- { mode: "any", entitlements: ["pro", "team"] },
109
- ents("team"),
110
- );
111
- expect(missing).toEqual([]);
112
- });
113
-
114
- test("returns all listed when none active", () => {
115
- const missing = findMissing(
116
- { mode: "any", entitlements: ["pro", "team"] },
117
- ents("other"),
118
- );
119
- expect(missing).toEqual(["pro", "team"]);
120
- });
121
- });
package/src/spec.ts DELETED
@@ -1,63 +0,0 @@
1
- import type { Entitlements } from "@superwall/core";
2
- import type { EntitlementSpec } from "./types.ts";
3
-
4
- export interface NormalizedSpec {
5
- readonly mode: "all" | "any";
6
- readonly entitlements: ReadonlyArray<string>;
7
- }
8
-
9
- /**
10
- * Normalize any accepted spec shape into `{ mode, entitlements }`.
11
- * Throws on empty / malformed input — fail loud at config time, not
12
- * at request time.
13
- */
14
- export const normalizeSpec = (spec: EntitlementSpec): NormalizedSpec => {
15
- if (typeof spec === "string") {
16
- if (spec.length === 0) {
17
- throw new TypeError("Entitlement spec cannot be an empty string.");
18
- }
19
- return { mode: "all", entitlements: [spec] };
20
- }
21
- if (Array.isArray(spec)) {
22
- if (spec.length === 0) {
23
- throw new TypeError("Entitlement spec array cannot be empty.");
24
- }
25
- return { mode: "all", entitlements: spec };
26
- }
27
- if (typeof spec === "object" && spec !== null) {
28
- if ("all" in spec) {
29
- if (!Array.isArray(spec.all) || spec.all.length === 0) {
30
- throw new TypeError("`{ all: [...] }` must be a non-empty string array.");
31
- }
32
- return { mode: "all", entitlements: spec.all };
33
- }
34
- if ("any" in spec) {
35
- if (!Array.isArray(spec.any) || spec.any.length === 0) {
36
- throw new TypeError("`{ any: [...] }` must be a non-empty string array.");
37
- }
38
- return { mode: "any", entitlements: spec.any };
39
- }
40
- }
41
- throw new TypeError(
42
- `Unrecognized entitlement spec: ${JSON.stringify(spec)}. Expected string, string[], { all: string[] }, or { any: string[] }.`,
43
- );
44
- };
45
-
46
- /**
47
- * Returns the entitlement IDs from `spec` that are NOT active on `ents`.
48
- * If `mode === "all"`, this is the set of unmet entitlements. If
49
- * `mode === "any"`, returns empty when at least one is met, otherwise
50
- * returns all listed entitlements (none satisfied).
51
- */
52
- export const findMissing = (
53
- spec: NormalizedSpec,
54
- ents: Entitlements,
55
- ): ReadonlyArray<string> => {
56
- const activeIds = new Set(ents.active.map((e) => e.id));
57
- if (spec.mode === "all") {
58
- return spec.entitlements.filter((id) => !activeIds.has(id));
59
- }
60
- // any
61
- const anyMet = spec.entitlements.some((id) => activeIds.has(id));
62
- return anyMet ? [] : spec.entitlements;
63
- };
@@ -1,151 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { Superwall } from "./superwall.ts";
3
- import type { FetchLike } from "./types.ts";
4
-
5
- const realFetch = globalThis.fetch;
6
- const setFetch = (impl: FetchLike): void => {
7
- (globalThis as unknown as { fetch: FetchLike }).fetch = impl;
8
- };
9
-
10
- const okResponse = (
11
- entitlements: Array<{ id: string; isActive: boolean }>,
12
- ): Response =>
13
- new Response(JSON.stringify({ entitlements }), {
14
- status: 200,
15
- headers: { "content-type": "application/json" },
16
- });
17
-
18
- afterEach(() => {
19
- (globalThis as unknown as { fetch: typeof realFetch }).fetch = realFetch;
20
- });
21
-
22
- describe("Superwall() factory", () => {
23
- test("rejects missing apiKey", () => {
24
- expect(() => Superwall({ apiKey: "" })).toThrow(TypeError);
25
- });
26
-
27
- test("getEntitlements returns parsed bucket", async () => {
28
- setFetch(async () =>
29
- okResponse([
30
- { id: "pro", isActive: true },
31
- { id: "team", isActive: false },
32
- ]),
33
- );
34
- const sw = Superwall({ apiKey: "k" });
35
- const ents = await sw.getEntitlements("u1");
36
- expect(ents.active.map((e) => e.id)).toEqual(["pro"]);
37
- expect(ents.inactive.map((e) => e.id)).toEqual(["team"]);
38
- });
39
-
40
- test("userHas (string) returns true when entitlement is active", async () => {
41
- setFetch(async () => okResponse([{ id: "pro", isActive: true }]));
42
- const sw = Superwall({ apiKey: "k" });
43
- expect(await sw.userHas("u", "pro")).toBe(true);
44
- });
45
-
46
- test("userHas (string) returns false when entitlement is inactive", async () => {
47
- setFetch(async () => okResponse([{ id: "pro", isActive: false }]));
48
- const sw = Superwall({ apiKey: "k" });
49
- expect(await sw.userHas("u", "pro")).toBe(false);
50
- });
51
-
52
- test("userHas (array) requires all", async () => {
53
- setFetch(async () =>
54
- okResponse([
55
- { id: "pro", isActive: true },
56
- { id: "team", isActive: true },
57
- ]),
58
- );
59
- const sw = Superwall({ apiKey: "k" });
60
- expect(await sw.userHas("u", ["pro", "team"])).toBe(true);
61
- expect(await sw.userHas("u", ["pro", "missing"])).toBe(false);
62
- });
63
-
64
- test("userHas ({ any }) returns true when any active", async () => {
65
- setFetch(async () => okResponse([{ id: "team", isActive: true }]));
66
- const sw = Superwall({ apiKey: "k" });
67
- expect(await sw.userHas("u", { any: ["pro", "team"] })).toBe(true);
68
- });
69
-
70
- test("caches subsequent reads for the same userId", async () => {
71
- let calls = 0;
72
- setFetch(async () => {
73
- calls++;
74
- return okResponse([{ id: "pro", isActive: true }]);
75
- });
76
- const sw = Superwall({ apiKey: "k" });
77
- await sw.getEntitlements("u1");
78
- await sw.getEntitlements("u1");
79
- await sw.userHas("u1", "pro");
80
- expect(calls).toBe(1);
81
- });
82
-
83
- test("invalidate forces a refetch", async () => {
84
- let calls = 0;
85
- setFetch(async () => {
86
- calls++;
87
- return okResponse([{ id: "pro", isActive: true }]);
88
- });
89
- const sw = Superwall({ apiKey: "k" });
90
- await sw.getEntitlements("u1");
91
- await sw.invalidate("u1");
92
- await sw.getEntitlements("u1");
93
- expect(calls).toBe(2);
94
- });
95
-
96
- test("invalidateAll clears every entry", async () => {
97
- let calls = 0;
98
- setFetch(async () => {
99
- calls++;
100
- return okResponse([{ id: "pro", isActive: true }]);
101
- });
102
- const sw = Superwall({ apiKey: "k" });
103
- await sw.getEntitlements("a");
104
- await sw.getEntitlements("b");
105
- await sw.invalidateAll();
106
- await sw.getEntitlements("a");
107
- await sw.getEntitlements("b");
108
- expect(calls).toBe(4);
109
- });
110
-
111
- test("TTL expiry forces a refetch", async () => {
112
- let calls = 0;
113
- setFetch(async () => {
114
- calls++;
115
- return okResponse([{ id: "pro", isActive: true }]);
116
- });
117
- const sw = Superwall({ apiKey: "k", cache: { ttlMs: 1 } });
118
- await sw.getEntitlements("u1");
119
- await new Promise((r) => setTimeout(r, 5));
120
- await sw.getEntitlements("u1");
121
- expect(calls).toBe(2);
122
- });
123
-
124
- test("onRequest fires with cache hit info", async () => {
125
- const calls: Array<{ cacheHit: boolean; userId: string }> = [];
126
- setFetch(async () => okResponse([{ id: "pro", isActive: true }]));
127
- const sw = Superwall({
128
- apiKey: "k",
129
- onRequest: (info) =>
130
- calls.push({ cacheHit: info.cacheHit, userId: info.userId }),
131
- });
132
- await sw.getEntitlements("u1");
133
- await sw.getEntitlements("u1");
134
- expect(calls).toEqual([
135
- { cacheHit: false, userId: "u1" },
136
- { cacheHit: true, userId: "u1" },
137
- ]);
138
- });
139
-
140
- test("onRequest throwing does not break the request", async () => {
141
- setFetch(async () => okResponse([{ id: "pro", isActive: true }]));
142
- const sw = Superwall({
143
- apiKey: "k",
144
- onRequest: () => {
145
- throw new Error("boom");
146
- },
147
- });
148
- const ents = await sw.getEntitlements("u1");
149
- expect(ents.active).toHaveLength(1);
150
- });
151
- });