@nonext/husk 0.2.3
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/CHANGELOG.md +148 -0
- package/LICENSE +166 -0
- package/README.md +140 -0
- package/api/public-api.md +1070 -0
- package/dist/adapter-C_0mhENy.d.ts +320 -0
- package/dist/admin/admin.css +5 -0
- package/dist/admin/index.d.ts +265 -0
- package/dist/admin/index.js +393 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/tokens.css +597 -0
- package/dist/admin/ui.d.ts +3013 -0
- package/dist/admin/ui.js +9783 -0
- package/dist/admin/ui.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.js +9 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth-B2gBtHl5.d.ts +28 -0
- package/dist/can-Chy7zmx7.d.ts +69 -0
- package/dist/chunk-3I6337WJ.js +83 -0
- package/dist/chunk-3I6337WJ.js.map +1 -0
- package/dist/chunk-4Z6P3XCE.js +62 -0
- package/dist/chunk-4Z6P3XCE.js.map +1 -0
- package/dist/chunk-6E3ZS5Z5.js +1007 -0
- package/dist/chunk-6E3ZS5Z5.js.map +1 -0
- package/dist/chunk-77JDRXXU.js +3 -0
- package/dist/chunk-77JDRXXU.js.map +1 -0
- package/dist/chunk-FHYWJCG6.js +171 -0
- package/dist/chunk-FHYWJCG6.js.map +1 -0
- package/dist/chunk-JAE7HHMM.js +506 -0
- package/dist/chunk-JAE7HHMM.js.map +1 -0
- package/dist/chunk-NPDBQBLW.js +68 -0
- package/dist/chunk-NPDBQBLW.js.map +1 -0
- package/dist/chunk-ORMYARXD.js +6 -0
- package/dist/chunk-ORMYARXD.js.map +1 -0
- package/dist/chunk-R4O7APIX.js +51 -0
- package/dist/chunk-R4O7APIX.js.map +1 -0
- package/dist/chunk-ROLKRJWI.js +187 -0
- package/dist/chunk-ROLKRJWI.js.map +1 -0
- package/dist/chunk-SOTQ3WP5.js +31 -0
- package/dist/chunk-SOTQ3WP5.js.map +1 -0
- package/dist/chunk-TCFFHTY3.js +97 -0
- package/dist/chunk-TCFFHTY3.js.map +1 -0
- package/dist/chunk-TS7Y7GEZ.js +7 -0
- package/dist/chunk-TS7Y7GEZ.js.map +1 -0
- package/dist/chunk-VLGR5PZX.js +3 -0
- package/dist/chunk-VLGR5PZX.js.map +1 -0
- package/dist/chunk-ZW2PUSVS.js +40 -0
- package/dist/chunk-ZW2PUSVS.js.map +1 -0
- package/dist/chunk-ZYNBMYJ6.js +392 -0
- package/dist/chunk-ZYNBMYJ6.js.map +1 -0
- package/dist/cli/nonext-husk.js +3360 -0
- package/dist/cli/nonext-husk.js.map +1 -0
- package/dist/cli/templates/firestore.rules +393 -0
- package/dist/cli/templates/storage.rules +165 -0
- package/dist/client/index.d.ts +27 -0
- package/dist/client/index.js +15 -0
- package/dist/client/index.js.map +1 -0
- package/dist/cms-9fSA3uQV.d.ts +158 -0
- package/dist/core/index.d.ts +1267 -0
- package/dist/core/index.js +12 -0
- package/dist/core/index.js.map +1 -0
- package/dist/entry-D3kiE8T1.d.ts +163 -0
- package/dist/field-Diyf5siF.d.ts +158 -0
- package/dist/field-type-D25Vojb0.d.ts +157 -0
- package/dist/firebase/index.d.ts +251 -0
- package/dist/firebase/index.js +991 -0
- package/dist/firebase/index.js.map +1 -0
- package/dist/index-5ylaMBUH.d.ts +155 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/index.d.ts +101 -0
- package/dist/permissions/index.js +7 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/registry-CUC25qay.d.ts +59 -0
- package/dist/richtext/index.d.ts +85 -0
- package/dist/richtext/index.js +127 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/schema/index.d.ts +50 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +25 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/write-limits-Djmm6L3R.d.ts +215 -0
- package/package.json +136 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/firebase/converters.ts","../src/firebase/paths.ts","../src/firebase/users.ts","../src/firebase/auth.ts","../src/firebase/claims.ts"],"names":["setDoc","doc","serverTimestamp","getDoc","firebaseSignOut","updateFirebaseProfile"],"mappings":"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","file":"chunk-ZYNBMYJ6.js","sourcesContent":["import { Timestamp } from \"firebase/firestore\"\n\nimport type { FieldTypeRegistry, NestedConverter } from \"../schema/field-type.js\"\nimport type { AnyContentTypeShape } from \"../types/entry.js\"\nimport type { Entry, EntryMeta } from \"../types/entry.js\"\nimport type { FieldDefinition, FieldMap } from \"../types/field.js\"\n\n/**\n * Translation between the read shape the rest of the CMS uses and the shape Firestore\n * stores. This is the only place that knows a `Timestamp` exists.\n *\n * When a field type registry is supplied, its specs own their own round trip (layer 2 of\n * the field type contract) and are used in preference to the defaults here. Every field type\n * in the product now has a spec, so the defaults below are reached only when no registry is\n * passed: `createAdapter`'s `fieldTypes` is optional, and these functions are exported from\n * `@nonext/husk/firebase`. They are the reason an entry keeps its data rather than throwing\n * when the host forgot to register anything.\n */\n\nconst META_KEYS = [\n \"id\",\n \"status\",\n \"createdAt\",\n \"createdBy\",\n \"updatedAt\",\n \"updatedBy\",\n \"publishedAt\",\n \"schemaVersion\",\n] as const\n\nexport function toIso(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (value instanceof Timestamp) return value.toDate().toISOString()\n if (value instanceof Date) return value.toISOString()\n if (typeof value === \"string\") return value\n return null\n}\n\nexport function toTimestamp(value: unknown): Timestamp | null {\n if (value === null || value === undefined) return null\n if (value instanceof Timestamp) return value\n if (value instanceof Date) return Timestamp.fromDate(value)\n if (typeof value === \"string\") {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : Timestamp.fromDate(parsed)\n }\n return null\n}\n\nfunction defaultToStored(value: unknown, field: FieldDefinition): unknown {\n if (value === null || value === undefined) return null\n\n switch (field.type) {\n case \"repeater\": {\n if (!Array.isArray(value)) return null\n return value.map((row) =>\n typeof row === \"object\" && row !== null && !Array.isArray(row)\n ? valuesToStored(field.fields, row as Record<string, unknown>, undefined)\n : null,\n )\n }\n // A date is stored as the string the editor typed, not as a Timestamp. It is a calendar\n // value chosen by a human, not an instant, and converting it would move it across a\n // time-zone boundary on the way back out.\n default:\n return value\n }\n}\n\nfunction defaultFromStored(raw: unknown, field: FieldDefinition): unknown {\n if (raw === null || raw === undefined) return null\n\n switch (field.type) {\n case \"repeater\": {\n if (!Array.isArray(raw)) return null\n return raw.map((row) =>\n typeof row === \"object\" && row !== null && !Array.isArray(row)\n ? valuesFromStored(field.fields, row as Record<string, unknown>, undefined)\n : null,\n )\n }\n case \"date\":\n // Tolerates a Timestamp written by an older version or by hand in the console.\n return raw instanceof Timestamp ? toIso(raw) : raw\n default:\n return raw\n }\n}\n\n/**\n * The recursion a nesting field type needs, bound to this registry.\n *\n * A `repeater` spec converts its rows by calling back into the same two functions, so a\n * `richtext` or a `date` inside a repeater row round trips through its own spec rather\n * than through the defaults above. The spec cannot import the registry itself without\n * closing a cycle, so the side that already holds it hands it over.\n */\nfunction nestedConverter(fieldTypes: FieldTypeRegistry | undefined): NestedConverter {\n return {\n toStored: (fields, values) => valuesToStored(fields, values, fieldTypes),\n fromStored: (fields, data) => valuesFromStored(fields, data, fieldTypes),\n }\n}\n\nexport function valuesToStored(\n fields: FieldMap,\n values: Record<string, unknown>,\n fieldTypes: FieldTypeRegistry | undefined,\n): Record<string, unknown> {\n const stored: Record<string, unknown> = {}\n const nested = nestedConverter(fieldTypes)\n\n for (const [key, field] of Object.entries(fields)) {\n const value = values[key] ?? null\n const spec = fieldTypes?.get(field.type)\n stored[key] = spec\n ? spec.toStored(value, field, nested)\n : defaultToStored(value, field)\n }\n\n // Values belonging to fields the schema no longer declares are carried through untouched.\n // M8 removes a field by marking it removed and keeping its data until an explicit purge,\n // so dropping them here would destroy exactly what that promise protects.\n for (const [key, value] of Object.entries(values)) {\n if (key in fields) continue\n if ((META_KEYS as readonly string[]).includes(key)) continue\n stored[key] = value\n }\n\n return stored\n}\n\nexport function valuesFromStored(\n fields: FieldMap,\n data: Record<string, unknown>,\n fieldTypes: FieldTypeRegistry | undefined,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n const nested = nestedConverter(fieldTypes)\n\n for (const [key, field] of Object.entries(fields)) {\n const raw = data[key] ?? null\n const spec = fieldTypes?.get(field.type)\n values[key] = spec\n ? spec.fromStored(raw, field, nested)\n : defaultFromStored(raw, field)\n }\n\n return values\n}\n\nexport function metaFromStored(\n entryId: string,\n data: Record<string, unknown>,\n): EntryMeta {\n const status = data[\"status\"]\n\n return {\n id: entryId,\n status:\n status === \"published\" || status === \"archived\" || status === \"draft\"\n ? status\n : \"draft\",\n createdAt: toIso(data[\"createdAt\"]) ?? new Date(0).toISOString(),\n createdBy: typeof data[\"createdBy\"] === \"string\" ? data[\"createdBy\"] : \"\",\n updatedAt: toIso(data[\"updatedAt\"]) ?? new Date(0).toISOString(),\n updatedBy: typeof data[\"updatedBy\"] === \"string\" ? data[\"updatedBy\"] : \"\",\n publishedAt: toIso(data[\"publishedAt\"]),\n schemaVersion: typeof data[\"schemaVersion\"] === \"number\" ? data[\"schemaVersion\"] : 1,\n }\n}\n\nexport function entryFromStored(\n type: AnyContentTypeShape,\n entryId: string,\n data: Record<string, unknown>,\n fieldTypes: FieldTypeRegistry | undefined,\n): Entry {\n return {\n ...metaFromStored(entryId, data),\n ...valuesFromStored(type.fields, data, fieldTypes),\n } as Entry\n}\n","/**\n * Every Firestore path the CMS uses, in one file.\n *\n * The layout is settled in .docs/decisions/2026-09-18-firestore-layout.md: definition and\n * data are separate, and entries always live under `content/`, never under the document\n * that defines their type. Nothing outside this file builds a path by hand.\n */\n\nexport const SCHEMAS = \"schemas\"\nexport const CONTENT = \"content\"\nexport const ENTRIES = \"entries\"\nexport const MEDIA = \"media\"\nexport const USERS = \"users\"\nexport const SETTINGS = \"settings\"\n\n/**\n * Top-level collections the CMS owns. A content type slug may not collide with one.\n *\n * Defined in core, next to the Schema Builder check that enforces it, and re-exported here\n * so the storage layer and the collision check cannot drift apart. A test asserts it covers\n * every constant above.\n */\nexport { RESERVED_TYPE_SLUGS as RESERVED_COLLECTIONS } from \"../core/schema-builder.js\"\n\n/** `schemas/{slug}`: a database-defined content type. Code-defined types have no document. */\nexport function schemaPath(slug: string): string {\n return `${SCHEMAS}/${slug}`\n}\n\n/** `content/{slug}`: per-type metadata and the parent of the entries subcollection. */\nexport function contentPath(slug: string): string {\n return `${CONTENT}/${slug}`\n}\n\n/** `content/{slug}/entries`: the entries of one content type. */\nexport function entriesPath(slug: string): string {\n return `${CONTENT}/${slug}/${ENTRIES}`\n}\n\n/** `content/{slug}/entries/{entryId}`: one entry. */\nexport function entryPath(slug: string, entryId: string): string {\n return `${entriesPath(slug)}/${entryId}`\n}\n\nexport function mediaPath(mediaId: string): string {\n return `${MEDIA}/${mediaId}`\n}\n\nexport function userPath(uid: string): string {\n return `${USERS}/${uid}`\n}\n\nexport function settingsPath(key: string): string {\n return `${SETTINGS}/${key}`\n}\n\n/**\n * `settings/bootstrap`: the create-once first-run marker.\n *\n * Its existence is what says \"this project already has an admin\". The rules let any signed\n * in account create it once and let nobody update or delete it, which is what makes the\n * first sign-up a race the database settles rather than the client. See auth.ts.\n */\nexport const BOOTSTRAP_KEY = \"bootstrap\"\n\nexport function bootstrapPath(): string {\n return settingsPath(BOOTSTRAP_KEY)\n}\n\n/** Storage object path for an uploaded file. Kept here so rules and code agree on it. */\nexport function storageObjectPath(mediaId: string, fileName: string): string {\n return `${MEDIA}/${mediaId}/${fileName}`\n}\n","/**\n * The `users/{uid}` document: read, write, and the translation into `AuthUser`.\n *\n * The document is the source of truth for a role; the custom claim is a cache of it that\n * the security rules can read without a document read. Everything that changes a role goes\n * through here and then through `ClaimSync`, never through one alone, because a document\n * and a token that disagree is the one state in which the UI and the rules answer the same\n * question differently.\n */\nimport { doc, getDoc, serverTimestamp, setDoc, updateDoc } from \"firebase/firestore\"\nimport type { DocumentData, Firestore } from \"firebase/firestore\"\n\nimport type { AuthUser, Role } from \"../types/auth.js\"\nimport { ROLES } from \"../types/auth.js\"\n\nimport { toIso } from \"./converters.js\"\nimport { userPath } from \"./paths.js\"\n\n/** The stored profile, exactly the fields M3 promises, plus the uid the path carries. */\nexport interface StoredUser {\n uid: string\n email: string\n displayName: string | null\n role: Role\n createdAt: string\n disabled: boolean\n}\n\n/** Fields a caller may change on an existing profile. */\nexport interface UserPatch {\n displayName?: string | null\n role?: Role\n disabled?: boolean\n}\n\nexport interface NewUser {\n uid: string\n email: string\n displayName?: string | null\n role: Role\n}\n\nfunction isRole(value: unknown): value is Role {\n return typeof value === \"string\" && (ROLES as readonly string[]).includes(value)\n}\n\n/**\n * A stored document is untrusted input: it can predate a schema change or have been\n * written by hand in the console. An unrecognised role reads as the least privileged one\n * rather than as a crash or, worse, as an admin.\n */\nexport function userFromStored(uid: string, data: DocumentData): StoredUser {\n return {\n uid,\n email: typeof data[\"email\"] === \"string\" ? data[\"email\"] : \"\",\n displayName: typeof data[\"displayName\"] === \"string\" ? data[\"displayName\"] : null,\n role: isRole(data[\"role\"]) ? data[\"role\"] : \"author\",\n createdAt: toIso(data[\"createdAt\"]) ?? new Date(0).toISOString(),\n disabled: data[\"disabled\"] === true,\n }\n}\n\n/** The shape the rest of the CMS asks `can()` about. */\nexport function toAuthUser(stored: StoredUser): AuthUser {\n return {\n uid: stored.uid,\n email: stored.email,\n displayName: stored.displayName,\n role: stored.role,\n disabled: stored.disabled,\n }\n}\n\nexport async function readUser(db: Firestore, uid: string): Promise<StoredUser | null> {\n const snapshot = await getDoc(doc(db, userPath(uid)))\n return snapshot.exists() ? userFromStored(uid, snapshot.data()) : null\n}\n\n/**\n * Creates the profile. `createdAt` is a server timestamp rather than a client clock,\n * because a first-run bootstrap is decided by ordering and a client clock can be wrong by\n * years.\n */\nexport async function writeUser(db: Firestore, user: NewUser): Promise<StoredUser> {\n const reference = doc(db, userPath(user.uid))\n\n await setDoc(reference, {\n email: user.email,\n displayName: user.displayName ?? null,\n role: user.role,\n disabled: false,\n createdAt: serverTimestamp(),\n })\n\n const written = await getDoc(reference)\n return userFromStored(user.uid, written.data() ?? {})\n}\n\n/**\n * Patches the profile. Role and disabled are included here for completeness, but a caller\n * that changes either without syncing the claim leaves the rules reading a stale token, so\n * go through `setRole` and `setDisabled` on the auth client instead.\n */\nexport async function patchUser(\n db: Firestore,\n uid: string,\n patch: UserPatch,\n): Promise<StoredUser> {\n const reference = doc(db, userPath(uid))\n const changes: DocumentData = {}\n\n if (patch.displayName !== undefined) changes[\"displayName\"] = patch.displayName\n if (patch.role !== undefined) changes[\"role\"] = patch.role\n if (patch.disabled !== undefined) changes[\"disabled\"] = patch.disabled\n\n if (Object.keys(changes).length > 0) await updateDoc(reference, changes)\n\n const written = await getDoc(reference)\n return userFromStored(uid, written.data() ?? {})\n}\n","/**\n * Email and password auth for Husk, plus everything that has to happen around it: the\n * `users/{uid}` profile, the custom claim the security rules read, and the first-run\n * bootstrap that makes the first account an admin.\n *\n * This is the only file that talks to Firebase Auth. Callers get `AuthUser`, the same\n * shape `can()` takes, and never a Firebase `User`.\n *\n * Two invariants hold the layer together:\n *\n * 1. The user document is the source of truth for a role. The custom claim is a cache of\n * it that the rules can read without a document read, and every session reconciles the\n * two, so a claim that was lost or written by hand is repaired at the next sign-in.\n * 2. Nothing here can mint a claim on its own. `ClaimSync` is a dependency, deliberately\n * required rather than optional, so a deployment has to say which privileged context\n * writes claims instead of discovering the question after launch.\n */\nimport {\n createUserWithEmailAndPassword,\n onAuthStateChanged,\n signInWithEmailAndPassword,\n signOut as firebaseSignOut,\n updateProfile as updateFirebaseProfile,\n} from \"firebase/auth\"\nimport type { Auth, User } from \"firebase/auth\"\nimport { doc, getDoc, serverTimestamp, setDoc } from \"firebase/firestore\"\nimport type { Firestore } from \"firebase/firestore\"\n\nimport type { AuthUser, Role } from \"../types/auth.js\"\n\nimport type { ClaimSet, ClaimSync } from \"./claims.js\"\nimport { bootstrapPath } from \"./paths.js\"\nimport { patchUser, readUser, toAuthUser, writeUser } from \"./users.js\"\nimport type { StoredUser, UserPatch } from \"./users.js\"\n\n/** Every way this layer refuses, as a code a UI can branch on without parsing a message. */\nexport type AuthErrorCode = \"disabled\" | \"invalid-credentials\"\n\nexport class AuthError extends Error {\n override readonly name = \"AuthError\"\n readonly code: AuthErrorCode\n\n constructor(code: AuthErrorCode, message: string) {\n super(message)\n this.code = code\n }\n}\n\nexport interface HuskAuthDeps {\n auth: Auth\n db: Firestore\n /**\n * Required, not optional. See claims.ts: an absent claim writer is a security decision,\n * so it has to be made explicitly, even when the decision is\n * `createUnavailableClaimSync()`.\n */\n claimSync: ClaimSync\n /**\n * The role every account after the first one gets. The least privileged role by default:\n * a new account should never arrive with more rights than someone had to grant it.\n *\n * Raising it above `author` only works where an admin is doing the provisioning, because\n * firestore.rules pins a self-created profile to `author`. That is the rule doing its\n * job, not a limitation to work around.\n */\n defaultRole?: Role\n}\n\nexport interface SignUpInput {\n email: string\n password: string\n displayName?: string\n}\n\nexport interface HuskAuth {\n /** Signs in, reconciles the claim, and refuses a disabled account. */\n signIn(email: string, password: string): Promise<AuthUser>\n /** Creates an account. The first one in an empty project becomes admin. */\n signUp(input: SignUpInput): Promise<AuthUser>\n signOut(): Promise<void>\n /** The signed-in user resolved against their profile, or null when signed out. */\n currentUser(): Promise<AuthUser | null>\n /** Subscribes to session changes. Returns the unsubscribe function. */\n onChange(\n listener: (user: AuthUser | null) => void,\n onError?: (error: unknown) => void,\n ): () => void\n getUser(uid: string): Promise<StoredUser | null>\n /** Profile edits that carry no privilege. Role and disabled are not settable here. */\n updateProfile(\n uid: string,\n patch: Omit<UserPatch, \"role\" | \"disabled\">,\n ): Promise<StoredUser>\n /** Changes a role in the document and in the claim, in that order. */\n setRole(uid: string, role: Role): Promise<StoredUser>\n /** Disables or re-enables an account, document and claim together. */\n setDisabled(uid: string, disabled: boolean): Promise<StoredUser>\n}\n\nconst CREDENTIAL_CODES = new Set([\n \"auth/invalid-credential\",\n \"auth/wrong-password\",\n \"auth/user-not-found\",\n \"auth/invalid-email\",\n])\n\nfunction errorCode(thrown: unknown): string | undefined {\n return (thrown as { code?: string }).code\n}\n\nexport function createHuskAuth(deps: HuskAuthDeps): HuskAuth {\n const { auth, db, claimSync } = deps\n const defaultRole: Role = deps.defaultRole ?? \"author\"\n\n /**\n * Claims the project, once, by creating `settings/bootstrap`.\n *\n * The create is both the question and the answer: the rules allow exactly one create of\n * that document and no update and no delete, so two accounts signing up at the same\n * moment cannot both win, and a later account cannot re-open the bootstrap by deleting\n * the marker. The loser gets a permission denial, which reads here as \"somebody else is\n * already the first admin\".\n *\n * What this does not do is prove to a privileged process that the claimant deserves\n * admin. In the emulator the claim writer is this same client, so the honest description\n * is: the database settles the race, and the claim writer trusts the outcome. A\n * production claim writer has to re-check the marker itself before minting an admin\n * claim. See .docs/decisions/2026-09-18-claim-sync-seam.md.\n *\n * A refused create is not the same question as \"somebody else won\". The marker is\n * written before the profile, so a first run that dies between the two writes leaves a\n * marker naming an account that has no profile. Asking only whether the marker exists\n * demotes exactly the account it was meant to promote, and because the rules refuse\n * every update and delete of the marker, that project then has no admin and no\n * client-side way back. So the refusal is followed by reading the marker: an account the\n * marker already names holds the proof and is the first admin, whether or not its\n * profile was ever written. See .docs/decisions/2026-09-19-first-run-recovery.md.\n */\n async function claimFirstAdmin(uid: string): Promise<boolean> {\n try {\n await setDoc(doc(db, bootstrapPath()), {\n firstAdminUid: uid,\n createdAt: serverTimestamp(),\n })\n return true\n } catch (thrown) {\n const code = errorCode(thrown)\n if (code === \"permission-denied\" || code === \"already-exists\") {\n return await markerNames(uid)\n }\n throw thrown\n }\n }\n\n /**\n * Whether the existing marker names this uid.\n *\n * A read failure answers false rather than throwing: not being able to see the marker is\n * not evidence of having won it, and a sign-in that cannot read one document should end\n * as the least privileged role rather than as an error screen.\n */\n async function markerNames(uid: string): Promise<boolean> {\n try {\n const marker = await getDoc(doc(db, bootstrapPath()))\n return marker.exists() && marker.data()[\"firstAdminUid\"] === uid\n } catch {\n return false\n }\n }\n\n /** The claims currently on the session token, as the rules would read them. */\n async function tokenClaims(user: User): Promise<Partial<ClaimSet>> {\n const result = await user.getIdTokenResult()\n const role = result.claims[\"role\"]\n const disabled = result.claims[\"disabled\"]\n\n return {\n ...(typeof role === \"string\" ? { role: role as Role } : {}),\n ...(typeof disabled === \"boolean\" ? { disabled } : {}),\n }\n }\n\n /**\n * Writes the claim and forces a fresh token.\n *\n * A claim lands on the account, not on a token that was already issued, so without the\n * refresh the current session would keep presenting the old role and the rules would\n * keep refusing. The refresh is what makes a role change take effect now rather than\n * whenever the token happens to expire.\n */\n async function syncClaims(uid: string, claims: ClaimSet): Promise<void> {\n await claimSync.sync(uid, claims)\n if (auth.currentUser?.uid === uid) await auth.currentUser.getIdToken(true)\n }\n\n /** Repairs a token that disagrees with the stored profile. A no-op when they agree. */\n async function reconcile(user: User, stored: StoredUser): Promise<void> {\n const claims = await tokenClaims(user)\n if (claims.role === stored.role && claims.disabled === stored.disabled) return\n\n await syncClaims(stored.uid, { role: stored.role, disabled: stored.disabled })\n }\n\n async function provision(user: User): Promise<AuthUser> {\n const role: Role = (await claimFirstAdmin(user.uid)) ? \"admin\" : defaultRole\n\n // Claim first, document second. Creating an admin profile needs the admin claim to be\n // on the token already, because that is what firestore.rules checks.\n await syncClaims(user.uid, { role, disabled: false })\n\n const stored = await writeUser(db, {\n uid: user.uid,\n email: user.email ?? \"\",\n displayName: user.displayName,\n role,\n })\n\n return toAuthUser(stored)\n }\n\n /**\n * Turns a Firebase session into an `AuthUser`, provisioning the profile when missing.\n *\n * A missing profile is the normal state exactly once per account (straight after\n * sign-up) and an abnormal one otherwise (an account created in the Firebase console, or\n * a profile deleted by hand). Both take the same path, because the bootstrap marker, not\n * the absence of a profile, decides whether admin is still available.\n */\n async function resolve(user: User): Promise<AuthUser> {\n const existing = await readUser(db, user.uid)\n\n if (existing === null) return provision(user)\n\n if (existing.disabled) {\n // Sign the session out rather than return a disabled user: the caller is about to\n // render an admin, and a live session for a disabled account is a session only the\n // rules are stopping.\n await firebaseSignOut(auth)\n throw new AuthError(\"disabled\", \"This account has been disabled.\")\n }\n\n await reconcile(user, existing)\n return toAuthUser(existing)\n }\n\n return {\n async signIn(email, password) {\n try {\n const credential = await signInWithEmailAndPassword(auth, email, password)\n return await resolve(credential.user)\n } catch (thrown) {\n const code = errorCode(thrown)\n if (code !== undefined && CREDENTIAL_CODES.has(code)) {\n throw new AuthError(\"invalid-credentials\", \"Email or password is incorrect.\")\n }\n throw thrown\n }\n },\n\n async signUp(input) {\n const credential = await createUserWithEmailAndPassword(\n auth,\n input.email,\n input.password,\n )\n\n if (input.displayName !== undefined) {\n await updateFirebaseProfile(credential.user, { displayName: input.displayName })\n }\n\n return resolve(credential.user)\n },\n\n async signOut() {\n await firebaseSignOut(auth)\n },\n\n async currentUser() {\n const user = auth.currentUser\n return user === null ? null : await resolve(user)\n },\n\n onChange(listener, onError) {\n return onAuthStateChanged(auth, (user) => {\n if (user === null) {\n listener(null)\n return\n }\n\n resolve(user).then(listener, (error: unknown) => {\n // A rejected resolve means there is a session but no usable user: a disabled\n // account, or a claim that could not be written. The listener has to hear \"no\n // user\" either way, or a guard would wait forever for a call that never comes.\n listener(null)\n onError?.(error)\n })\n })\n },\n\n getUser(uid) {\n return readUser(db, uid)\n },\n\n updateProfile(uid, patch) {\n return patchUser(db, uid, patch)\n },\n\n async setRole(uid, role) {\n const stored = await patchUser(db, uid, { role })\n await syncClaims(uid, { role: stored.role, disabled: stored.disabled })\n return stored\n },\n\n async setDisabled(uid, disabled) {\n const stored = await patchUser(db, uid, { disabled })\n await syncClaims(uid, { role: stored.role, disabled: stored.disabled })\n return stored\n },\n }\n}\n","/**\n * Custom claim sync: how a role reaches `request.auth.token.role`.\n *\n * The security rules read the role from a custom claim so a rule never has to read a user\n * document. Nothing in the Firebase client SDK can set a custom claim: minting one needs a\n * privileged context (the Admin SDK with a service account, a Cloud Function, or the Auth\n * emulator's admin endpoints). This file is therefore an interface plus the one\n * implementation this repository can honestly provide today, and a loud failure for every\n * other context, so the gap is visible rather than silently open.\n *\n * See .docs/decisions/2026-09-18-claim-sync-seam.md for what production has to plug in.\n */\nimport type { Role } from \"../types/auth.js\"\n\n/**\n * The exact claim shape `firestore.rules` reads. Keep this in step with the `role()` and\n * `enabled()` helpers there: a key renamed on one side locks everyone out on the other.\n */\nexport interface ClaimSet {\n role: Role\n disabled: boolean\n}\n\n/**\n * The seam. The CMS calls `sync` whenever the stored role or disabled flag changes, or\n * when a session is found carrying a token that disagrees with the user document.\n *\n * An implementation must be idempotent: sign-in reconciles claims on every session, so\n * syncing a value that is already current has to be a no-op rather than an error.\n */\nexport interface ClaimSync {\n sync(uid: string, claims: ClaimSet): Promise<void>\n}\n\n/** A claim could not be written, so the role never reached the token the rules read. */\nexport class ClaimSyncError extends Error {\n override readonly name = \"ClaimSyncError\"\n readonly uid: string\n\n constructor(uid: string, message: string) {\n super(message)\n this.uid = uid\n }\n}\n\n/** Loopback hosts only. The emulator endpoint is unauthenticated by design. */\nconst LOOPBACK = new Set([\"127.0.0.1\", \"localhost\", \"[::1]\", \"::1\"])\n\nfunction isLoopback(host: string): boolean {\n const [hostname] = host.split(\":\")\n return hostname !== undefined && LOOPBACK.has(hostname)\n}\n\nexport interface EmulatorClaimSyncOptions {\n /** `host:port` of the Auth emulator, 9099 by default per firebase.json. */\n authHost: string\n projectId: string\n}\n\n/**\n * Claim sync against the Auth emulator.\n *\n * The emulator exposes the Identity Toolkit admin endpoints to anyone presenting the\n * literal bearer token `owner`, which is how a local process sets a role without a service\n * account. That token means nothing outside the emulator, so this refuses any host that is\n * not loopback: shipping it at a real Identity Toolkit endpoint would send an unauthorized\n * request and, worse, suggest that production claim sync is handled.\n */\nexport function createEmulatorClaimSync(options: EmulatorClaimSyncOptions): ClaimSync {\n const { authHost, projectId } = options\n\n if (!isLoopback(authHost)) {\n throw new ClaimSyncError(\n \"*\",\n `Emulator claim sync refuses the host \"${authHost}\". The bearer token \"owner\" is an emulator affordance and is not accepted by the real Identity Toolkit, so this implementation is loopback-only.`,\n )\n }\n\n return {\n async sync(uid, claims) {\n const response = await fetch(\n `http://${authHost}/identitytoolkit.googleapis.com/v1/projects/${projectId}/accounts:update`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer owner\" },\n body: JSON.stringify({\n localId: uid,\n customAttributes: JSON.stringify(claims),\n }),\n },\n )\n\n if (!response.ok) {\n throw new ClaimSyncError(\n uid,\n `Could not set custom claims on ${uid}: ${response.status} ${await response.text()}`,\n )\n }\n },\n }\n}\n\n/**\n * The deliberate hole-with-a-sign-on-it, for any host that has no privileged context wired\n * up yet. It fails the write instead of pretending the claim landed, so a deployment that\n * forgot to provide one finds out at the first sign-up rather than at the first breach.\n *\n * Failing here is fail-closed: without the claim the rules see role `none`, and `enabled()`\n * denies every collection.\n */\nexport function createUnavailableClaimSync(reason?: string): ClaimSync {\n return {\n sync(uid) {\n return Promise.reject(\n new ClaimSyncError(\n uid,\n reason ??\n \"No custom claim writer is configured. Setting a custom claim needs a privileged context (Admin SDK, Cloud Function, or the Auth emulator), which the client SDK cannot provide. Pass a ClaimSync implementation to createHuskAuth.\",\n ),\n )\n },\n }\n}\n"]}
|