@nexpress/core 0.1.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/audit-54XLVCWD.js +14 -0
  4. package/dist/audit-54XLVCWD.js.map +1 -0
  5. package/dist/auth.d.ts +640 -0
  6. package/dist/auth.js +94 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/can-YLUHRJAB.js +19 -0
  9. package/dist/can-YLUHRJAB.js.map +1 -0
  10. package/dist/chunk-2G264RCD.js +68 -0
  11. package/dist/chunk-2G264RCD.js.map +1 -0
  12. package/dist/chunk-2YDGE7YX.js +92 -0
  13. package/dist/chunk-2YDGE7YX.js.map +1 -0
  14. package/dist/chunk-473S4TER.js +538 -0
  15. package/dist/chunk-473S4TER.js.map +1 -0
  16. package/dist/chunk-4ZLMEKFX.js +18 -0
  17. package/dist/chunk-4ZLMEKFX.js.map +1 -0
  18. package/dist/chunk-55FU6WED.js +179 -0
  19. package/dist/chunk-55FU6WED.js.map +1 -0
  20. package/dist/chunk-6YI5K2TI.js +1959 -0
  21. package/dist/chunk-6YI5K2TI.js.map +1 -0
  22. package/dist/chunk-BHK3AD3Q.js +41 -0
  23. package/dist/chunk-BHK3AD3Q.js.map +1 -0
  24. package/dist/chunk-CRUQBZUF.js +39 -0
  25. package/dist/chunk-CRUQBZUF.js.map +1 -0
  26. package/dist/chunk-CTSQ7BRI.js +175 -0
  27. package/dist/chunk-CTSQ7BRI.js.map +1 -0
  28. package/dist/chunk-DK2JBJH7.js +81 -0
  29. package/dist/chunk-DK2JBJH7.js.map +1 -0
  30. package/dist/chunk-DP2PREDU.js +597 -0
  31. package/dist/chunk-DP2PREDU.js.map +1 -0
  32. package/dist/chunk-EQ2Z3KMD.js +24 -0
  33. package/dist/chunk-EQ2Z3KMD.js.map +1 -0
  34. package/dist/chunk-FZ7O6DWI.js +305 -0
  35. package/dist/chunk-FZ7O6DWI.js.map +1 -0
  36. package/dist/chunk-ISLYFQWL.js +1270 -0
  37. package/dist/chunk-ISLYFQWL.js.map +1 -0
  38. package/dist/chunk-JJL74ZPK.js +68 -0
  39. package/dist/chunk-JJL74ZPK.js.map +1 -0
  40. package/dist/chunk-JKXAPSU4.js +24 -0
  41. package/dist/chunk-JKXAPSU4.js.map +1 -0
  42. package/dist/chunk-KU5M27ZC.js +24 -0
  43. package/dist/chunk-KU5M27ZC.js.map +1 -0
  44. package/dist/chunk-LSHHRDVR.js +34 -0
  45. package/dist/chunk-LSHHRDVR.js.map +1 -0
  46. package/dist/chunk-M43PGOQY.js +715 -0
  47. package/dist/chunk-M43PGOQY.js.map +1 -0
  48. package/dist/chunk-MEJAHXIO.js +150 -0
  49. package/dist/chunk-MEJAHXIO.js.map +1 -0
  50. package/dist/chunk-NUCGHWCF.js +101 -0
  51. package/dist/chunk-NUCGHWCF.js.map +1 -0
  52. package/dist/chunk-OK5HOCQI.js +845 -0
  53. package/dist/chunk-OK5HOCQI.js.map +1 -0
  54. package/dist/chunk-OROPGO65.js +13 -0
  55. package/dist/chunk-OROPGO65.js.map +1 -0
  56. package/dist/chunk-PPAS4SZR.js +176 -0
  57. package/dist/chunk-PPAS4SZR.js.map +1 -0
  58. package/dist/chunk-PPBWRKO2.js +171 -0
  59. package/dist/chunk-PPBWRKO2.js.map +1 -0
  60. package/dist/chunk-PZ5AY32C.js +10 -0
  61. package/dist/chunk-PZ5AY32C.js.map +1 -0
  62. package/dist/chunk-QO7LAQZH.js +321 -0
  63. package/dist/chunk-QO7LAQZH.js.map +1 -0
  64. package/dist/chunk-QVJ2HCAX.js +225 -0
  65. package/dist/chunk-QVJ2HCAX.js.map +1 -0
  66. package/dist/chunk-RIPHIRPP.js +68 -0
  67. package/dist/chunk-RIPHIRPP.js.map +1 -0
  68. package/dist/chunk-S27S42QY.js +134 -0
  69. package/dist/chunk-S27S42QY.js.map +1 -0
  70. package/dist/chunk-SBCVAC2Z.js +40 -0
  71. package/dist/chunk-SBCVAC2Z.js.map +1 -0
  72. package/dist/chunk-TFJ4MKPH.js +694 -0
  73. package/dist/chunk-TFJ4MKPH.js.map +1 -0
  74. package/dist/chunk-THX3SHYA.js +75 -0
  75. package/dist/chunk-THX3SHYA.js.map +1 -0
  76. package/dist/chunk-UGQSQO5B.js +222 -0
  77. package/dist/chunk-UGQSQO5B.js.map +1 -0
  78. package/dist/chunk-V2UNHGAP.js +26 -0
  79. package/dist/chunk-V2UNHGAP.js.map +1 -0
  80. package/dist/chunk-VGTPQXNQ.js +2790 -0
  81. package/dist/chunk-VGTPQXNQ.js.map +1 -0
  82. package/dist/chunk-VNIHXQ7W.js +194 -0
  83. package/dist/chunk-VNIHXQ7W.js.map +1 -0
  84. package/dist/chunk-WV272MPW.js +31 -0
  85. package/dist/chunk-WV272MPW.js.map +1 -0
  86. package/dist/chunk-X5KKBOUS.js +26 -0
  87. package/dist/chunk-X5KKBOUS.js.map +1 -0
  88. package/dist/chunk-XANPEOJC.js +17 -0
  89. package/dist/chunk-XANPEOJC.js.map +1 -0
  90. package/dist/chunk-XPVQIHAQ.js +83 -0
  91. package/dist/chunk-XPVQIHAQ.js.map +1 -0
  92. package/dist/chunk-ZCINJSS4.js +75 -0
  93. package/dist/chunk-ZCINJSS4.js.map +1 -0
  94. package/dist/community.d.ts +1425 -0
  95. package/dist/community.js +206 -0
  96. package/dist/community.js.map +1 -0
  97. package/dist/config-2GDU7PCK.js +32 -0
  98. package/dist/config-2GDU7PCK.js.map +1 -0
  99. package/dist/context-MNZ4QXPC.js +16 -0
  100. package/dist/context-MNZ4QXPC.js.map +1 -0
  101. package/dist/db-schema.d.ts +4 -0
  102. package/dist/db-schema.js +102 -0
  103. package/dist/db-schema.js.map +1 -0
  104. package/dist/db.d.ts +7 -0
  105. package/dist/db.js +117 -0
  106. package/dist/db.js.map +1 -0
  107. package/dist/digest-SY42GQSU.js +17 -0
  108. package/dist/digest-SY42GQSU.js.map +1 -0
  109. package/dist/errors-5OS3S2J3.js +22 -0
  110. package/dist/errors-5OS3S2J3.js.map +1 -0
  111. package/dist/host-OBOI4MJK.js +51 -0
  112. package/dist/host-OBOI4MJK.js.map +1 -0
  113. package/dist/i18n.d.ts +301 -0
  114. package/dist/i18n.js +68 -0
  115. package/dist/i18n.js.map +1 -0
  116. package/dist/index-B6-_vr_m.d.ts +590 -0
  117. package/dist/index-CY55LC0u.d.ts +4722 -0
  118. package/dist/index-CeiTvwbp.d.ts +168 -0
  119. package/dist/index-XwP1ET8b.d.ts +61 -0
  120. package/dist/index.d.ts +2037 -0
  121. package/dist/index.js +2205 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/job-log-VZXWQUDK.js +24 -0
  124. package/dist/job-log-VZXWQUDK.js.map +1 -0
  125. package/dist/jobs.d.ts +4 -0
  126. package/dist/jobs.js +76 -0
  127. package/dist/jobs.js.map +1 -0
  128. package/dist/logger-DqGaOU_j.d.ts +29 -0
  129. package/dist/logger-S7REWDNE.js +16 -0
  130. package/dist/logger-S7REWDNE.js.map +1 -0
  131. package/dist/media.d.ts +5 -0
  132. package/dist/media.js +41 -0
  133. package/dist/media.js.map +1 -0
  134. package/dist/mentions-2IHFVSHW.js +23 -0
  135. package/dist/mentions-2IHFVSHW.js.map +1 -0
  136. package/dist/mutes-EWAE5FZR.js +21 -0
  137. package/dist/mutes-EWAE5FZR.js.map +1 -0
  138. package/dist/notification-prefs-VPJDU7I6.js +21 -0
  139. package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
  140. package/dist/observability.d.ts +156 -0
  141. package/dist/observability.js +32 -0
  142. package/dist/observability.js.map +1 -0
  143. package/dist/profanity-adapter-NU2JQSLX.js +12 -0
  144. package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
  145. package/dist/queue-XE5BC75T.js +14 -0
  146. package/dist/queue-XE5BC75T.js.map +1 -0
  147. package/dist/rate-limit.d.ts +99 -0
  148. package/dist/rate-limit.js +14 -0
  149. package/dist/rate-limit.js.map +1 -0
  150. package/dist/registry-XIXDEPVI.js +31 -0
  151. package/dist/registry-XIXDEPVI.js.map +1 -0
  152. package/dist/reputation-JRL2YQHM.js +11 -0
  153. package/dist/reputation-JRL2YQHM.js.map +1 -0
  154. package/dist/routes.d.ts +43 -0
  155. package/dist/routes.js +12 -0
  156. package/dist/routes.js.map +1 -0
  157. package/dist/scheduled-CIQM57HT.js +20 -0
  158. package/dist/scheduled-CIQM57HT.js.map +1 -0
  159. package/dist/seo.d.ts +410 -0
  160. package/dist/seo.js +44 -0
  161. package/dist/seo.js.map +1 -0
  162. package/dist/settings-FOBIESPB.js +17 -0
  163. package/dist/settings-FOBIESPB.js.map +1 -0
  164. package/dist/spam-adapter-XX3G737Z.js +12 -0
  165. package/dist/spam-adapter-XX3G737Z.js.map +1 -0
  166. package/dist/strings-VAE47B2C.js +29 -0
  167. package/dist/strings-VAE47B2C.js.map +1 -0
  168. package/dist/templates-IFVJMCJ6.js +12 -0
  169. package/dist/templates-IFVJMCJ6.js.map +1 -0
  170. package/dist/types-TlsbXS0T.d.ts +871 -0
  171. package/package.json +129 -0
