@payez/next-mvp 4.4.6 → 4.4.8

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 (54) hide show
  1. package/dist/api-handlers/account/change-password.js +28 -6
  2. package/dist/auth/better-auth.d.ts +62 -15
  3. package/dist/auth/better-auth.js +180 -19
  4. package/dist/client/better-auth-client.d.ts +101 -956
  5. package/dist/config/env.d.ts +0 -2
  6. package/dist/config/env.js +0 -2
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +8 -1
  9. package/dist/lib/idp-client-config.js +8 -1
  10. package/dist/lib/never-in-response.d.ts +30 -0
  11. package/dist/lib/never-in-response.js +71 -0
  12. package/dist/lib/nextauth-secret.d.ts +51 -0
  13. package/dist/lib/nextauth-secret.js +141 -0
  14. package/dist/lib/safe-error-response.d.ts +6 -6
  15. package/dist/lib/safe-error-response.js +6 -6
  16. package/dist/lib/two-factor-verify-failure.d.ts +36 -3
  17. package/dist/lib/two-factor-verify-failure.js +79 -2
  18. package/dist/middleware/create-middleware.d.ts +2 -0
  19. package/dist/middleware/create-middleware.js +36 -0
  20. package/dist/pages/login/page.js +5 -0
  21. package/dist/pages/verify-code/page.js +4 -2
  22. package/dist/routes/auth/settings.js +4 -1
  23. package/dist/server/auth.d.ts +36 -294
  24. package/dist/server/auth.js +22 -25
  25. package/dist/vibe/client.d.ts +132 -16
  26. package/dist/vibe/client.js +288 -82
  27. package/dist/vibe/document-unwrap.d.ts +50 -0
  28. package/dist/vibe/document-unwrap.js +78 -0
  29. package/dist/vibe/errors.d.ts +17 -0
  30. package/dist/vibe/errors.js +21 -1
  31. package/dist/vibe/generic.d.ts +2 -49
  32. package/dist/vibe/generic.js +17 -78
  33. package/dist/vibe/index.d.ts +1 -1
  34. package/dist/vibe/index.js +2 -1
  35. package/package.json +17 -2
  36. package/src/api-handlers/account/change-password.ts +31 -6
  37. package/src/auth/better-auth.ts +188 -21
  38. package/src/config/env.ts +0 -2
  39. package/src/index.ts +4 -0
  40. package/src/lib/idp-client-config.ts +8 -1
  41. package/src/lib/never-in-response.ts +64 -0
  42. package/src/lib/nextauth-secret.ts +168 -0
  43. package/src/lib/safe-error-response.ts +6 -6
  44. package/src/lib/two-factor-verify-failure.ts +86 -5
  45. package/src/middleware/create-middleware.ts +42 -0
  46. package/src/pages/login/page.tsx +4 -0
  47. package/src/pages/verify-code/page.tsx +4 -2
  48. package/src/routes/auth/settings.ts +5 -1
  49. package/src/server/auth.ts +23 -29
  50. package/src/vibe/client.ts +378 -91
  51. package/src/vibe/document-unwrap.ts +103 -0
  52. package/src/vibe/errors.ts +24 -0
  53. package/src/vibe/generic.ts +8 -100
  54. package/src/vibe/index.ts +1 -0
