release: WRNexusJS 0.5.0
This commit is contained in:
1 parent
76c768099d
commit
6afe32f63f
456 files changed
+40879
-8850
No files matched your search
@@ -0,0 +1,318 @@
|
||||
import type { AuthStore } from "../store.ts";
|
||||
import { inferIdentityType, normalizeIdentity } from "../normalize.ts";
|
||||
import type {
|
||||
AuthIdentity,
|
||||
AuthSecurityEvent,
|
||||
AuthSession,
|
||||
AuthUser,
|
||||
LoginAttempt,
|
||||
OAuthAccount,
|
||||
OneTimeToken,
|
||||
OtpChallenge,
|
||||
PasskeyCredential,
|
||||
PasswordCredential,
|
||||
RecoveryCodeRecord,
|
||||
TotpCredential,
|
||||
TrustedDevice,
|
||||
} from "../types.ts";
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
export class MemoryAuthStore implements AuthStore {
|
||||
private readonly users = new Map<string, AuthUser>();
|
||||
private readonly identities = new Map<string, AuthIdentity>();
|
||||
private readonly passwords = new Map<string, PasswordCredential>();
|
||||
private readonly sessions = new Map<string, AuthSession>();
|
||||
private readonly trustedDevices = new Map<string, TrustedDevice>();
|
||||
private readonly tokens = new Map<string, OneTimeToken>();
|
||||
private readonly otps = new Map<string, OtpChallenge>();
|
||||
private readonly totp = new Map<string, TotpCredential>();
|
||||
private readonly recoveryCodes = new Map<string, RecoveryCodeRecord>();
|
||||
private readonly passkeys = new Map<string, PasskeyCredential>();
|
||||
private readonly oauthAccounts = new Map<string, OAuthAccount>();
|
||||
private readonly attempts: LoginAttempt[] = [];
|
||||
private readonly events: AuthSecurityEvent[] = [];
|
||||
|
||||
async createUser(user: AuthUser): Promise<void> {
|
||||
if (this.users.has(user.id)) throw new Error(`WRN-AUTH-USER-EXISTS: ${user.id}`);
|
||||
this.users.set(user.id, clone(user));
|
||||
}
|
||||
async updateUser(user: AuthUser): Promise<void> {
|
||||
if (!this.users.has(user.id)) throw new Error(`WRN-AUTH-USER-MISSING: ${user.id}`);
|
||||
this.users.set(user.id, clone(user));
|
||||
}
|
||||
async findUserById(id: string): Promise<AuthUser | undefined> {
|
||||
const value = this.users.get(id);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
async listUsers(): Promise<AuthUser[]> {
|
||||
return [...this.users.values()].map(clone);
|
||||
}
|
||||
|
||||
async createIdentity(identity: AuthIdentity): Promise<void> {
|
||||
const key = `${identity.type}:${identity.normalizedValue}`;
|
||||
if (
|
||||
this.identities.has(key) ||
|
||||
[...this.identities.values()].some((item) => item.id === identity.id)
|
||||
) {
|
||||
throw new Error("WRN-AUTH-IDENTITY-EXISTS");
|
||||
}
|
||||
this.identities.set(key, clone(identity));
|
||||
}
|
||||
async updateIdentity(identity: AuthIdentity): Promise<void> {
|
||||
const currentEntry = [...this.identities.entries()].find(
|
||||
([, existing]) => existing.id === identity.id,
|
||||
);
|
||||
if (!currentEntry) throw new Error("WRN-AUTH-IDENTITY-MISSING");
|
||||
const [currentKey, current] = currentEntry;
|
||||
if (identity.userId !== current.userId || identity.type !== current.type) {
|
||||
throw new Error("WRN-AUTH-IDENTITY-IMMUTABLE");
|
||||
}
|
||||
const nextKey = `${identity.type}:${identity.normalizedValue}`;
|
||||
const collision = this.identities.get(nextKey);
|
||||
if (collision && collision.id !== identity.id) {
|
||||
throw new Error("WRN-AUTH-IDENTITY-EXISTS");
|
||||
}
|
||||
if (currentKey !== nextKey) this.identities.delete(currentKey);
|
||||
this.identities.set(nextKey, clone(identity));
|
||||
}
|
||||
async findIdentity(
|
||||
type: AuthIdentity["type"],
|
||||
normalizedValue: string,
|
||||
): Promise<AuthIdentity | undefined> {
|
||||
const value = this.identities.get(`${type}:${normalizedValue}`);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
async listIdentities(userId: string): Promise<AuthIdentity[]> {
|
||||
return [...this.identities.values()].filter((item) => item.userId === userId).map(clone);
|
||||
}
|
||||
|
||||
async setPassword(credential: PasswordCredential): Promise<void> {
|
||||
this.passwords.set(credential.userId, clone(credential));
|
||||
}
|
||||
async getPassword(userId: string): Promise<PasswordCredential | undefined> {
|
||||
const value = this.passwords.get(userId);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
|
||||
async createSession(session: AuthSession): Promise<void> {
|
||||
if (this.sessions.has(session.id)) throw new Error("WRN-AUTH-SESSION-EXISTS");
|
||||
this.sessions.set(session.id, clone(session));
|
||||
}
|
||||
async updateSession(session: AuthSession): Promise<void> {
|
||||
this.sessions.set(session.id, clone(session));
|
||||
}
|
||||
async findSession(id: string): Promise<AuthSession | undefined> {
|
||||
const value = this.sessions.get(id);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
async listSessions(userId: string): Promise<AuthSession[]> {
|
||||
return [...this.sessions.values()].filter((item) => item.userId === userId).map(clone);
|
||||
}
|
||||
async deleteSession(id: string): Promise<void> {
|
||||
this.sessions.delete(id);
|
||||
}
|
||||
|
||||
async createTrustedDevice(device: TrustedDevice): Promise<void> {
|
||||
if (
|
||||
this.trustedDevices.has(device.id) ||
|
||||
[...this.trustedDevices.values()].some(
|
||||
(item) => item.userId === device.userId && item.fingerprintHash === device.fingerprintHash,
|
||||
)
|
||||
) {
|
||||
throw new Error("WRN-AUTH-TRUSTED-DEVICE-EXISTS");
|
||||
}
|
||||
this.trustedDevices.set(device.id, clone(device));
|
||||
}
|
||||
async updateTrustedDevice(device: TrustedDevice): Promise<void> {
|
||||
this.trustedDevices.set(device.id, clone(device));
|
||||
}
|
||||
async findTrustedDeviceByFingerprint(
|
||||
userId: string,
|
||||
fingerprintHash: string,
|
||||
): Promise<TrustedDevice | undefined> {
|
||||
const value = [...this.trustedDevices.values()].find(
|
||||
(item) => item.userId === userId && item.fingerprintHash === fingerprintHash,
|
||||
);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
async listTrustedDevices(userId: string): Promise<TrustedDevice[]> {
|
||||
return [...this.trustedDevices.values()].filter((item) => item.userId === userId).map(clone);
|
||||
}
|
||||
|
||||
async createToken(token: OneTimeToken): Promise<void> {
|
||||
if (
|
||||
this.tokens.has(token.tokenHash) ||
|
||||
[...this.tokens.values()].some((item) => item.id === token.id)
|
||||
) {
|
||||
throw new Error("WRN-AUTH-TOKEN-EXISTS");
|
||||
}
|
||||
this.tokens.set(token.tokenHash, clone(token));
|
||||
}
|
||||
async updateToken(token: OneTimeToken): Promise<void> {
|
||||
this.tokens.set(token.tokenHash, clone(token));
|
||||
}
|
||||
async findTokenByHash(hash: string): Promise<OneTimeToken | undefined> {
|
||||
const value = this.tokens.get(hash);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
|
||||
async createOtp(challenge: OtpChallenge): Promise<void> {
|
||||
if (this.otps.has(challenge.id)) throw new Error("WRN-AUTH-OTP-EXISTS");
|
||||
this.otps.set(challenge.id, clone(challenge));
|
||||
}
|
||||
async updateOtp(challenge: OtpChallenge): Promise<void> {
|
||||
this.otps.set(challenge.id, clone(challenge));
|
||||
}
|
||||
async findOtp(id: string): Promise<OtpChallenge | undefined> {
|
||||
const value = this.otps.get(id);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
|
||||
async createTotp(credential: TotpCredential): Promise<void> {
|
||||
if (this.totp.has(credential.id)) throw new Error("WRN-AUTH-TOTP-EXISTS");
|
||||
this.totp.set(credential.id, clone(credential));
|
||||
}
|
||||
async updateTotp(credential: TotpCredential): Promise<void> {
|
||||
this.totp.set(credential.id, clone(credential));
|
||||
}
|
||||
async listTotp(userId: string): Promise<TotpCredential[]> {
|
||||
return [...this.totp.values()].filter((item) => item.userId === userId).map(clone);
|
||||
}
|
||||
async deleteTotp(id: string): Promise<void> {
|
||||
this.totp.delete(id);
|
||||
}
|
||||
|
||||
async createRecoveryCodes(codes: RecoveryCodeRecord[]): Promise<void> {
|
||||
const incoming = new Set<string>();
|
||||
for (const code of codes) {
|
||||
if (incoming.has(code.id) || this.recoveryCodes.has(code.id)) {
|
||||
throw new Error("WRN-AUTH-RECOVERY-CODE-EXISTS");
|
||||
}
|
||||
incoming.add(code.id);
|
||||
}
|
||||
for (const code of codes) this.recoveryCodes.set(code.id, clone(code));
|
||||
}
|
||||
async updateRecoveryCode(code: RecoveryCodeRecord): Promise<void> {
|
||||
this.recoveryCodes.set(code.id, clone(code));
|
||||
}
|
||||
async listRecoveryCodes(userId: string): Promise<RecoveryCodeRecord[]> {
|
||||
return [...this.recoveryCodes.values()].filter((item) => item.userId === userId).map(clone);
|
||||
}
|
||||
async deleteRecoveryCodes(userId: string): Promise<void> {
|
||||
for (const [id, code] of this.recoveryCodes) {
|
||||
if (code.userId === userId) this.recoveryCodes.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
async createPasskey(credential: PasskeyCredential): Promise<void> {
|
||||
if (
|
||||
this.passkeys.has(credential.id) ||
|
||||
[...this.passkeys.values()].some((item) => item.credentialId === credential.credentialId)
|
||||
) {
|
||||
throw new Error("WRN-AUTH-PASSKEY-EXISTS");
|
||||
}
|
||||
this.passkeys.set(credential.id, clone(credential));
|
||||
}
|
||||
async updatePasskey(credential: PasskeyCredential): Promise<void> {
|
||||
const current = this.passkeys.get(credential.id);
|
||||
if (!current) throw new Error("WRN-AUTH-PASSKEY-MISSING");
|
||||
if (
|
||||
credential.userId !== current.userId ||
|
||||
credential.credentialId !== current.credentialId ||
|
||||
credential.createdAt !== current.createdAt
|
||||
) {
|
||||
throw new Error("WRN-AUTH-PASSKEY-IMMUTABLE");
|
||||
}
|
||||
this.passkeys.set(credential.id, clone(credential));
|
||||
}
|
||||
async findPasskeyByCredentialId(credentialId: string): Promise<PasskeyCredential | undefined> {
|
||||
const value = [...this.passkeys.values()].find((item) => item.credentialId === credentialId);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
async listPasskeys(userId: string): Promise<PasskeyCredential[]> {
|
||||
return [...this.passkeys.values()].filter((item) => item.userId === userId).map(clone);
|
||||
}
|
||||
async deletePasskey(id: string): Promise<void> {
|
||||
this.passkeys.delete(id);
|
||||
}
|
||||
|
||||
async createOAuthAccount(account: OAuthAccount): Promise<void> {
|
||||
if (
|
||||
this.oauthAccounts.has(account.id) ||
|
||||
[...this.oauthAccounts.values()].some(
|
||||
(item) =>
|
||||
item.provider === account.provider &&
|
||||
item.providerAccountId === account.providerAccountId,
|
||||
)
|
||||
) {
|
||||
throw new Error("WRN-AUTH-OAUTH-ACCOUNT-EXISTS");
|
||||
}
|
||||
this.oauthAccounts.set(account.id, clone(account));
|
||||
}
|
||||
async updateOAuthAccount(account: OAuthAccount): Promise<void> {
|
||||
const current = this.oauthAccounts.get(account.id);
|
||||
if (!current) throw new Error("WRN-AUTH-OAUTH-ACCOUNT-MISSING");
|
||||
if (
|
||||
account.userId !== current.userId ||
|
||||
account.provider !== current.provider ||
|
||||
account.providerAccountId !== current.providerAccountId ||
|
||||
account.createdAt !== current.createdAt
|
||||
) {
|
||||
throw new Error("WRN-AUTH-OAUTH-ACCOUNT-IMMUTABLE");
|
||||
}
|
||||
this.oauthAccounts.set(account.id, clone(account));
|
||||
}
|
||||
async findOAuthAccount(
|
||||
provider: string,
|
||||
providerAccountId: string,
|
||||
): Promise<OAuthAccount | undefined> {
|
||||
const value = [...this.oauthAccounts.values()].find(
|
||||
(item) => item.provider === provider && item.providerAccountId === providerAccountId,
|
||||
);
|
||||
return value ? clone(value) : undefined;
|
||||
}
|
||||
async listOAuthAccounts(userId: string): Promise<OAuthAccount[]> {
|
||||
return [...this.oauthAccounts.values()].filter((item) => item.userId === userId).map(clone);
|
||||
}
|
||||
async deleteOAuthAccount(id: string): Promise<void> {
|
||||
this.oauthAccounts.delete(id);
|
||||
}
|
||||
|
||||
async createLoginAttempt(attempt: LoginAttempt): Promise<void> {
|
||||
if (this.attempts.some((item) => item.id === attempt.id)) {
|
||||
throw new Error("WRN-AUTH-LOGIN-ATTEMPT-EXISTS");
|
||||
}
|
||||
const identifier = attempt.identifier
|
||||
? normalizeIdentity(inferIdentityType(attempt.identifier), attempt.identifier)
|
||||
: undefined;
|
||||
this.attempts.push(clone({ ...attempt, identifier }));
|
||||
}
|
||||
async listRecentLoginAttempts(identifier: string, since: number): Promise<LoginAttempt[]> {
|
||||
const normalized = normalizeIdentity(inferIdentityType(identifier), identifier);
|
||||
return this.attempts
|
||||
.filter((item) => {
|
||||
if (item.createdAt < since || !item.identifier) return false;
|
||||
return (
|
||||
normalizeIdentity(inferIdentityType(item.identifier), item.identifier) === normalized
|
||||
);
|
||||
})
|
||||
.map(clone);
|
||||
}
|
||||
|
||||
async createSecurityEvent(event: AuthSecurityEvent): Promise<void> {
|
||||
if (this.events.some((item) => item.id === event.id)) {
|
||||
throw new Error("WRN-AUTH-SECURITY-EVENT-EXISTS");
|
||||
}
|
||||
this.events.push(clone(event));
|
||||
}
|
||||
async listSecurityEvents(userId: string, limit = 100): Promise<AuthSecurityEvent[]> {
|
||||
return this.events
|
||||
.filter((item) => item.userId === userId)
|
||||
.sort((left, right) => right.createdAt - left.createdAt)
|
||||
.slice(0, limit)
|
||||
.map(clone);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user