release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
+10 -7
View File
@@ -1,8 +1,4 @@
import type {
CaptchaChallengeRecord,
CaptchaResponseTokenRecord,
CaptchaStore,
} from "../types.ts";
import type { CaptchaChallengeRecord, CaptchaResponseTokenRecord, CaptchaStore } from "../types.ts";
function cloneChallenge(record: CaptchaChallengeRecord): CaptchaChallengeRecord {
return structuredClone(record);
@@ -72,7 +68,10 @@ export class MemoryCaptchaStore implements CaptchaStore {
return record ? cloneToken(record) : undefined;
}
async consumeToken(tokenHash: string, now: number): Promise<CaptchaResponseTokenRecord | undefined> {
async consumeToken(
tokenHash: string,
now: number,
): Promise<CaptchaResponseTokenRecord | undefined> {
const record = this.tokens.get(tokenHash);
if (!record || record.expiresAt <= now || record.consumedAt) return undefined;
record.consumedAt = now;
@@ -96,7 +95,11 @@ export class MemoryCaptchaStore implements CaptchaStore {
}
}
private evict<T extends { expiresAt: number }>(map: Map<string, T>, max: number, now: number): void {
private evict<T extends { expiresAt: number }>(
map: Map<string, T>,
max: number,
now: number,
): void {
for (const [key, record] of map) if (record.expiresAt <= now) map.delete(key);
while (map.size >= max) map.delete(map.keys().next().value!);
}
+11 -8
View File
@@ -1,8 +1,4 @@
import type {
CaptchaChallengeRecord,
CaptchaResponseTokenRecord,
CaptchaStore,
} from "../types.ts";
import type { CaptchaChallengeRecord, CaptchaResponseTokenRecord, CaptchaStore } from "../types.ts";
export interface RedisCaptchaClient {
get(key: string): Promise<string | null> | string | null;
@@ -94,7 +90,10 @@ export class RedisCaptchaStore implements CaptchaStore {
return this.read<CaptchaResponseTokenRecord>(this.tokenKey(tokenHash));
}
async consumeToken(tokenHash: string, now: number): Promise<CaptchaResponseTokenRecord | undefined> {
async consumeToken(
tokenHash: string,
now: number,
): Promise<CaptchaResponseTokenRecord | undefined> {
const key = this.tokenKey(tokenHash);
if (this.redis.eval) {
const raw = await this.redis.eval(CONSUME_TOKEN, {
@@ -107,7 +106,9 @@ export class RedisCaptchaStore implements CaptchaStore {
const current = await this.read<CaptchaResponseTokenRecord>(key);
if (!current || current.expiresAt <= now || current.consumedAt) return undefined;
current.consumedAt = now;
await this.redis.set(key, JSON.stringify(current), { px: Math.max(1, current.expiresAt - now) });
await this.redis.set(key, JSON.stringify(current), {
px: Math.max(1, current.expiresAt - now),
});
return current;
});
}
@@ -138,7 +139,9 @@ export class RedisCaptchaStore implements CaptchaStore {
if (!current || current.expiresAt <= now || current.consumedAt) return undefined;
if (operation === "attempt") current.attempts += 1;
else current.consumedAt = now;
await this.redis.set(key, JSON.stringify(current), { px: Math.max(1, current.expiresAt - now) });
await this.redis.set(key, JSON.stringify(current), {
px: Math.max(1, current.expiresAt - now),
});
return current;
});
}
+23 -11
View File
@@ -1,8 +1,4 @@
import type {
CaptchaChallengeRecord,
CaptchaResponseTokenRecord,
CaptchaStore,
} from "../types.ts";
import type { CaptchaChallengeRecord, CaptchaResponseTokenRecord, CaptchaStore } from "../types.ts";
export interface SqliteStatementLike {
run(...params: unknown[]): unknown;
@@ -21,16 +17,21 @@ export interface SqliteCaptchaStoreOptions {
}
function safeIdentifier(value: string): string {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) throw new TypeError(`Unsafe SQL identifier: ${value}`);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value))
throw new TypeError(`Unsafe SQL identifier: ${value}`);
return value;
}
function parseChallenge(row: Record<string, unknown> | undefined): CaptchaChallengeRecord | undefined {
function parseChallenge(
row: Record<string, unknown> | undefined,
): CaptchaChallengeRecord | undefined {
if (!row) return undefined;
return JSON.parse(String(row.payload)) as CaptchaChallengeRecord;
}
function parseToken(row: Record<string, unknown> | undefined): CaptchaResponseTokenRecord | undefined {
function parseToken(
row: Record<string, unknown> | undefined,
): CaptchaResponseTokenRecord | undefined {
if (!row) return undefined;
return JSON.parse(String(row.payload)) as CaptchaResponseTokenRecord;
}
@@ -77,7 +78,13 @@ export class SqliteCaptchaStore implements CaptchaStore {
(id, payload, expires_at, consumed_at, attempts)
VALUES (?, ?, ?, ?, ?)`,
)
.run(record.id, JSON.stringify(record), record.expiresAt, record.consumedAt ?? null, record.attempts);
.run(
record.id,
JSON.stringify(record),
record.expiresAt,
record.consumedAt ?? null,
record.attempts,
);
}
async getChallenge(id: string): Promise<CaptchaChallengeRecord | undefined> {
@@ -96,7 +103,9 @@ export class SqliteCaptchaStore implements CaptchaStore {
SET payload = ?, attempts = ?
WHERE id = ? AND expires_at > ? AND consumed_at IS NULL AND attempts = ?`,
)
.run(JSON.stringify(current), current.attempts, id, now, current.attempts - 1) as { changes?: number };
.run(JSON.stringify(current), current.attempts, id, now, current.attempts - 1) as {
changes?: number;
};
return result?.changes === 0 ? undefined : current;
}
@@ -134,7 +143,10 @@ export class SqliteCaptchaStore implements CaptchaStore {
);
}
async consumeToken(tokenHash: string, now: number): Promise<CaptchaResponseTokenRecord | undefined> {
async consumeToken(
tokenHash: string,
now: number,
): Promise<CaptchaResponseTokenRecord | undefined> {
const current = await this.getToken(tokenHash);
if (!current || current.expiresAt <= now || current.consumedAt) return undefined;
current.consumedAt = now;