package/dist/auth.d.ts ADDED
@@ -0,0 +1,640 @@
1
+ import { p as NpAccessFunction, n as NpUserRole, e as NpAuthUser } from './types-TlsbXS0T.js';
2
+ export { F as NpPrincipal } from './types-TlsbXS0T.js';
3
+ import { NodePgDatabase } from 'drizzle-orm/node-postgres';
4
+ import { Options } from '@node-rs/argon2';
5
+
6
+ declare const authenticated: NpAccessFunction;
7
+ declare const isAdmin: NpAccessFunction;
8
+ declare const isEditorOrAbove: NpAccessFunction;
9
+ declare const isOwnerOrAdmin: NpAccessFunction;
10
+
11
+ /**
12
+ * Staff-side JWT helpers. Both access (`np-session`) and refresh
13
+ * (`np-refresh`) cookies are signed with this module; the
14
+ * `use: "access" | "refresh"` claim separates them so a stolen
15
+ * refresh JWT cannot be replayed as a session cookie. Without this
16
+ * separation a leaked 7-day refresh became a 7-day admin bearer
17
+ * because both cookies decoded to the same `{ sub, role, ver }`
18
+ * payload through `verifyToken` (#94).
19
+ *
20
+ * The fix mirrors the member-side fix from #92/#93: the `use` claim
21
+ * is required, no legacy fallback for tokens missing the claim. The
22
+ * cost is one forced re-login for staff sessions issued before the
23
+ * deploy; bounded by the 7-day refresh TTL.
24
+ */
25
+ type NpTokenUse = "access" | "refresh";
26
+ interface NpTokenPayload {
27
+ sub: string;
28
+ role: NpUserRole;
29
+ ver: number;
30
+ /** Required. `verifyToken` refuses tokens missing this claim so
31
+ * legacy refresh JWTs cannot be smuggled into the session
32
+ * cookie path. */
33
+ use: NpTokenUse;
34
+ /** Random per-token id — needed if rotation lands on the staff
35
+ * side (mirrors the member-side `jti` for #45). Optional today
36
+ * but populated on every newly-minted token. */
37
+ jti?: string;
38
+ iat: number;
39
+ exp: number;
40
+ }
41
+ declare function signToken(user: {
42
+ id: string;
43
+ role: NpUserRole;
44
+ tokenVersion: number;
45
+ }, secret: string, expirationSeconds?: number, tokenUse?: NpTokenUse): Promise<string>;
46
+ /**
47
+ * Verify a staff JWT. When `expectedUse` is provided, refuses tokens
48
+ * whose `use` claim doesn't match — that's how `getSessionUser`
49
+ * rejects a refresh token used as a session cookie and how the
50
+ * refresh route rejects an access token as a refresh trigger.
51
+ *
52
+ * Tokens minted before the `use` claim landed have NO `use` payload
53
+ * field. We refuse those outright rather than treating them as
54
+ * `access` — the prior fallback would let still-live legacy refresh
55
+ * JWTs be smuggled into the session cookie and pass the access
56
+ * check. Cost: staff logged in before this deploy must log in once.
57
+ * Bounded by the refresh-token TTL (default 7 days).
58
+ */
59
+ declare function verifyToken(token: string, secret: string, expectedUse?: NpTokenUse): Promise<NpTokenPayload>;
60
+ /**
61
+ * True when `err` represents a token-verification failure rather than
62
+ * an unrelated runtime fault (DB outage, misconfiguration, …). Auth
63
+ * helpers use this to keep the existing "bad token → 401" behavior
64
+ * silent while letting infrastructure failures surface as 5xx.
65
+ *
66
+ * Covers:
67
+ * - `NpAuthError` — `verifyToken` / `verifyMemberToken` rejecting a
68
+ * missing or wrong `use` claim, or `verifyCsrf` failing.
69
+ * - `jose.errors.JOSEError` — every JWT signature / format /
70
+ * expiration failure, including subclasses like `JWTExpired`,
71
+ * `JWSSignatureVerificationFailed`, `JWTInvalid`.
72
+ */
73
+ declare function isTokenVerificationError(err: unknown): boolean;
74
+
75
+ declare const ARGON2_OPTIONS: Options;
76
+ declare function hashPassword(password: string): Promise<string>;
77
+ declare function verifyPassword(passwordHash: string, password: string): Promise<boolean>;
78
+
79
+ declare function verifyCsrf(method: string, cookieToken: string | undefined, headerToken: string | undefined): boolean;
80
+
81
+ /**
82
+ * Capability-based authorization (#273).
83
+ *
84
+ * Replaced the previous parallel `hasRole(user, minRole)` /
85
+ * `isStaffMod(user)` model. Naming the *behavior* instead of a role
86
+ * hierarchy means a reviewer spots `can(user, "community.moderate")`
87
+ * on a comment-mod path regardless of how the role table evolves
88
+ * later — and the previous trap where a `hasRole(user, "editor")`
89
+ * check silently dropped moderators is gone by construction.
90
+ *
91
+ * Capability vocabulary:
92
+ * - `content.publish` — change publication state on staff-owned
93
+ * content. Editor or admin.
94
+ * - `content.author` — create / edit content. Author, moderator,
95
+ * editor, or admin (moderators get author-
96
+ * level write access in this model so they
97
+ * can leave moderation notes / pinned
98
+ * replies on the content surface).
99
+ * - `community.moderate` — comment hide/restore, report triage, ban
100
+ * operations. Admin, editor, or moderator.
101
+ * - `admin.manage` — admin-only surfaces (site CRUD,
102
+ * super-admin-adjacent settings).
103
+ *
104
+ * Add new capabilities by extending the union AND the exhaustive
105
+ * switch below — TypeScript will surface the missing branch.
106
+ */
107
+ type NpCapability = "content.publish" | "content.author" | "community.moderate" | "admin.manage";
108
+ declare function can(user: NpAuthUser | null | undefined, capability: NpCapability): boolean;
109
+
110
+ /**
111
+ * OAuth provider registry — extension point for SSO. A provider plugin
112
+ * (e.g. `@nexpress/plugin-oauth-github`) registers itself at startup
113
+ * via `registerOAuthProvider()`; the framework's `/api/auth/oauth/{id}`
114
+ * routes look it up by id.
115
+ *
116
+ * The provider is responsible for:
117
+ * - Building the authorize URL (`authorize`).
118
+ * - Exchanging the callback code for a normalized profile (`exchange`).
119
+ *
120
+ * The framework owns state-cookie signing, identity ↔ user resolution,
121
+ * session minting, and audit. Providers must NOT touch cookies, the DB,
122
+ * or response objects directly.
123
+ */
124
+ /**
125
+ * Profile returned from a successful `exchange()`. The framework uses
126
+ * `providerUserId` as the durable identifier — `email` may change at the
127
+ * provider but `providerUserId` should not. If the provider doesn't
128
+ * surface `email`, the framework falls back to creating a synthetic
129
+ * placeholder (`<providerUserId>@<provider>.oauth.local`) so the
130
+ * `np_users.email NOT NULL UNIQUE` constraint is still satisfied.
131
+ */
132
+ interface OAuthProfile {
133
+ /** Stable per-user id from the provider. Required. */
134
+ providerUserId: string;
135
+ /** Optional — falls back to synthetic if missing. */
136
+ email?: string | null;
137
+ /** Optional — defaults to email local-part on user creation. */
138
+ name?: string | null;
139
+ /** Optional — written into `np_user_oauth_identities.metadata`. */
140
+ avatarUrl?: string | null;
141
+ /** Optional — full payload the provider wants to remember (e.g. scopes). */
142
+ metadata?: Record<string, unknown>;
143
+ }
144
+ /**
145
+ * Inputs the provider receives at the two callback boundaries. The
146
+ * framework picks `redirectUri` from `SITE_URL` (or the request origin
147
+ * in dev) so the provider doesn't have to know its own deployment URL.
148
+ */
149
+ interface OAuthAuthorizeParams {
150
+ state: string;
151
+ redirectUri: string;
152
+ /**
153
+ * PKCE code verifier (32+ char URL-safe random). The framework
154
+ * generates one for every login and threads it through the state
155
+ * cookie. Providers that don't support PKCE (e.g. GitHub) ignore it;
156
+ * providers that require it (e.g. Google) hash it into the
157
+ * `code_challenge` query param.
158
+ */
159
+ codeVerifier: string;
160
+ }
161
+ interface OAuthExchangeParams {
162
+ code: string;
163
+ state: string;
164
+ redirectUri: string;
165
+ /** Same verifier minted at /start, recovered from the state cookie. */
166
+ codeVerifier: string;
167
+ }
168
+ interface OAuthProvider {
169
+ /** Stable id used in route paths and `np_user_oauth_identities.provider`. */
170
+ id: string;
171
+ /** Human-readable label for admin UI / login buttons. */
172
+ label?: string;
173
+ /**
174
+ * Returns a fully-qualified URL the framework should redirect the
175
+ * browser to. Async to allow providers that need to mint per-request
176
+ * client credentials.
177
+ */
178
+ authorize(params: OAuthAuthorizeParams): Promise<string> | string;
179
+ /**
180
+ * Validates the callback and returns the normalized profile.
181
+ * Throwing here aborts the login with `OAUTH_EXCHANGE_FAILED`.
182
+ */
183
+ exchange(params: OAuthExchangeParams): Promise<OAuthProfile>;
184
+ }
185
+ /**
186
+ * Register a provider. Idempotent: re-registering with the same id
187
+ * overwrites — useful in dev when a plugin's `setup()` runs again on
188
+ * reload.
189
+ */
190
+ declare function registerOAuthProvider(provider: OAuthProvider): void;
191
+ declare function getOAuthProvider(id: string): OAuthProvider | undefined;
192
+ declare function listOAuthProviders(): OAuthProvider[];
193
+ /** Reset the registry — tests use this between cases. Not for runtime use. */
194
+ declare function resetOAuthProviders(): void;
195
+
196
+ /**
197
+ * Resolves an `OAuthProfile` to a real `np_users` row, in this order:
198
+ *
199
+ * 1. Lookup by `(provider, provider_user_id)` — the durable link. This
200
+ * is the only path that survives an email change at the provider.
201
+ * 2. Email-match — if the provider gave us an email and an existing
202
+ * user has it, link the OAuth identity to that user. Lets a staff
203
+ * member who originally signed up with a password later "sign in
204
+ * with Google" and have it just work, without an explicit linking
205
+ * UI.
206
+ * 3. Create — auto-provision a new user with the provider's profile,
207
+ * default role `viewer`. The password column is filled with an
208
+ * unrecoverable Argon2 hash of a random secret so the column
209
+ * constraints are satisfied; the user can later run the
210
+ * forgot-password flow to set a real password if they want one.
211
+ *
212
+ * Side effects: writes a row into `np_user_oauth_identities` for paths
213
+ * 2 and 3, updates `metadata` for path 1.
214
+ */
215
+ interface ResolveOAuthLoginResult {
216
+ user: ResolvedOAuthUser;
217
+ /** Tells the caller whether this login created the underlying user. */
218
+ created: boolean;
219
+ /** Tells the caller whether this login linked a new identity row. */
220
+ linked: boolean;
221
+ }
222
+ interface ResolvedOAuthUser {
223
+ id: string;
224
+ email: string;
225
+ name: string;
226
+ role: NpUserRole;
227
+ tokenVersion: number;
228
+ }
229
+ interface ResolveOAuthLoginInput {
230
+ provider: string;
231
+ profile: OAuthProfile;
232
+ /** Default role for auto-created users. Defaults to `"viewer"`. */
233
+ defaultRole?: NpUserRole;
234
+ }
235
+ declare function resolveOAuthLogin(input: ResolveOAuthLoginInput): Promise<ResolveOAuthLoginResult>;
236
+
237
+ /**
238
+ * Member-side mirror of `resolveOAuthLogin` (the staff resolver in
239
+ * `oauth-resolve.ts`). Walks the same three-step ladder:
240
+ *
241
+ * 1. Lookup by `(provider, subject)` in `np_member_identities` —
242
+ * durable provider link.
243
+ * 2. Email match — if the profile carries an email, link the
244
+ * identity to the existing `np_members` row.
245
+ * 3. Auto-provision a new member with status=`active`, default
246
+ * password = unrecoverable Argon2 of a random secret. The user
247
+ * can later run forgot-password to set a real password if they
248
+ * want one (or stay SSO-only).
249
+ *
250
+ * Members are kept distinct from staff users at every layer
251
+ * (different table, different cookies, different audience claim on
252
+ * the JWT). This resolver intentionally never touches `np_users`.
253
+ */
254
+ interface ResolvedOAuthMember {
255
+ id: string;
256
+ email: string;
257
+ handle: string;
258
+ displayName: string;
259
+ status: "active" | "pending" | "suspended" | "deleted";
260
+ tokenVersion: number;
261
+ }
262
+ interface ResolveMemberOAuthLoginInput {
263
+ provider: string;
264
+ profile: OAuthProfile;
265
+ }
266
+ interface ResolveMemberOAuthLoginResult {
267
+ member: ResolvedOAuthMember;
268
+ /** True when this login auto-provisioned the underlying member. */
269
+ created: boolean;
270
+ /** True when this login linked a new identity row (covers steps 2 + 3). */
271
+ linked: boolean;
272
+ }
273
+ declare function resolveMemberOAuthLogin(input: ResolveMemberOAuthLoginInput): Promise<ResolveMemberOAuthLoginResult>;
274
+
275
+ interface OAuthStatePayload {
276
+ providerId: string;
277
+ nonce: string;
278
+ expSeconds: number;
279
+ codeVerifier: string;
280
+ }
281
+ interface IssuedOAuthState {
282
+ /** The serialized state token (cookie + redirect query value). */
283
+ token: string;
284
+ /** The PKCE verifier — also embedded in the token, surfaced here so
285
+ * the route can pass it to `provider.authorize()` without re-parsing. */
286
+ codeVerifier: string;
287
+ }
288
+ declare function issueOAuthState(providerId: string, secret: string): IssuedOAuthState;
289
+ interface VerifyOAuthStateResult {
290
+ ok: boolean;
291
+ payload?: OAuthStatePayload;
292
+ reason?: "format" | "signature" | "expired";
293
+ }
294
+ /**
295
+ * Strict verification:
296
+ * - Format must be `<payload>.<sig>` with two segments.
297
+ * - HMAC must match (constant-time compare).
298
+ * - `expSeconds` must be in the future.
299
+ * - `providerId` in the payload must match the route's expected provider.
300
+ * - `codeVerifier` must be a non-empty string.
301
+ */
302
+ declare function verifyOAuthState(token: string, expectedProviderId: string, secret: string): VerifyOAuthStateResult;
303
+
304
+ /**
305
+ * Adapter that bridges any [arctic](https://arctic.js.org/) provider
306
+ * (`new GitHub(...)`, `new Google(...)`, `new Apple(...)`, etc.) to
307
+ * NexPress's `OAuthProvider` interface.
308
+ *
309
+ * Why this exists: arctic ships ~25 maintained providers and handles
310
+ * the OAuth dance — token exchange, PKCE hashing, refresh-token
311
+ * support — so plugin authors only have to write the **profile fetch**
312
+ * (the part that varies most by provider). Our framework still owns
313
+ * state cookies, identity ↔ user resolution, and session minting; this
314
+ * adapter just lets users skip the boilerplate token POST.
315
+ *
316
+ * Usage from a plugin:
317
+ *
318
+ * import { Apple } from "arctic";
319
+ * import { fromArctic, registerOAuthProvider } from "@nexpress/core";
320
+ *
321
+ * registerOAuthProvider(fromArctic(
322
+ * // Factory: framework calls this each request with the freshly-
323
+ * // resolved redirectUri (matters in dev when Next.js may bind a
324
+ * // non-default port).
325
+ * (redirectUri) => new Apple(clientId, teamId, keyId, privateKey, redirectUri),
326
+ * {
327
+ * id: "apple",
328
+ * scopes: ["name", "email"],
329
+ * fetchProfile: async (accessToken, tokens) => {
330
+ * // Apple returns the user payload INSIDE the token response
331
+ * // (not a separate userinfo endpoint) — pull it from
332
+ * // `tokens.idToken()` here and parse the JWT body.
333
+ * return { providerUserId: parseAppleSub(tokens.idToken()), email: null };
334
+ * },
335
+ * },
336
+ * ));
337
+ */
338
+ /**
339
+ * Minimal slice of arctic's provider classes that the adapter actually
340
+ * needs. Both `GitHub` (no PKCE) and `Google` (PKCE-required) match
341
+ * this — the third positional arg is "second positional" for
342
+ * non-PKCE providers (just unused) and "code verifier" for PKCE ones.
343
+ *
344
+ * Declared structurally so we don't drag arctic into the public type
345
+ * graph of `@nexpress/core`. Plugins that import a real arctic class
346
+ * pass it directly; the structural match keeps the signature lined up.
347
+ */
348
+ interface ArcticLikeProvider {
349
+ createAuthorizationURL(state: string, ...rest: never[]): URL;
350
+ validateAuthorizationCode(code: string, ...rest: never[]): Promise<ArcticLikeTokens>;
351
+ }
352
+ interface ArcticLikeTokens {
353
+ accessToken(): string;
354
+ hasRefreshToken?(): boolean;
355
+ refreshToken?(): string;
356
+ idToken?(): string;
357
+ }
358
+ interface FromArcticOptions {
359
+ /** Provider id used in route paths and `np_user_oauth_identities.provider`. */
360
+ id: string;
361
+ /** Human label for admin UI / login buttons. */
362
+ label?: string;
363
+ /** Scopes passed to `createAuthorizationURL`. Most providers default
364
+ * to nothing useful — set this. */
365
+ scopes?: string[];
366
+ /**
367
+ * Whether the underlying arctic provider expects a PKCE code verifier
368
+ * as the second arg to `createAuthorizationURL` and
369
+ * `validateAuthorizationCode`. Default `true` (Google, Apple, etc.).
370
+ * Set `false` for non-PKCE providers like GitHub.
371
+ */
372
+ pkce?: boolean;
373
+ /**
374
+ * Turns an access token (and the full token response, useful for
375
+ * providers like Apple that return the profile in the token) into the
376
+ * normalized `OAuthProfile` consumed by `resolveOAuthLogin`.
377
+ *
378
+ * Throwing aborts the login with `oauth_error=exchange_failed`.
379
+ */
380
+ fetchProfile: (accessToken: string, tokens: ArcticLikeTokens) => Promise<OAuthProfile>;
381
+ }
382
+ /**
383
+ * Wraps an arctic provider into the framework's `OAuthProvider`
384
+ * shape. The framework calls `authorize` and `exchange`; this adapter
385
+ * builds a fresh arctic instance per request via `factory(redirectUri)`
386
+ * so the redirect URI always matches what the framework computed for
387
+ * THIS request — critical in dev where Next.js may fall back to a
388
+ * non-3000 port and a setup-time-frozen redirectUri would diverge.
389
+ *
390
+ * Arctic provider classes are cheap to construct (just hold the three
391
+ * credential strings), so the per-request factory call has no
392
+ * meaningful cost.
393
+ */
394
+ declare function fromArctic(factory: (redirectUri: string) => ArcticLikeProvider, opts: FromArcticOptions): OAuthProvider;
395
+
396
+ /**
397
+ * Loose Drizzle handle type — every staff-auth caller passes
398
+ * the same NodePgDatabase, but TS over-narrows when the
399
+ * generated schema record is folded in. Using
400
+ * `Record<string, unknown>` keeps the helper portable across
401
+ * schema generations without surfacing as `any`.
402
+ */
403
+ type SessionDb = NodePgDatabase<Record<string, unknown>>;
404
+ declare function sha256(input: string): Promise<string>;
405
+ /**
406
+ * Verify a staff JWT and resolve the active user.
407
+ *
408
+ * `expectedUse` defaults to `"access"` because every caller of this
409
+ * helper outside the rotation endpoint reads `np-session` (server
410
+ * components, route handlers, the bootstrap layout). Defaulting
411
+ * means a fresh route or RSC page can't accidentally tolerate a
412
+ * refresh JWT in the session cookie just by forgetting the
413
+ * argument. The rotation route explicitly passes `"refresh"` for
414
+ * its `np-refresh` read.
415
+ *
416
+ * Tokens missing the `use` claim throw via `verifyToken`; we let
417
+ * that propagate so a `NpAuthError` surfaces as 401 at the API
418
+ * layer.
419
+ */
420
+ declare function verifyTokenFull(token: string, secret: string, db: SessionDb, expectedUse?: NpTokenUse): Promise<NpAuthUser | null>;
421
+ declare function invalidateAllSessions(userId: string, db: SessionDb): Promise<void>;
422
+
423
+ /**
424
+ * Admin-side helpers for listing and revoking OAuth identity links.
425
+ * Both staff (`np_user_oauth_identities`) and member
426
+ * (`np_member_identities`) tables use the same shape: one row per
427
+ * (account, provider) pair, holding the durable provider subject
428
+ * plus arbitrary metadata. These helpers are the source of truth for
429
+ * `/api/admin/users/[id]/identities` and the member equivalent.
430
+ *
431
+ * Revoking does not invalidate sessions — the user / member can
432
+ * re-link by signing in via OAuth again, which creates a fresh
433
+ * identity row through the resolver. Revocation is intentionally
434
+ * reversible because the durable link is the only thing dropped;
435
+ * the underlying account remains.
436
+ */
437
+ interface NpUserIdentityRow {
438
+ id: string;
439
+ userId: string;
440
+ provider: string;
441
+ providerUserId: string;
442
+ metadata: Record<string, unknown>;
443
+ createdAt: Date;
444
+ updatedAt: Date;
445
+ }
446
+ interface NpMemberIdentityRow {
447
+ id: string;
448
+ memberId: string;
449
+ provider: string;
450
+ subject: string;
451
+ email: string | null;
452
+ metadata: Record<string, unknown>;
453
+ createdAt: Date;
454
+ updatedAt: Date;
455
+ }
456
+ declare function listUserIdentities(userId: string): Promise<NpUserIdentityRow[]>;
457
+ declare function listMemberIdentities(memberId: string): Promise<NpMemberIdentityRow[]>;
458
+ interface RevokeIdentityInput {
459
+ /** Staff user id whose identity is being revoked (`actorKind: "staff"`). */
460
+ staffUserId: string;
461
+ }
462
+ declare function revokeUserIdentity(userId: string, identityId: string, actor: RevokeIdentityInput): Promise<void>;
463
+ declare function revokeMemberIdentity(memberId: string, identityId: string, actor: RevokeIdentityInput): Promise<void>;
464
+
465
+ type NpPasswordResetPurpose = "invite" | "reset";
466
+ interface NpIssuedResetToken {
467
+ /** The raw token — deliver to the user, never persist. */
468
+ token: string;
469
+ /** Matches `np_users.password_reset_expires_at`. */
470
+ expiresAt: Date;
471
+ purpose: NpPasswordResetPurpose;
472
+ }
473
+ interface NpCreateResetTokenOptions {
474
+ userId: string;
475
+ purpose: NpPasswordResetPurpose;
476
+ ttlMs: number;
477
+ }
478
+ /**
479
+ * Issues a new password reset token for `userId`. Stores the **hash** of the
480
+ * token in the `np_users` row alongside the expiry and purpose, then returns
481
+ * the raw token for the caller to deliver (email/link).
482
+ *
483
+ * Any previously-outstanding reset token for the user is replaced.
484
+ */
485
+ declare function createPasswordResetToken(db: NodePgDatabase<Record<string, unknown>>, options: NpCreateResetTokenOptions): Promise<NpIssuedResetToken>;
486
+ interface NpResetRequestResult {
487
+ userId: string | null;
488
+ name: string | null;
489
+ email: string | null;
490
+ issued: NpIssuedResetToken | null;
491
+ }
492
+ /**
493
+ * Handles the "forgot password" flow. If the email matches a user, issues a
494
+ * reset token and returns their name so the mailer can personalise the email.
495
+ * If not, silently returns nulls so callers can respond with a constant
496
+ * message and avoid email enumeration.
497
+ */
498
+ declare function requestPasswordReset(db: NodePgDatabase<Record<string, unknown>>, email: string, ttlMs: number): Promise<NpResetRequestResult>;
499
+ interface NpConsumeResetTokenOptions {
500
+ token: string;
501
+ newPassword: string;
502
+ }
503
+ interface NpConsumeResetTokenResult {
504
+ userId: string;
505
+ email: string;
506
+ purpose: NpPasswordResetPurpose;
507
+ }
508
+ /**
509
+ * Verifies a password reset token and atomically:
510
+ * - sets the new password hash
511
+ * - bumps `tokenVersion` and deletes all sessions (force logout everywhere)
512
+ * - clears the reset columns on the user row
513
+ *
514
+ * Throws `NpValidationError` when the token is unknown, expired, or the
515
+ * password is too short. Uses a single DB transaction for atomicity.
516
+ */
517
+ declare function consumePasswordResetToken(db: NodePgDatabase<Record<string, unknown>>, options: NpConsumeResetTokenOptions): Promise<NpConsumeResetTokenResult>;
518
+
519
+ /**
520
+ * Member-side JWT helpers. Mirrors `signToken` / `verifyToken` for
521
+ * staff but adds a fixed `aud: "member"` claim so a forged JWT signed
522
+ * for a staff user can't be replayed against member-only routes (and
523
+ * vice-versa).
524
+ *
525
+ * The signing secret is the same `NP_SECRET`; rotating it invalidates
526
+ * both staff and member sessions, which is the desired behavior.
527
+ *
528
+ * Every token gets a random `jti` so two tokens minted within the
529
+ * same second for the same member produce DIFFERENT JWT strings —
530
+ * needed for refresh-token rotation: without it, the rotated token
531
+ * hash would collide with the prior token hash and revocation by
532
+ * tokenHash would still resolve the rotated row.
533
+ *
534
+ * `use: "access" | "refresh"` separates the two token purposes. A
535
+ * refresh JWT cannot be presented as the `np-mb-session` cookie and
536
+ * a session JWT cannot drive the rotation endpoint — without this
537
+ * separation a leaked refresh token effectively became a long-lived
538
+ * bearer access token because both kinds were stored as fungible
539
+ * rows in `np_member_sessions` with no row-level kind column.
540
+ */
541
+ type NpMemberTokenUse = "access" | "refresh";
542
+ interface NpMemberTokenPayload {
543
+ sub: string;
544
+ aud: "member";
545
+ ver: number;
546
+ /** Required. `verifyMemberToken` refuses tokens missing this claim
547
+ * so legacy refresh JWTs from before #92 cannot be smuggled into
548
+ * the session cookie path (#91 reopen). */
549
+ use: NpMemberTokenUse;
550
+ /** Optional only for the deploy window; new tokens always carry
551
+ * one. */
552
+ jti?: string;
553
+ iat: number;
554
+ exp: number;
555
+ }
556
+ declare function signMemberToken(member: {
557
+ id: string;
558
+ tokenVersion: number;
559
+ }, secret: string, expirationSeconds?: number, tokenUse?: NpMemberTokenUse): Promise<string>;
560
+ /**
561
+ * Verify a member JWT and return the parsed payload. When
562
+ * `expectedUse` is provided, refuses tokens whose `use` claim doesn't
563
+ * match — that's how `getSessionMember` rejects a refresh token used
564
+ * as a session cookie and how the refresh route rejects an access
565
+ * token as a refresh trigger.
566
+ *
567
+ * Tokens minted before the `use` claim landed have NO `use` payload
568
+ * field. We refuse those outright rather than treating them as
569
+ * `access` — the prior fallback let still-live legacy refresh JWTs
570
+ * (already persisted in `np_member_sessions` per #45's fix) be
571
+ * smuggled into the session cookie and pass the access check (#91
572
+ * reopen). The cost: members logged in before this deploy must log
573
+ * in once. That's bounded by the access-token TTL (default 2h);
574
+ * legacy session rows that don't match a new login age out via
575
+ * `expiresAt` within 7 days regardless.
576
+ */
577
+ declare function verifyMemberToken(token: string, secret: string, expectedUse?: NpMemberTokenUse): Promise<NpMemberTokenPayload>;
578
+
579
+ /**
580
+ * Member-side session lookups, mirroring the staff helpers in session.ts
581
+ * but for `np_members` / `np_member_sessions`. The sha256 helper is
582
+ * reused (sessions store hashed tokens regardless of the principal kind).
583
+ */
584
+ interface NpMemberAuthRow {
585
+ id: string;
586
+ email: string;
587
+ handle: string;
588
+ displayName: string;
589
+ status: "active" | "pending" | "suspended" | "deleted";
590
+ tokenVersion: number;
591
+ }
592
+ /**
593
+ * Resolve a member from a verified JWT payload AND the raw access
594
+ * token. We hash the token and require a live row in
595
+ * `np_member_sessions` — without that row check, deleting a session in
596
+ * `/api/members/logout` had no effect and a stolen token kept working
597
+ * until JWT expiry. (#45)
598
+ *
599
+ * Backward-compat: when no `accessToken` is passed (legacy callers in
600
+ * tests / older routes), we fall back to the previous tokenVersion
601
+ * check only. New paths should always pass the token.
602
+ */
603
+ declare function getMemberFromTokenPayload(db: NodePgDatabase<Record<string, unknown>>, payload: {
604
+ sub: string;
605
+ ver: number;
606
+ }, accessToken?: string): Promise<NpMemberAuthRow | null>;
607
+ /**
608
+ * Bumps a member's tokenVersion + drops every session row, force-logging
609
+ * them out everywhere. Call inside the same transaction as a password
610
+ * change / soft-delete so a leaked old JWT can't outlive the change.
611
+ */
612
+ declare function invalidateAllMemberSessions(db: NodePgDatabase<Record<string, unknown>>, memberId: string): Promise<void>;
613
+
614
+ interface NpIssuedMemberToken {
615
+ /** The raw token to ship to the user. Never persist. */
616
+ token: string;
617
+ expiresAt: Date;
618
+ }
619
+ declare function createMemberEmailVerifyToken(db: NodePgDatabase<Record<string, unknown>>, memberId: string, ttlMs: number): Promise<NpIssuedMemberToken>;
620
+ interface NpConsumeMemberEmailVerifyResult {
621
+ memberId: string;
622
+ email: string;
623
+ handle: string;
624
+ displayName: string;
625
+ }
626
+ declare function consumeMemberEmailVerifyToken(db: NodePgDatabase<Record<string, unknown>>, token: string): Promise<NpConsumeMemberEmailVerifyResult>;
627
+ interface NpMemberResetRequestResult {
628
+ memberId: string | null;
629
+ displayName: string | null;
630
+ email: string | null;
631
+ issued: NpIssuedMemberToken | null;
632
+ }
633
+ declare function requestMemberPasswordReset(db: NodePgDatabase<Record<string, unknown>>, email: string, ttlMs: number): Promise<NpMemberResetRequestResult>;
634
+ interface NpConsumeMemberResetResult {
635
+ memberId: string;
636
+ email: string;
637
+ }
638
+ declare function consumeMemberPasswordReset(db: NodePgDatabase<Record<string, unknown>>, token: string, newPassword: string): Promise<NpConsumeMemberResetResult>;
639
+
640
+ export { ARGON2_OPTIONS, type ArcticLikeProvider, type ArcticLikeTokens, type FromArcticOptions, type IssuedOAuthState, type NpCapability, type NpConsumeMemberEmailVerifyResult, type NpConsumeMemberResetResult, type NpConsumeResetTokenOptions, type NpConsumeResetTokenResult, type NpCreateResetTokenOptions, type NpIssuedMemberToken, type NpIssuedResetToken, type NpMemberAuthRow, type NpMemberIdentityRow, type NpMemberResetRequestResult, type NpMemberTokenPayload, type NpPasswordResetPurpose, type NpResetRequestResult, type NpTokenPayload, type NpUserIdentityRow, type OAuthAuthorizeParams, type OAuthExchangeParams, type OAuthProfile, type OAuthProvider, type OAuthStatePayload, type ResolveMemberOAuthLoginInput, type ResolveMemberOAuthLoginResult, type ResolveOAuthLoginInput, type ResolveOAuthLoginResult, type ResolvedOAuthMember, type ResolvedOAuthUser, type VerifyOAuthStateResult, authenticated, can, consumeMemberEmailVerifyToken, consumeMemberPasswordReset, consumePasswordResetToken, createMemberEmailVerifyToken, createPasswordResetToken, fromArctic, getMemberFromTokenPayload, getOAuthProvider, hashPassword, invalidateAllMemberSessions, invalidateAllSessions, isAdmin, isEditorOrAbove, isOwnerOrAdmin, isTokenVerificationError, issueOAuthState, listMemberIdentities, listOAuthProviders, listUserIdentities, registerOAuthProvider, requestMemberPasswordReset, requestPasswordReset, resetOAuthProviders, resolveMemberOAuthLogin, resolveOAuthLogin, revokeMemberIdentity, revokeUserIdentity, sha256, signMemberToken, signToken, verifyCsrf, verifyMemberToken, verifyOAuthState, verifyPassword, verifyToken, verifyTokenFull };