UNPKG

@anysoftinc/anydb-sdk

Version:

AnyDB TypeScript SDK for querying and transacting with Datomic databases

443 lines (442 loc) 16.4 kB
import { kw, sym, uuid as uuidEdn } from "./client"; import { v4 as uuidv4 } from "uuid"; /** * Creates NextAuth cookie configuration with proper security settings * Uses __Host- prefix and secure cookies in production, regular cookies in development */ export function createNextAuthCookies(cookieIdentifier) { const isProduction = process.env.NODE_ENV === "production"; const prefix = isProduction ? "__Host-" : ""; const secure = isProduction; return { sessionToken: { name: `${prefix}${cookieIdentifier}.session-token`, options: { httpOnly: true, sameSite: "lax", path: "/", secure, }, }, callbackUrl: { name: `${prefix}${cookieIdentifier}.callback-url`, options: { httpOnly: true, sameSite: "lax", path: "/", secure, }, }, csrfToken: { name: `${prefix}${cookieIdentifier}.csrf-token`, options: { httpOnly: true, sameSite: "lax", path: "/", secure, }, }, }; } // Datomic schema idents const USER = { id: "anydb.auth.user.v1/id", // uuid identity name: "anydb.auth.user.v1/name", email: "anydb.auth.user.v1/email", emailVerified: "anydb.auth.user.v1/email-verified", image: "anydb.auth.user.v1/image", }; const ACCOUNT = { id: "anydb.auth.account.v1/id", // uuid identity userId: "anydb.auth.account.v1/user-id", // uuid ref by value type: "anydb.auth.account.v1/type", provider: "anydb.auth.account.v1/provider", providerAccountId: "anydb.auth.account.v1/provider-account-id", refreshToken: "anydb.auth.account.v1/refresh-token", accessToken: "anydb.auth.account.v1/access-token", expiresAt: "anydb.auth.account.v1/expires-at", tokenType: "anydb.auth.account.v1/token-type", scope: "anydb.auth.account.v1/scope", idToken: "anydb.auth.account.v1/id-token", sessionState: "anydb.auth.account.v1/session-state", }; const SESSION = { id: "anydb.auth.session.v1/id", // uuid identity sessionToken: "anydb.auth.session.v1/session-token", userId: "anydb.auth.session.v1/user-id", expires: "anydb.auth.session.v1/expires", }; const VTOKEN = { id: "anydb.auth.vtoken.v1/id", // uuid identity identifier: "anydb.auth.vtoken.v1/identifier", token: "anydb.auth.vtoken.v1/token", expires: "anydb.auth.vtoken.v1/expires", }; function toUser(m) { const get = (k) => m && (m[k] ?? m[`:${k}`]); // USER.id is ":auth.user.v1/id", so slice(1) gives "auth.user.v1/id" const idKey = USER.id; // "auth.user.v1/id" const idVal = get(idKey); const id = typeof idVal === "string" ? idVal : idVal?.value ?? String(idVal); const emailVerified = get(USER.emailVerified); return { id, name: get(USER.name) || null, email: get(USER.email) || null, emailVerified: emailVerified ? new Date(emailVerified) : null, image: get(USER.image) || null, }; } /** * Creates a NextAuth.js adapter for AnyDB/Datomic * * @param db - DatomicDatabase instance * @returns NextAuth.js Adapter * * @example * ```typescript * import { createDatomicDatabase } from '@anysoftinc/anydb-sdk'; * import { AnyDBAdapter } from '@anysoftinc/anydb-sdk/nextauth-adapter'; * * const db = createDatomicDatabase(client, 'storage', 'auth-db'); * * export default NextAuth({ * adapter: AnyDBAdapter(db), * // ... other config * }); * ``` */ export function AnyDBAdapter(db) { async function getUserByEmail(email) { const norm = typeof email === "string" ? email.toLowerCase() : email; const q = { find: [ [ sym("pull"), sym("?e"), [ kw(USER.id), kw(USER.name), kw(USER.email), kw(USER.emailVerified), kw(USER.image), ], ], ], where: [[sym("?e"), kw(USER.email), norm]], }; const res = await db.query(q); const rows = Array.isArray(res) ? res : []; return rows.length ? toUser(rows[0][0]) : null; } async function getUser(id) { const q = { find: [ [ sym("pull"), sym("?e"), [ kw(USER.id), kw(USER.name), kw(USER.email), kw(USER.emailVerified), kw(USER.image), ], ], ], where: [[sym("?e"), kw(USER.id), uuidEdn(id)]], }; const res = await db.query(q); const rows = Array.isArray(res) ? res : []; if (!rows.length) return null; return toUser(rows[0][0]); } return { // Users async createUser(data) { // Check if user already exists by email if (data.email) { const existing = await getUserByEmail(data.email); if (existing) { return existing; } } const id = uuidv4(); const tx = { [USER.id]: uuidEdn(id), ...(data.name ? { [USER.name]: data.name } : {}), ...(data.email ? { [USER.email]: String(data.email).toLowerCase() } : {}), ...(data.emailVerified ? { [USER.emailVerified]: data.emailVerified } : {}), ...(data.image ? { [USER.image]: data.image } : {}), }; await db.transact([tx]); return { id, name: data.name ?? null, email: (data.email ? String(data.email).toLowerCase() : null), emailVerified: data.emailVerified ?? null, image: data.image ?? null, }; }, getUser, getUserByEmail, async getUserByAccount({ provider, providerAccountId }) { const q = { find: [ [ sym("pull"), sym("?u"), [ kw(USER.id), kw(USER.name), kw(USER.email), kw(USER.emailVerified), kw(USER.image), ], ], ], where: [ [sym("?a"), kw(ACCOUNT.provider), provider], [sym("?a"), kw(ACCOUNT.providerAccountId), providerAccountId], [sym("?a"), kw(ACCOUNT.userId), sym("?uid")], [sym("?u"), kw(USER.id), sym("?uid")], ], }; const res = await db.query(q); const rows = Array.isArray(res) ? res : []; return rows.length ? toUser(rows[0][0]) : null; }, async updateUser(data) { if (!data.id) throw new Error("updateUser requires id"); const tx = { [USER.id]: uuidEdn(data.id), ...(data.name !== undefined ? { [USER.name]: data.name } : {}), ...(data.email !== undefined ? { [USER.email]: data.email ? String(data.email).toLowerCase() : null } : {}), ...(data.emailVerified !== undefined ? { [USER.emailVerified]: data.emailVerified } : {}), ...(data.image !== undefined ? { [USER.image]: data.image } : {}), }; await db.transact([tx]); const u = await getUser(data.id); return u; }, async deleteUser(id) { await db.transact([[kw("db/retractEntity"), [USER.id], uuidEdn(id)]]); return null; }, // Accounts async linkAccount(account) { const id = uuidv4(); const tx = { [ACCOUNT.id]: uuidEdn(id), [ACCOUNT.userId]: uuidEdn(account.userId), [ACCOUNT.type]: account.type, [ACCOUNT.provider]: account.provider, [ACCOUNT.providerAccountId]: account.providerAccountId, ...(account.refresh_token ? { [ACCOUNT.refreshToken]: account.refresh_token } : {}), ...(account.access_token ? { [ACCOUNT.accessToken]: account.access_token } : {}), ...(account.expires_at ? { [ACCOUNT.expiresAt]: account.expires_at } : {}), ...(account.token_type ? { [ACCOUNT.tokenType]: account.token_type } : {}), ...(account.scope ? { [ACCOUNT.scope]: account.scope } : {}), ...(account.id_token ? { [ACCOUNT.idToken]: account.id_token } : {}), ...(account.session_state ? { [ACCOUNT.sessionState]: account.session_state } : {}), }; await db.transact([tx]); return account; }, async unlinkAccount({ provider, providerAccountId }) { const q = { find: [sym("?id")], where: [ [sym("?a"), kw(ACCOUNT.provider), provider], [sym("?a"), kw(ACCOUNT.providerAccountId), providerAccountId], [sym("?a"), kw(ACCOUNT.id), sym("?id")], ], }; const res = await db.query(q); const id = Array.isArray(res) && res[0]?.[0]; if (id) { await db.transact([[kw("db/retractEntity"), [ACCOUNT.id], id]]); } }, // Sessions async createSession(session) { const id = uuidv4(); let expiresDate; if (session.expires instanceof Date && !isNaN(session.expires.getTime())) { expiresDate = session.expires; } else if (session.expires) { const d = new Date(session.expires); expiresDate = isNaN(d.getTime()) ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : d; } else { expiresDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); } const tx = { [SESSION.id]: uuidEdn(id), [SESSION.userId]: uuidEdn(session.userId), [SESSION.sessionToken]: session.sessionToken, [SESSION.expires]: expiresDate, }; await db.transact([tx]); return { ...session, expires: expiresDate }; }, async getSessionAndUser(sessionToken) { const q = { find: [ [ sym("pull"), sym("?s"), [ kw(SESSION.id), kw(SESSION.sessionToken), kw(SESSION.userId), kw(SESSION.expires), ], ], [ sym("pull"), sym("?u"), [ kw(USER.id), kw(USER.name), kw(USER.email), kw(USER.emailVerified), kw(USER.image), ], ], ], where: [ [sym("?s"), kw(SESSION.sessionToken), sessionToken], [sym("?s"), kw(SESSION.userId), sym("?uid")], [sym("?u"), kw(USER.id), sym("?uid")], ], }; const res = await db.query(q); const row = Array.isArray(res) ? res[0] : null; if (!row) return null; const s = row[0]; const u = row[1]; const getS = (k) => s && (s[k] ?? s[`:${k}`]); const expiresRaw = getS(SESSION.expires); const expires = expiresRaw instanceof Date ? expiresRaw : new Date(expiresRaw); const sessionResult = { sessionToken: getS(SESSION.sessionToken), userId: (getS(SESSION.userId)?.value ?? getS(SESSION.userId)), expires, }; return { session: sessionResult, user: toUser(u) }; }, async updateSession(partial) { let expiresDate; if (partial.expires instanceof Date) { expiresDate = isNaN(partial.expires.getTime()) ? undefined : partial.expires; } else if (partial.expires) { const d = new Date(partial.expires); expiresDate = isNaN(d.getTime()) ? undefined : d; } if (!partial.sessionToken) { throw new Error("updateSession requires sessionToken to upsert by identity"); } const tx = { ...(partial.sessionToken ? { [SESSION.sessionToken]: partial.sessionToken } : {}), ...(partial.userId ? { [SESSION.userId]: uuidEdn(partial.userId) } : {}), ...(expiresDate ? { [SESSION.expires]: expiresDate } : {}), }; await db.transact([tx]); return { ...partial, ...(expiresDate ? { expires: expiresDate } : {}), }; }, async deleteSession(sessionToken) { const q = { find: [sym("?id")], where: [ [sym("?s"), kw(SESSION.sessionToken), sessionToken], [sym("?s"), kw(SESSION.id), sym("?id")], ], }; const res = await db.query(q); const id = Array.isArray(res) && res[0]?.[0]; if (id) { await db.transact([[kw("db/retractEntity"), [SESSION.id], id]]); } }, // Verification tokens (email sign-in, passwordless) async createVerificationToken(token) { const id = uuidv4(); const tx = { [VTOKEN.id]: uuidEdn(id), [VTOKEN.identifier]: token.identifier, [VTOKEN.token]: token.token, [VTOKEN.expires]: token.expires, }; await db.transact([tx]); return token; }, async useVerificationToken(params) { const q = { find: [ [ sym("pull"), sym("?e"), [ kw(VTOKEN.id), kw(VTOKEN.identifier), kw(VTOKEN.token), kw(VTOKEN.expires), ], ], ], where: [ [sym("?e"), kw(VTOKEN.identifier), params.identifier], [sym("?e"), kw(VTOKEN.token), params.token], ], }; const res = await db.query(q); const rows = Array.isArray(res) ? res : []; if (!rows.length) return null; const m = rows[0][0]; const get = (k) => m && (m[k] ?? m[`:${k}`]); const entityId = get(VTOKEN.id); // Delete the token after using it await db.transact([[kw("db/retractEntity"), entityId]]); return { identifier: get(VTOKEN.identifier), token: get(VTOKEN.token), expires: new Date(get(VTOKEN.expires)), }; }, }; }