diff --git a/bun.lock b/bun.lock index 410ab2a2..00d8776c 100644 --- a/bun.lock +++ b/bun.lock @@ -235,7 +235,7 @@ }, "packages/auth": { "name": "@wrnexus/auth", - "version": "0.8.17", + "version": "0.8.18", "dependencies": { "@wrnexus/authz": "workspace:*", "@wrnexus/captcha": "workspace:*", @@ -342,7 +342,7 @@ }, "packages/csr": { "name": "@wrnexus/csr", - "version": "0.8.31", + "version": "0.8.32", "dependencies": { "@wrnexus/core": "workspace:*", }, @@ -501,7 +501,7 @@ }, "packages/metering": { "name": "@wrnexus/metering", - "version": "0.8.2", + "version": "0.8.3", }, "packages/mobile": { "name": "@wrnexus/mobile", diff --git a/packages/auth/package.json b/packages/auth/package.json index a9d60ce1..cad9d490 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/auth", - "version": "0.8.17", + "version": "0.8.18", "description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.", "type": "module", "sideEffects": false, diff --git a/packages/auth/src/engine.ts b/packages/auth/src/engine.ts index ed44709d..e1b74b82 100644 --- a/packages/auth/src/engine.ts +++ b/packages/auth/src/engine.ts @@ -1672,6 +1672,11 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine { (typeof metadata.deviceId === "string" ? metadata.deviceId : undefined), trusted: Boolean(rememberedDevice) || input.session?.trusted === true, fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined, + metadata: { + ...(input.session?.metadata ?? {}), + mfaVerifiedAt: now(), + mfaMethod: input.method, + }, }); user.lastLoginAt = now(); user.updatedAt = now(); diff --git a/packages/csr/package.json b/packages/csr/package.json index e1c3604b..0ed2eba1 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.8.31", + "version": "0.8.32", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/src/index.ts b/packages/csr/src/index.ts index 7d0e0438..47e85ac6 100644 --- a/packages/csr/src/index.ts +++ b/packages/csr/src/index.ts @@ -19,6 +19,21 @@ export { ACTION_RUNTIME } from "./action-runtime.ts"; export { createApiClient } from "./api-client.ts"; export type { ApiClientOptions, ApiRequest } from "./api-client.ts"; +/** Public client transport used by `.wrn` client functions for dynamic URLs. */ +export function useFetch( + path: string, + methodOrOptions?: + string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, + input?: unknown, +): Promise { + const client = ( + globalThis as typeof globalThis & { useFetch?: (...args: unknown[]) => Promise } + ).useFetch; + if (!client) + return Promise.reject(new Error("WRN-CSR: useFetch is only available in the browser runtime")); + return client(path, methodOrOptions, input); +} + const CONTROLLER_SECTIONS = ["PRIMARY", "UI", "PIN"] as const; function runtimeForMode(development: boolean): string { diff --git a/packages/metering/package.json b/packages/metering/package.json index 9a5062f7..e873fdeb 100644 --- a/packages/metering/package.json +++ b/packages/metering/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/metering", - "version": "0.8.2", + "version": "0.8.3", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/metering/src/index.ts b/packages/metering/src/index.ts index dfa555c3..2a65b044 100644 --- a/packages/metering/src/index.ts +++ b/packages/metering/src/index.ts @@ -90,6 +90,10 @@ export function defineMeter(options: MeterOptions) { Number.isSafeInteger(units) && units > 0 ? null : { ok: false, reason: `${label} must be a positive whole number` }; + const validSigned = (units: number): MeterResult | null => + Number.isSafeInteger(units) && units !== 0 + ? null + : { ok: false, reason: `${label} adjustment must be a non-zero whole number` }; const fault = (error: unknown, operation: string): MeterResult => { options.onFault?.(error, operation); return { @@ -138,7 +142,15 @@ export function defineMeter(options: MeterOptions) { add("purchase", subjectId, units, reason, reference), refund: (subjectId: string, units: number, reason: string, reference = "") => add("refund", subjectId, units, reason, reference), - adjust: (subjectId: string, units: number, reason: string, reference = "") => - add("adjust", subjectId, units, reason, reference), + async adjust(subjectId: string, units: number, reason: string, reference = "") { + const invalid = validSigned(units); + if (invalid) return invalid; + try { + await options.store.write(subjectId, units, "adjust", reason, reference); + return { ok: true } as const; + } catch (error) { + return fault(error, "adjust"); + } + }, }; } diff --git a/packages/metering/test/metering.test.ts b/packages/metering/test/metering.test.ts index d6ada45d..8d61186c 100644 --- a/packages/metering/test/metering.test.ts +++ b/packages/metering/test/metering.test.ts @@ -40,3 +40,27 @@ test("meter distinguishes refusals from storage faults", async () => { fault: true, }); }); + +test("adjust accepts signed non-zero amounts while other additions stay positive", async () => { + const writes: number[] = []; + const meter = defineMeter({ + store: { + balance: async () => 0, + reserve: async () => true, + write: async (_subject, units) => { + writes.push(units); + }, + }, + }); + expect(await meter.adjust("u1", -25, "correction")).toEqual({ ok: true }); + expect(await meter.adjust("u1", 10, "correction")).toEqual({ ok: true }); + expect(await meter.adjust("u1", 0, "correction")).toEqual({ + ok: false, + reason: "units adjustment must be a non-zero whole number", + }); + expect(await meter.grant("u1", -1, "invalid")).toEqual({ + ok: false, + reason: "units must be a positive whole number", + }); + expect(writes).toEqual([-25, 10]); +});