@ttt-productions/auth-core 1.6.10 → 1.6.11
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/auth-core",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.11",
|
|
4
4
|
"description": "Shared Firebase authentication utilities and React hooks for TTT Productions apps",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,12 +66,11 @@
|
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/react": "^19.0.0",
|
|
68
68
|
"@types/react-dom": "^19.0.0",
|
|
69
|
-
"firebase": "^
|
|
69
|
+
"firebase": "^12.14.0",
|
|
70
70
|
"firebase-admin": "^14.0.0",
|
|
71
71
|
"firebase-functions": "^7.0.0",
|
|
72
72
|
"react": "^19.2.0",
|
|
73
|
-
"react-dom": "^19.2.0"
|
|
74
|
-
"typescript": "^5.8.3"
|
|
73
|
+
"react-dom": "^19.2.0"
|
|
75
74
|
},
|
|
76
75
|
"author": "DJ (TTT Productions)",
|
|
77
76
|
"license": "MIT"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"assertAuth.test.d.ts","sourceRoot":"","sources":["../../src/server/assertAuth.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
// NOTE: there is deliberately NO `vi.mock("firebase-functions/v2/https")` here.
|
|
3
|
-
// assertAuth no longer constructs HttpsError — it throws AuthAssertionError and the
|
|
4
|
-
// consuming app maps it (see authError.ts for why). The only remaining reference to
|
|
5
|
-
// firebase-functions in assertAuth.ts is a TYPE import, which is erased at runtime.
|
|
6
|
-
import { createAssertAuth } from "./assertAuth.js";
|
|
7
|
-
import { AuthAssertionError } from "./authError.js";
|
|
8
|
-
import { ACCEPTANCE_REQUIRED_REASON, isAcceptanceRequiredError } from "../acceptance.js";
|
|
9
|
-
import { EMAIL_VERIFICATION_REQUIRED_REASON, isEmailVerificationRequiredError, } from "../email-verification.js";
|
|
10
|
-
function makeRequest(opts) {
|
|
11
|
-
const uid = opts?.uid ?? "uid-123";
|
|
12
|
-
return {
|
|
13
|
-
data: {},
|
|
14
|
-
rawRequest: {},
|
|
15
|
-
auth: {
|
|
16
|
-
uid,
|
|
17
|
-
token: { uid, email_verified: opts?.emailVerified ?? true },
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
function makeUnauthRequest() {
|
|
22
|
-
return { data: {}, rawRequest: {}, auth: undefined };
|
|
23
|
-
}
|
|
24
|
-
const activeUserDoc = {
|
|
25
|
-
uid: "uid-123",
|
|
26
|
-
displayName: "Test User",
|
|
27
|
-
status: "active",
|
|
28
|
-
};
|
|
29
|
-
// Minimal Firestore stub returning the active user doc, for tests that build
|
|
30
|
-
// their own config instead of using the shared beforeEach harness.
|
|
31
|
-
function fakeDbWithUser() {
|
|
32
|
-
const userDocRef = {
|
|
33
|
-
get: vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc }),
|
|
34
|
-
};
|
|
35
|
-
return { doc: vi.fn(() => userDocRef) };
|
|
36
|
-
}
|
|
37
|
-
// --- harness ---------------------------------------------------------------
|
|
38
|
-
let mockUserGet;
|
|
39
|
-
let mockRequireAdmin;
|
|
40
|
-
let mockDbDoc;
|
|
41
|
-
let assertAuth;
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
mockUserGet = vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc });
|
|
44
|
-
mockRequireAdmin = vi.fn().mockResolvedValue(undefined);
|
|
45
|
-
const userDocRef = { get: mockUserGet };
|
|
46
|
-
mockDbDoc = vi.fn(() => userDocRef);
|
|
47
|
-
const fakeDb = { doc: mockDbDoc };
|
|
48
|
-
const config = {
|
|
49
|
-
firestore: () => fakeDb,
|
|
50
|
-
userProfilePath: (uid) => `userProfiles/${uid}`,
|
|
51
|
-
getUserStatus: (user) => {
|
|
52
|
-
if (user.status === "banned")
|
|
53
|
-
return "banned";
|
|
54
|
-
if (user.status === "suspended")
|
|
55
|
-
return "suspended";
|
|
56
|
-
return "active";
|
|
57
|
-
},
|
|
58
|
-
requireAdmin: mockRequireAdmin,
|
|
59
|
-
};
|
|
60
|
-
assertAuth = createAssertAuth(config);
|
|
61
|
-
});
|
|
62
|
-
// --- tests -----------------------------------------------------------------
|
|
63
|
-
// REGRESSION GUARD (2026-07-20). assertAuth must throw a plain, framework-free
|
|
64
|
-
// AuthAssertionError — never an HttpsError built inside this package. firebase-functions
|
|
65
|
-
// 7.2.5 ships two distinct HttpsError classes (ESM entry vs the CJS callable runtime), so
|
|
66
|
-
// a package-built HttpsError fails the runtime's `instanceof` check and onCall converts a
|
|
67
|
-
// correct 400 into a 500 INTERNAL. Since assertAuth is the first line of every callable,
|
|
68
|
-
// that silently broke EVERY auth rejection in deployed environments. If someone
|
|
69
|
-
// reintroduces `new HttpsError(...)` here, these assertions fail.
|
|
70
|
-
describe("throws a framework-free typed error (never a package-built HttpsError)", () => {
|
|
71
|
-
it("rejects with an AuthAssertionError instance carrying the HttpsError code string", async () => {
|
|
72
|
-
const err = await assertAuth(makeUnauthRequest()).catch((e) => e);
|
|
73
|
-
expect(err).toBeInstanceOf(AuthAssertionError);
|
|
74
|
-
expect(err.name).toBe("AuthAssertionError");
|
|
75
|
-
expect(err.code).toBe("unauthenticated");
|
|
76
|
-
});
|
|
77
|
-
it("does not decorate the error with HttpsError's own shape", async () => {
|
|
78
|
-
const err = (await assertAuth(makeUnauthRequest()).catch((e) => e));
|
|
79
|
-
// `httpErrorCode`/`toJSON` are HttpsError internals — their absence proves the
|
|
80
|
-
// package is not constructing one, which is the whole point of the fix.
|
|
81
|
-
expect(err.httpErrorCode).toBeUndefined();
|
|
82
|
-
expect(err.toJSON).toBeUndefined();
|
|
83
|
-
});
|
|
84
|
-
it("uses AuthAssertionError for the email-verification rejection too", async () => {
|
|
85
|
-
const err = await assertAuth(makeRequest({ emailVerified: false }), {
|
|
86
|
-
emailVerified: true,
|
|
87
|
-
allowAnyStatus: true,
|
|
88
|
-
}).catch((e) => e);
|
|
89
|
-
expect(err).toBeInstanceOf(AuthAssertionError);
|
|
90
|
-
expect(err.code).toBe("failed-precondition");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
describe("Authentication", () => {
|
|
94
|
-
it("throws unauthenticated when request.auth is absent", async () => {
|
|
95
|
-
await expect(assertAuth(makeUnauthRequest())).rejects.toMatchObject({
|
|
96
|
-
code: "unauthenticated",
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
it("succeeds with no Firestore reads when only allowAnyStatus is set", async () => {
|
|
100
|
-
const ctx = await assertAuth(makeRequest(), { allowAnyStatus: true });
|
|
101
|
-
expect(ctx.uid).toBe("uid-123");
|
|
102
|
-
expect(ctx.userDoc).toBeUndefined();
|
|
103
|
-
expect(mockDbDoc).not.toHaveBeenCalled();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
describe("Email verification", () => {
|
|
107
|
-
it("throws failed-precondition when emailVerified required and token email_verified is false", async () => {
|
|
108
|
-
await expect(assertAuth(makeRequest({ emailVerified: false }), {
|
|
109
|
-
emailVerified: true,
|
|
110
|
-
allowAnyStatus: true,
|
|
111
|
-
})).rejects.toMatchObject({ code: "failed-precondition" });
|
|
112
|
-
});
|
|
113
|
-
it("carries the email-verification reason in details, with the message unchanged", async () => {
|
|
114
|
-
const err = await assertAuth(makeRequest({ emailVerified: false }), {
|
|
115
|
-
emailVerified: true,
|
|
116
|
-
allowAnyStatus: true,
|
|
117
|
-
}).catch((e) => e);
|
|
118
|
-
expect(err).toBeInstanceOf(AuthAssertionError);
|
|
119
|
-
expect(err).toMatchObject({
|
|
120
|
-
code: "failed-precondition",
|
|
121
|
-
message: "Email must be verified",
|
|
122
|
-
details: { reason: EMAIL_VERIFICATION_REQUIRED_REASON },
|
|
123
|
-
});
|
|
124
|
-
expect(isEmailVerificationRequiredError(err)).toBe(true);
|
|
125
|
-
expect(isAcceptanceRequiredError(err)).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
it("succeeds when emailVerified required and token email_verified is true", async () => {
|
|
128
|
-
await expect(assertAuth(makeRequest({ emailVerified: true }), {
|
|
129
|
-
emailVerified: true,
|
|
130
|
-
allowAnyStatus: true,
|
|
131
|
-
})).resolves.toMatchObject({ uid: "uid-123" });
|
|
132
|
-
});
|
|
133
|
-
it("succeeds when allowUnverified is true even if email_verified is false", async () => {
|
|
134
|
-
await expect(assertAuth(makeRequest({ emailVerified: false }), {
|
|
135
|
-
emailVerified: true,
|
|
136
|
-
allowUnverified: true,
|
|
137
|
-
allowAnyStatus: true,
|
|
138
|
-
})).resolves.toMatchObject({ uid: "uid-123" });
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
describe("Not-banned check (default behavior)", () => {
|
|
142
|
-
it("throws permission-denied when user status is banned", async () => {
|
|
143
|
-
mockUserGet.mockResolvedValue({
|
|
144
|
-
exists: true,
|
|
145
|
-
data: () => ({ ...activeUserDoc, status: "banned" }),
|
|
146
|
-
});
|
|
147
|
-
await expect(assertAuth(makeRequest())).rejects.toMatchObject({
|
|
148
|
-
code: "permission-denied",
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
it("throws permission-denied when user status is suspended", async () => {
|
|
152
|
-
mockUserGet.mockResolvedValue({
|
|
153
|
-
exists: true,
|
|
154
|
-
data: () => ({ ...activeUserDoc, status: "suspended" }),
|
|
155
|
-
});
|
|
156
|
-
await expect(assertAuth(makeRequest())).rejects.toMatchObject({
|
|
157
|
-
code: "permission-denied",
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
it("throws not-found when user doc does not exist", async () => {
|
|
161
|
-
mockUserGet.mockResolvedValue({ exists: false, data: () => null });
|
|
162
|
-
await expect(assertAuth(makeRequest())).rejects.toMatchObject({
|
|
163
|
-
code: "not-found",
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
it("succeeds when user doc has no status field", async () => {
|
|
167
|
-
mockUserGet.mockResolvedValue({
|
|
168
|
-
exists: true,
|
|
169
|
-
data: () => ({ uid: "uid-123", displayName: "Test" }),
|
|
170
|
-
});
|
|
171
|
-
const ctx = await assertAuth(makeRequest());
|
|
172
|
-
expect(ctx.uid).toBe("uid-123");
|
|
173
|
-
expect(ctx.userDoc).toBeDefined();
|
|
174
|
-
});
|
|
175
|
-
it("succeeds when user status is active", async () => {
|
|
176
|
-
const ctx = await assertAuth(makeRequest());
|
|
177
|
-
expect(ctx.uid).toBe("uid-123");
|
|
178
|
-
expect(ctx.userDoc).toMatchObject({ status: "active" });
|
|
179
|
-
});
|
|
180
|
-
it("skips user doc read entirely when allowAnyStatus is true", async () => {
|
|
181
|
-
await assertAuth(makeRequest(), { allowAnyStatus: true });
|
|
182
|
-
expect(mockUserGet).not.toHaveBeenCalled();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
describe("allowSuspended opt-in", () => {
|
|
186
|
-
it("allows a suspended user when allowSuspended is true", async () => {
|
|
187
|
-
mockUserGet.mockResolvedValue({
|
|
188
|
-
exists: true,
|
|
189
|
-
data: () => ({ ...activeUserDoc, status: "suspended" }),
|
|
190
|
-
});
|
|
191
|
-
const ctx = await assertAuth(makeRequest(), { allowSuspended: true });
|
|
192
|
-
expect(ctx.uid).toBe("uid-123");
|
|
193
|
-
expect(ctx.userDoc).toMatchObject({ status: "suspended" });
|
|
194
|
-
});
|
|
195
|
-
it("still blocks a banned user even when allowSuspended is true", async () => {
|
|
196
|
-
mockUserGet.mockResolvedValue({
|
|
197
|
-
exists: true,
|
|
198
|
-
data: () => ({ ...activeUserDoc, status: "banned" }),
|
|
199
|
-
});
|
|
200
|
-
await expect(assertAuth(makeRequest(), { allowSuspended: true })).rejects.toMatchObject({ code: "permission-denied" });
|
|
201
|
-
});
|
|
202
|
-
it("still requires the user doc to exist when allowSuspended is true", async () => {
|
|
203
|
-
mockUserGet.mockResolvedValue({ exists: false, data: () => null });
|
|
204
|
-
await expect(assertAuth(makeRequest(), { allowSuspended: true })).rejects.toMatchObject({ code: "not-found" });
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
describe("Admin check", () => {
|
|
208
|
-
it("propagates the error when requireAdmin throws", async () => {
|
|
209
|
-
mockRequireAdmin.mockRejectedValue(Object.assign(new Error("Administrator access required"), {
|
|
210
|
-
code: "permission-denied",
|
|
211
|
-
}));
|
|
212
|
-
await expect(assertAuth(makeRequest(), {
|
|
213
|
-
admin: { allowJrAdmin: false },
|
|
214
|
-
allowAnyStatus: true,
|
|
215
|
-
})).rejects.toMatchObject({ code: "permission-denied" });
|
|
216
|
-
});
|
|
217
|
-
it("succeeds and returns ctx when requireAdmin resolves", async () => {
|
|
218
|
-
mockRequireAdmin.mockResolvedValue(undefined);
|
|
219
|
-
const ctx = await assertAuth(makeRequest(), {
|
|
220
|
-
admin: { allowJrAdmin: true },
|
|
221
|
-
allowAnyStatus: true,
|
|
222
|
-
});
|
|
223
|
-
expect(ctx.uid).toBe("uid-123");
|
|
224
|
-
expect(mockRequireAdmin).toHaveBeenCalledWith("uid-123", expect.any(Object), { allowJrAdmin: true });
|
|
225
|
-
});
|
|
226
|
-
it("populates ctx.admin with the requireAdmin result when an admin check runs", async () => {
|
|
227
|
-
const adminMock = vi.fn().mockResolvedValue({ role: "superadmin" });
|
|
228
|
-
const config = {
|
|
229
|
-
firestore: () => fakeDbWithUser(),
|
|
230
|
-
userProfilePath: (uid) => `userProfiles/${uid}`,
|
|
231
|
-
getUserStatus: (user) => {
|
|
232
|
-
if (user.status === "banned")
|
|
233
|
-
return "banned";
|
|
234
|
-
if (user.status === "suspended")
|
|
235
|
-
return "suspended";
|
|
236
|
-
return "active";
|
|
237
|
-
},
|
|
238
|
-
requireAdmin: adminMock,
|
|
239
|
-
};
|
|
240
|
-
const typedAssertAuth = createAssertAuth(config);
|
|
241
|
-
const ctx = await typedAssertAuth(makeRequest(), {
|
|
242
|
-
admin: { allowJrAdmin: true },
|
|
243
|
-
});
|
|
244
|
-
expect(ctx.admin).toEqual({ role: "superadmin" });
|
|
245
|
-
});
|
|
246
|
-
it("leaves ctx.admin undefined when no admin requirement is set", async () => {
|
|
247
|
-
const ctx = await assertAuth(makeRequest());
|
|
248
|
-
expect(ctx.admin).toBeUndefined();
|
|
249
|
-
expect(mockRequireAdmin).not.toHaveBeenCalled();
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
describe("Combined checks", () => {
|
|
253
|
-
it("returns fully populated ctx when all checks pass", async () => {
|
|
254
|
-
mockUserGet.mockResolvedValue({ exists: true, data: () => activeUserDoc });
|
|
255
|
-
mockRequireAdmin.mockResolvedValue(undefined);
|
|
256
|
-
const ctx = await assertAuth(makeRequest({ emailVerified: true }), {
|
|
257
|
-
emailVerified: true,
|
|
258
|
-
admin: { allowJrAdmin: true },
|
|
259
|
-
});
|
|
260
|
-
expect(ctx.uid).toBe("uid-123");
|
|
261
|
-
expect(ctx.userDoc).toMatchObject({ status: "active" });
|
|
262
|
-
expect(mockRequireAdmin).toHaveBeenCalled();
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
// Acceptance-level gate (optional `acceptance` config). Every case below that expects a
|
|
266
|
-
// refusal passes on the pre-gate factory only if the gate is ignored — i.e. it fails there.
|
|
267
|
-
describe("Acceptance-level gate", () => {
|
|
268
|
-
const CLAIM = "acceptedLevelClaim";
|
|
269
|
-
function gatedAssertAuth(requiredLevel) {
|
|
270
|
-
const requireAdmin = vi.fn().mockResolvedValue("admin");
|
|
271
|
-
const config = {
|
|
272
|
-
firestore: () => fakeDbWithUser(),
|
|
273
|
-
userProfilePath: (uid) => `userProfiles/${uid}`,
|
|
274
|
-
getUserStatus: (user) => (user.status === "banned" ? "banned" : "active"),
|
|
275
|
-
requireAdmin: requireAdmin,
|
|
276
|
-
acceptance: { claimKey: CLAIM, requiredLevel },
|
|
277
|
-
};
|
|
278
|
-
return { assert: createAssertAuth(config), requireAdmin };
|
|
279
|
-
}
|
|
280
|
-
function requestWithClaim(claim) {
|
|
281
|
-
const req = makeRequest();
|
|
282
|
-
if (claim !== undefined)
|
|
283
|
-
req.auth.token[CLAIM] = claim;
|
|
284
|
-
return req;
|
|
285
|
-
}
|
|
286
|
-
it("refuses a caller below the required level with a structured failed-precondition", async () => {
|
|
287
|
-
const { assert } = gatedAssertAuth(() => 3);
|
|
288
|
-
const err = await assert(requestWithClaim(2)).catch((e) => e);
|
|
289
|
-
expect(err).toBeInstanceOf(AuthAssertionError);
|
|
290
|
-
expect(err).toMatchObject({
|
|
291
|
-
code: "failed-precondition",
|
|
292
|
-
details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 3, acceptedLevel: 2 },
|
|
293
|
-
});
|
|
294
|
-
expect(isAcceptanceRequiredError(err)).toBe(true);
|
|
295
|
-
expect(isEmailVerificationRequiredError(err)).toBe(false);
|
|
296
|
-
});
|
|
297
|
-
it("treats a missing claim as accepted level 0", async () => {
|
|
298
|
-
const { assert } = gatedAssertAuth(() => 1);
|
|
299
|
-
await expect(assert(requestWithClaim(undefined))).rejects.toMatchObject({
|
|
300
|
-
details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 1, acceptedLevel: 0 },
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
it.each([["a string", "5"], ["NaN", Number.NaN], ["a negative", -4], ["null", null]])("treats %s claim as accepted level 0 (fails closed)", async (_label, claim) => {
|
|
304
|
-
const { assert } = gatedAssertAuth(() => 1);
|
|
305
|
-
await expect(assert(requestWithClaim(claim))).rejects.toMatchObject({
|
|
306
|
-
details: { acceptedLevel: 0 },
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
it("passes a caller at or above the required level", async () => {
|
|
310
|
-
const { assert } = gatedAssertAuth(() => 3);
|
|
311
|
-
await expect(assert(requestWithClaim(3))).resolves.toMatchObject({ uid: "uid-123" });
|
|
312
|
-
await expect(assert(requestWithClaim(7))).resolves.toMatchObject({ uid: "uid-123" });
|
|
313
|
-
});
|
|
314
|
-
it("awaits an async required-level reader", async () => {
|
|
315
|
-
const { assert } = gatedAssertAuth(async () => 2);
|
|
316
|
-
await expect(assert(requestWithClaim(1))).rejects.toMatchObject({ code: "failed-precondition" });
|
|
317
|
-
});
|
|
318
|
-
it.each([["zero", 0], ["NaN", Number.NaN], ["a negative", -1]])("requires nothing when the required level is %s", async (_label, level) => {
|
|
319
|
-
const { assert } = gatedAssertAuth(() => level);
|
|
320
|
-
await expect(assert(requestWithClaim(undefined))).resolves.toMatchObject({ uid: "uid-123" });
|
|
321
|
-
});
|
|
322
|
-
it("lets an allowUnaccepted callable through without reading the required level", async () => {
|
|
323
|
-
const requiredLevel = vi.fn(() => 9);
|
|
324
|
-
const { assert } = gatedAssertAuth(requiredLevel);
|
|
325
|
-
await expect(assert(requestWithClaim(undefined), { allowUnaccepted: true })).resolves.toMatchObject({
|
|
326
|
-
uid: "uid-123",
|
|
327
|
-
});
|
|
328
|
-
expect(requiredLevel).not.toHaveBeenCalled();
|
|
329
|
-
});
|
|
330
|
-
it("propagates a throwing required-level reader", async () => {
|
|
331
|
-
const failure = new Error("config unavailable");
|
|
332
|
-
const { assert } = gatedAssertAuth(() => {
|
|
333
|
-
throw failure;
|
|
334
|
-
});
|
|
335
|
-
await expect(assert(requestWithClaim(5))).rejects.toBe(failure);
|
|
336
|
-
});
|
|
337
|
-
it("refuses before the admin check runs", async () => {
|
|
338
|
-
const { assert, requireAdmin } = gatedAssertAuth(() => 2);
|
|
339
|
-
await expect(assert(requestWithClaim(1), { admin: { allowJrAdmin: true }, allowAnyStatus: true })).rejects.toMatchObject({ code: "failed-precondition" });
|
|
340
|
-
expect(requireAdmin).not.toHaveBeenCalled();
|
|
341
|
-
});
|
|
342
|
-
it("still gates a callable that skips the status check (allowAnyStatus)", async () => {
|
|
343
|
-
const { assert } = gatedAssertAuth(() => 1);
|
|
344
|
-
await expect(assert(requestWithClaim(0), { allowAnyStatus: true })).rejects.toMatchObject({
|
|
345
|
-
code: "failed-precondition",
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
it("keeps authentication first — an unauthenticated caller never reaches the gate", async () => {
|
|
349
|
-
const requiredLevel = vi.fn(() => 1);
|
|
350
|
-
const { assert } = gatedAssertAuth(requiredLevel);
|
|
351
|
-
await expect(assert(makeUnauthRequest())).rejects.toMatchObject({ code: "unauthenticated" });
|
|
352
|
-
expect(requiredLevel).not.toHaveBeenCalled();
|
|
353
|
-
});
|
|
354
|
-
it("is absent without config: an app that never sets `acceptance` behaves exactly as before", async () => {
|
|
355
|
-
// The shared harness config has no `acceptance`; a token with no level claim still passes.
|
|
356
|
-
await expect(assertAuth(makeRequest(), { allowUnaccepted: false })).resolves.toMatchObject({
|
|
357
|
-
uid: "uid-123",
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
it("leaves `details` undefined on every other rejection", async () => {
|
|
361
|
-
const unauthenticated = (await assertAuth(makeUnauthRequest()).catch((e) => e));
|
|
362
|
-
expect(unauthenticated.details).toBeUndefined();
|
|
363
|
-
mockUserGet.mockResolvedValue({ exists: false, data: () => null });
|
|
364
|
-
const missingProfile = (await assertAuth(makeRequest()).catch((e) => e));
|
|
365
|
-
expect(missingProfile.code).toBe("not-found");
|
|
366
|
-
expect(missingProfile.details).toBeUndefined();
|
|
367
|
-
mockUserGet.mockResolvedValue({ exists: true, data: () => ({ ...activeUserDoc, status: "banned" }) });
|
|
368
|
-
const banned = (await assertAuth(makeRequest()).catch((e) => e));
|
|
369
|
-
expect(banned.code).toBe("permission-denied");
|
|
370
|
-
expect(banned.details).toBeUndefined();
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
//# sourceMappingURL=assertAuth.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"assertAuth.test.js","sourceRoot":"","sources":["../../src/server/assertAuth.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi, beforeEach } from \"vitest\";\n\n// NOTE: there is deliberately NO `vi.mock(\"firebase-functions/v2/https\")` here.\n// assertAuth no longer constructs HttpsError — it throws AuthAssertionError and the\n// consuming app maps it (see authError.ts for why). The only remaining reference to\n// firebase-functions in assertAuth.ts is a TYPE import, which is erased at runtime.\nimport { createAssertAuth } from \"./assertAuth.js\";\nimport { AuthAssertionError } from \"./authError.js\";\nimport { ACCEPTANCE_REQUIRED_REASON, isAcceptanceRequiredError } from \"../acceptance.js\";\nimport {\n EMAIL_VERIFICATION_REQUIRED_REASON,\n isEmailVerificationRequiredError,\n} from \"../email-verification.js\";\nimport type { AssertAuthConfig, UserStatus } from \"./types.js\";\n\ntype TestUser = {\n uid: string;\n displayName?: string;\n status?: \"active\" | \"suspended\" | \"banned\";\n};\n\n\nfunction makeRequest(opts?: { uid?: string; emailVerified?: boolean }): any {\n const uid = opts?.uid ?? \"uid-123\";\n return {\n data: {},\n rawRequest: {},\n auth: {\n uid,\n token: { uid, email_verified: opts?.emailVerified ?? true },\n },\n };\n}\n\nfunction makeUnauthRequest(): any {\n return { data: {}, rawRequest: {}, auth: undefined };\n}\n\nconst activeUserDoc: TestUser = {\n uid: \"uid-123\",\n displayName: \"Test User\",\n status: \"active\",\n};\n\n// Minimal Firestore stub returning the active user doc, for tests that build\n// their own config instead of using the shared beforeEach harness.\nfunction fakeDbWithUser(): any {\n const userDocRef = {\n get: vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc }),\n };\n return { doc: vi.fn(() => userDocRef) };\n}\n\n// --- harness ---------------------------------------------------------------\n\nlet mockUserGet: ReturnType<typeof vi.fn>;\nlet mockRequireAdmin: ReturnType<typeof vi.fn>;\nlet mockDbDoc: ReturnType<typeof vi.fn>;\nlet assertAuth: ReturnType<typeof createAssertAuth<TestUser>>;\n\nbeforeEach(() => {\n mockUserGet = vi.fn().mockResolvedValue({ exists: true, data: () => activeUserDoc });\n mockRequireAdmin = vi.fn().mockResolvedValue(undefined);\n\n const userDocRef = { get: mockUserGet };\n\n mockDbDoc = vi.fn(() => userDocRef);\n\n const fakeDb = { doc: mockDbDoc } as any;\n\n const config: AssertAuthConfig<TestUser> = {\n firestore: () => fakeDb,\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => {\n if (user.status === \"banned\") return \"banned\";\n if (user.status === \"suspended\") return \"suspended\";\n return \"active\";\n },\n requireAdmin: mockRequireAdmin as any,\n };\n\n assertAuth = createAssertAuth<TestUser>(config);\n});\n\n// --- tests -----------------------------------------------------------------\n\n// REGRESSION GUARD (2026-07-20). assertAuth must throw a plain, framework-free\n// AuthAssertionError — never an HttpsError built inside this package. firebase-functions\n// 7.2.5 ships two distinct HttpsError classes (ESM entry vs the CJS callable runtime), so\n// a package-built HttpsError fails the runtime's `instanceof` check and onCall converts a\n// correct 400 into a 500 INTERNAL. Since assertAuth is the first line of every callable,\n// that silently broke EVERY auth rejection in deployed environments. If someone\n// reintroduces `new HttpsError(...)` here, these assertions fail.\ndescribe(\"throws a framework-free typed error (never a package-built HttpsError)\", () => {\n it(\"rejects with an AuthAssertionError instance carrying the HttpsError code string\", async () => {\n const err = await assertAuth(makeUnauthRequest()).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect((err as AuthAssertionError).name).toBe(\"AuthAssertionError\");\n expect((err as AuthAssertionError).code).toBe(\"unauthenticated\");\n });\n\n it(\"does not decorate the error with HttpsError's own shape\", async () => {\n const err = (await assertAuth(makeUnauthRequest()).catch(\n (e: unknown) => e\n )) as AuthAssertionError & { httpErrorCode?: unknown; toJSON?: unknown };\n // `httpErrorCode`/`toJSON` are HttpsError internals — their absence proves the\n // package is not constructing one, which is the whole point of the fix.\n expect(err.httpErrorCode).toBeUndefined();\n expect(err.toJSON).toBeUndefined();\n });\n\n it(\"uses AuthAssertionError for the email-verification rejection too\", async () => {\n const err = await assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n }).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect((err as AuthAssertionError).code).toBe(\"failed-precondition\");\n });\n});\n\ndescribe(\"Authentication\", () => {\n it(\"throws unauthenticated when request.auth is absent\", async () => {\n await expect(assertAuth(makeUnauthRequest())).rejects.toMatchObject({\n code: \"unauthenticated\",\n });\n });\n\n it(\"succeeds with no Firestore reads when only allowAnyStatus is set\", async () => {\n const ctx = await assertAuth(makeRequest(), { allowAnyStatus: true });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toBeUndefined();\n expect(mockDbDoc).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"Email verification\", () => {\n it(\"throws failed-precondition when emailVerified required and token email_verified is false\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n })\n ).rejects.toMatchObject({ code: \"failed-precondition\" });\n });\n\n it(\"carries the email-verification reason in details, with the message unchanged\", async () => {\n const err = await assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowAnyStatus: true,\n }).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect(err).toMatchObject({\n code: \"failed-precondition\",\n message: \"Email must be verified\",\n details: { reason: EMAIL_VERIFICATION_REQUIRED_REASON },\n });\n expect(isEmailVerificationRequiredError(err)).toBe(true);\n expect(isAcceptanceRequiredError(err)).toBe(false);\n });\n\n it(\"succeeds when emailVerified required and token email_verified is true\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: true }), {\n emailVerified: true,\n allowAnyStatus: true,\n })\n ).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n\n it(\"succeeds when allowUnverified is true even if email_verified is false\", async () => {\n await expect(\n assertAuth(makeRequest({ emailVerified: false }), {\n emailVerified: true,\n allowUnverified: true,\n allowAnyStatus: true,\n })\n ).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n});\n\ndescribe(\"Not-banned check (default behavior)\", () => {\n it(\"throws permission-denied when user status is banned\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"banned\" }),\n });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"permission-denied\",\n });\n });\n\n it(\"throws permission-denied when user status is suspended\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"suspended\" }),\n });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"permission-denied\",\n });\n });\n\n it(\"throws not-found when user doc does not exist\", async () => {\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n await expect(assertAuth(makeRequest())).rejects.toMatchObject({\n code: \"not-found\",\n });\n });\n\n it(\"succeeds when user doc has no status field\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ uid: \"uid-123\", displayName: \"Test\" }),\n });\n const ctx = await assertAuth(makeRequest());\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toBeDefined();\n });\n\n it(\"succeeds when user status is active\", async () => {\n const ctx = await assertAuth(makeRequest());\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"active\" });\n });\n\n it(\"skips user doc read entirely when allowAnyStatus is true\", async () => {\n await assertAuth(makeRequest(), { allowAnyStatus: true });\n expect(mockUserGet).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"allowSuspended opt-in\", () => {\n it(\"allows a suspended user when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"suspended\" }),\n });\n const ctx = await assertAuth(makeRequest(), { allowSuspended: true });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"suspended\" });\n });\n\n it(\"still blocks a banned user even when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({\n exists: true,\n data: () => ({ ...activeUserDoc, status: \"banned\" }),\n });\n await expect(\n assertAuth(makeRequest(), { allowSuspended: true })\n ).rejects.toMatchObject({ code: \"permission-denied\" });\n });\n\n it(\"still requires the user doc to exist when allowSuspended is true\", async () => {\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n await expect(\n assertAuth(makeRequest(), { allowSuspended: true })\n ).rejects.toMatchObject({ code: \"not-found\" });\n });\n});\n\n\ndescribe(\"Admin check\", () => {\n it(\"propagates the error when requireAdmin throws\", async () => {\n mockRequireAdmin.mockRejectedValue(\n Object.assign(new Error(\"Administrator access required\"), {\n code: \"permission-denied\",\n })\n );\n await expect(\n assertAuth(makeRequest(), {\n admin: { allowJrAdmin: false },\n allowAnyStatus: true,\n })\n ).rejects.toMatchObject({ code: \"permission-denied\" });\n });\n\n it(\"succeeds and returns ctx when requireAdmin resolves\", async () => {\n mockRequireAdmin.mockResolvedValue(undefined);\n const ctx = await assertAuth(makeRequest(), {\n admin: { allowJrAdmin: true },\n allowAnyStatus: true,\n });\n expect(ctx.uid).toBe(\"uid-123\");\n expect(mockRequireAdmin).toHaveBeenCalledWith(\n \"uid-123\",\n expect.any(Object),\n { allowJrAdmin: true }\n );\n });\n\n it(\"populates ctx.admin with the requireAdmin result when an admin check runs\", async () => {\n type AdminResult = { role: string };\n const adminMock = vi.fn().mockResolvedValue({ role: \"superadmin\" });\n const config: AssertAuthConfig<TestUser, AdminResult> = {\n firestore: () => fakeDbWithUser(),\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => {\n if (user.status === \"banned\") return \"banned\";\n if (user.status === \"suspended\") return \"suspended\";\n return \"active\";\n },\n requireAdmin: adminMock as any,\n };\n const typedAssertAuth = createAssertAuth<TestUser, AdminResult>(config);\n\n const ctx = await typedAssertAuth(makeRequest(), {\n admin: { allowJrAdmin: true },\n });\n\n expect(ctx.admin).toEqual({ role: \"superadmin\" });\n });\n\n it(\"leaves ctx.admin undefined when no admin requirement is set\", async () => {\n const ctx = await assertAuth(makeRequest());\n expect(ctx.admin).toBeUndefined();\n expect(mockRequireAdmin).not.toHaveBeenCalled();\n });\n});\n\ndescribe(\"Combined checks\", () => {\n it(\"returns fully populated ctx when all checks pass\", async () => {\n mockUserGet.mockResolvedValue({ exists: true, data: () => activeUserDoc });\n mockRequireAdmin.mockResolvedValue(undefined);\n\n const ctx = await assertAuth(makeRequest({ emailVerified: true }), {\n emailVerified: true,\n admin: { allowJrAdmin: true },\n });\n\n expect(ctx.uid).toBe(\"uid-123\");\n expect(ctx.userDoc).toMatchObject({ status: \"active\" });\n expect(mockRequireAdmin).toHaveBeenCalled();\n });\n});\n\n// Acceptance-level gate (optional `acceptance` config). Every case below that expects a\n// refusal passes on the pre-gate factory only if the gate is ignored — i.e. it fails there.\ndescribe(\"Acceptance-level gate\", () => {\n const CLAIM = \"acceptedLevelClaim\";\n\n function gatedAssertAuth(requiredLevel: () => number | Promise<number>) {\n const requireAdmin = vi.fn().mockResolvedValue(\"admin\");\n const config: AssertAuthConfig<TestUser, string> = {\n firestore: () => fakeDbWithUser(),\n userProfilePath: (uid) => `userProfiles/${uid}`,\n getUserStatus: (user): UserStatus => (user.status === \"banned\" ? \"banned\" : \"active\"),\n requireAdmin: requireAdmin as any,\n acceptance: { claimKey: CLAIM, requiredLevel },\n };\n return { assert: createAssertAuth<TestUser, string>(config), requireAdmin };\n }\n\n function requestWithClaim(claim: unknown): any {\n const req = makeRequest();\n if (claim !== undefined) req.auth.token[CLAIM] = claim;\n return req;\n }\n\n it(\"refuses a caller below the required level with a structured failed-precondition\", async () => {\n const { assert } = gatedAssertAuth(() => 3);\n const err = await assert(requestWithClaim(2)).catch((e: unknown) => e);\n expect(err).toBeInstanceOf(AuthAssertionError);\n expect(err).toMatchObject({\n code: \"failed-precondition\",\n details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 3, acceptedLevel: 2 },\n });\n expect(isAcceptanceRequiredError(err)).toBe(true);\n expect(isEmailVerificationRequiredError(err)).toBe(false);\n });\n\n it(\"treats a missing claim as accepted level 0\", async () => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(undefined))).rejects.toMatchObject({\n details: { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel: 1, acceptedLevel: 0 },\n });\n });\n\n it.each([[\"a string\", \"5\"], [\"NaN\", Number.NaN], [\"a negative\", -4], [\"null\", null]])(\n \"treats %s claim as accepted level 0 (fails closed)\",\n async (_label, claim) => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(claim))).rejects.toMatchObject({\n details: { acceptedLevel: 0 },\n });\n }\n );\n\n it(\"passes a caller at or above the required level\", async () => {\n const { assert } = gatedAssertAuth(() => 3);\n await expect(assert(requestWithClaim(3))).resolves.toMatchObject({ uid: \"uid-123\" });\n await expect(assert(requestWithClaim(7))).resolves.toMatchObject({ uid: \"uid-123\" });\n });\n\n it(\"awaits an async required-level reader\", async () => {\n const { assert } = gatedAssertAuth(async () => 2);\n await expect(assert(requestWithClaim(1))).rejects.toMatchObject({ code: \"failed-precondition\" });\n });\n\n it.each([[\"zero\", 0], [\"NaN\", Number.NaN], [\"a negative\", -1]])(\n \"requires nothing when the required level is %s\",\n async (_label, level) => {\n const { assert } = gatedAssertAuth(() => level);\n await expect(assert(requestWithClaim(undefined))).resolves.toMatchObject({ uid: \"uid-123\" });\n }\n );\n\n it(\"lets an allowUnaccepted callable through without reading the required level\", async () => {\n const requiredLevel = vi.fn(() => 9);\n const { assert } = gatedAssertAuth(requiredLevel);\n await expect(assert(requestWithClaim(undefined), { allowUnaccepted: true })).resolves.toMatchObject({\n uid: \"uid-123\",\n });\n expect(requiredLevel).not.toHaveBeenCalled();\n });\n\n it(\"propagates a throwing required-level reader\", async () => {\n const failure = new Error(\"config unavailable\");\n const { assert } = gatedAssertAuth(() => {\n throw failure;\n });\n await expect(assert(requestWithClaim(5))).rejects.toBe(failure);\n });\n\n it(\"refuses before the admin check runs\", async () => {\n const { assert, requireAdmin } = gatedAssertAuth(() => 2);\n await expect(\n assert(requestWithClaim(1), { admin: { allowJrAdmin: true }, allowAnyStatus: true })\n ).rejects.toMatchObject({ code: \"failed-precondition\" });\n expect(requireAdmin).not.toHaveBeenCalled();\n });\n\n it(\"still gates a callable that skips the status check (allowAnyStatus)\", async () => {\n const { assert } = gatedAssertAuth(() => 1);\n await expect(assert(requestWithClaim(0), { allowAnyStatus: true })).rejects.toMatchObject({\n code: \"failed-precondition\",\n });\n });\n\n it(\"keeps authentication first — an unauthenticated caller never reaches the gate\", async () => {\n const requiredLevel = vi.fn(() => 1);\n const { assert } = gatedAssertAuth(requiredLevel);\n await expect(assert(makeUnauthRequest())).rejects.toMatchObject({ code: \"unauthenticated\" });\n expect(requiredLevel).not.toHaveBeenCalled();\n });\n\n it(\"is absent without config: an app that never sets `acceptance` behaves exactly as before\", async () => {\n // The shared harness config has no `acceptance`; a token with no level claim still passes.\n await expect(assertAuth(makeRequest(), { allowUnaccepted: false })).resolves.toMatchObject({\n uid: \"uid-123\",\n });\n });\n\n it(\"leaves `details` undefined on every other rejection\", async () => {\n const unauthenticated = (await assertAuth(makeUnauthRequest()).catch((e: unknown) => e)) as AuthAssertionError;\n expect(unauthenticated.details).toBeUndefined();\n\n mockUserGet.mockResolvedValue({ exists: false, data: () => null });\n const missingProfile = (await assertAuth(makeRequest()).catch((e: unknown) => e)) as AuthAssertionError;\n expect(missingProfile.code).toBe(\"not-found\");\n expect(missingProfile.details).toBeUndefined();\n\n mockUserGet.mockResolvedValue({ exists: true, data: () => ({ ...activeUserDoc, status: \"banned\" }) });\n const banned = (await assertAuth(makeRequest()).catch((e: unknown) => e)) as AuthAssertionError;\n expect(banned.code).toBe(\"permission-denied\");\n expect(banned.details).toBeUndefined();\n });\n});\n"]}
|