@connextar/house 0.17.0 → 0.19.0

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.
Files changed (123) hide show
  1. package/README.md +850 -0
  2. package/dist/admin/access.d.ts +134 -0
  3. package/dist/admin/access.d.ts.map +1 -0
  4. package/dist/admin/access.js +157 -0
  5. package/dist/admin/access.js.map +1 -0
  6. package/dist/admin/audit.d.ts +130 -0
  7. package/dist/admin/audit.d.ts.map +1 -0
  8. package/dist/admin/audit.js +132 -0
  9. package/dist/admin/audit.js.map +1 -0
  10. package/dist/admin/bootstrap.d.ts +80 -0
  11. package/dist/admin/bootstrap.d.ts.map +1 -0
  12. package/dist/admin/bootstrap.js +106 -0
  13. package/dist/admin/bootstrap.js.map +1 -0
  14. package/dist/admin/client-index.d.ts +7 -0
  15. package/dist/admin/client-index.d.ts.map +1 -0
  16. package/dist/admin/client-index.js +6 -0
  17. package/dist/admin/client-index.js.map +1 -0
  18. package/dist/admin/client.d.ts +72 -0
  19. package/dist/admin/client.d.ts.map +1 -0
  20. package/dist/admin/client.js +90 -0
  21. package/dist/admin/client.js.map +1 -0
  22. package/dist/admin/index.d.ts +13 -0
  23. package/dist/admin/index.d.ts.map +1 -0
  24. package/dist/admin/index.js +13 -0
  25. package/dist/admin/index.js.map +1 -0
  26. package/dist/admin/proof.d.ts +34 -0
  27. package/dist/admin/proof.d.ts.map +1 -0
  28. package/dist/admin/proof.js +30 -0
  29. package/dist/admin/proof.js.map +1 -0
  30. package/dist/admin/second-factor-index.d.ts +8 -0
  31. package/dist/admin/second-factor-index.d.ts.map +1 -0
  32. package/dist/admin/second-factor-index.js +8 -0
  33. package/dist/admin/second-factor-index.js.map +1 -0
  34. package/dist/admin/second-factor.d.ts +156 -0
  35. package/dist/admin/second-factor.d.ts.map +1 -0
  36. package/dist/admin/second-factor.js +271 -0
  37. package/dist/admin/second-factor.js.map +1 -0
  38. package/dist/admin/testing-index.d.ts +6 -0
  39. package/dist/admin/testing-index.d.ts.map +1 -0
  40. package/dist/admin/testing-index.js +6 -0
  41. package/dist/admin/testing-index.js.map +1 -0
  42. package/dist/admin/testing.d.ts +84 -0
  43. package/dist/admin/testing.d.ts.map +1 -0
  44. package/dist/admin/testing.js +223 -0
  45. package/dist/admin/testing.js.map +1 -0
  46. package/dist/admin/view-as.d.ts +209 -0
  47. package/dist/admin/view-as.d.ts.map +1 -0
  48. package/dist/admin/view-as.js +251 -0
  49. package/dist/admin/view-as.js.map +1 -0
  50. package/dist/guides/guide-article.d.ts +13 -2
  51. package/dist/guides/guide-article.d.ts.map +1 -1
  52. package/dist/guides/guide-article.js +5 -3
  53. package/dist/guides/guide-article.js.map +1 -1
  54. package/dist/guides/guide-figure-zoom.d.ts +73 -0
  55. package/dist/guides/guide-figure-zoom.d.ts.map +1 -0
  56. package/dist/guides/guide-figure-zoom.js +279 -0
  57. package/dist/guides/guide-figure-zoom.js.map +1 -0
  58. package/dist/guides/guide-figure.d.ts +21 -0
  59. package/dist/guides/guide-figure.d.ts.map +1 -0
  60. package/dist/guides/guide-figure.js +19 -0
  61. package/dist/guides/guide-figure.js.map +1 -0
  62. package/dist/guides/index.d.ts +2 -0
  63. package/dist/guides/index.d.ts.map +1 -1
  64. package/dist/guides/index.js +2 -0
  65. package/dist/guides/index.js.map +1 -1
  66. package/dist/guides/library.d.ts +19 -1
  67. package/dist/guides/library.d.ts.map +1 -1
  68. package/dist/guides/library.js +9 -0
  69. package/dist/guides/library.js.map +1 -1
  70. package/dist/health/health.d.ts +65 -0
  71. package/dist/health/health.d.ts.map +1 -0
  72. package/dist/health/health.js +93 -0
  73. package/dist/health/health.js.map +1 -0
  74. package/dist/health/index.d.ts +2 -0
  75. package/dist/health/index.d.ts.map +1 -0
  76. package/dist/health/index.js +2 -0
  77. package/dist/health/index.js.map +1 -0
  78. package/dist/markdown/figure.d.ts +44 -0
  79. package/dist/markdown/figure.d.ts.map +1 -0
  80. package/dist/markdown/figure.js +56 -0
  81. package/dist/markdown/figure.js.map +1 -0
  82. package/dist/markdown/figures.d.ts +94 -0
  83. package/dist/markdown/figures.d.ts.map +1 -0
  84. package/dist/markdown/figures.js +126 -0
  85. package/dist/markdown/figures.js.map +1 -0
  86. package/dist/markdown/guide.d.ts +26 -7
  87. package/dist/markdown/guide.d.ts.map +1 -1
  88. package/dist/markdown/guide.js +27 -6
  89. package/dist/markdown/guide.js.map +1 -1
  90. package/dist/markdown/index.d.ts +2 -0
  91. package/dist/markdown/index.d.ts.map +1 -1
  92. package/dist/markdown/index.js +2 -0
  93. package/dist/markdown/index.js.map +1 -1
  94. package/dist/release/changelog.d.ts +19 -0
  95. package/dist/release/changelog.d.ts.map +1 -1
  96. package/dist/release/changelog.js +37 -6
  97. package/dist/release/changelog.js.map +1 -1
  98. package/dist/release/index.d.ts +1 -1
  99. package/dist/release/index.d.ts.map +1 -1
  100. package/dist/release/index.js +1 -1
  101. package/dist/release/index.js.map +1 -1
  102. package/dist/whats-new/body.d.ts +81 -0
  103. package/dist/whats-new/body.d.ts.map +1 -0
  104. package/dist/whats-new/body.js +29 -0
  105. package/dist/whats-new/body.js.map +1 -0
  106. package/dist/whats-new/index.d.ts +24 -0
  107. package/dist/whats-new/index.d.ts.map +1 -0
  108. package/dist/whats-new/index.js +24 -0
  109. package/dist/whats-new/index.js.map +1 -0
  110. package/dist/whats-new/seen.d.ts +23 -0
  111. package/dist/whats-new/seen.d.ts.map +1 -0
  112. package/dist/whats-new/seen.js +33 -0
  113. package/dist/whats-new/seen.js.map +1 -0
  114. package/dist/whats-new/select.d.ts +89 -0
  115. package/dist/whats-new/select.d.ts.map +1 -0
  116. package/dist/whats-new/select.js +117 -0
  117. package/dist/whats-new/select.js.map +1 -0
  118. package/dist/whats-new/use-whats-new.d.ts +36 -0
  119. package/dist/whats-new/use-whats-new.d.ts.map +1 -0
  120. package/dist/whats-new/use-whats-new.js +81 -0
  121. package/dist/whats-new/use-whats-new.js.map +1 -0
  122. package/docs/ReleaseProcess.md +12 -0
  123. package/package.json +25 -1