@@ -81,3 +81,20 @@ export declare class VibeConflictError extends VibeError {
81
81
  export declare class VibeServiceError extends VibeError {
82
82
  constructor(message?: string, details?: Record<string, unknown>);
83
83
  }
84
+ /**
85
+ * PAY-1767 (NightHawk 64534): thrown when a NON-ATOMIC `createMany` fails part-way. It CARRIES what was
86
+ * already created, so the caller can reconcile instead of guessing whether k-1 rows persisted. This is the
87
+ * point of the class: a partial write whose extent is invisible is the hazard, not the partial write itself.
88
+ */
89
+ export declare class VibeBatchCreateError extends VibeError {
90
+ /** How many records were created before the failure. */
91
+ readonly createdCount: number;
92
+ /** The document ids that were created before the failure. */
93
+ readonly createdIds: Array<number | string>;
94
+ constructor(message: string, details: {
95
+ createdCount: number;
96
+ createdIds: Array<number | string>;
97
+ failedIndex: number;
98
+ total: number;
99
+ }, cause?: unknown);
100
+ }
@@ -21,7 +21,7 @@
21
21
  * =============================================================================
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.VibeServiceError = exports.VibeConflictError = exports.VibeRateLimitError = exports.VibeAuthError = exports.VibeValidationError = exports.VibeNotFoundError = exports.VibeError = void 0;
24
+ exports.VibeBatchCreateError = exports.VibeServiceError = exports.VibeConflictError = exports.VibeRateLimitError = exports.VibeAuthError = exports.VibeValidationError = exports.VibeNotFoundError = exports.VibeError = void 0;
25
25
  /**
26
26
  * Base error class for all Vibe API errors
27
27
  */
@@ -144,3 +144,23 @@ class VibeServiceError extends VibeError {
144
144
  }
145
145
  }
146
146
  exports.VibeServiceError = VibeServiceError;
147
+ /**
148
+ * PAY-1767 (NightHawk 64534): thrown when a NON-ATOMIC `createMany` fails part-way. It CARRIES what was
149
+ * already created, so the caller can reconcile instead of guessing whether k-1 rows persisted. This is the
150
+ * point of the class: a partial write whose extent is invisible is the hazard, not the partial write itself.
151
+ */
152
+ class VibeBatchCreateError extends VibeError {
153
+ /** How many records were created before the failure. */
154
+ createdCount;
155
+ /** The document ids that were created before the failure. */
156
+ createdIds;
157
+ constructor(message, details, cause) {
158
+ super(message, 'BATCH_PARTIAL_FAILURE', 500, details);
159
+ this.name = 'VibeBatchCreateError';
160
+ this.createdCount = details.createdCount;
161
+ this.createdIds = details.createdIds;
162
+ if (cause !== undefined)
163
+ this.cause = cause;
164
+ }
165
+ }
166
+ exports.VibeBatchCreateError = VibeBatchCreateError;
@@ -52,55 +52,8 @@ export declare function vibeQueryPath(collection: string, table: string): string
52
52
  * @returns Grid API path string
53
53
  */
54
54
  export declare function vibeGridPath(collection: string, table: string): string;
