@ttt-productions/auth-core 1.6.3 → 1.6.4
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/dist/server/assertAuth.js +6 -6
- package/dist/server/assertAuth.js.map +1 -1
- package/dist/server/assertAuth.test.js +35 -11
- package/dist/server/assertAuth.test.js.map +1 -1
- package/dist/server/authError.d.ts +42 -0
- package/dist/server/authError.d.ts.map +1 -0
- package/dist/server/authError.js +9 -0
- package/dist/server/authError.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
// 3. User doc Firestore read (when status check is needed)
|
|
9
9
|
// 4. Status check (suspended / banned)
|
|
10
10
|
// 5. Admin check (delegated to config.requireAdmin)
|
|
11
|
-
import {
|
|
11
|
+
import { AuthAssertionError } from "./authError.js";
|
|
12
12
|
export function createAssertAuth(config) {
|
|
13
13
|
return async function assertAuth(request, requirements = {}) {
|
|
14
14
|
// 1. Authentication check (always first)
|
|
15
15
|
if (!request.auth) {
|
|
16
|
-
throw new
|
|
16
|
+
throw new AuthAssertionError("unauthenticated", "You must be authenticated");
|
|
17
17
|
}
|
|
18
18
|
const uid = request.auth.uid;
|
|
19
19
|
const token = request.auth.token;
|
|
@@ -21,7 +21,7 @@ export function createAssertAuth(config) {
|
|
|
21
21
|
if (requirements.emailVerified === true &&
|
|
22
22
|
requirements.allowUnverified !== true) {
|
|
23
23
|
if (token.email_verified !== true) {
|
|
24
|
-
throw new
|
|
24
|
+
throw new AuthAssertionError("failed-precondition", "Email must be verified");
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
// 3. User doc Firestore read (skipped when allowAnyStatus is set)
|
|
@@ -33,15 +33,15 @@ export function createAssertAuth(config) {
|
|
|
33
33
|
// 4. Status check (default: block suspended + banned; skipped if allowAnyStatus)
|
|
34
34
|
if (requirements.allowAnyStatus !== true) {
|
|
35
35
|
if (!userSnap || !userSnap.exists) {
|
|
36
|
-
throw new
|
|
36
|
+
throw new AuthAssertionError("not-found", "User profile not found");
|
|
37
37
|
}
|
|
38
38
|
const userData = userSnap.data();
|
|
39
39
|
const status = config.getUserStatus(userData);
|
|
40
40
|
if (status === "banned") {
|
|
41
|
-
throw new
|
|
41
|
+
throw new AuthAssertionError("permission-denied", "Account is not in good standing");
|
|
42
42
|
}
|
|
43
43
|
if (status === "suspended" && requirements.allowSuspended !== true) {
|
|
44
|
-
throw new
|
|
44
|
+
throw new AuthAssertionError("permission-denied", "Account is not in good standing");
|
|
45
45
|
}
|
|
46
46
|
userDoc = userData;
|
|
47
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertAuth.js","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,gDAAgD;AAChD,EAAE;AACF,wEAAwE;AACxE,wEAAwE;AACxE,sBAAsB;AACtB,0BAA0B;AAC1B,6DAA6D;AAC7D,yCAAyC;AACzC,sDAAsD;
|
|
1
|
+
{"version":3,"file":"assertAuth.js","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,gDAAgD;AAChD,EAAE;AACF,wEAAwE;AACxE,wEAAwE;AACxE,sBAAsB;AACtB,0BAA0B;AAC1B,6DAA6D;AAC7D,yCAAyC;AACzC,sDAAsD;AAGtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAQpD,MAAM,UAAU,gBAAgB,CAC9B,MAAuC;IAEvC,OAAO,KAAK,UAAU,UAAU,CAC9B,OAAwB,EACxB,eAAiC,EAAE;QAEnC,yCAAyC;QACzC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClB,MAAM,IAAI,kBAAkB,CAAC,iBAAiB,EAAE,2BAA2B,CAAC,CAAC;QAC/E,CAAC;QAED,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAEjC,8BAA8B;QAC9B,IACE,YAAY,CAAC,aAAa,KAAK,IAAI;YACnC,YAAY,CAAC,eAAe,KAAK,IAAI,EACrC,CAAC;YACD,IAAI,KAAK,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;gBAClC,MAAM,IAAI,kBAAkB,CAAC,qBAAqB,EAAE,wBAAwB,CAAC,CAAC;YAChF,CAAC;QACH,CAAC;QAED,kEAAkE;QAClE,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QAC9B,MAAM,QAAQ,GACZ,YAAY,CAAC,cAAc,KAAK,IAAI;YAClC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE;YACjD,CAAC,CAAC,IAAI,CAAC;QAEX,IAAI,OAA0B,CAAC;QAE/B,iFAAiF;QACjF,IAAI,YAAY,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;YACzC,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAClC,MAAM,IAAI,kBAAkB,CAAC,WAAW,EAAE,wBAAwB,CAAC,CAAC;YACtE,CAAC;YACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,EAAW,CAAC;YAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC9C,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACxB,MAAM,IAAI,kBAAkB,CAC1B,mBAAmB,EACnB,iCAAiC,CAClC,CAAC;YACJ,CAAC;YACD,IAAI,MAAM,KAAK,WAAW,IAAI,YAAY,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;gBACnE,MAAM,IAAI,kBAAkB,CAC1B,mBAAmB,EACnB,iCAAiC,CAClC,CAAC;YACJ,CAAC;YACD,OAAO,GAAG,QAAQ,CAAC;QACrB,CAAC;QAED,oDAAoD;QACpD,IAAI,WAA+B,CAAC;QACpC,IAAI,YAAY,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACrC,WAAW,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC1E,CAAC;QAED,oDAAoD;QACpD,MAAM,GAAG,GAA+B,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;QACvD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC;QACxB,CAAC;QACD,IAAI,YAAY,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACrC,GAAG,CAAC,KAAK,GAAG,WAAqB,CAAC;QACpC,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Factory that produces an assertAuth function bound to a consuming app's\n// Firestore paths, user shape, and admin check.\n//\n// Every Cloud Functions callable in the consuming app should invoke the\n// returned assertAuth as its first executable line. The check order is:\n// 1. Authentication\n// 2. Email verification\n// 3. User doc Firestore read (when status check is needed)\n// 4. Status check (suspended / banned)\n// 5. Admin check (delegated to config.requireAdmin)\n\nimport type { CallableRequest } from \"firebase-functions/v2/https\";\nimport { AuthAssertionError } from \"./authError.js\";\nimport type {\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n} from \"./types.js\";\n\nexport function createAssertAuth<TUser, TAdmin = void>(\n config: AssertAuthConfig<TUser, TAdmin>\n): AssertAuthFn<TUser, TAdmin> {\n return async function assertAuth(\n request: CallableRequest,\n requirements: AuthRequirements = {}\n ): Promise<AuthContext<TUser, TAdmin>> {\n // 1. Authentication check (always first)\n if (!request.auth) {\n throw new AuthAssertionError(\"unauthenticated\", \"You must be authenticated\");\n }\n\n const uid = request.auth.uid;\n const token = request.auth.token;\n\n // 2. Email verification check\n if (\n requirements.emailVerified === true &&\n requirements.allowUnverified !== true\n ) {\n if (token.email_verified !== true) {\n throw new AuthAssertionError(\"failed-precondition\", \"Email must be verified\");\n }\n }\n\n // 3. User doc Firestore read (skipped when allowAnyStatus is set)\n const db = config.firestore();\n const userSnap =\n requirements.allowAnyStatus !== true\n ? await db.doc(config.userProfilePath(uid)).get()\n : null;\n\n let userDoc: TUser | undefined;\n\n // 4. Status check (default: block suspended + banned; skipped if allowAnyStatus)\n if (requirements.allowAnyStatus !== true) {\n if (!userSnap || !userSnap.exists) {\n throw new AuthAssertionError(\"not-found\", \"User profile not found\");\n }\n const userData = userSnap.data() as TUser;\n const status = config.getUserStatus(userData);\n if (status === \"banned\") {\n throw new AuthAssertionError(\n \"permission-denied\",\n \"Account is not in good standing\"\n );\n }\n if (status === \"suspended\" && requirements.allowSuspended !== true) {\n throw new AuthAssertionError(\n \"permission-denied\",\n \"Account is not in good standing\"\n );\n }\n userDoc = userData;\n }\n\n // 5. Admin check (delegates to config.requireAdmin)\n let adminResult: TAdmin | undefined;\n if (requirements.admin !== undefined) {\n adminResult = await config.requireAdmin(uid, token, requirements.admin);\n }\n\n // 6. Return context with whatever docs were fetched\n const ctx: AuthContext<TUser, TAdmin> = { uid, token };\n if (userDoc !== undefined) {\n ctx.userDoc = userDoc;\n }\n if (requirements.admin !== undefined) {\n ctx.admin = adminResult as TAdmin;\n }\n return ctx;\n };\n}\n"]}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
vi.mock("firebase-functions/v2/https"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
super(message);
|
|
7
|
-
this.code = code;
|
|
8
|
-
this.name = "HttpsError";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return { HttpsError };
|
|
12
|
-
});
|
|
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.
|
|
13
6
|
import { createAssertAuth } from "./assertAuth.js";
|
|
7
|
+
import { AuthAssertionError } from "./authError.js";
|
|
14
8
|
function makeRequest(opts) {
|
|
15
9
|
const uid = opts?.uid ?? "uid-123";
|
|
16
10
|
return {
|
|
@@ -64,6 +58,36 @@ beforeEach(() => {
|
|
|
64
58
|
assertAuth = createAssertAuth(config);
|
|
65
59
|
});
|
|
66
60
|
// --- tests -----------------------------------------------------------------
|
|
61
|
+
// REGRESSION GUARD (2026-07-20). assertAuth must throw a plain, framework-free
|
|
62
|
+
// AuthAssertionError — never an HttpsError built inside this package. firebase-functions
|
|
63
|
+
// 7.2.5 ships two distinct HttpsError classes (ESM entry vs the CJS callable runtime), so
|
|
64
|
+
// a package-built HttpsError fails the runtime's `instanceof` check and onCall converts a
|
|
65
|
+
// correct 400 into a 500 INTERNAL. Since assertAuth is the first line of every callable,
|
|
66
|
+
// that silently broke EVERY auth rejection in deployed environments. If someone
|
|
67
|
+
// reintroduces `new HttpsError(...)` here, these assertions fail.
|
|
68
|
+
describe("throws a framework-free typed error (never a package-built HttpsError)", () => {
|
|
69
|
+
it("rejects with an AuthAssertionError instance carrying the HttpsError code string", async () => {
|
|
70
|
+
const err = await assertAuth(makeUnauthRequest()).catch((e) => e);
|
|
71
|
+
expect(err).toBeInstanceOf(AuthAssertionError);
|
|
72
|
+
expect(err.name).toBe("AuthAssertionError");
|
|
73
|
+
expect(err.code).toBe("unauthenticated");
|
|
74
|
+
});
|
|
75
|
+
it("does not decorate the error with HttpsError's own shape", async () => {
|
|
76
|
+
const err = (await assertAuth(makeUnauthRequest()).catch((e) => e));
|
|
77
|
+
// `httpErrorCode`/`toJSON` are HttpsError internals — their absence proves the
|
|
78
|
+
// package is not constructing one, which is the whole point of the fix.
|
|
79
|
+
expect(err.httpErrorCode).toBeUndefined();
|
|
80
|
+
expect(err.toJSON).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
it("uses AuthAssertionError for the email-verification rejection too", async () => {
|
|
83
|
+
const err = await assertAuth(makeRequest({ emailVerified: false }), {
|
|
84
|
+
emailVerified: true,
|
|
85
|
+
allowAnyStatus: true,
|
|
86
|
+
}).catch((e) => e);
|
|
87
|
+
expect(err).toBeInstanceOf(AuthAssertionError);
|
|
88
|
+
expect(err.code).toBe("failed-precondition");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
67
91
|
describe("Authentication", () => {
|
|
68
92
|
it("throws unauthenticated when request.auth is absent", async () => {
|
|
69
93
|
await expect(assertAuth(makeUnauthRequest())).rejects.toMatchObject({
|
|
@@ -1 +1 @@
|
|
|
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\nvi.mock(\"firebase-functions/v2/https\", () => {\n class HttpsError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.code = code;\n this.name = \"HttpsError\";\n }\n }\n return { HttpsError };\n});\n\nimport { createAssertAuth } from \"./assertAuth.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\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(\"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"]}
|
|
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 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(\"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"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure channel for `createAssertAuth`.
|
|
3
|
+
*
|
|
4
|
+
* Every EXPECTED auth outcome (not signed in, email unverified, missing profile,
|
|
5
|
+
* account not in good standing) throws an `AuthAssertionError` whose `code` is
|
|
6
|
+
* deliberately the matching `HttpsError` code string, so a consuming Cloud
|
|
7
|
+
* Function maps it 1:1 at the app boundary:
|
|
8
|
+
*
|
|
9
|
+
* catch (err) {
|
|
10
|
+
* if (err instanceof AuthAssertionError) throw new HttpsError(err.code, err.message);
|
|
11
|
+
* throw err;
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* WHY THE PACKAGE MUST NOT BUILD THE HttpsError ITSELF (2026-07-20, live on hosted
|
|
15
|
+
* dev): firebase-functions 7.2.5 ships TWO distinct `HttpsError` classes — the one
|
|
16
|
+
* exported from the ESM entry `firebase-functions/v2/https`, and the one the CJS
|
|
17
|
+
* callable runtime closes over and `instanceof`-checks at
|
|
18
|
+
* `lib/common/providers/https.js`. An `HttpsError` constructed by this package's
|
|
19
|
+
* ESM `dist` fails that identity check, so `onCall` logged "Unhandled error" and
|
|
20
|
+
* replaced a correct 400 with `500 INTERNAL`. Measured:
|
|
21
|
+
*
|
|
22
|
+
* ESM class === CJS class ? false
|
|
23
|
+
* esm-built error instanceof CJS class ? false
|
|
24
|
+
* httpErrorCode.status: 400 (the error itself was correct)
|
|
25
|
+
*
|
|
26
|
+
* Because `assertAuth` is the first line of every callable, that turned EVERY auth
|
|
27
|
+
* rejection — unauthenticated, unverified email, suspended, banned — into a generic
|
|
28
|
+
* 500: users saw "An Error Occurred" instead of the verify-email dialog, and Sentry
|
|
29
|
+
* filled with 500s that were really expected answers. Constructing the HttpsError in
|
|
30
|
+
* APP module space fixes the identity permanently and cannot regress when
|
|
31
|
+
* firebase-functions repackages.
|
|
32
|
+
*
|
|
33
|
+
* This also brings auth-core in line with architectural-preferences Rule 17 (generic
|
|
34
|
+
* packages stay framework-agnostic; the TTT-side adapter binds the framework) and
|
|
35
|
+
* mirrors `report-core`'s `ReportCoreTaskError`, which already works this way.
|
|
36
|
+
*/
|
|
37
|
+
export type AuthAssertionErrorCode = "unauthenticated" | "failed-precondition" | "not-found" | "permission-denied";
|
|
38
|
+
export declare class AuthAssertionError extends Error {
|
|
39
|
+
readonly code: AuthAssertionErrorCode;
|
|
40
|
+
constructor(code: AuthAssertionErrorCode, message: string);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=authError.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authError.d.ts","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,qBAAqB,GACrB,WAAW,GACX,mBAAmB,CAAC;AAExB,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAK1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authError.js","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AA0CA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,CAAyB;IAEtC,YAAY,IAA4B,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF","sourcesContent":["/**\n * Typed failure channel for `createAssertAuth`.\n *\n * Every EXPECTED auth outcome (not signed in, email unverified, missing profile,\n * account not in good standing) throws an `AuthAssertionError` whose `code` is\n * deliberately the matching `HttpsError` code string, so a consuming Cloud\n * Function maps it 1:1 at the app boundary:\n *\n * catch (err) {\n * if (err instanceof AuthAssertionError) throw new HttpsError(err.code, err.message);\n * throw err;\n * }\n *\n * WHY THE PACKAGE MUST NOT BUILD THE HttpsError ITSELF (2026-07-20, live on hosted\n * dev): firebase-functions 7.2.5 ships TWO distinct `HttpsError` classes — the one\n * exported from the ESM entry `firebase-functions/v2/https`, and the one the CJS\n * callable runtime closes over and `instanceof`-checks at\n * `lib/common/providers/https.js`. An `HttpsError` constructed by this package's\n * ESM `dist` fails that identity check, so `onCall` logged \"Unhandled error\" and\n * replaced a correct 400 with `500 INTERNAL`. Measured:\n *\n * ESM class === CJS class ? false\n * esm-built error instanceof CJS class ? false\n * httpErrorCode.status: 400 (the error itself was correct)\n *\n * Because `assertAuth` is the first line of every callable, that turned EVERY auth\n * rejection — unauthenticated, unverified email, suspended, banned — into a generic\n * 500: users saw \"An Error Occurred\" instead of the verify-email dialog, and Sentry\n * filled with 500s that were really expected answers. Constructing the HttpsError in\n * APP module space fixes the identity permanently and cannot regress when\n * firebase-functions repackages.\n *\n * This also brings auth-core in line with architectural-preferences Rule 17 (generic\n * packages stay framework-agnostic; the TTT-side adapter binds the framework) and\n * mirrors `report-core`'s `ReportCoreTaskError`, which already works this way.\n */\nexport type AuthAssertionErrorCode =\n | \"unauthenticated\"\n | \"failed-precondition\"\n | \"not-found\"\n | \"permission-denied\";\n\nexport class AuthAssertionError extends Error {\n readonly code: AuthAssertionErrorCode;\n\n constructor(code: AuthAssertionErrorCode, message: string) {\n super(message);\n this.name = \"AuthAssertionError\";\n this.code = code;\n }\n}\n"]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createAssertAuth } from "./assertAuth.js";
|
|
2
|
+
export { AuthAssertionError } from "./authError.js";
|
|
3
|
+
export type { AuthAssertionErrorCode } from "./authError.js";
|
|
2
4
|
export type { AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements, AdminCheckOptions, UserStatus, } from "./types.js";
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/server/index.js
CHANGED
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["// Public surface of @ttt-productions/auth-core/server.\n\nexport { createAssertAuth } from \"./assertAuth.js\";\nexport type {\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n AdminCheckOptions,\n UserStatus,\n} from \"./types.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["// Public surface of @ttt-productions/auth-core/server.\n\nexport { createAssertAuth } from \"./assertAuth.js\";\nexport { AuthAssertionError } from \"./authError.js\";\nexport type { AuthAssertionErrorCode } from \"./authError.js\";\nexport type {\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n AdminCheckOptions,\n UserStatus,\n} from \"./types.js\";\n"]}
|