@@ -0,0 +1,156 @@
1
+ import { type AdminAuditEvent, type AdminAuditScope, type AdminSignInMethod } from "./audit.js";
2
+ /**
3
+ * Which administrators' sessions were made with a second factor — one Better Auth plugin for the six apps that each
4
+ * wrote their own (AltEd's `admin-second-factor.ts`, Quickie's `operator-access.ts`, and `second-factor-session.ts` in
5
+ * PharmaLine, PM Tool, senco and FB Web).
6
+ *
7
+ * Nothing on a Better Auth session says how it was made, so the plugin writes it down at the one moment it is known —
8
+ * Better Auth's own `session.create` database hook, which runs inside the endpoint creating the session — as a row in
9
+ * the `verification` table: `<recordPrefix><session id>`, the user's id as its value, expiring with the session as it
10
+ * was issued (not with its rolling extensions, so an administrator proves the factor again at least once a session
11
+ * lifetime). No request can write one. `adminAccess` reads it back.
12
+ *
13
+ * A session made by one of these proves the second factor, for an administrator:
14
+ *
15
+ * - `/two-factor/verify-totp` and `/two-factor/verify-backup-code` — the code at sign-in, after a password, an email
16
+ * link or a social sign-in (`secondFactorEverywhere()`); and the code that ends enrolment, which replaces the
17
+ * enrolling session with a new one;
18
+ * - `/passkey/verify-authentication` — possession of a device, and phishing-resistant;
19
+ * - `/sign-in/email` for an account with two-factor on (`trustedDeviceCounts`, default on — AltEd's rule): the
20
+ * two-factor plugin deletes that session straight after it is made unless the browser carries the trusted-device
21
+ * cookie, which only a correct code issues. A record for a deleted session matches nothing;
22
+ * - `/change-password` **inherits** the proof of the session it replaces (the request's own), and never outlives it.
23
+ *
24
+ * Anything else proves nothing — `/two-factor/verify-otp` (an emailed code is no second factor where email is already
25
+ * a way in), an email link or social sign-in on a trusted device, `/verify-email`, and any sign-in route a later
26
+ * Better Auth adds. Fail-closed by construction: no record, no administration.
27
+ *
28
+ * **Only `/change-password` inherits.** Every other route that mints a session for a signed-in account —
29
+ * `/passkey/verify-registration` with `createSession: true` (1.7), `/two-factor/disable`, `/two-factor/enable` with
30
+ * `skipVerificationOnEnable`, a social or email-link sign-in on a trusted device — proves nothing. FB Web's plugin
31
+ * let any of them inherit with a fresh lifetime, and a passkey registration's new session opened its admin tools.
32
+ *
33
+ * **A revoked session is refused on the factor-change routes, for everyone (401).** The routes' own checks believe the
34
+ * five-minute cookie cache, so a signed-out administrator's kept cookies could read the authenticator's secret.
35
+ *
36
+ * **The back door.** From a session without the record, an administrator's factors cannot change: re-keying two-factor
37
+ * (`/two-factor/enable` once it is on), reading its key (`/two-factor/get-totp-uri`), new backup codes, turning it off,
38
+ * or adding a passkey. Each would hand whoever holds a Google account or a mailbox — and the password — a factor that
39
+ * then writes the record. Turning two-factor on for the first time stays open, or nobody could get out of the
40
+ * refusal. An administrator adds a passkey only from a recorded session, so with two-factor still off, only after a
41
+ * passkey sign-in. `stepUp()` still applies on top, to everybody; either refusal stops a registration.
42
+ *
43
+ * **Order**: list it after `twoFactor()` and `secondFactorEverywhere()` — `onAdminSignedIn` must see the session
44
+ * their hooks leave standing, and `init` refuses to start otherwise — and before `nextCookies()`.
45
+ */
46
+ export interface AdminSecondFactorSessionsOptions {
47
+ /** Whether this account is an administrator now — from the database, never the session payload. */
48
+ isAdmin: (userId: string) => boolean | null | undefined | Promise<boolean | null | undefined>;
49
+ /** The record's prefix. Default `admin-second-factor:`. An adopting app passes its own, so its records keep counting. */
50
+ recordPrefix?: string;
51
+ /** A password sign-in that a trusted device let skip the code counts, for an account with two-factor on. Default true. */
52
+ trustedDeviceCounts?: boolean;
53
+ /**
54
+ * Called for each session an administrator signs in with — proven or not — that is still standing when the response
55
+ * leaves; never for a password change or a two-factor change rotating the session. Cannot fail the sign-in: an error
56
+ * is logged. Runs in the background with Better Auth's `advanced.backgroundTasks`.
57
+ */
58
+ onAdminSignedIn?: (event: AdminAuditEvent, signIn: AdminSignIn) => void | Promise<void>;
59
+ /** The actor's standing in the sign-in event. Default `"admin"`. */
60
+ standing?: string;
61
+ /** The sign-in event's scope. Default platform. */
62
+ scope?: AdminAuditScope;
63
+ /** The refusals' words. */
64
+ messages?: {
65
+ factorChange?: string;
66
+ passkeyNeedsTwoFactor?: string;
67
+ };
68
+ }
69
+ export interface AdminSignIn {
70
+ userId: string;
71
+ sessionId: string;
72
+ method: AdminSignInMethod;
73
+ proven: boolean;
74
+ path: string;
75
+ request?: Request;
76
+ }
77
+ export declare const ADMIN_SECOND_FACTOR_SESSIONS_ID = "admin-second-factor-sessions";
78
+ /** Routes that create a session only after Better Auth has checked a second factor or a passkey. */
79
+ export declare const ADMIN_PROVING_PATHS: readonly string[];
80
+ /** Two-factor routes that change or read an administrator's factor. `enable` counts only once two-factor is on. */
81
+ export declare const ADMIN_TWO_FACTOR_CHANGE_PATHS: readonly string[];
82
+ export declare const ADMIN_PASSKEY_ADD_PATHS: readonly string[];
83
+ export declare const ADMIN_SECOND_FACTOR_ERROR_CODES: {
84
+ readonly ADMIN_SECOND_FACTOR_REQUIRED: {
85
+ readonly code: "ADMIN_SECOND_FACTOR_REQUIRED";
86
+ readonly message: "Changing an administrator’s two-factor or passkeys needs a sign-in that used your authenticator code or a passkey. Sign out, then sign in with your email, password and code — or with a passkey — and try again.";
87
+ };
88
+ readonly ADMIN_PASSKEY_NEEDS_TWO_FACTOR: {
89
+ readonly code: "ADMIN_PASSKEY_NEEDS_TWO_FACTOR";
90
+ readonly message: "Administrators add passkeys once two-factor authentication is on. Turn it on first, then sign in with your code and add the passkey.";
91
+ };
92
+ };
93
+ interface SessionRow {
94
+ id: string;
95
+ userId: string;
96
+ expiresAt: Date | string;
97
+ ipAddress?: string | null;
98
+ userAgent?: string | null;
99
+ }
100
+ export declare function adminSecondFactorSessions(options: AdminSecondFactorSessionsOptions): {
101
+ id: "admin-second-factor-sessions";
102
+ options: {
103
+ recordPrefix: string;
104
+ trustedDeviceCounts: boolean;
105
+ /** Whether this account is an administrator now — from the database, never the session payload. */
106
+ isAdmin: (userId: string) => boolean | null | undefined | Promise<boolean | null | undefined>;
107
+ /**
108
+ * Called for each session an administrator signs in with — proven or not — that is still standing when the response
109
+ * leaves; never for a password change or a two-factor change rotating the session. Cannot fail the sign-in: an error
110
+ * is logged. Runs in the background with Better Auth's `advanced.backgroundTasks`.
111
+ */
112
+ onAdminSignedIn?: (event: AdminAuditEvent, signIn: AdminSignIn) => void | Promise<void>;
113
+ /** The actor's standing in the sign-in event. Default `"admin"`. */
114
+ standing?: string;
115
+ /** The sign-in event's scope. Default platform. */
116
+ scope?: AdminAuditScope;
117
+ /** The refusals' words. */
118
+ messages?: {
119
+ factorChange?: string;
120
+ passkeyNeedsTwoFactor?: string;
121
+ };
122
+ };
123
+ init(ctx: import("better-auth").AuthContext): {
124
+ options: {
125
+ databaseHooks: {
126
+ session: {
127
+ create: {
128
+ after: (session: SessionRow, endpoint: unknown) => Promise<void>;
129
+ };
130
+ };
131
+ };
132
+ };
133
+ };
134
+ hooks: {
135
+ before: {
136
+ matcher: (context: import("better-auth").HookEndpointContext) => boolean;
137
+ handler: import("better-auth/api").AuthMiddleware;
138
+ }[];
139
+ after: {
140
+ matcher: (context: import("better-auth").HookEndpointContext) => boolean;
141
+ handler: import("better-auth/api").AuthMiddleware;
142
+ }[];
143
+ };
144
+ $ERROR_CODES: {
145
+ readonly ADMIN_SECOND_FACTOR_REQUIRED: {
146
+ readonly code: "ADMIN_SECOND_FACTOR_REQUIRED";
147
+ readonly message: "Changing an administrator’s two-factor or passkeys needs a sign-in that used your authenticator code or a passkey. Sign out, then sign in with your email, password and code — or with a passkey — and try again.";
148
+ };
149
+ readonly ADMIN_PASSKEY_NEEDS_TWO_FACTOR: {
150
+ readonly code: "ADMIN_PASSKEY_NEEDS_TWO_FACTOR";
151
+ readonly message: "Administrators add passkeys once two-factor authentication is on. Turn it on first, then sign in with your code and add the passkey.";
152
+ };
153
+ };
154
+ };
155
+ export {};
156
+ //# sourceMappingURL=second-factor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"second-factor.d.ts","sourceRoot":"","sources":["../../src/admin/second-factor.ts"],"names":[],"mappings":"AASA,OAAO,EAAiB,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAG/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,gCAAgC;IAC/C,mGAAmG;IACnG,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC9F,yHAAyH;IACzH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0HAA0H;IAC1H,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxF,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACtE;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,+BAA+B,iCAAiC,CAAC;AAE9E,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAIhD,CAAC;AAMF,mHAAmH;AACnH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAK1D,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAGpD,CAAC;AAEF,eAAO,MAAM,+BAA+B;;;;;;;;;CAWlC,CAAC;AA0BX,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAiBD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC;;;;;QAnHjF,mGAAmG;iBAC1F,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;QAK7F;;;;WAIG;0BACe,CAAC,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACvF,oEAAoE;mBACzD,MAAM;QACjB,mDAAmD;gBAC3C,eAAe;QACvB,2BAA2B;mBAChB;YAAE,YAAY,CAAC,EAAE,MAAM,CAAC;YAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;SAAE;;;;;;;yCA+S/B,UAAU,YAAY,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;EAenE"}
@@ -0,0 +1,271 @@
1
+ import { APIError, createAuthMiddleware, getAuthoritativeSessionFromCtx, getSessionFromCtx, isAPIError, } from "better-auth/api";
2
+ import { adminSignedIn } from "./audit.js";
3
+ import { DEFAULT_ADMIN_PROOF_PREFIX, adminProofIdentifier, isLiveProof } from "./proof.js";
4
+ export const ADMIN_SECOND_FACTOR_SESSIONS_ID = "admin-second-factor-sessions";
5
+ /** Routes that create a session only after Better Auth has checked a second factor or a passkey. */
6
+ export const ADMIN_PROVING_PATHS = [
7
+ "/two-factor/verify-totp",
8
+ "/two-factor/verify-backup-code",
9
+ "/passkey/verify-authentication",
10
+ ];
11
+ const PASSWORD_SIGN_IN = "/sign-in/email";
12
+ const CHANGE_PASSWORD = "/change-password";
13
+ const ENABLE = "/two-factor/enable";
14
+ /** Two-factor routes that change or read an administrator's factor. `enable` counts only once two-factor is on. */
15
+ export const ADMIN_TWO_FACTOR_CHANGE_PATHS = [
16
+ ENABLE,
17
+ "/two-factor/disable",
18
+ "/two-factor/get-totp-uri",
19
+ "/two-factor/generate-backup-codes",
20
+ ];
21
+ export const ADMIN_PASSKEY_ADD_PATHS = [
22
+ "/passkey/generate-register-options",
23
+ "/passkey/verify-registration",
24
+ ];
25
+ export const ADMIN_SECOND_FACTOR_ERROR_CODES = {
26
+ ADMIN_SECOND_FACTOR_REQUIRED: {
27
+ code: "ADMIN_SECOND_FACTOR_REQUIRED",
28
+ message: "Changing an administrator’s two-factor or passkeys needs a sign-in that used your authenticator code or a passkey. Sign out, then sign in with your email, password and code — or with a passkey — and try again.",
29
+ },
30
+ ADMIN_PASSKEY_NEEDS_TWO_FACTOR: {
31
+ code: "ADMIN_PASSKEY_NEEDS_TWO_FACTOR",
32
+ message: "Administrators add passkeys once two-factor authentication is on. Turn it on first, then sign in with your code and add the passkey.",
33
+ },
34
+ };
35
+ const SOCIAL_PATHS = new Set(["/callback/:id", "/oauth2/callback/:providerId", "/sign-in/social", "/one-tap/callback"]);
36
+ function methodOf(path, proven) {
37
+ switch (path) {
38
+ case "/two-factor/verify-totp":
39
+ return "code";
40
+ case "/two-factor/verify-backup-code":
41
+ return "backup-code";
42
+ case "/passkey/verify-authentication":
43
+ return "passkey";
44
+ case PASSWORD_SIGN_IN:
45
+ return proven ? "trusted-device" : "password";
46
+ case "/magic-link/verify":
47
+ return "email-link";
48
+ default:
49
+ return SOCIAL_PATHS.has(path) ? "social" : "other";
50
+ }
51
+ }
52
+ const toDate = (value) => (value instanceof Date ? value : new Date(value));
53
+ /** An error the route answered with. A redirect — an email link landing, an OAuth callback — is an APIError too. */
54
+ function failed(returned) {
55
+ if (!isAPIError(returned))
56
+ return false;
57
+ const status = returned.statusCode ?? 500;
58
+ return status < 300 || status >= 400;
59
+ }
60
+ export function adminSecondFactorSessions(options) {
61
+ if (typeof options?.isAdmin !== "function") {
62
+ throw new Error("[admin-second-factor] adminSecondFactorSessions needs `isAdmin(userId)`.");
63
+ }
64
+ const prefix = options.recordPrefix ?? DEFAULT_ADMIN_PROOF_PREFIX;
65
+ const trustedDeviceCounts = options.trustedDeviceCounts !== false;
66
+ const standing = options.standing ?? "admin";
67
+ const key = (sessionId) => adminProofIdentifier(prefix, sessionId);
68
+ const isAdmin = async (userId) => (await options.isAdmin(userId)) === true;
69
+ let auth = null;
70
+ async function proofOf(sessionId, userId) {
71
+ const row = await auth.internalAdapter.findVerificationValue(key(sessionId));
72
+ return isLiveProof(row, userId) ? row : null;
73
+ }
74
+ /** Better Auth's `session.create.after`: write the record when the creating route proved the second factor. */
75
+ async function record(session, endpoint) {
76
+ const path = endpoint?.path;
77
+ if (!path || !auth)
78
+ return;
79
+ const proving = ADMIN_PROVING_PATHS.includes(path);
80
+ const trusted = trustedDeviceCounts && path === PASSWORD_SIGN_IN;
81
+ const inherits = path === CHANGE_PASSWORD;
82
+ if (!proving && !trusted && !inherits)
83
+ return;
84
+ try {
85
+ if (!(await isAdmin(session.userId)))
86
+ return;
87
+ let expiresAt = toDate(session.expiresAt);
88
+ if (trusted) {
89
+ const user = (await auth.internalAdapter.findUserById(session.userId));
90
+ if (user?.twoFactorEnabled !== true)
91
+ return;
92
+ }
93
+ if (inherits) {
94
+ // The session being replaced — the request's own — is already deleted by now, so its record is what's asked,
95
+ // not whether it is still live. Inherited, it lasts no longer than the proof it came from.
96
+ const previous = endpoint?.context?.session?.session;
97
+ if (!previous?.id || previous.userId !== session.userId || previous.id === session.id)
98
+ return;
99
+ const proof = await proofOf(previous.id, session.userId);
100
+ if (!proof)
101
+ return;
102
+ const until = toDate(proof.expiresAt);
103
+ if (until < expiresAt)
104
+ expiresAt = until;
105
+ }
106
+ await auth.internalAdapter.createVerificationValue({
107
+ identifier: key(session.id),
108
+ value: session.userId,
109
+ expiresAt,
110
+ });
111
+ await sweep(session.userId);
112
+ }
113
+ catch (error) {
114
+ // Without the record the administrator is asked to sign in again; failing the sign-in over a bookkeeping row
115
+ // would lock every account out instead.
116
+ auth.logger.error("[admin-second-factor] could not record how an administrator's session was made", error);
117
+ }
118
+ }
119
+ /** Sessions end on their own; their records are cleared here rather than by a job. */
120
+ async function sweep(userId) {
121
+ try {
122
+ await auth.adapter.deleteMany({
123
+ model: "verification",
124
+ where: [
125
+ { field: "identifier", operator: "starts_with", value: prefix },
126
+ { field: "value", value: userId },
127
+ { field: "expiresAt", operator: "lte", value: new Date() },
128
+ ],
129
+ });
130
+ }
131
+ catch {
132
+ // Housekeeping only.
133
+ }
134
+ }
135
+ const refuseFactorChange = {
136
+ matcher: (ctx) => {
137
+ const path = ctx.path ?? "";
138
+ return ADMIN_TWO_FACTOR_CHANGE_PATHS.includes(path) || ADMIN_PASSKEY_ADD_PATHS.includes(path);
139
+ },
140
+ handler: createAuthMiddleware(async (ctx) => {
141
+ const live = (await getAuthoritativeSessionFromCtx(ctx));
142
+ if (!live?.session) {
143
+ // The routes' own session checks (`sessionMiddleware`, `freshSessionMiddleware`) believe the cookie cache for
144
+ // minutes after a session is revoked or signed out — long enough for kept cookies to read the authenticator's
145
+ // secret, mint backup codes, re-key two-factor or register a passkey. So a cookie whose session row is gone or
146
+ // expired is refused here, for everyone. Only a request with no session at all (a passkey-first sign-up) is
147
+ // left to the route's own rules.
148
+ const cached = await getSessionFromCtx(ctx).catch(() => null);
149
+ if (cached?.session) {
150
+ throw new APIError("UNAUTHORIZED", { message: "Unauthorized", code: "UNAUTHORIZED" });
151
+ }
152
+ return;
153
+ }
154
+ if (!(await isAdmin(live.user.id)))
155
+ return;
156
+ const twoFactorOn = live.user.twoFactorEnabled === true;
157
+ const addsPasskey = ADMIN_PASSKEY_ADD_PATHS.includes(ctx.path);
158
+ // Turning two-factor on for the first time is how an administrator gets out of administration's refusal.
159
+ if (ctx.path === ENABLE && !twoFactorOn)
160
+ return;
161
+ if (await proofOf(live.session.id, live.user.id))
162
+ return;
163
+ const refusal = addsPasskey && !twoFactorOn
164
+ ? ADMIN_SECOND_FACTOR_ERROR_CODES.ADMIN_PASSKEY_NEEDS_TWO_FACTOR
165
+ : ADMIN_SECOND_FACTOR_ERROR_CODES.ADMIN_SECOND_FACTOR_REQUIRED;
166
+ const message = refusal.code === "ADMIN_PASSKEY_NEEDS_TWO_FACTOR"
167
+ ? (options.messages?.passkeyNeedsTwoFactor ?? refusal.message)
168
+ : (options.messages?.factorChange ?? refusal.message);
169
+ throw new APIError("FORBIDDEN", { code: refusal.code, message });
170
+ }),
171
+ };
172
+ const signedIn = {
173
+ matcher: () => !!options.onAdminSignedIn,
174
+ handler: createAuthMiddleware(async (ctx) => {
175
+ const notify = options.onAdminSignedIn;
176
+ // `newSession` is what the request leaves standing: the two-factor plugin (and `secondFactorEverywhere()`)
177
+ // clear it when they take the session away for the code screen.
178
+ const made = ctx.context.newSession;
179
+ if (!notify || !made?.session || failed(ctx.context.returned))
180
+ return;
181
+ const previous = ctx.context.session ?? null;
182
+ // A refresh keeps its id; a password or two-factor change rotates a signed-in session. Neither is a sign-in.
183
+ if (previous?.session?.id === made.session.id)
184
+ return;
185
+ if (previous?.user?.id === made.session.userId && !ADMIN_PROVING_PATHS.includes(ctx.path))
186
+ return;
187
+ try {
188
+ if (!(await isAdmin(made.session.userId)))
189
+ return;
190
+ // By the record's own rule rather than by reading the record: Better Auth 1.6 runs database after-hooks once
191
+ // the endpoint has finished, after this hook. A password change, the one inheriting route, is not reported.
192
+ const proven = ADMIN_PROVING_PATHS.includes(ctx.path) ||
193
+ (trustedDeviceCounts && ctx.path === PASSWORD_SIGN_IN && made.user.twoFactorEnabled === true);
194
+ const method = methodOf(ctx.path, proven);
195
+ const event = adminSignedIn({
196
+ actor: { id: made.session.userId, name: made.user.name || made.user.email || made.session.userId, standing },
197
+ sessionId: made.session.id,
198
+ method,
199
+ proven,
200
+ ...(options.scope ? { scope: options.scope } : {}),
201
+ ip: made.session.ipAddress ?? null,
202
+ userAgent: made.session.userAgent ?? null,
203
+ });
204
+ const detail = {
205
+ userId: made.session.userId,
206
+ sessionId: made.session.id,
207
+ method,
208
+ proven,
209
+ path: ctx.path,
210
+ ...(ctx.request ? { request: ctx.request } : {}),
211
+ };
212
+ const logger = ctx.context.logger;
213
+ const run = (async () => {
214
+ try {
215
+ await notify(event, detail);
216
+ }
217
+ catch (error) {
218
+ logger.error("[admin-second-factor] onAdminSignedIn failed; the sign-in stands", error);
219
+ }
220
+ })();
221
+ await ctx.context.runInBackgroundOrAwait(run);
222
+ }
223
+ catch (error) {
224
+ ctx.context.logger.error("[admin-second-factor] could not describe an administrator's sign-in", error);
225
+ }
226
+ }),
227
+ };
228
+ return {
229
+ id: ADMIN_SECOND_FACTOR_SESSIONS_ID,
230
+ options: { ...options, recordPrefix: prefix, trustedDeviceCounts },
231
+ init(ctx) {
232
+ auth = ctx;
233
+ const plugins = ctx.options.plugins ?? [];
234
+ const own = plugins.findIndex((plugin) => plugin.id === ADMIN_SECOND_FACTOR_SESSIONS_ID);
235
+ const twoFactor = plugins.findIndex((plugin) => plugin.id === "two-factor");
236
+ const everywhere = plugins.findIndex((plugin) => plugin.id === "second-factor-everywhere");
237
+ if (twoFactor === -1) {
238
+ ctx.logger.warn("[admin-second-factor] no twoFactor() plugin is installed: only a passkey sign-in can prove the second factor.");
239
+ }
240
+ if (options.onAdminSignedIn &&
241
+ ((twoFactor !== -1 && twoFactor > own) || (everywhere !== -1 && everywhere > own))) {
242
+ throw new Error("[admin-second-factor] list adminSecondFactorSessions() after twoFactor() and secondFactorEverywhere(): " +
243
+ "onAdminSignedIn has to see the session their hooks leave standing, or it would record a sign-in that is " +
244
+ "still waiting for its code.");
245
+ }
246
+ const nextCookies = plugins.findIndex((plugin) => plugin.id === "next-cookies");
247
+ if (nextCookies !== -1 && nextCookies < own) {
248
+ ctx.logger.warn("[admin-second-factor] is listed after nextCookies(); list it before.");
249
+ }
250
+ return {
251
+ options: {
252
+ databaseHooks: {
253
+ session: {
254
+ create: {
255
+ after: async (session, endpoint) => {
256
+ await record(session, endpoint);
257
+ },
258
+ },
259
+ },
260
+ },
261
+ },
262
+ };
263
+ },
264
+ hooks: {
265
+ before: [refuseFactorChange],
266
+ after: [signedIn],
267
+ },
268
+ $ERROR_CODES: ADMIN_SECOND_FACTOR_ERROR_CODES,
269
+ };
270
+ }
271
+ //# sourceMappingURL=second-factor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"second-factor.js","sourceRoot":"","sources":["../../src/admin/second-factor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `@connextar/house/admin/testing` — Node helpers for an app's own tests: the guard-coverage sweep, and a cookie store
3
+ * for exercising "view as". Reads the file system, so it belongs in tests and nowhere else.
4
+ */
5
+ export { AdminGuardCoverageError, assertAdminGuardCoverage, checkAdminGuardCoverage, memoryCookieJar, stripComments, type AdminGuardCoverageOptions, type AdminGuardCoverageReport, } from "./testing.js";
6
+ //# sourceMappingURL=testing-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-index.d.ts","sourceRoot":"","sources":["../../src/admin/testing-index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,aAAa,EACb,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,cAAc,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `@connextar/house/admin/testing` — Node helpers for an app's own tests: the guard-coverage sweep, and a cookie store
3
+ * for exercising "view as". Reads the file system, so it belongs in tests and nowhere else.
4
+ */
5
+ export { AdminGuardCoverageError, assertAdminGuardCoverage, checkAdminGuardCoverage, memoryCookieJar, stripComments, } from "./testing.js";
6
+ //# sourceMappingURL=testing-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-index.js","sourceRoot":"","sources":["../../src/admin/testing-index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,aAAa,GAGd,MAAM,cAAc,CAAC"}
@@ -0,0 +1,84 @@
1
+ import type { ViewAsCookieOptions } from "./view-as.js";
2
+ /**
3
+ * The bypass sweep: every admin surface goes through the one guard — generalised from PharmaLine's
4
+ * `admin-guard.test.ts`, the strongest of the apps' versions, with Quickie's checks that nothing escapes it.
5
+ *
6
+ * The rule lives in one guard. A page or route that forgot to call it serves administration without it, and no type
7
+ * or test of the guard itself notices — so this reads every one of them, and fails the day one is added without:
8
+ *
9
+ * - **Routes:** in every `route.ts` under `apiDirs` (and under `pageDirs`), the guard is the **first `await`** of every
10
+ * handler — not merely called somewhere, after a body was parsed or a row read. Handlers must be exported as
11
+ * `export async function GET(…)`; `export const GET = …` or `export { x as GET }` is reported, since the sweep
12
+ * cannot read it. A handler that grants nothing (stopping a "view as") goes in `exempt`, with why.
13
+ * - **Pages:** every page file under `pageDirs` awaits the page guard itself — a layout is not re-rendered on every
14
+ * navigation between its children, so a guard only there guards only the first page visited (PM Tool).
15
+ * - **No server actions** under either: a `"use server"` file is an endpoint the route sweep cannot see.
16
+ * - **The flag:** only allow-listed files mention the raw admin flag, each with a reason. A new reader fails until
17
+ * someone has checked it goes through the guard; an entry that no longer reads it fails too, so the list stays true.
18
+ *
19
+ * Comments are ignored when looking for the guard (a comment saying `await requireAdmin()` is not a call), and kept
20
+ * when looking for the flag (a comment naming it is still listed, with "comment" as its reason — as PharmaLine does).
21
+ */
22
+ export interface AdminGuardCoverageOptions {
23
+ /** The app's root. Default `process.cwd()`. */
24
+ root?: string;
25
+ /** Where the admin API routes are, relative to the root: `["app/api/admin"]`. */
26
+ apiDirs: readonly string[];
27
+ /** Where the admin pages are: `["app/dashboard/admin"]`. */
28
+ pageDirs: readonly string[];
29
+ /** The route guard's name — or names — as called: `"requirePlatformAdmin"`. */
30
+ apiGuard: string | readonly string[];
31
+ /** The page guard's name — or names: `"requirePlatformAdminPage"`. */
32
+ pageGuard: string | readonly string[];
33
+ /** Who may mention the raw admin flag. */
34
+ flagReaders?: {
35
+ pattern: RegExp;
36
+ /** Where to look: `["app", "components", "lib"]`. */
37
+ dirs: readonly string[];
38
+ /** Each file that may, relative to the root, and why that is safe. */
39
+ allowed: Record<string, string>;
40
+ /** Files to leave out besides tests — generated code. */
41
+ ignore?: (file: string) => boolean;
42
+ };
43
+ /** `"<route file> <METHOD>"` → why it needs no guard. */
44
+ exempt?: Record<string, string>;
45
+ /** Which files are pages. Default `page.tsx`, `page.ts`, `page.jsx`, `page.js`. */
46
+ pageFile?: RegExp;
47
+ /** Fewer than this many found is a problem: a moved folder would otherwise pass with nothing checked. Default 1 each. */
48
+ minimum?: {
49
+ routes?: number;
50
+ handlers?: number;
51
+ pages?: number;
52
+ };
53
+ }
54
+ export interface AdminGuardCoverageReport {
55
+ routes: string[];
56
+ /** `"<route file> <METHOD>"`, each checked. */
57
+ handlers: string[];
58
+ pages: string[];
59
+ flagReaders: string[];
60
+ problems: string[];
61
+ }
62
+ export declare class AdminGuardCoverageError extends Error {
63
+ readonly problems: string[];
64
+ constructor(problems: string[]);
65
+ }
66
+ /** Source with its comments blanked out, strings and template literals left as they are. */
67
+ export declare function stripComments(source: string): string;
68
+ /** The sweep, answering every problem it found rather than stopping at the first. */
69
+ export declare function checkAdminGuardCoverage(options: AdminGuardCoverageOptions): AdminGuardCoverageReport;
70
+ /** The sweep, throwing every problem at once. Answers what it checked, so a test can say how much. */
71
+ export declare function assertAdminGuardCoverage(options: AdminGuardCoverageOptions): AdminGuardCoverageReport;
72
+ /** A cookie store in memory, shaped like Next's `cookies()`, for testing an app's "view as". */
73
+ export declare function memoryCookieJar(initial?: Record<string, string>): {
74
+ get(name: string): {
75
+ name: string;
76
+ value: string;
77
+ } | undefined;
78
+ set(name: string, value: string, options?: ViewAsCookieOptions): void;
79
+ delete(name: string): void;
80
+ /** What `set` was given for a cookie. */
81
+ options(name: string): ViewAsCookieOptions | undefined;
82
+ has(name: string): boolean;
83
+ };
84
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/admin/testing.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAa,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,yBAAyB;IACxC,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACrC,sEAAsE;IACtE,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACtC,0CAA0C;IAC1C,WAAW,CAAC,EAAE;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,qDAAqD;QACrD,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;QACxB,sEAAsE;QACtE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,yDAAyD;QACzD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;KACpC,CAAC;IACF,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yHAAyH;IACzH,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;gBAEhB,QAAQ,EAAE,MAAM,EAAE;CAO/B;AAeD,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CA6CpD;AAkBD,qFAAqF;AACrF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAyGpG;AAMD,sGAAsG;AACtG,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,CAIrG;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM;cAItD,MAAM;;;;cAIN,MAAM,SAAS,MAAM,YAAY,mBAAmB;iBAIjD,MAAM;IAInB,yCAAyC;kBAC3B,MAAM;cAGV,MAAM;EAKnB"}