55
- /**
56
- * Vibe document wrapper structure.
57
- * Vibe stores actual table data as JSON string in the 'data' field.
58
- */
59
- export interface VibeDocumentWrapper {
60
- document_id: number;
61
- client_id?: number;
62
- user_id?: number;
63
- collection?: string;
64
- table_name?: string;
65
- data: string | Record<string, unknown>;
66
- collection_schema_id?: number;
67
- created_at?: string;
68
- created_by?: number;
69
- updated_at?: string | null;
70
- updated_by?: number | null;
71
- deleted_at?: string | null;
72
- }
73
- /**
74
- * Unwrap a Vibe document response.
75
- * Vibe returns documents with a wrapper where actual data is a JSON string.
76
- *
77
- * @param doc - Raw Vibe document (wrapper format)
78
- * @returns Unwrapped document with schema fields only.
79
- * Storage-layer document_id preserved as _vibe_doc_id for update/delete paths.
80
- * See PayEz-Core/docs/vibe-primary-key-standard.md
81
- *
82
- * @example
83
- * const raw = { document_id: 123, data: '{"user_id":15,"name":"John","email":"john@example.com"}' };
84
- * const unwrapped = unwrapVibeDocument(raw);
85
- * // => { user_id: 15, name: 'John', email: 'john@example.com', _vibe_doc_id: 123 }
86
- */
87
- export declare function unwrapVibeDocument<T extends Record<string, unknown> = Record<string, unknown>>(doc: VibeDocumentWrapper | Record<string, unknown> | null | undefined): (T & {
88
- _vibe_doc_id?: number;
89
- }) | null;
90
- /**
91
- * Extract and unwrap array of documents from Vibe response.
92
- *
93
- * @param responseData - Raw Vibe API response
94
- * @returns Array of unwrapped documents
95
- *
96
- * @example
97
- * const response = await fetch('/api/vibe/data/ideal_resume/resumes');
98
- * const data = await response.json();
99
- * const resumes = extractVibeDocuments(data);
100
- */
101
- export declare function extractVibeDocuments<T extends Record<string, unknown> = Record<string, unknown>>(responseData: unknown): Array<T & {
102
- _vibe_doc_id?: number;
103
- }>;
55
+ export { unwrapVibeDocument, extractVibeDocuments } from './document-unwrap';
56
+ export type { VibeDocumentWrapper } from './document-unwrap';
104
57
  /**
105
58
  * Generic table delegate for dynamic collection/table access.
106
59
  * Provides Prisma-like methods for any table.
@@ -24,12 +24,10 @@
24
24
  * =============================================================================
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.GenericCollection = exports.GenericTableDelegate = void 0;
27
+ exports.GenericCollection = exports.GenericTableDelegate = exports.extractVibeDocuments = exports.unwrapVibeDocument = void 0;
28
28
  exports.vibeTablePath = vibeTablePath;
29
29
  exports.vibeQueryPath = vibeQueryPath;
30
30
  exports.vibeGridPath = vibeGridPath;
31
- exports.unwrapVibeDocument = unwrapVibeDocument;
32
- exports.extractVibeDocuments = extractVibeDocuments;
33
31
  exports.vibeCollection = vibeCollection;
34
32
  exports.vibeTable = vibeTable;
35
33
  const client_1 = require("./client");
@@ -73,76 +71,17 @@ function vibeQueryPath(collection, table) {
73
71
  function vibeGridPath(collection, table) {
74
72
  return `/v1/collections/${collection}/tables/${table}/grid`;
75
73
  }
76
- /**
77
- * Unwrap a Vibe document response.
78
- * Vibe returns documents with a wrapper where actual data is a JSON string.
79
- *
80
- * @param doc - Raw Vibe document (wrapper format)
81
- * @returns Unwrapped document with schema fields only.
82
- * Storage-layer document_id preserved as _vibe_doc_id for update/delete paths.
83
- * See PayEz-Core/docs/vibe-primary-key-standard.md
84
- *
85
- * @example
86
- * const raw = { document_id: 123, data: '{"user_id":15,"name":"John","email":"john@example.com"}' };
87
- * const unwrapped = unwrapVibeDocument(raw);
88
- * // => { user_id: 15, name: 'John', email: 'john@example.com', _vibe_doc_id: 123 }
89
- */
90
- function unwrapVibeDocument(doc) {
91
- if (!doc)
92
- return null;
93
- // Handle case where doc is already unwrapped (has schema fields directly)
94
- if (!('document_id' in doc) && !('data' in doc)) {
95
- return doc;
96
- }
97
- const wrapper = doc;
98
- let parsedData = {};
99
- if (typeof wrapper.data === 'string') {
100
- try {
101
- parsedData = JSON.parse(wrapper.data);
102
- }
103
- catch {
104
- console.warn('[vibe-generic] Failed to parse document data JSON:', wrapper.data);
105
- parsedData = {};
106
- }
107
- }
108
- else if (typeof wrapper.data === 'object' && wrapper.data !== null) {
109
- parsedData = wrapper.data;
110
- }
111
- // Preserve document_id as _vibe_doc_id for Vibe API update/delete paths only
112
- const documentId = wrapper.document_id ?? doc.document_id;
113
- if (documentId != null) {
114
- parsedData._vibe_doc_id = documentId;
115
- }
116
- return parsedData;
117
- }
118
- /**
119
- * Extract and unwrap array of documents from Vibe response.
120
- *
121
- * @param responseData - Raw Vibe API response
122
- * @returns Array of unwrapped documents
123
- *
124
- * @example
125
- * const response = await fetch('/api/vibe/data/ideal_resume/resumes');
126
- * const data = await response.json();
127
- * const resumes = extractVibeDocuments(data);
128
- */
129
- function extractVibeDocuments(responseData) {
130
- if (!responseData || typeof responseData !== 'object') {
131
- return [];
132
- }
133
- const data = responseData;
134
- // Try common response shapes
135
- const docs = data.data ??
136
- data.items ??
137
- data.documents ??
138
- (Array.isArray(responseData) ? responseData : null);
139
- if (!Array.isArray(docs)) {
140
- return [];
141
- }
142
- return docs
143
- .map((doc) => unwrapVibeDocument(doc))
144
- .filter((d) => d !== null);
145
- }
74
+ // -----------------------------------------------------------------------------
75
+ // DOCUMENT UNWRAPPING
76
+ //
77
+ // PAY-1767: the wrapper type and the two helpers MOVED to ./document-unwrap, so the typed client
78
+ // (client.ts) can unwrap reads without importing this module (which imports client.ts). Re-exported
79
+ // here so every existing `from './generic'` / package import keeps working unchanged.
80
+ // -----------------------------------------------------------------------------
81
+ var document_unwrap_1 = require("./document-unwrap");
82
+ Object.defineProperty(exports, "unwrapVibeDocument", { enumerable: true, get: function () { return document_unwrap_1.unwrapVibeDocument; } });
83
+ Object.defineProperty(exports, "extractVibeDocuments", { enumerable: true, get: function () { return document_unwrap_1.extractVibeDocuments; } });
84
+ const document_unwrap_2 = require("./document-unwrap");
146
85
  // -----------------------------------------------------------------------------
