@lunora/auth 1.0.0-alpha.64 → 1.0.0-alpha.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.d.mts +33 -1
- package/dist/audit.d.ts +33 -1
- package/dist/audit.mjs +3 -2
- package/dist/index.d.mts +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/AUTH_DO_AUDIT_PATH-C4897amZ.mjs +1 -0
- package/dist/packem_shared/authAuditHook-DG_ZNO53.mjs +1 -0
- package/dist/packem_shared/{createDoAuthWiring-DlOR_nIq.mjs → createDoAuthWiring-acnXUGZr.mjs} +1 -1
- package/package.json +4 -4
- package/dist/packem_shared/AUTH_DO_AUDIT_PATH-DkkUg061.mjs +0 -1
- package/dist/packem_shared/authAuditHook-Dx3sqf3G.mjs +0 -1
package/dist/audit.d.mts
CHANGED
|
@@ -8,7 +8,7 @@ declare const AUTH_AUDIT_TABLE = "__lunora_auth_audit__";
|
|
|
8
8
|
* plugins can record their own events without a union change — the union just
|
|
9
9
|
* gives autocomplete for the common ones.
|
|
10
10
|
*/
|
|
11
|
-
type AuthAuditEvent = "account-link" | "account-unlink" | "email-verification" | "mfa-disable" | "mfa-enable" | "password-change" | "password-reset" | "session-revoke" | "sign-in" | "sign-out" | "sign-up" | "token-refresh" | (string & {});
|
|
11
|
+
type AuthAuditEvent = "account-link" | "account-unlink" | "email-verification" | "mfa-disable" | "mfa-enable" | "password-change" | "password-reset" | "session-revoke" | "sign-in" | "sign-in-initiated" | "sign-out" | "sign-up" | "token-refresh" | (string & {});
|
|
12
12
|
/** Whether the recorded operation succeeded or failed (e.g. a rejected sign-in). */
|
|
13
13
|
type AuthAuditOutcome = "failure" | "success";
|
|
14
14
|
/** One recorded auth/security event, in monotonic `seq` order. */
|
|
@@ -27,6 +27,18 @@ interface AuthAuditEntry {
|
|
|
27
27
|
outcome: AuthAuditOutcome;
|
|
28
28
|
/** Monotonic per-database cursor — strictly increasing, never reused. */
|
|
29
29
|
seq: number;
|
|
30
|
+
/**
|
|
31
|
+
* The identifier (email or username) a sign-in-family request ATTEMPTED,
|
|
32
|
+
* read from the request body. Present for both successful and failed
|
|
33
|
+
* attempts — unlike `actorEmail` (which requires an authenticated
|
|
34
|
+
* session), this is what lets a FAILED credential-stuffing attempt be
|
|
35
|
+
* grouped by target. Same redaction exemption as `actorEmail`: a
|
|
36
|
+
* top-level column, not a `detail` key, because `AUDIT_REDACT_RULES`
|
|
37
|
+
* scrubs email-shaped values inside `detail` regardless of key name —
|
|
38
|
+
* putting it there would erase exactly this datum. Length-capped to 320
|
|
39
|
+
* chars (RFC 5321) since it carries attacker-controlled request-body text.
|
|
40
|
+
*/
|
|
41
|
+
targetEmail?: string;
|
|
30
42
|
/** Wall-clock millis when the event was recorded. */
|
|
31
43
|
ts: number;
|
|
32
44
|
/** Client User-Agent, when present on the request. */
|
|
@@ -40,6 +52,8 @@ interface AppendAuthAuditEntry {
|
|
|
40
52
|
event: string;
|
|
41
53
|
ip?: string;
|
|
42
54
|
outcome: AuthAuditOutcome;
|
|
55
|
+
/** See {@link AuthAuditEntry.targetEmail}. */
|
|
56
|
+
targetEmail?: string;
|
|
43
57
|
ts: number;
|
|
44
58
|
userAgent?: string;
|
|
45
59
|
}
|
|
@@ -72,6 +86,16 @@ interface ReadAuthAuditOptions {
|
|
|
72
86
|
* Create the `__lunora_auth_audit__` table. `seq` is an `AUTOINCREMENT` primary
|
|
73
87
|
* key giving the database a monotonic cursor the Security page pages through.
|
|
74
88
|
* Idempotent, so read and write paths can call it defensively.
|
|
89
|
+
*
|
|
90
|
+
* `target_email` is added via a guarded `ALTER TABLE` rather than baked only
|
|
91
|
+
* into the `CREATE`, mirroring `@lunora/observability`'s
|
|
92
|
+
* `ensureRequestLogTable`/`ensureFunctionMetricsTables` — `CREATE TABLE IF NOT
|
|
93
|
+
* EXISTS` only helps a table that doesn't exist yet, so a database whose audit
|
|
94
|
+
* table predates this column needs the `ALTER` to gain it. SQLite has no `ADD
|
|
95
|
+
* COLUMN IF NOT EXISTS`; the duplicate-column error from a re-run (or from the
|
|
96
|
+
* column already existing on the freshly-created schema above) is swallowed —
|
|
97
|
+
* anything else re-throws, so a genuinely broken executor is not silently
|
|
98
|
+
* papered over.
|
|
75
99
|
*/
|
|
76
100
|
declare const ensureAuthAuditTable: (executor: SqlExecutor) => Promise<void>;
|
|
77
101
|
/**
|
|
@@ -86,6 +110,14 @@ declare const appendAuthAuditEntry: (executor: SqlExecutor, entry: AppendAuthAud
|
|
|
86
110
|
* paged past `sinceSeq`, up to `limit` (clamped to [1, 10000]). Parses each row's
|
|
87
111
|
* `detail` JSON back into an object. Creates the table first so reads on a
|
|
88
112
|
* never-audited database return `[]` instead of throwing.
|
|
113
|
+
*
|
|
114
|
+
* `limit` is NaN-safe: a non-finite/non-number value (e.g. a caller passing
|
|
115
|
+
* `Number.NaN`, or an upstream boundary that failed to reject one) falls back
|
|
116
|
+
* to {@link DEFAULT_READ_LIMIT} rather than reaching `Math.min`/`Math.max`,
|
|
117
|
+
* which both propagate `NaN` and would otherwise bind it as the SQL `LIMIT`
|
|
118
|
+
* parameter. This is the library-level fix; `#readAudit` (`./auth-do`) also
|
|
119
|
+
* rejects a non-numeric `limit` at the boundary with a 400 — this clamp is the
|
|
120
|
+
* safety net for every OTHER caller of this function too.
|
|
89
121
|
*/
|
|
90
122
|
declare const readAuthAuditLog: (executor: SqlExecutor, options?: ReadAuthAuditOptions) => Promise<AuthAuditEntry[]>;
|
|
91
123
|
/**
|
package/dist/audit.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ declare const AUTH_AUDIT_TABLE = "__lunora_auth_audit__";
|
|
|
8
8
|
* plugins can record their own events without a union change — the union just
|
|
9
9
|
* gives autocomplete for the common ones.
|
|
10
10
|
*/
|
|
11
|
-
type AuthAuditEvent = "account-link" | "account-unlink" | "email-verification" | "mfa-disable" | "mfa-enable" | "password-change" | "password-reset" | "session-revoke" | "sign-in" | "sign-out" | "sign-up" | "token-refresh" | (string & {});
|
|
11
|
+
type AuthAuditEvent = "account-link" | "account-unlink" | "email-verification" | "mfa-disable" | "mfa-enable" | "password-change" | "password-reset" | "session-revoke" | "sign-in" | "sign-in-initiated" | "sign-out" | "sign-up" | "token-refresh" | (string & {});
|
|
12
12
|
/** Whether the recorded operation succeeded or failed (e.g. a rejected sign-in). */
|
|
13
13
|
type AuthAuditOutcome = "failure" | "success";
|
|
14
14
|
/** One recorded auth/security event, in monotonic `seq` order. */
|
|
@@ -27,6 +27,18 @@ interface AuthAuditEntry {
|
|
|
27
27
|
outcome: AuthAuditOutcome;
|
|
28
28
|
/** Monotonic per-database cursor — strictly increasing, never reused. */
|
|
29
29
|
seq: number;
|
|
30
|
+
/**
|
|
31
|
+
* The identifier (email or username) a sign-in-family request ATTEMPTED,
|
|
32
|
+
* read from the request body. Present for both successful and failed
|
|
33
|
+
* attempts — unlike `actorEmail` (which requires an authenticated
|
|
34
|
+
* session), this is what lets a FAILED credential-stuffing attempt be
|
|
35
|
+
* grouped by target. Same redaction exemption as `actorEmail`: a
|
|
36
|
+
* top-level column, not a `detail` key, because `AUDIT_REDACT_RULES`
|
|
37
|
+
* scrubs email-shaped values inside `detail` regardless of key name —
|
|
38
|
+
* putting it there would erase exactly this datum. Length-capped to 320
|
|
39
|
+
* chars (RFC 5321) since it carries attacker-controlled request-body text.
|
|
40
|
+
*/
|
|
41
|
+
targetEmail?: string;
|
|
30
42
|
/** Wall-clock millis when the event was recorded. */
|
|
31
43
|
ts: number;
|
|
32
44
|
/** Client User-Agent, when present on the request. */
|
|
@@ -40,6 +52,8 @@ interface AppendAuthAuditEntry {
|
|
|
40
52
|
event: string;
|
|
41
53
|
ip?: string;
|
|
42
54
|
outcome: AuthAuditOutcome;
|
|
55
|
+
/** See {@link AuthAuditEntry.targetEmail}. */
|
|
56
|
+
targetEmail?: string;
|
|
43
57
|
ts: number;
|
|
44
58
|
userAgent?: string;
|
|
45
59
|
}
|
|
@@ -72,6 +86,16 @@ interface ReadAuthAuditOptions {
|
|
|
72
86
|
* Create the `__lunora_auth_audit__` table. `seq` is an `AUTOINCREMENT` primary
|
|
73
87
|
* key giving the database a monotonic cursor the Security page pages through.
|
|
74
88
|
* Idempotent, so read and write paths can call it defensively.
|
|
89
|
+
*
|
|
90
|
+
* `target_email` is added via a guarded `ALTER TABLE` rather than baked only
|
|
91
|
+
* into the `CREATE`, mirroring `@lunora/observability`'s
|
|
92
|
+
* `ensureRequestLogTable`/`ensureFunctionMetricsTables` — `CREATE TABLE IF NOT
|
|
93
|
+
* EXISTS` only helps a table that doesn't exist yet, so a database whose audit
|
|
94
|
+
* table predates this column needs the `ALTER` to gain it. SQLite has no `ADD
|
|
95
|
+
* COLUMN IF NOT EXISTS`; the duplicate-column error from a re-run (or from the
|
|
96
|
+
* column already existing on the freshly-created schema above) is swallowed —
|
|
97
|
+
* anything else re-throws, so a genuinely broken executor is not silently
|
|
98
|
+
* papered over.
|
|
75
99
|
*/
|
|
76
100
|
declare const ensureAuthAuditTable: (executor: SqlExecutor) => Promise<void>;
|
|
77
101
|
/**
|
|
@@ -86,6 +110,14 @@ declare const appendAuthAuditEntry: (executor: SqlExecutor, entry: AppendAuthAud
|
|
|
86
110
|
* paged past `sinceSeq`, up to `limit` (clamped to [1, 10000]). Parses each row's
|
|
87
111
|
* `detail` JSON back into an object. Creates the table first so reads on a
|
|
88
112
|
* never-audited database return `[]` instead of throwing.
|
|
113
|
+
*
|
|
114
|
+
* `limit` is NaN-safe: a non-finite/non-number value (e.g. a caller passing
|
|
115
|
+
* `Number.NaN`, or an upstream boundary that failed to reject one) falls back
|
|
116
|
+
* to {@link DEFAULT_READ_LIMIT} rather than reaching `Math.min`/`Math.max`,
|
|
117
|
+
* which both propagate `NaN` and would otherwise bind it as the SQL `LIMIT`
|
|
118
|
+
* parameter. This is the library-level fix; `#readAudit` (`./auth-do`) also
|
|
119
|
+
* rejects a non-numeric `limit` at the boundary with a 400 — this clamp is the
|
|
120
|
+
* safety net for every OTHER caller of this function too.
|
|
89
121
|
*/
|
|
90
122
|
declare const readAuthAuditLog: (executor: SqlExecutor, options?: ReadAuthAuditOptions) => Promise<AuthAuditEntry[]>;
|
|
91
123
|
/**
|
package/dist/audit.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import{redact as A,standardRules as
|
|
1
|
+
import{redact as A,standardRules as g,piiRules as L}from"@visulima/redact";const u="__lunora_auth_audit__",N=[...g,...L],v=1e3,R=1e4,s=null,o=t=>{if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="bigint"||typeof t=="boolean")return String(t)},p=async t=>{await t.run(`CREATE TABLE IF NOT EXISTS "${u}" (
|
|
2
2
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
3
|
ts REAL NOT NULL,
|
|
4
4
|
event TEXT NOT NULL,
|
|
5
5
|
outcome TEXT NOT NULL,
|
|
6
6
|
actor_id TEXT,
|
|
7
7
|
actor_email TEXT,
|
|
8
|
+
target_email TEXT,
|
|
8
9
|
ip TEXT,
|
|
9
10
|
user_agent TEXT,
|
|
10
11
|
detail TEXT
|
|
11
|
-
)`,[])},
|
|
12
|
+
)`,[]);try{await t.run(`ALTER TABLE "${u}" ADD COLUMN target_email TEXT`,[])}catch(e){if(!(e instanceof Error?e.message.toLowerCase():String(e).toLowerCase()).includes("duplicate column"))throw e}},h=async(t,e,n={})=>{await p(t);let i;return e.detail!==void 0&&(i=n.redactDetail===!1?e.detail:A(e.detail,N)),await t.run(`INSERT INTO "${u}" (ts, event, outcome, actor_id, actor_email, target_email, ip, user_agent, detail) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,[e.ts,e.event,e.outcome,e.actorId??s,e.actorEmail??s,e.targetEmail??s,e.ip??s,e.userAgent??s,i===void 0?s:JSON.stringify(i)]),typeof n.retention=="number"&&n.retention>0&&await t.run(`DELETE FROM "${u}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${u}")`,[n.retention]),{...e,detail:i}},f=async(t,e={})=>{await p(t);const n=Number.isFinite(e.limit)?Math.max(1,Math.min(e.limit,R)):v,i=["seq > ?"],d=[e.sinceSeq??0];return e.actorId!==void 0&&(i.push("actor_id = ?"),d.push(e.actorId)),e.event!==void 0&&(i.push("event = ?"),d.push(e.event)),d.push(n),(await t.all(`SELECT seq, ts, event, outcome, actor_id, actor_email, target_email, ip, user_agent, detail FROM "${u}" WHERE ${i.join(" AND ")} ORDER BY seq DESC LIMIT ?`,d)).map(a=>{const r={event:o(a.event)??"",outcome:a.outcome==="failure"?"failure":"success",seq:Number(a.seq),ts:Number(a.ts)},E=o(a.actor_id),l=o(a.actor_email),c=o(a.target_email),T=o(a.ip),m=o(a.user_agent),_=o(a.detail);return E!==void 0&&(r.actorId=E),l!==void 0&&(r.actorEmail=l),c!==void 0&&(r.targetEmail=c),T!==void 0&&(r.ip=T),m!==void 0&&(r.userAgent=m),_!==void 0&&(r.detail=JSON.parse(_)),r})},O=t=>({read:e=>f(t,e)});export{u as AUTH_AUDIT_TABLE,h as appendAuthAuditEntry,O as createAuthAuditReader,p as ensureAuthAuditTable,f as readAuthAuditLog};
|
package/dist/index.d.mts
CHANGED
|
@@ -502,8 +502,16 @@ interface AuthAuditHookConfig extends AppendAuthAuditOptions {
|
|
|
502
502
|
*/
|
|
503
503
|
onRecord?: (entry: AppendAuthAuditEntry) => Promise<void> | void;
|
|
504
504
|
}
|
|
505
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Structural view of the fields we read off better-auth's after-hook context —
|
|
507
|
+
* kept loose to avoid coupling to internal types.
|
|
508
|
+
*
|
|
509
|
+
* `body` is the parsed request body better-auth's middleware context exposes
|
|
510
|
+
* as `ctx.body` — pinned present and populated (e.g. `.email`) in the after-hook
|
|
511
|
+
* by `__tests__/audit-hooks.behaviour.test.ts` (plan 280 S0).
|
|
512
|
+
*/
|
|
506
513
|
interface AuditHookContext {
|
|
514
|
+
body?: Record<string, unknown>;
|
|
507
515
|
context?: {
|
|
508
516
|
newSession?: {
|
|
509
517
|
session?: {
|
|
@@ -533,12 +541,45 @@ interface AuditHookContext {
|
|
|
533
541
|
* Map a better-auth endpoint path to the security event it represents, or
|
|
534
542
|
* `undefined` for endpoints not worth auditing (session reads, config, …). Match
|
|
535
543
|
* is by suffix so a caller `basePath` prefix (`/api/auth`) never affects it.
|
|
544
|
+
*
|
|
545
|
+
* Sign-in is split by what the endpoint actually DOES (plan 280 §4):
|
|
546
|
+
*
|
|
547
|
+
* - `/sign-in/social`, `/sign-in/magic-link` only DISPATCH — the first mints a
|
|
548
|
+
* provider redirect URL, the second sends an email. Nobody is authenticated
|
|
549
|
+
* yet, so these are `sign-in-initiated`, not `sign-in`.
|
|
550
|
+
* - `/callback/:id` (social + generic-oauth), `/magic-link/verify`, and every
|
|
551
|
+
* `/two-factor/verify-*` (`verify-totp` / `verify-otp` / `verify-backup-code`
|
|
552
|
+
* — all three complete a challenged sign-in the same way) are where a
|
|
553
|
+
* session actually gets issued, so they join credential sign-ins
|
|
554
|
+
* (`/sign-in/email`, `/sign-in/username`, `/sign-in/phone-number`, …) as
|
|
555
|
+
* plain `sign-in`. They were NOT recorded at all before this change.
|
|
556
|
+
*
|
|
557
|
+
* `/oauth2/callback/*` is deliberately NOT matched here: it does not exist as
|
|
558
|
+
* an endpoint in the pinned `1.7.0-rc.2` (checked against the installed
|
|
559
|
+
* `better-auth` and `@better-auth/*` dist — generic-oauth reuses the core
|
|
560
|
+
* `/callback/:id` endpoint, it does not register its own path). `@better-auth/sso`
|
|
561
|
+
* does expose its own `/sso/callback/:providerId`, but `@lunora/auth`'s
|
|
562
|
+
* `plugins.ts` does not currently re-export `sso` — recorded as a gap for a
|
|
563
|
+
* follow-up (plan 280 §9 Q1), not matched here since it is unreachable through
|
|
564
|
+
* this package today.
|
|
536
565
|
*/
|
|
537
566
|
declare const eventForPath: (path: string) => AuthAuditEvent | undefined;
|
|
538
567
|
/**
|
|
539
568
|
* Build the entry a given after-hook context should record, or `undefined` when
|
|
540
569
|
* the path is not an audited security event. Exported for direct unit testing of
|
|
541
570
|
* the classification/extraction without spinning up better-auth.
|
|
571
|
+
*
|
|
572
|
+
* Does NOT attempt to distinguish a 2FA-challenged credential sign-in from a
|
|
573
|
+
* fully successful one (a `sign-in-challenged` event, as an earlier design for
|
|
574
|
+
* this change proposed) — pinned in `__tests__/audit-hooks.behaviour.test.ts`
|
|
575
|
+
* (plan 280 S0): better-auth runs the APP's own `hooks.after` BEFORE the
|
|
576
|
+
* `twoFactor` plugin's own after-hook that rewrites the response to
|
|
577
|
+
* `{ twoFactorRedirect: true }` and nulls `ctx.context.newSession`. By the time
|
|
578
|
+
* THIS hook runs, `context.returned`/`context.newSession` still reflect the
|
|
579
|
+
* pre-interception, fully-successful sign-in — there is nothing here to detect
|
|
580
|
+
* the challenge from. Distinguishing it would need a different seam (e.g. a
|
|
581
|
+
* plugin-ordered-after-`twoFactor` hook, or reading `twoFactor`'s own
|
|
582
|
+
* database state) and is left to a follow-up.
|
|
542
583
|
*/
|
|
543
584
|
declare const buildAuditEntry: (context: AuditHookContext, now?: number) => AppendAuthAuditEntry | undefined;
|
|
544
585
|
/**
|
|
@@ -552,6 +593,33 @@ declare const buildAuditEntry: (context: AuditHookContext, now?: number) => Appe
|
|
|
552
593
|
* hooks: { after: authAuditHook({ executor: d1Executor(env.DB), retention: 100_000 }) },
|
|
553
594
|
* });
|
|
554
595
|
* ```
|
|
596
|
+
*
|
|
597
|
+
* ## Behaviour change (plan 280) — `onRecord`/SIEM consumers keyed on `event` strings, read this
|
|
598
|
+
*
|
|
599
|
+
* | Endpoint | Before | After |
|
|
600
|
+
* | -------------------------------------------------- | ------------------- | --------------------- |
|
|
601
|
+
* | `/sign-in/email` (and other credential sign-ins) | `sign-in` | `sign-in` (unchanged) |
|
|
602
|
+
* | `/sign-in/social` | `sign-in` | `sign-in-initiated` |
|
|
603
|
+
* | `/sign-in/magic-link` | `sign-in` | `sign-in-initiated` |
|
|
604
|
+
* | `/callback/:id` (social + generic-oauth) | _(not recorded)_ | `sign-in` |
|
|
605
|
+
* | `/magic-link/verify` | _(not recorded)_ | `sign-in` |
|
|
606
|
+
* | `/two-factor/verify-totp` / `-otp` / `-backup-code`| _(not recorded)_ | `sign-in` |
|
|
607
|
+
*
|
|
608
|
+
* A caller matching on `event === "sign-in"` now sees FEWER events for
|
|
609
|
+
* `/sign-in/social` and `/sign-in/magic-link` (they never actually authenticated
|
|
610
|
+
* anyone) and MORE events for the four previously-unrecorded completion
|
|
611
|
+
* endpoints — the net effect is a more truthful count, not a strictly larger or
|
|
612
|
+
* smaller one. `sign-in-initiated` is a new event name (open `AuthAuditEvent`
|
|
613
|
+
* union, so no wire/type break, but SIEM rules enumerating event names should
|
|
614
|
+
* add it). A failed sign-in now also carries `targetEmail` (the attempted
|
|
615
|
+
* address/username) when the request body supplied one — see
|
|
616
|
+
* {@link AppendAuthAuditEntry.targetEmail} — so credential-stuffing attempts can
|
|
617
|
+
* be grouped by target even though they never produce an `actorEmail`.
|
|
618
|
+
*
|
|
619
|
+
* NOT changed: `/sign-in/email` under an active 2FA challenge still records
|
|
620
|
+
* plain `sign-in` / `success` (not a distinct `sign-in-challenged` event) — see
|
|
621
|
+
* {@link buildAuditEntry}'s docblock for why that distinction turned out not to
|
|
622
|
+
* be buildable from this hook.
|
|
555
623
|
*/
|
|
556
624
|
declare const authAuditHook: (config: AuthAuditHookConfig) => ReturnType<typeof createAuthMiddleware>;
|
|
557
625
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -502,8 +502,16 @@ interface AuthAuditHookConfig extends AppendAuthAuditOptions {
|
|
|
502
502
|
*/
|
|
503
503
|
onRecord?: (entry: AppendAuthAuditEntry) => Promise<void> | void;
|
|
504
504
|
}
|
|
505
|
-
/**
|
|
505
|
+
/**
|
|
506
|
+
* Structural view of the fields we read off better-auth's after-hook context —
|
|
507
|
+
* kept loose to avoid coupling to internal types.
|
|
508
|
+
*
|
|
509
|
+
* `body` is the parsed request body better-auth's middleware context exposes
|
|
510
|
+
* as `ctx.body` — pinned present and populated (e.g. `.email`) in the after-hook
|
|
511
|
+
* by `__tests__/audit-hooks.behaviour.test.ts` (plan 280 S0).
|
|
512
|
+
*/
|
|
506
513
|
interface AuditHookContext {
|
|
514
|
+
body?: Record<string, unknown>;
|
|
507
515
|
context?: {
|
|
508
516
|
newSession?: {
|
|
509
517
|
session?: {
|
|
@@ -533,12 +541,45 @@ interface AuditHookContext {
|
|
|
533
541
|
* Map a better-auth endpoint path to the security event it represents, or
|
|
534
542
|
* `undefined` for endpoints not worth auditing (session reads, config, …). Match
|
|
535
543
|
* is by suffix so a caller `basePath` prefix (`/api/auth`) never affects it.
|
|
544
|
+
*
|
|
545
|
+
* Sign-in is split by what the endpoint actually DOES (plan 280 §4):
|
|
546
|
+
*
|
|
547
|
+
* - `/sign-in/social`, `/sign-in/magic-link` only DISPATCH — the first mints a
|
|
548
|
+
* provider redirect URL, the second sends an email. Nobody is authenticated
|
|
549
|
+
* yet, so these are `sign-in-initiated`, not `sign-in`.
|
|
550
|
+
* - `/callback/:id` (social + generic-oauth), `/magic-link/verify`, and every
|
|
551
|
+
* `/two-factor/verify-*` (`verify-totp` / `verify-otp` / `verify-backup-code`
|
|
552
|
+
* — all three complete a challenged sign-in the same way) are where a
|
|
553
|
+
* session actually gets issued, so they join credential sign-ins
|
|
554
|
+
* (`/sign-in/email`, `/sign-in/username`, `/sign-in/phone-number`, …) as
|
|
555
|
+
* plain `sign-in`. They were NOT recorded at all before this change.
|
|
556
|
+
*
|
|
557
|
+
* `/oauth2/callback/*` is deliberately NOT matched here: it does not exist as
|
|
558
|
+
* an endpoint in the pinned `1.7.0-rc.2` (checked against the installed
|
|
559
|
+
* `better-auth` and `@better-auth/*` dist — generic-oauth reuses the core
|
|
560
|
+
* `/callback/:id` endpoint, it does not register its own path). `@better-auth/sso`
|
|
561
|
+
* does expose its own `/sso/callback/:providerId`, but `@lunora/auth`'s
|
|
562
|
+
* `plugins.ts` does not currently re-export `sso` — recorded as a gap for a
|
|
563
|
+
* follow-up (plan 280 §9 Q1), not matched here since it is unreachable through
|
|
564
|
+
* this package today.
|
|
536
565
|
*/
|
|
537
566
|
declare const eventForPath: (path: string) => AuthAuditEvent | undefined;
|
|
538
567
|
/**
|
|
539
568
|
* Build the entry a given after-hook context should record, or `undefined` when
|
|
540
569
|
* the path is not an audited security event. Exported for direct unit testing of
|
|
541
570
|
* the classification/extraction without spinning up better-auth.
|
|
571
|
+
*
|
|
572
|
+
* Does NOT attempt to distinguish a 2FA-challenged credential sign-in from a
|
|
573
|
+
* fully successful one (a `sign-in-challenged` event, as an earlier design for
|
|
574
|
+
* this change proposed) — pinned in `__tests__/audit-hooks.behaviour.test.ts`
|
|
575
|
+
* (plan 280 S0): better-auth runs the APP's own `hooks.after` BEFORE the
|
|
576
|
+
* `twoFactor` plugin's own after-hook that rewrites the response to
|
|
577
|
+
* `{ twoFactorRedirect: true }` and nulls `ctx.context.newSession`. By the time
|
|
578
|
+
* THIS hook runs, `context.returned`/`context.newSession` still reflect the
|
|
579
|
+
* pre-interception, fully-successful sign-in — there is nothing here to detect
|
|
580
|
+
* the challenge from. Distinguishing it would need a different seam (e.g. a
|
|
581
|
+
* plugin-ordered-after-`twoFactor` hook, or reading `twoFactor`'s own
|
|
582
|
+
* database state) and is left to a follow-up.
|
|
542
583
|
*/
|
|
543
584
|
declare const buildAuditEntry: (context: AuditHookContext, now?: number) => AppendAuthAuditEntry | undefined;
|
|
544
585
|
/**
|
|
@@ -552,6 +593,33 @@ declare const buildAuditEntry: (context: AuditHookContext, now?: number) => Appe
|
|
|
552
593
|
* hooks: { after: authAuditHook({ executor: d1Executor(env.DB), retention: 100_000 }) },
|
|
553
594
|
* });
|
|
554
595
|
* ```
|
|
596
|
+
*
|
|
597
|
+
* ## Behaviour change (plan 280) — `onRecord`/SIEM consumers keyed on `event` strings, read this
|
|
598
|
+
*
|
|
599
|
+
* | Endpoint | Before | After |
|
|
600
|
+
* | -------------------------------------------------- | ------------------- | --------------------- |
|
|
601
|
+
* | `/sign-in/email` (and other credential sign-ins) | `sign-in` | `sign-in` (unchanged) |
|
|
602
|
+
* | `/sign-in/social` | `sign-in` | `sign-in-initiated` |
|
|
603
|
+
* | `/sign-in/magic-link` | `sign-in` | `sign-in-initiated` |
|
|
604
|
+
* | `/callback/:id` (social + generic-oauth) | _(not recorded)_ | `sign-in` |
|
|
605
|
+
* | `/magic-link/verify` | _(not recorded)_ | `sign-in` |
|
|
606
|
+
* | `/two-factor/verify-totp` / `-otp` / `-backup-code`| _(not recorded)_ | `sign-in` |
|
|
607
|
+
*
|
|
608
|
+
* A caller matching on `event === "sign-in"` now sees FEWER events for
|
|
609
|
+
* `/sign-in/social` and `/sign-in/magic-link` (they never actually authenticated
|
|
610
|
+
* anyone) and MORE events for the four previously-unrecorded completion
|
|
611
|
+
* endpoints — the net effect is a more truthful count, not a strictly larger or
|
|
612
|
+
* smaller one. `sign-in-initiated` is a new event name (open `AuthAuditEvent`
|
|
613
|
+
* union, so no wire/type break, but SIEM rules enumerating event names should
|
|
614
|
+
* add it). A failed sign-in now also carries `targetEmail` (the attempted
|
|
615
|
+
* address/username) when the request body supplied one — see
|
|
616
|
+
* {@link AppendAuthAuditEntry.targetEmail} — so credential-stuffing attempts can
|
|
617
|
+
* be grouped by target even though they never produce an `actorEmail`.
|
|
618
|
+
*
|
|
619
|
+
* NOT changed: `/sign-in/email` under an active 2FA challenge still records
|
|
620
|
+
* plain `sign-in` / `success` (not a distinct `sign-in-challenged` event) — see
|
|
621
|
+
* {@link buildAuditEntry}'s docblock for why that distinction turned out not to
|
|
622
|
+
* be buildable from this hook.
|
|
555
623
|
*/
|
|
556
624
|
declare const authAuditHook: (config: AuthAuditHookConfig) => ReturnType<typeof createAuthMiddleware>;
|
|
557
625
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{s as r,l as o,w as a}from"./packem_shared/adapter-RvDcm0Zy.mjs";import{LunoraAuthAdminError as u,createAuthAdmin as i}from"./packem_shared/LunoraAuthAdminError-CiHsF1qZ.mjs";import{AUTH_AUDIT_TABLE as m,appendAuthAuditEntry as l,createAuthAuditReader as d,ensureAuthAuditTable as h,readAuthAuditLog as E}from"./audit.mjs";import{authAuditHook as p,buildAuditEntry as T,eventForPath as f,withAuthAudit as _}from"./packem_shared/authAuditHook-
|
|
1
|
+
import{s as r,l as o,w as a}from"./packem_shared/adapter-RvDcm0Zy.mjs";import{LunoraAuthAdminError as u,createAuthAdmin as i}from"./packem_shared/LunoraAuthAdminError-CiHsF1qZ.mjs";import{AUTH_AUDIT_TABLE as m,appendAuthAuditEntry as l,createAuthAuditReader as d,ensureAuthAuditTable as h,readAuthAuditLog as E}from"./audit.mjs";import{authAuditHook as p,buildAuditEntry as T,eventForPath as f,withAuthAudit as _}from"./packem_shared/authAuditHook-DG_ZNO53.mjs";import{READ_AUDIT_PATH as D,INTERNAL_SECRET_HEADER as S,RESOLVE_SESSION_PATH as H,LunoraAuthDO as c}from"./packem_shared/AUTH_DO_AUDIT_PATH-C4897amZ.mjs";import{createAuth as L,resolveAuthOptions as P}from"./packem_shared/createAuth-DRtd4q6u.mjs";import{authDoColumnAdditions as I,authDoSchemaStatements as O}from"./packem_shared/authDoColumnAdditions-B8BRbdzn.mjs";import{createDoAuthWiring as y}from"./packem_shared/createDoAuthWiring-acnXUGZr.mjs";import{emailGateDatabaseHooks as g,withEmailGate as v}from"./packem_shared/emailGateDatabaseHooks-DzBD1Qoq.mjs";import{assertEmailAllowed as b,classifyEmail as q,emailGateMiddleware as C,loadEmailDomainLists as F}from"./email-guard.mjs";import{DEFAULT_AUTH_BASE_PATH as k,handleAuthRequest as B}from"./packem_shared/DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs";import{LunoraAuthHeadersError as W,withAuthPlugins as Y}from"./middleware.mjs";import{compileMigrationsSql as z,ensureMigrated as J}from"./packem_shared/compileMigrationsSql-BcvcHAqo.mjs";import{default as Q}from"./schema.mjs";import{sessionPresets as Z,validateSessionPolicy as $}from"./packem_shared/sessionPresets-DpEFjXKV.mjs";import{createSqlAuthStore as te,d1Executor as re}from"./sql-store.mjs";import{createMemoryAuthStore as ae,matchesWhere as Ae}from"./store.mjs";import{TURNSTILE_VERIFY_ENDPOINT as ie,verifyTurnstile as se}from"./turnstile.mjs";import{verifyTurnstileMiddleware as le}from"./turnstile-middleware.mjs";export{m as AUTH_AUDIT_TABLE,D as AUTH_DO_AUDIT_PATH,S as AUTH_DO_SECRET_HEADER,H as AUTH_DO_SESSION_PATH,k as DEFAULT_AUTH_BASE_PATH,u as LunoraAuthAdminError,c as LunoraAuthDO,W as LunoraAuthHeadersError,ie as TURNSTILE_VERIFY_ENDPOINT,l as appendAuthAuditEntry,b as assertEmailAllowed,p as authAuditHook,I as authDoColumnAdditions,O as authDoSchemaStatements,Q as authTables,T as buildAuditEntry,q as classifyEmail,z as compileMigrationsSql,L as createAuth,i as createAuthAdmin,d as createAuthAuditReader,y as createDoAuthWiring,ae as createMemoryAuthStore,te as createSqlAuthStore,re as d1Executor,g as emailGateDatabaseHooks,C as emailGateMiddleware,h as ensureAuthAuditTable,J as ensureMigrated,f as eventForPath,B as handleAuthRequest,F as loadEmailDomainLists,r as lunoraAuthAdapter,o as lunoraD1Adapter,a as lunoraDoAdapter,Ae as matchesWhere,E as readAuthAuditLog,P as resolveAuthOptions,Z as sessionPresets,$ as validateSessionPolicy,se as verifyTurnstile,le as verifyTurnstileMiddleware,_ as withAuthAudit,Y as withAuthPlugins,v as withEmailGate};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{w as i,d as u}from"./adapter-RvDcm0Zy.mjs";import{ensureAuthAuditTable as h,createAuthAuditReader as c}from"../audit.mjs";import{resolveAuthOptions as l,createAuth as f}from"./createAuth-DRtd4q6u.mjs";import{authDoSchemaStatements as d,authDoColumnAdditions as m}from"./authDoColumnAdditions-B8BRbdzn.mjs";import{handleAuthRequest as p}from"./DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs";const A=(o,e)=>{const t=Math.max(o.length,e.length);let r=o.length^e.length;for(let s=0;s<t;s+=1){const n=s<o.length?o.charCodeAt(s):0,a=s<e.length?e.charCodeAt(s):0;r|=n^a}return r===0},b="/__lunora/auth/session",R="/__lunora/auth/audit",S="x-lunora-auth-do-secret",g=o=>{if(o===null||typeof o!="object"||Array.isArray(o))return{error:"body must be a JSON object"};const e={};for(const[t,r]of Object.entries(o))if(t==="limit"||t==="sinceSeq"){if(typeof r!="number"||!Number.isFinite(r))return{error:`"${t}" must be a finite number`};e[t]=r}else if(t==="actorId"||t==="event"){if(typeof r!="string")return{error:`"${t}" must be a string`};e[t]=r}else return{error:`unknown audit read option "${t}"`};return{options:e}};class T{#r;#s;#t;#e;#o=!1;constructor(e,t,r={}){this.#t=e.storage,this.#s=t,this.#r=r}#n(){if(this.#e!==void 0)return this.#e;const e=this.#s();if(!this.#o){const t=l(e);for(const r of d(t))[...this.#t.sql.exec(r)];for(const r of m(t,s=>this.#i(s)))[...this.#t.sql.exec(r)];this.#o=!0}return this.#e=f({...e,database:i(this.#t)}),this.#e}#i(e){return[...this.#t.sql.exec("SELECT name FROM pragma_table_info(?)",e)].map(t=>String(t.name))}async#u(e){if(!this.#a(e))return Response.json({error:"unauthorized"},{status:401});let t;try{t=await e.json()}catch{return Response.json({error:"invalid body"},{status:400})}const r=g(t??{});if("error"in r)return Response.json({error:r.error},{status:400});const s=u(this.#t);await h(s);const n=await c(s).read(r.options);return Response.json({entries:n})}#a(e){const{internalSecret:t}=this.#r;if(t===void 0||t==="")return!1;const r=e.headers.get(S);return r!==null&&A(r,t)}async#h(e){if(!this.#a(e))return Response.json({error:"unauthorized"},{status:401});const t=(await this.#n().api.getSession({headers:e.headers}))?.user.id;return Response.json(t===void 0?{}:{userId:t})}async fetch(e){const t=new URL(e.url);if(t.pathname===b)return this.#h(e);if(t.pathname===R)return this.#u(e);const r=this.#n();return await p(r,e,this.#r.basePath)??Response.json({error:"not an auth route"},{status:404})}}export{S as INTERNAL_SECRET_HEADER,T as LunoraAuthDO,R as READ_AUDIT_PATH,b as RESOLVE_SESSION_PATH};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createAuthMiddleware as u}from"better-auth/api";import{appendAuthAuditEntry as c}from"../audit.mjs";const d=t=>{const i=t.toLowerCase(),e=r=>i===r||i.endsWith(r);if(e("/sign-up/email")||e("/sign-up"))return"sign-up";if(e("/sign-in/social")||e("/sign-in/magic-link"))return"sign-in-initiated";if(i.includes("/sign-in/")||i.includes("/callback/")||e("/magic-link/verify")||i.includes("/two-factor/verify-"))return"sign-in";if(e("/sign-out"))return"sign-out";if(e("/change-password")||e("/set-password"))return"password-change";if(e("/reset-password")||e("/request-password-reset")||e("/forget-password"))return"password-reset";if(e("/verify-email"))return"email-verification";if(i.includes("/two-factor/enable")||i.includes("/totp/enable"))return"mfa-enable";if(i.includes("/two-factor/disable")||i.includes("/totp/disable"))return"mfa-disable";if(e("/refresh-token")||e("/token"))return"token-refresh";if(e("/revoke-session")||e("/revoke-sessions")||e("/revoke-other-sessions"))return"session-revoke";if(e("/link-social"))return"account-link";if(e("/unlink-account"))return"account-unlink"},n=(t,i)=>t.headers?.get(i)??t.request?.headers.get(i)??void 0,l=t=>{const i=n(t,"x-forwarded-for");return n(t,"cf-connecting-ip")??(i===void 0?void 0:i.split(",")[0]?.trim())??n(t,"x-real-ip")},f=t=>{const i=t.context?.newSession??t.context?.session,e=i?.user?.id??i?.session?.userId,r=i?.user?.email;return{...e===void 0?{}:{actorId:e},...r===void 0?{}:{actorEmail:r}}},p=t=>{const i=t.context?.returned;if(i instanceof Error)return"failure";if(typeof i=="object"&&i!==null&&"status"in i){const e=Number(i.status);if(Number.isFinite(e)&&e>=400)return"failure"}return"success"},a=320,g=(t,i)=>{if(i!=="sign-in"&&i!=="sign-in-initiated")return;const e=t.body?.email??t.body?.username;if(!(typeof e!="string"||e.length===0))return e.length>a?e.slice(0,a):e},h=(t,i=Date.now())=>{const e=t.path===void 0?void 0:d(t.path);if(e===void 0)return;const r=l(t),s=n(t,"user-agent"),o=g(t,e);return{...f(t),event:e,outcome:p(t),ts:i,...r===void 0?{}:{ip:r},...o===void 0?{}:{targetEmail:o},...s===void 0?{}:{userAgent:s},detail:{path:t.path}}},v=t=>u(async i=>{try{const e=h(i);if(e!==void 0){const r=await c(t.executor,e,{redactDetail:t.redactDetail,retention:t.retention});t.onRecord!==void 0&&await t.onRecord(r)}}catch(e){console.error("@lunora/auth: audit hook failed to record event",e)}}),w=(t,i)=>{const e=v(i),r=t.hooks?.after,s=r?async o=>(await r(o),e(o)):e;return{...t,hooks:{...t.hooks,after:s}}};export{v as authAuditHook,h as buildAuditEntry,d as eventForPath,w as withAuthAudit};
|
package/dist/packem_shared/{createDoAuthWiring-DlOR_nIq.mjs → createDoAuthWiring-acnXUGZr.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as f,READ_AUDIT_PATH as w}from"./AUTH_DO_AUDIT_PATH-
|
|
1
|
+
import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as f,READ_AUDIT_PATH as w}from"./AUTH_DO_AUDIT_PATH-C4897amZ.mjs";import{DEFAULT_AUTH_BASE_PATH as A}from"./DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs";const m=c=>{const{basePath:d=A,internalSecret:a,namespace:i,objectName:h="auth"}=c,o=()=>{if(i)return i.get(i.idFromName(h))},l=async(t,e,s)=>{if(!a)return;const n=o();if(!n)return;const r=await n.fetch(new Request(new URL(t,e),{body:JSON.stringify(s),headers:{"content-type":"application/json",[u]:a},method:"POST"}));return r.ok?r:void 0};return{auditReader:{read:async t=>{const e=await l(w,"https://auth-do.invalid",t);return e?(await e.json())?.entries??[]:[]}},authHandler:async t=>{if(new URL(t.url).pathname.startsWith(d))return o()?.fetch(t)},resolveIdentity:async t=>{if(!a)return null;const e=o();if(!e)return null;const s=new Headers(t.headers);s.set(u,a);const n=await e.fetch(new Request(new URL(f,t.url),{headers:s}));if(!n.ok)return null;const r=await n.json();return r?.userId?{userId:r.userId}:null}}};export{m as createDoAuthWiring};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/auth",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.66",
|
|
4
4
|
"description": "Auth for Lunora — a thin better-auth wrapper: email/password, OAuth, plugins, D1-backed",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
"@better-auth/oauth-provider": "1.7.0-rc.2",
|
|
103
103
|
"@better-auth/passkey": "1.7.0-rc.2",
|
|
104
104
|
"@better-auth/scim": "1.7.0-rc.2",
|
|
105
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
106
|
-
"@lunora/server": "1.0.0-alpha.
|
|
107
|
-
"@lunora/values": "1.0.0-alpha.
|
|
105
|
+
"@lunora/errors": "1.0.0-alpha.12",
|
|
106
|
+
"@lunora/server": "1.0.0-alpha.58",
|
|
107
|
+
"@lunora/values": "1.0.0-alpha.15",
|
|
108
108
|
"@visulima/disposable-email-domains": "1.0.1",
|
|
109
109
|
"@visulima/email-verifier": "1.0.1",
|
|
110
110
|
"@visulima/free-email-domains": "1.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{w as i,d as h}from"./adapter-RvDcm0Zy.mjs";import{ensureAuthAuditTable as u,createAuthAuditReader as c}from"../audit.mjs";import{resolveAuthOptions as l,createAuth as d}from"./createAuth-DRtd4q6u.mjs";import{authDoSchemaStatements as f,authDoColumnAdditions as m}from"./authDoColumnAdditions-B8BRbdzn.mjs";import{handleAuthRequest as A}from"./DEFAULT_AUTH_BASE_PATH-kIwlEt8i.mjs";const p=(o,t)=>{const s=Math.max(o.length,t.length);let e=o.length^t.length;for(let r=0;r<s;r+=1){const a=r<o.length?o.charCodeAt(r):0,n=r<t.length?t.charCodeAt(r):0;e|=a^n}return e===0},R="/__lunora/auth/session",E="/__lunora/auth/audit",_="x-lunora-auth-do-secret";class j{#e;#r;#t;#s;#o=!1;constructor(t,s,e={}){this.#t=t.storage,this.#r=s,this.#e=e}#a(){if(this.#s!==void 0)return this.#s;const t=this.#r();if(!this.#o){const s=l(t);for(const e of f(s))[...this.#t.sql.exec(e)];for(const e of m(s,r=>this.#i(r)))[...this.#t.sql.exec(e)];this.#o=!0}return this.#s=d({...t,database:i(this.#t)}),this.#s}#i(t){return[...this.#t.sql.exec("SELECT name FROM pragma_table_info(?)",t)].map(s=>String(s.name))}async#h(t){if(!this.#n(t))return Response.json({error:"unauthorized"},{status:401});const s=h(this.#t);await u(s);const e=await t.json(),r=await c(s).read(e??{});return Response.json({entries:r})}#n(t){const{internalSecret:s}=this.#e;if(s===void 0||s==="")return!1;const e=t.headers.get(_);return e!==null&&p(e,s)}async#u(t){if(!this.#n(t))return Response.json({error:"unauthorized"},{status:401});const s=(await this.#a().api.getSession({headers:t.headers}))?.user.id;return Response.json(s===void 0?{}:{userId:s})}async fetch(t){const s=new URL(t.url);if(s.pathname===R)return this.#u(t);if(s.pathname===E)return this.#h(t);const e=this.#a();return await A(e,t,this.#e.basePath)??Response.json({error:"not an auth route"},{status:404})}}export{_ as INTERNAL_SECRET_HEADER,j as LunoraAuthDO,E as READ_AUDIT_PATH,R as RESOLVE_SESSION_PATH};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createAuthMiddleware as a}from"better-auth/api";import{appendAuthAuditEntry as u}from"../audit.mjs";const c=r=>{const t=r.toLowerCase(),e=n=>t===n||t.endsWith(n);if(e("/sign-up/email")||e("/sign-up"))return"sign-up";if(t.includes("/sign-in/"))return"sign-in";if(e("/sign-out"))return"sign-out";if(e("/change-password")||e("/set-password"))return"password-change";if(e("/reset-password")||e("/request-password-reset")||e("/forget-password"))return"password-reset";if(e("/verify-email"))return"email-verification";if(t.includes("/two-factor/enable")||t.includes("/totp/enable"))return"mfa-enable";if(t.includes("/two-factor/disable")||t.includes("/totp/disable"))return"mfa-disable";if(e("/refresh-token")||e("/token"))return"token-refresh";if(e("/revoke-session")||e("/revoke-sessions")||e("/revoke-other-sessions"))return"session-revoke";if(e("/link-social"))return"account-link";if(e("/unlink-account"))return"account-unlink"},s=(r,t)=>r.headers?.get(t)??r.request?.headers.get(t)??void 0,d=r=>{const t=s(r,"x-forwarded-for");return s(r,"cf-connecting-ip")??(t===void 0?void 0:t.split(",")[0]?.trim())??s(r,"x-real-ip")},f=r=>{const t=r.context?.newSession??r.context?.session,e=t?.user?.id??t?.session?.userId,n=t?.user?.email;return{...e===void 0?{}:{actorId:e},...n===void 0?{}:{actorEmail:n}}},l=r=>{const t=r.context?.returned;if(t instanceof Error)return"failure";if(typeof t=="object"&&t!==null&&"status"in t){const e=Number(t.status);if(Number.isFinite(e)&&e>=400)return"failure"}return"success"},p=(r,t=Date.now())=>{const e=r.path===void 0?void 0:c(r.path);if(e===void 0)return;const n=d(r),o=s(r,"user-agent");return{...f(r),event:e,outcome:l(r),ts:t,...n===void 0?{}:{ip:n},...o===void 0?{}:{userAgent:o},detail:{path:r.path}}},h=r=>a(async t=>{try{const e=p(t);if(e!==void 0){const n=await u(r.executor,e,{redactDetail:r.redactDetail,retention:r.retention});r.onRecord!==void 0&&await r.onRecord(n)}}catch(e){console.error("@lunora/auth: audit hook failed to record event",e)}return{}}),k=(r,t)=>{const e=h(t),n=r.hooks?.after,o=n?async i=>(await n(i),e(i)):e;return{...r,hooks:{...r.hooks,after:o}}};export{h as authAuditHook,p as buildAuditEntry,c as eventForPath,k as withAuthAudit};
|