@everystack/auth 0.2.5 → 0.2.6

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/README.md CHANGED
@@ -247,4 +247,8 @@ Part of [everystack](https://github.com/scalable-technology/everystack) — a se
247
247
 
248
248
  ## License
249
249
 
250
- MIT
250
+ [AGPL-3.0-only](https://www.gnu.org/licenses/agpl-3.0.html) © Scalable Technology, Inc.
251
+
252
+ A commercial license is available for organizations that cannot or do not wish to
253
+ comply with the AGPL-3.0 terms. For commercial licensing, contact
254
+ licensing@scalable.technology.
package/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "@everystack/auth",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "JWT authentication handlers, password hashing, and React auth context",
5
5
  "license": "AGPL-3.0-only",
6
+ "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/scalable-technology/everystack.git",
10
+ "directory": "packages/auth"
11
+ },
12
+ "homepage": "https://github.com/scalable-technology/everystack#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/scalable-technology/everystack/issues"
15
+ },
6
16
  "publishConfig": {
7
17
  "access": "public"
8
18
  },
@@ -75,11 +85,6 @@
75
85
  "default": "./src/testing.ts"
76
86
  }
77
87
  },
78
- "scripts": {
79
- "test": "jest",
80
- "build": "tsc --build",
81
- "lint": "tsc --noEmit"
82
- },
83
88
  "dependencies": {
84
89
  "bcryptjs": "3.0.3"
85
90
  },
@@ -101,7 +106,6 @@
101
106
  }
102
107
  },
103
108
  "devDependencies": {
104
- "@everystack/api": "workspace:*",
105
109
  "@types/bcryptjs": "3.0.0",
106
110
  "@types/jest": "29.5.14",
107
111
  "@types/node": "22.19.18",
@@ -110,6 +114,12 @@
110
114
  "jest": "29.7.0",
111
115
  "postgres": "3.4.9",
112
116
  "ts-jest": "29.4.9",
113
- "typescript": "5.9.3"
117
+ "typescript": "5.9.3",
118
+ "@everystack/api": "0.2.9"
119
+ },
120
+ "scripts": {
121
+ "test": "jest",
122
+ "build": "tsc --build",
123
+ "lint": "tsc --noEmit"
114
124
  }
115
- }
125
+ }
package/src/index.ts CHANGED
@@ -147,10 +147,22 @@ export interface AuthOptions {
147
147
  /** One-time token operations for email confirmation and password recovery.
148
148
  * Required for confirmEmail, forgotPassword, and resetPassword handlers. */
149
149
  tokens?: TokenOperations;
150
- /** Called after successful signup when tokens are configured. App sends confirmation email. */
150
+ /** Called after successful signup when tokens are configured. App sends confirmation email.
151
+ * Receives a token MINTED IN THE API — owner-only, so this breaks under least-privilege.
152
+ * Postgres least-priv path uses `onConfirmationRequested` instead (mint on the worker). */
151
153
  onConfirmationRequired?: (user: { id: string; email: string }, token: string) => Promise<void>;
152
- /** Called on forgot-password — app sends recovery email. */
154
+ /** Called on forgot-password with a token MINTED IN THE API (owner-only — breaks under
155
+ * least-privilege). Postgres least-priv path uses `onRecoveryRequested` instead. */
153
156
  onRecoveryRequired?: (user: { id: string; email: string }, token: string) => Promise<void>;
157
+ /** Least-privilege confirmation: called after signup with only the new user's identity.
158
+ * The API does NOT mint a token — the app enqueues a job; the privileged worker mints the
159
+ * one-time token and sends the email. Takes precedence over onConfirmationRequired. */
160
+ onConfirmationRequested?: (user: { id: string; email: string }) => Promise<void>;
161
+ /** Least-privilege recovery: called on forgot-password with ONLY the email. The API does
162
+ * not read the user table or mint a token — it enqueues a job; the privileged worker
163
+ * looks up the user, mints the recovery token, and sends the email. Always responds the
164
+ * same to the client (no user enumeration). Takes precedence over onRecoveryRequired. */
165
+ onRecoveryRequested?: (email: string) => Promise<void>;
154
166
  /** Called after email confirmed via one-time token — app updates user record. */
155
167
  onEmailConfirmed?: (userId: string) => Promise<void>;
156
168
  }
@@ -209,6 +221,11 @@ function assertSafeCookieConfig(cookies: CookieConfig | undefined): void {
209
221
  /**
210
222
  * Add mode-independent email confirmation and password management handlers.
211
223
  * Called after base handlers are created (works with both node and postgres modes).
224
+ *
225
+ * Uses `??=` so a handler already provided by the base (e.g. the least-privilege
226
+ * postgres confirmEmail/resetPassword that run via SECDEF wrappers under withRole)
227
+ * is NOT overwritten by the generic owner-connection version. Node mode provides
228
+ * none of these, so it gets the generic implementations.
212
229
  */
213
230
  function addEmailPasswordFlows(
214
231
  handlers: AuthHandlers,
@@ -217,7 +234,7 @@ function addEmailPasswordFlows(
217
234
  options?: AuthOptions,
218
235
  ): void {
219
236
  if (options?.tokens) {
220
- handlers.confirmEmail = async (request: Request): Promise<Response> => {
237
+ handlers.confirmEmail ??= async (request: Request): Promise<Response> => {
221
238
  const csrfError = csrfCheck(request, options?.cookies);
222
239
  if (csrfError) return csrfError;
223
240
 
@@ -244,7 +261,7 @@ function addEmailPasswordFlows(
244
261
  }
245
262
  };
246
263
 
247
- handlers.forgotPassword = async (request: Request): Promise<Response> => {
264
+ handlers.forgotPassword ??= async (request: Request): Promise<Response> => {
248
265
  const csrfError = csrfCheck(request, options?.cookies);
249
266
  if (csrfError) return csrfError;
250
267
 
@@ -279,7 +296,7 @@ function addEmailPasswordFlows(
279
296
  return Response.json({ sent: true });
280
297
  };
281
298
 
282
- handlers.resetPassword = async (request: Request): Promise<Response> => {
299
+ handlers.resetPassword ??= async (request: Request): Promise<Response> => {
283
300
  const csrfError = csrfCheck(request, options?.cookies);
284
301
  if (csrfError) return csrfError;
285
302
 
@@ -313,7 +330,7 @@ function addEmailPasswordFlows(
313
330
  };
314
331
  }
315
332
 
316
- handlers.changePassword = async (request: Request): Promise<Response> => {
333
+ handlers.changePassword ??= async (request: Request): Promise<Response> => {
317
334
  const csrfError = csrfCheck(request, options?.cookies);
318
335
  if (csrfError) return csrfError;
319
336