147
86
  // GENERIC TABLE DELEGATE
148
87
  // -----------------------------------------------------------------------------
@@ -209,7 +148,7 @@ class GenericTableDelegate {
209
148
  const path = vibeTablePath(this.collection, this.tableName);
210
149
  const url = `${this.client.getBaseUrl()}${path}${queryString ? `?${queryString}` : ''}`;
211
150
  const response = await this.client.request(url, 'GET');
212
- const unwrapped = extractVibeDocuments(response);
151
+ const unwrapped = (0, document_unwrap_2.extractVibeDocuments)(response);
213
152
  return {
214
153
  data: unwrapped,
215
154
  meta: response.meta || { total: unwrapped.length, limit: options?.take || 50, offset: options?.skip || 0 },
@@ -223,7 +162,7 @@ class GenericTableDelegate {
223
162
  const path = vibeTablePath(this.collection, this.tableName, options.where.id);
224
163
  const url = `${this.client.getBaseUrl()}${path}`;
225
164
  const response = await this.client.request(url, 'GET');
226
- const unwrapped = unwrapVibeDocument(response.data);
165
+ const unwrapped = (0, document_unwrap_2.unwrapVibeDocument)(response.data);
227
166
  if (!unwrapped) {
228
167
  throw new errors_1.VibeNotFoundError(`Document ${options.where.id} not found in ${this.collection}/${this.tableName}`, 'NOT_FOUND', { collection: this.collection, table: this.tableName, id: options.where.id });
229
168
  }
@@ -257,7 +196,7 @@ class GenericTableDelegate {
257
196
  const path = vibeTablePath(this.collection, this.tableName);
258
197
  const url = `${this.client.getBaseUrl()}${path}`;
259
198
  const response = await this.client.request(url, 'POST', options.data);
260
- const unwrapped = unwrapVibeDocument(response.data);
199
+ const unwrapped = (0, document_unwrap_2.unwrapVibeDocument)(response.data);
261
200
  if (!unwrapped) {
262
201
  throw new errors_1.VibeServiceError('Failed to parse created document');
263
202
  }
@@ -270,7 +209,7 @@ class GenericTableDelegate {
270
209
  const path = vibeTablePath(this.collection, this.tableName, options.where.id);
271
210
  const url = `${this.client.getBaseUrl()}${path}`;
272
211
  const response = await this.client.request(url, 'PUT', options.data);
273
- const unwrapped = unwrapVibeDocument(response.data);
212
+ const unwrapped = (0, document_unwrap_2.unwrapVibeDocument)(response.data);
274
213
  if (!unwrapped) {
275
214
  throw new errors_1.VibeServiceError('Failed to parse updated document');
276
215
  }
@@ -283,7 +222,7 @@ class GenericTableDelegate {
283
222
  const path = vibeTablePath(this.collection, this.tableName, options.where.id);
284
223
  const url = `${this.client.getBaseUrl()}${path}`;
285
224
  const response = await this.client.request(url, 'DELETE');
286
- const unwrapped = unwrapVibeDocument(response.data);
225
+ const unwrapped = (0, document_unwrap_2.unwrapVibeDocument)(response.data);
287
226
  if (!unwrapped) {
288
227
  throw new errors_1.VibeServiceError('Failed to parse deleted document');
289
228
  }
@@ -16,7 +16,7 @@ export { vibe, createVibeClient, VibeClient, VibeTableDelegate } from './client'
16
16
  export type { VibeClientConfig, FindManyOptions, FindUniqueOptions, CreateOptions, UpdateOptions, DeleteOptions, CountOptions, FindManyResult, WhereClause, OrderByClause, FilterOperator, FilterValue, OrderDirection, } from './client';
17
17
  export type { VibeAuditFields, VibeMeta, VibeResponse, VibeErrorResponse, VibeTableTypes, VibeTableName, VibeTableType, IVibeUser, IVibeLoginSession, SessionStatus, IVibeProfile, IVibeSetting, IVibeFile, IVibeNotification, NotificationType, IVibeActivityLog, ActivityAction, IVibeTag, IVibeComment, IVibeSiteLog, LogLevel, } from './types';
18
18
  export { VibeUser, VibeLoginSession, VibeProfile, VibeSetting, VibeFile, VibeNotification, VibeActivityLog, VibeTag, VibeComment, VibeSiteLog, } from './types';
19
- export { VibeError, VibeNotFoundError, VibeValidationError, VibeAuthError, VibeRateLimitError, VibeConflictError, VibeServiceError, } from './errors';
19
+ export { VibeError, VibeNotFoundError, VibeValidationError, VibeAuthError, VibeRateLimitError, VibeConflictError, VibeServiceError, VibeBatchCreateError, } from './errors';
20
20
  export { createLoginSession, getUserSessions, getAllSessions, getSessionById, revokeSession, revokeAllUserSessions, updateSessionActivity, isSessionValid, checkSessionRevocation, getSessionStats, } from './sessions';
21
21
  export type { CreateSessionInput, SessionQueryOptions } from './sessions';
22
22
  export { vibeCollection, vibeTable, vibeTablePath, vibeQueryPath, vibeGridPath, unwrapVibeDocument, extractVibeDocuments, GenericCollection, GenericTableDelegate, } from './generic';
@@ -14,7 +14,7 @@
14
14
  * =============================================================================
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.generateBackendHmacSignature = exports.hasEnterpriseAuthHeaders = exports.validateEnterpriseAuth = exports.GenericTableDelegate = exports.GenericCollection = exports.extractVibeDocuments = exports.unwrapVibeDocument = exports.vibeGridPath = exports.vibeQueryPath = exports.vibeTablePath = exports.vibeTable = exports.vibeCollection = exports.getSessionStats = exports.checkSessionRevocation = exports.isSessionValid = exports.updateSessionActivity = exports.revokeAllUserSessions = exports.revokeSession = exports.getSessionById = exports.getAllSessions = exports.getUserSessions = exports.createLoginSession = exports.VibeServiceError = exports.VibeConflictError = exports.VibeRateLimitError = exports.VibeAuthError = exports.VibeValidationError = exports.VibeNotFoundError = exports.VibeError = exports.VibeSiteLog = exports.VibeComment = exports.VibeTag = exports.VibeActivityLog = exports.VibeNotification = exports.VibeFile = exports.VibeSetting = exports.VibeProfile = exports.VibeLoginSession = exports.VibeUser = exports.VibeTableDelegate = exports.VibeClient = exports.createVibeClient = exports.vibe = void 0;
17
+ exports.generateBackendHmacSignature = exports.hasEnterpriseAuthHeaders = exports.validateEnterpriseAuth = exports.GenericTableDelegate = exports.GenericCollection = exports.extractVibeDocuments = exports.unwrapVibeDocument = exports.vibeGridPath = exports.vibeQueryPath = exports.vibeTablePath = exports.vibeTable = exports.vibeCollection = exports.getSessionStats = exports.checkSessionRevocation = exports.isSessionValid = exports.updateSessionActivity = exports.revokeAllUserSessions = exports.revokeSession = exports.getSessionById = exports.getAllSessions = exports.getUserSessions = exports.createLoginSession = exports.VibeBatchCreateError = exports.VibeServiceError = exports.VibeConflictError = exports.VibeRateLimitError = exports.VibeAuthError = exports.VibeValidationError = exports.VibeNotFoundError = exports.VibeError = exports.VibeSiteLog = exports.VibeComment = exports.VibeTag = exports.VibeActivityLog = exports.VibeNotification = exports.VibeFile = exports.VibeSetting = exports.VibeProfile = exports.VibeLoginSession = exports.VibeUser = exports.VibeTableDelegate = exports.VibeClient = exports.createVibeClient = exports.vibe = void 0;
18
18
  // Client exports
19
19
  var client_1 = require("./client");
20
20
  Object.defineProperty(exports, "vibe", { enumerable: true, get: function () { return client_1.vibe; } });
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "VibeAuthError", { enumerable: true, get: functio
42
42
  Object.defineProperty(exports, "VibeRateLimitError", { enumerable: true, get: function () { return errors_1.VibeRateLimitError; } });
43
43
  Object.defineProperty(exports, "VibeConflictError", { enumerable: true, get: function () { return errors_1.VibeConflictError; } });
44
44
  Object.defineProperty(exports, "VibeServiceError", { enumerable: true, get: function () { return errors_1.VibeServiceError; } });
45
+ Object.defineProperty(exports, "VibeBatchCreateError", { enumerable: true, get: function () { return errors_1.VibeBatchCreateError; } });
45
46
  // Session management exports
46
47
  var sessions_1 = require("./sessions");
47
48
  Object.defineProperty(exports, "createLoginSession", { enumerable: true, get: function () { return sessions_1.createLoginSession; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.4.6",
3
+ "version": "4.4.8",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -64,6 +64,11 @@
64
64
  "require": "./dist/auth/better-auth.js",
65
65
  "default": "./dist/auth/better-auth.js"
66
66
  },
67
+ "./auth/safe-callback": {
68
+ "types": "./dist/auth/safe-callback.d.ts",
69
+ "require": "./dist/auth/safe-callback.js",
70
+ "default": "./dist/auth/safe-callback.js"
71
+ },
67
72
  "./components/auth/FederatedAuthSection": {
68
73
  "types": "./dist/components/auth/FederatedAuthSection.d.ts",
69
74
  "require": "./dist/components/auth/FederatedAuthSection.js",
@@ -124,6 +129,11 @@
124
129
  "require": "./dist/lib/session-store.js",
125
130
  "default": "./dist/lib/session-store.js"
126
131
  },
132
+ "./lib/nextauth-secret": {
133
+ "types": "./dist/lib/nextauth-secret.d.ts",
134
+ "require": "./dist/lib/nextauth-secret.js",
135
+ "default": "./dist/lib/nextauth-secret.js"
136
+ },
127
137
  "./lib/ensure-fresh-access-token": {
128
138
  "types": "./dist/lib/ensure-fresh-access-token.d.ts",
129
139
  "require": "./dist/lib/ensure-fresh-access-token.js",
@@ -179,6 +189,11 @@
179
189
  "require": "./dist/lib/idp-client-config.js",
180
190
  "default": "./dist/lib/idp-client-config.js"
181
191
  },
192
+ "./lib/never-in-response": {
193
+ "types": "./dist/lib/never-in-response.d.ts",
194
+ "require": "./dist/lib/never-in-response.js",
195
+ "default": "./dist/lib/never-in-response.js"
196
+ },
182
197
  "./hooks/useAuthSettings": {
183
198
  "types": "./dist/hooks/useAuthSettings.d.ts",
184
199
  "require": "./dist/hooks/useAuthSettings.js",
@@ -944,7 +959,7 @@
944
959
  "@azure/identity": "^4.0.1",
945
960
  "@azure/keyvault-secrets": "^4.7.0",
946
961
  "@upstash/redis": "^1.35.6",
947
- "better-auth": "1.5.6",
962
+ "better-auth": "1.6.33",
948
963
  "ioredis": "^5.3.2",
949
964
  "jose": "^5.2.4",
950
965
  "jsonwebtoken": "^9.0.2",
@@ -9,6 +9,28 @@ interface ChangePasswordRequest {
9
9
  }
10
10
 
11
11
  import { nanoid } from 'nanoid';
12
+
13
+ /**
14
+ * PAY-1596: the failure code comes from the IdP's REAL envelope { success, error: { code, message, details } } - the handler
15
+ * used to read a top-level `error_code` the IdP never sends, so every refusal was CHANGE_PASSWORD_FAILED and "this account has
16
+ * no password" / "wrong current password" could not be told apart. Allowlisted: nothing but these codes ever leaves here.
17
+ * - error.code NO_PASSWORD_SET / INVALID_CURRENT_PASSWORD (IdP PR 412) / VALIDATION_ERROR
18
+ * - the pre-412 wrong-current shape: VALIDATION_ERROR + details.value[] { code: InvalidCurrentPassword, field_name: currentPassword }
19
+ * - a legacy top-level error_code, for older IdPs
20
+ */
21
+ const CHANGE_PASSWORD_CODES = new Set(['NO_PASSWORD_SET', 'INVALID_CURRENT_PASSWORD', 'VALIDATION_ERROR']);
22
+
23
+ function changePasswordFailureCode(body: any): string {
24
+ const err = body && typeof body.error === 'object' && body.error ? body.error : null;
25
+ const code: string = typeof err?.code === 'string' ? err.code : typeof body?.error_code === 'string' ? body.error_code : '';
26
+ if (code === 'VALIDATION_ERROR' && Array.isArray(err?.details?.value)) {
27
+ const wrongCurrent = err.details.value.some(
28
+ (e: any) => e && typeof e === 'object' && (String(e.field_name ?? e.fieldName ?? '').toLowerCase() === 'currentpassword' || String(e.code ?? '').toLowerCase() === 'invalidcurrentpassword'),
29
+ );
30
+ if (wrongCurrent) return 'INVALID_CURRENT_PASSWORD';
31
+ }
32
+ return CHANGE_PASSWORD_CODES.has(code) ? code : 'CHANGE_PASSWORD_FAILED';
33
+ }
12
34
  import { projectPasswordPolicyDetails } from '../../lib/safe-error-response';
13
35
 
14
36
  // ...
@@ -102,22 +124,25 @@ export async function POST(req: NextRequest) {
102
124
  // Extract error message from IDP response
103
125
  let errorMessage = 'Failed to change password';
104
126
 
127
+ const failureCode = changePasswordFailureCode(responseData);
128
+ // The real envelope nests details under `error`; older IdPs put them at the top level.
129
+ const idpDetails = (responseData.error && typeof responseData.error === 'object' ? responseData.error.details : undefined) ?? responseData.details;
105
130
  if (responseData.message) {
106
131
  errorMessage = responseData.message;
107
- } else if (responseData.details?.value && Array.isArray(responseData.details.value) && responseData.details.value.length > 0) {
108
- errorMessage = responseData.details.value[0].message || errorMessage;
109
- } else if (responseData.details?.message) {
110
- errorMessage = responseData.details.message;
132
+ } else if (idpDetails?.value && Array.isArray(idpDetails.value) && idpDetails.value.length > 0) {
133
+ errorMessage = idpDetails.value[0].message || errorMessage;
134
+ } else if (idpDetails?.message) {
135
+ errorMessage = idpDetails.message;
111
136
  }
112
137
 
113
138
  return NextResponse.json(
114
139
  {
115
140
  success: false,
116
141
  message: errorMessage,
117
- error_code: responseData.error_code || 'CHANGE_PASSWORD_FAILED',
142
+ error_code: failureCode,
118
143
  request_id: requestId,
119
144
  // PAY-1438: only the password-policy messages the security pages display, never the raw envelope.
120
- details: projectPasswordPolicyDetails(responseData.details),
145
+ details: projectPasswordPolicyDetails(idpDetails),
121
146
  },
122
147
  { status: idpResponse.status }
123
148
  );