@lunora/auth 1.0.0-alpha.63 → 1.0.0-alpha.65

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 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 _,piiRules as v}from"@visulima/redact";const n="__lunora_auth_audit__",R=[..._,...v],N=1e3,L=1e4,u=null,s=t=>{if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="bigint"||typeof t=="boolean")return String(t)},m=async t=>{await t.run(`CREATE TABLE IF NOT EXISTS "${n}" (
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
- )`,[])},O=async(t,e,r={})=>{await m(t);let i;return e.detail!==void 0&&(i=r.redactDetail===!1?e.detail:A(e.detail,R)),await t.run(`INSERT INTO "${n}" (ts, event, outcome, actor_id, actor_email, ip, user_agent, detail) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,[e.ts,e.event,e.outcome,e.actorId??u,e.actorEmail??u,e.ip??u,e.userAgent??u,i===void 0?u:JSON.stringify(i)]),typeof r.retention=="number"&&r.retention>0&&await t.run(`DELETE FROM "${n}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${n}")`,[r.retention]),{...e,detail:i}},I=async(t,e={})=>{await m(t);const r=Math.max(1,Math.min(e.limit??N,L)),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(r),(await t.all(`SELECT seq, ts, event, outcome, actor_id, actor_email, ip, user_agent, detail FROM "${n}" WHERE ${i.join(" AND ")} ORDER BY seq DESC LIMIT ?`,d)).map(a=>{const o={event:s(a.event)??"",outcome:a.outcome==="failure"?"failure":"success",seq:Number(a.seq),ts:Number(a.ts)},E=s(a.actor_id),c=s(a.actor_email),T=s(a.ip),l=s(a.user_agent),p=s(a.detail);return E!==void 0&&(o.actorId=E),c!==void 0&&(o.actorEmail=c),T!==void 0&&(o.ip=T),l!==void 0&&(o.userAgent=l),p!==void 0&&(o.detail=JSON.parse(p)),o})},h=t=>({read:e=>I(t,e)});export{n as AUTH_AUDIT_TABLE,O as appendAuthAuditEntry,h as createAuthAuditReader,m as ensureAuthAuditTable,I as readAuthAuditLog};
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
- /** Structural view of the fields we read off better-auth's after-hook context — kept loose to avoid coupling to internal types. */
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
- /** Structural view of the fields we read off better-auth's after-hook context — kept loose to avoid coupling to internal types. */
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-Dx3sqf3G.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-DkkUg061.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-DlOR_nIq.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};
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};
@@ -1 +1 @@
1
- import{INTERNAL_SECRET_HEADER as u,RESOLVE_SESSION_PATH as f,READ_AUDIT_PATH as w}from"./AUTH_DO_AUDIT_PATH-DkkUg061.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};
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.63",
3
+ "version": "1.0.0-alpha.65",
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.10",
106
- "@lunora/server": "1.0.0-alpha.55",
107
- "@lunora/values": "1.0.0-alpha.13",
105
+ "@lunora/errors": "1.0.0-alpha.12",
106
+ "@lunora/server": "1.0.0-alpha.57",
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};