feat(sdk): add initial browser and server authentication SDK exports

This commit is contained in:
2026-05-13 23:11:56 +00:00
commit cb41ec6e6f
22 changed files with 10964 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import * as plugins from './plugins.js';
import { IdpSdkAccountDoc, setAccountDocSmartdataDb } from './classes.account-doc.js';
import { PasswordHasher } from './classes.password-hasher.js';
import type { ICreateIdpSdkAccountOptions, IIdpSdkAccount, TIdpAccountAuthSource } from './interfaces.js';
export class SmartdataAccountStore {
constructor(private optionsArg: { smartdataDb: plugins.smartdata.SmartdataDb }) {
setAccountDocSmartdataDb(optionsArg.smartdataDb);
}
public normalizeEmail(emailArg: string): string {
return emailArg.trim().toLowerCase();
}
public async createAccount(optionsArg: ICreateIdpSdkAccountOptions): Promise<IIdpSdkAccount> {
const emailNormalized = this.normalizeEmail(optionsArg.email);
if (!emailNormalized || !emailNormalized.includes('@')) {
throw new Error('A valid account email is required');
}
const existing = await IdpSdkAccountDoc.findByEmailNormalized(emailNormalized);
if (existing) {
throw new Error(`Account already exists for ${emailNormalized}`);
}
const authSources = this.normalizeAuthSources(optionsArg.authSources);
if (authSources.length === 0) {
throw new Error('At least one auth source is required');
}
if (authSources.includes('local') && !optionsArg.password) {
throw new Error('A local password is required for local auth');
}
const now = Date.now();
const doc = new IdpSdkAccountDoc();
doc.id = plugins.crypto.randomUUID();
doc.email = optionsArg.email.trim();
doc.emailNormalized = emailNormalized;
doc.name = optionsArg.name.trim() || doc.email;
doc.role = optionsArg.role;
doc.status = optionsArg.status || 'active';
doc.authSources = authSources;
doc.passwordHash = optionsArg.password ? await PasswordHasher.hashPassword(optionsArg.password) : undefined;
doc.idpSubject = optionsArg.idpSubject;
doc.createdAt = now;
doc.updatedAt = now;
await doc.save();
return doc.toAccount();
}
public async getAccountByEmail(emailArg: string): Promise<IIdpSdkAccount | null> {
const doc = await IdpSdkAccountDoc.findByEmailNormalized(this.normalizeEmail(emailArg));
return doc?.toAccount() || null;
}
public async getAccountById(idArg: string): Promise<IIdpSdkAccount | null> {
const doc = await IdpSdkAccountDoc.findById(idArg);
return doc?.toAccount() || null;
}
public async listAccounts(): Promise<IIdpSdkAccount[]> {
const docs = await IdpSdkAccountDoc.getInstances({});
return docs.map((docArg) => docArg.toAccount());
}
public async hasActiveAdminAccount(): Promise<boolean> {
const admins = await IdpSdkAccountDoc.findAdmins();
return admins.length > 0;
}
public async verifyLocalPassword(accountArg: IIdpSdkAccount, passwordArg: string): Promise<boolean> {
if (accountArg.status !== 'active' || !accountArg.authSources.includes('local')) {
return false;
}
return PasswordHasher.verifyPassword(passwordArg, accountArg.passwordHash);
}
public async updateLoginState(accountIdArg: string, patchArg: { idpSubject?: string }): Promise<IIdpSdkAccount | null> {
const doc = await IdpSdkAccountDoc.findById(accountIdArg);
if (!doc) {
return null;
}
if (patchArg.idpSubject !== undefined) {
doc.idpSubject = patchArg.idpSubject;
}
doc.lastLoginAt = Date.now();
doc.updatedAt = Date.now();
await doc.save();
return doc.toAccount();
}
private normalizeAuthSources(authSourcesArg: TIdpAccountAuthSource[]): TIdpAccountAuthSource[] {
return [...new Set(authSourcesArg.filter((sourceArg) => sourceArg === 'local' || sourceArg === 'idp.global'))];
}
}