Add generateSecureBase58Token to shared utils with login_ prefix
- Create packages/utils/src/generate-base58-token.ts with typed prefix support
- Function returns `${TPrefix}${string}` for type-safe prefixed tokens
- Add isBase58() validator and parseBase58Token() helper
- Add comprehensive tests (13 test cases)
- Update login request tokens to use "login_" prefix
- Fix login-password.ts to not replace token (cookie/DB mismatch bug)
- Migrate all token generation from generateSecureToken (hex) to
generateSecureBase58Token (base58)
- Remove duplicate token generation from api-server/utils/crypto.ts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,9 @@ import {
|
|||||||
setCookie,
|
setCookie,
|
||||||
} from "../../utils/cookies.js";
|
} from "../../utils/cookies.js";
|
||||||
import {
|
import {
|
||||||
generateBase58Token,
|
|
||||||
generateDeviceFingerprint,
|
generateDeviceFingerprint,
|
||||||
generateExpiry,
|
generateExpiry,
|
||||||
|
generateSecureBase58Token,
|
||||||
} from "../../utils/crypto.js";
|
} from "../../utils/crypto.js";
|
||||||
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
|
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
|
||||||
import { isDeviceTrusted } from "../../utils/session.js";
|
import { isDeviceTrusted } from "../../utils/session.js";
|
||||||
@@ -62,7 +62,7 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
// Generate placeholder token (base58) for anti-enumeration
|
// Generate placeholder token (base58) for anti-enumeration
|
||||||
// This prevents attackers from knowing if an email exists based on response
|
// This prevents attackers from knowing if an email exists based on response
|
||||||
const placeholderToken = generateBase58Token();
|
const placeholderToken = generateSecureBase58Token("login_");
|
||||||
|
|
||||||
// Set placeholder login request token cookie
|
// Set placeholder login request token cookie
|
||||||
setCookie(
|
setCookie(
|
||||||
@@ -107,7 +107,7 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
|
|||||||
|
|
||||||
// Create login request with secure token
|
// Create login request with secure token
|
||||||
const expiresAt = generateExpiry(COOKIE_DURATIONS.LOGIN_REQUEST);
|
const expiresAt = generateExpiry(COOKIE_DURATIONS.LOGIN_REQUEST);
|
||||||
const token = generateBase58Token();
|
const token = generateSecureBase58Token("login_");
|
||||||
|
|
||||||
await context.db
|
await context.db
|
||||||
.insertInto("login_requests")
|
.insertInto("login_requests")
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
|
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
|
||||||
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
|
import {
|
||||||
|
generateExpiry,
|
||||||
|
generateSecureBase58Token,
|
||||||
|
} from "../../utils/crypto.js";
|
||||||
import { sendPasswordResetEmail } from "../../utils/email.js";
|
import { sendPasswordResetEmail } from "../../utils/email.js";
|
||||||
import { os } from "../base.js";
|
import { os } from "../base.js";
|
||||||
|
|
||||||
@@ -33,8 +36,8 @@ export const forgotPassword = os.auth.forgotPassword.handler(
|
|||||||
.where("user_id", "=", user.id)
|
.where("user_id", "=", user.id)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// Generate secure token (64 hex chars)
|
// Generate secure base58 token
|
||||||
const token = generateSecureToken();
|
const token = generateSecureBase58Token();
|
||||||
|
|
||||||
// Create password reset record with 1 hour expiry
|
// Create password reset record with 1 hour expiry
|
||||||
const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET);
|
const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
import { ORPCError } from "@orpc/server";
|
import { ORPCError } from "@orpc/server";
|
||||||
import { COOKIE_NAMES, getCookie } from "../../utils/cookies.js";
|
import { COOKIE_NAMES, getCookie } from "../../utils/cookies.js";
|
||||||
import { generateSecureToken } from "../../utils/crypto.js";
|
|
||||||
import { sendLoginConfirmationEmail } from "../../utils/email.js";
|
import { sendLoginConfirmationEmail } from "../../utils/email.js";
|
||||||
import { verifyPassword } from "../../utils/password.js";
|
import { verifyPassword } from "../../utils/password.js";
|
||||||
import { isDeviceTrusted } from "../../utils/session.js";
|
import { isDeviceTrusted } from "../../utils/session.js";
|
||||||
@@ -47,6 +46,7 @@ export const loginPassword = os.auth.loginPassword.handler(
|
|||||||
"login_requests.id",
|
"login_requests.id",
|
||||||
"login_requests.user_id",
|
"login_requests.user_id",
|
||||||
"login_requests.email",
|
"login_requests.email",
|
||||||
|
"login_requests.token",
|
||||||
"login_requests.device_fingerprint",
|
"login_requests.device_fingerprint",
|
||||||
"login_requests.expires_at",
|
"login_requests.expires_at",
|
||||||
"login_requests.completed_at",
|
"login_requests.completed_at",
|
||||||
@@ -106,19 +106,9 @@ export const loginPassword = os.auth.loginPassword.handler(
|
|||||||
.where("id", "=", result.id)
|
.where("id", "=", result.id)
|
||||||
.execute();
|
.execute();
|
||||||
} else {
|
} else {
|
||||||
// Device is untrusted - generate confirmation token and send email
|
// Device is untrusted - send confirmation email with existing token
|
||||||
const confirmationToken = generateSecureToken();
|
// The same base58 token is used for both cookie lookup and email confirmation
|
||||||
|
await sendLoginConfirmationEmail(result.email, result.token);
|
||||||
await context.db
|
|
||||||
.updateTable("login_requests")
|
|
||||||
.set({
|
|
||||||
token: confirmationToken,
|
|
||||||
})
|
|
||||||
.where("id", "=", result.id)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
// Send login confirmation email
|
|
||||||
await sendLoginConfirmationEmail(result.email, confirmationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return void (success)
|
// Return void (success)
|
||||||
|
|||||||
@@ -5,13 +5,16 @@
|
|||||||
* Flow:
|
* Flow:
|
||||||
* 1. Check if email is already verified (return early if so)
|
* 1. Check if email is already verified (return early if so)
|
||||||
* 2. Delete any existing verification tokens for this user
|
* 2. Delete any existing verification tokens for this user
|
||||||
* 3. Generate new secure token (64 hex chars)
|
* 3. Generate new secure base58 token
|
||||||
* 4. Create new email_verifications record with 24 hour expiry
|
* 4. Create new email_verifications record with 24 hour expiry
|
||||||
* 5. Send verification email (stubbed)
|
* 5. Send verification email (stubbed)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
|
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
|
||||||
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
|
import {
|
||||||
|
generateExpiry,
|
||||||
|
generateSecureBase58Token,
|
||||||
|
} from "../../utils/crypto.js";
|
||||||
import { sendVerificationEmail } from "../../utils/email.js";
|
import { sendVerificationEmail } from "../../utils/email.js";
|
||||||
import { authMiddleware, os } from "../base.js";
|
import { authMiddleware, os } from "../base.js";
|
||||||
|
|
||||||
@@ -30,8 +33,8 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail
|
|||||||
.where("user_id", "=", context.user.id)
|
.where("user_id", "=", context.user.id)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// Generate new secure token
|
// Generate new secure base58 token
|
||||||
const token = generateSecureToken();
|
const token = generateSecureBase58Token();
|
||||||
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
|
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
|
||||||
|
|
||||||
// Create new verification record
|
// Create new verification record
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import {
|
|||||||
setCookie,
|
setCookie,
|
||||||
TOKEN_DURATIONS,
|
TOKEN_DURATIONS,
|
||||||
} from "../../utils/cookies.js";
|
} from "../../utils/cookies.js";
|
||||||
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
|
import {
|
||||||
|
generateExpiry,
|
||||||
|
generateSecureBase58Token,
|
||||||
|
} from "../../utils/crypto.js";
|
||||||
import { sendVerificationEmail } from "../../utils/email.js";
|
import { sendVerificationEmail } from "../../utils/email.js";
|
||||||
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
|
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
|
||||||
import { hashPassword, validatePassword } from "../../utils/password.js";
|
import { hashPassword, validatePassword } from "../../utils/password.js";
|
||||||
@@ -262,7 +265,7 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Generate verification token
|
// Generate verification token
|
||||||
const verificationToken = generateSecureToken();
|
const verificationToken = generateSecureBase58Token();
|
||||||
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
|
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
|
||||||
|
|
||||||
// Store verification token (store raw token, not hash - it's already high-entropy)
|
// Store verification token (store raw token, not hash - it's already high-entropy)
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
import { ORPCError } from "@orpc/server";
|
import { ORPCError } from "@orpc/server";
|
||||||
import { ORG_INVITE_EXPIRY_DAYS } from "../../constants.js";
|
import { ORG_INVITE_EXPIRY_DAYS } from "../../constants.js";
|
||||||
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
|
import {
|
||||||
|
generateExpiry,
|
||||||
|
generateSecureBase58Token,
|
||||||
|
} from "../../utils/crypto.js";
|
||||||
import { sendOrgInviteEmail } from "../../utils/email.js";
|
import { sendOrgInviteEmail } from "../../utils/email.js";
|
||||||
import { authMiddleware, os } from "../base.js";
|
import { authMiddleware, os } from "../base.js";
|
||||||
import { getMembership, lookupOrgBySlug, requireRole } from "./helpers.js";
|
import { getMembership, lookupOrgBySlug, requireRole } from "./helpers.js";
|
||||||
@@ -88,7 +91,7 @@ export const invitesCreate = os.orgs.invites.create
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate invite token and expiry
|
// Generate invite token and expiry
|
||||||
const token = generateSecureToken();
|
const token = generateSecureBase58Token();
|
||||||
const expiresAt = generateExpiry(ORG_INVITE_EXPIRY_DAYS * 24 * 60 * 60);
|
const expiresAt = generateExpiry(ORG_INVITE_EXPIRY_DAYS * 24 * 60 * 60);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { base58 } from "@scure/base";
|
import { base58 } from "@scure/base";
|
||||||
|
|
||||||
|
// Re-export generateSecureBase58Token from shared utils
|
||||||
|
export { generateSecureBase58Token } from "@reviq/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token prefix for all RevIQ API tokens
|
* Token prefix for all RevIQ API tokens
|
||||||
*/
|
*/
|
||||||
@@ -62,58 +65,6 @@ export const generateDeviceFingerprint = (): string => {
|
|||||||
return crypto.randomUUID();
|
return crypto.randomUUID();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a secure random token for email verification, password reset, etc.
|
|
||||||
* Uses 32 bytes (256 bits) of entropy
|
|
||||||
* Uses Web Crypto API for Cloudflare Workers compatibility
|
|
||||||
*/
|
|
||||||
export const generateSecureToken = (): string => {
|
|
||||||
const bytes = new Uint8Array(32);
|
|
||||||
crypto.getRandomValues(bytes);
|
|
||||||
return Array.from(bytes)
|
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
|
||||||
.join("");
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base58 alphabet (Bitcoin-style, no 0, O, I, l)
|
|
||||||
*/
|
|
||||||
const BASE58_ALPHABET =
|
|
||||||
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a cryptographically secure base58 token
|
|
||||||
* Uses 24 bytes (192 bits) of entropy, producing ~33 character output
|
|
||||||
*/
|
|
||||||
export const generateBase58Token = (byteLength = 24): string => {
|
|
||||||
const bytes = new Uint8Array(byteLength);
|
|
||||||
crypto.getRandomValues(bytes);
|
|
||||||
|
|
||||||
// Convert bytes to base58
|
|
||||||
let result = "";
|
|
||||||
let num = BigInt(0);
|
|
||||||
for (const byte of bytes) {
|
|
||||||
num = num * 256n + BigInt(byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (num > 0n) {
|
|
||||||
const remainder = Number(num % 58n);
|
|
||||||
result = BASE58_ALPHABET.charAt(remainder) + result;
|
|
||||||
num /= 58n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle leading zeros
|
|
||||||
for (const byte of bytes) {
|
|
||||||
if (byte === 0) {
|
|
||||||
result = BASE58_ALPHABET.charAt(0) + result;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate expiration date
|
* Generate expiration date
|
||||||
*/
|
*/
|
||||||
|
|||||||
105
packages/utils/src/generate-base58-token.test.ts
Normal file
105
packages/utils/src/generate-base58-token.test.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
generateSecureBase58Token,
|
||||||
|
isBase58,
|
||||||
|
parseBase58Token,
|
||||||
|
} from "./generate-base58-token.js";
|
||||||
|
|
||||||
|
const BASE58_ALPHABET =
|
||||||
|
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
describe("isBase58", () => {
|
||||||
|
it("should return true for valid base58 strings", () => {
|
||||||
|
expect(isBase58("123456789")).toBe(true);
|
||||||
|
expect(isBase58("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(isBase58("9JKmn")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for strings with invalid characters", () => {
|
||||||
|
// 0, O, I, l are not in base58 alphabet
|
||||||
|
expect(isBase58("0")).toBe(false);
|
||||||
|
expect(isBase58("O")).toBe(false);
|
||||||
|
expect(isBase58("I")).toBe(false);
|
||||||
|
expect(isBase58("l")).toBe(false);
|
||||||
|
expect(isBase58("abc0def")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for empty strings", () => {
|
||||||
|
expect(isBase58("")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for strings with special characters", () => {
|
||||||
|
expect(isBase58("abc+def")).toBe(false);
|
||||||
|
expect(isBase58("abc/def")).toBe(false);
|
||||||
|
expect(isBase58("abc=def")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateSecureBase58Token", () => {
|
||||||
|
it("should generate a valid base58 token", () => {
|
||||||
|
const token = generateSecureBase58Token();
|
||||||
|
expect(isBase58(token)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate tokens of expected length (~33 chars for 24 bytes)", () => {
|
||||||
|
const token = generateSecureBase58Token();
|
||||||
|
// 24 bytes = 192 bits, base58 encoding gives roughly 33 chars
|
||||||
|
expect(token.length).toBeGreaterThanOrEqual(30);
|
||||||
|
expect(token.length).toBeLessThanOrEqual(35);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate unique tokens", () => {
|
||||||
|
const tokens = new Set<string>();
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
tokens.add(generateSecureBase58Token());
|
||||||
|
}
|
||||||
|
expect(tokens.size).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only use base58 alphabet characters", () => {
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const token = generateSecureBase58Token();
|
||||||
|
for (const char of token) {
|
||||||
|
expect(BASE58_ALPHABET.includes(char)).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prepend prefix when provided", () => {
|
||||||
|
const token = generateSecureBase58Token("lrt_");
|
||||||
|
expect(token.startsWith("lrt_")).toBe(true);
|
||||||
|
// The part after prefix should be valid base58
|
||||||
|
expect(isBase58(token.slice(4))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseBase58Token", () => {
|
||||||
|
it("should parse token with correct prefix", () => {
|
||||||
|
const token = generateSecureBase58Token("lrt_");
|
||||||
|
const parsed = parseBase58Token(token, "lrt_");
|
||||||
|
expect(parsed).not.toBeNull();
|
||||||
|
if (parsed !== null) {
|
||||||
|
expect(isBase58(parsed)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for wrong prefix", () => {
|
||||||
|
const token = generateSecureBase58Token("lrt_");
|
||||||
|
const parsed = parseBase58Token(token, "wrong_");
|
||||||
|
expect(parsed).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for missing prefix", () => {
|
||||||
|
const token = generateSecureBase58Token();
|
||||||
|
const parsed = parseBase58Token(token, "lrt_");
|
||||||
|
expect(parsed).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for invalid base58 after prefix", () => {
|
||||||
|
const invalidToken = "lrt_abc0def"; // 0 is not valid base58
|
||||||
|
const parsed = parseBase58Token(invalidToken, "lrt_");
|
||||||
|
expect(parsed).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
75
packages/utils/src/generate-base58-token.ts
Normal file
75
packages/utils/src/generate-base58-token.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Generate cryptographically secure base58 tokens
|
||||||
|
* Uses Bitcoin-style base58 alphabet (no 0, O, I, l to avoid confusion)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASE58_ALPHABET =
|
||||||
|
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is valid base58
|
||||||
|
*/
|
||||||
|
export const isBase58 = (str: string): boolean => {
|
||||||
|
for (const char of str) {
|
||||||
|
if (!BASE58_ALPHABET.includes(char)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cryptographically secure base58 token
|
||||||
|
* Uses 24 bytes (192 bits) of entropy, producing ~33 character output
|
||||||
|
*
|
||||||
|
* @param prefix - Optional prefix to prepend (e.g., "login_" for login request tokens)
|
||||||
|
*/
|
||||||
|
export function generateSecureBase58Token<TPrefix extends string = "">(
|
||||||
|
prefix?: TPrefix,
|
||||||
|
): `${TPrefix}${string}` {
|
||||||
|
const byteLength = 24;
|
||||||
|
const bytes = new Uint8Array(byteLength);
|
||||||
|
crypto.getRandomValues(bytes);
|
||||||
|
|
||||||
|
// Convert bytes to base58
|
||||||
|
let result = "";
|
||||||
|
let num = BigInt(0);
|
||||||
|
for (const byte of bytes) {
|
||||||
|
num = num * 256n + BigInt(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (num > 0n) {
|
||||||
|
const remainder = Number(num % 58n);
|
||||||
|
result = BASE58_ALPHABET.charAt(remainder) + result;
|
||||||
|
num /= 58n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle leading zeros
|
||||||
|
for (const byte of bytes) {
|
||||||
|
if (byte === 0) {
|
||||||
|
result = BASE58_ALPHABET.charAt(0) + result;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix ?? ""}${result}` as `${TPrefix}${string}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a token with an expected prefix
|
||||||
|
* Returns the token without prefix if valid, null if invalid
|
||||||
|
*/
|
||||||
|
export const parseBase58Token = (
|
||||||
|
token: string,
|
||||||
|
expectedPrefix: string,
|
||||||
|
): string | null => {
|
||||||
|
if (!token.startsWith(expectedPrefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const base58Part = token.slice(expectedPrefix.length);
|
||||||
|
if (!isBase58(base58Part)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return base58Part;
|
||||||
|
};
|
||||||
@@ -1 +1,6 @@
|
|||||||
|
export {
|
||||||
|
generateSecureBase58Token,
|
||||||
|
isBase58,
|
||||||
|
parseBase58Token,
|
||||||
|
} from "./generate-base58-token.js";
|
||||||
export { hashPassword, verifyPassword } from "./hash-password.js";
|
export { hashPassword, verifyPassword } from "./hash-password.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user