@payez/next-mvp 4.4.6 → 4.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-handlers/account/change-password.js +28 -6
- package/dist/auth/better-auth.d.ts +44 -14
- package/dist/auth/better-auth.js +92 -7
- package/dist/client/better-auth-client.d.ts +101 -956
- package/dist/config/env.d.ts +0 -2
- package/dist/config/env.js +0 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -1
- package/dist/lib/idp-client-config.js +8 -1
- package/dist/lib/never-in-response.d.ts +30 -0
- package/dist/lib/never-in-response.js +71 -0
- package/dist/lib/safe-error-response.d.ts +6 -6
- package/dist/lib/safe-error-response.js +6 -6
- package/dist/lib/two-factor-verify-failure.d.ts +36 -3
- package/dist/lib/two-factor-verify-failure.js +79 -2
- package/dist/middleware/create-middleware.d.ts +2 -0
- package/dist/middleware/create-middleware.js +36 -0
- package/dist/pages/login/page.js +5 -0
- package/dist/pages/verify-code/page.js +4 -2
- package/dist/routes/auth/settings.js +4 -1
- package/dist/server/auth.d.ts +36 -294
- package/dist/server/auth.js +22 -25
- package/dist/vibe/client.d.ts +132 -16
- package/dist/vibe/client.js +288 -82
- package/dist/vibe/document-unwrap.d.ts +50 -0
- package/dist/vibe/document-unwrap.js +78 -0
- package/dist/vibe/errors.d.ts +17 -0
- package/dist/vibe/errors.js +21 -1
- package/dist/vibe/generic.d.ts +2 -49
- package/dist/vibe/generic.js +17 -78
- package/dist/vibe/index.d.ts +1 -1
- package/dist/vibe/index.js +2 -1
- package/package.json +12 -2
- package/src/api-handlers/account/change-password.ts +31 -6
- package/src/auth/better-auth.ts +93 -8
- package/src/config/env.ts +0 -2
- package/src/index.ts +4 -0
- package/src/lib/idp-client-config.ts +8 -1
- package/src/lib/never-in-response.ts +64 -0
- package/src/lib/safe-error-response.ts +6 -6
- package/src/lib/two-factor-verify-failure.ts +86 -5
- package/src/middleware/create-middleware.ts +42 -0
- package/src/pages/login/page.tsx +4 -0
- package/src/pages/verify-code/page.tsx +4 -2
- package/src/routes/auth/settings.ts +5 -1
- package/src/server/auth.ts +23 -29
- package/src/vibe/client.ts +378 -91
- package/src/vibe/document-unwrap.ts +103 -0
- package/src/vibe/errors.ts +24 -0
- package/src/vibe/generic.ts +8 -100
- package/src/vibe/index.ts +1 -0
package/dist/vibe/errors.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/vibe/errors.js
CHANGED
|
@@ -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;
|
package/dist/vibe/generic.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/vibe/generic.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
}
|
package/dist/vibe/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/vibe/index.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "4.4.7",
|
|
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",
|
|
@@ -179,6 +184,11 @@
|
|
|
179
184
|
"require": "./dist/lib/idp-client-config.js",
|
|
180
185
|
"default": "./dist/lib/idp-client-config.js"
|
|
181
186
|
},
|
|
187
|
+
"./lib/never-in-response": {
|
|
188
|
+
"types": "./dist/lib/never-in-response.d.ts",
|
|
189
|
+
"require": "./dist/lib/never-in-response.js",
|
|
190
|
+
"default": "./dist/lib/never-in-response.js"
|
|
191
|
+
},
|
|
182
192
|
"./hooks/useAuthSettings": {
|
|
183
193
|
"types": "./dist/hooks/useAuthSettings.d.ts",
|
|
184
194
|
"require": "./dist/hooks/useAuthSettings.js",
|
|
@@ -944,7 +954,7 @@
|
|
|
944
954
|
"@azure/identity": "^4.0.1",
|
|
945
955
|
"@azure/keyvault-secrets": "^4.7.0",
|
|
946
956
|
"@upstash/redis": "^1.35.6",
|
|
947
|
-
"better-auth": "1.
|
|
957
|
+
"better-auth": "1.6.33",
|
|
948
958
|
"ioredis": "^5.3.2",
|
|
949
959
|
"jose": "^5.2.4",
|
|
950
960
|
"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 (
|
|
108
|
-
errorMessage =
|
|
109
|
-
} else if (
|
|
110
|
-
errorMessage =
|
|
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:
|
|
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(
|
|
145
|
+
details: projectPasswordPolicyDetails(idpDetails),
|
|
121
146
|
},
|
|
122
147
|
{ status: idpResponse.status }
|
|
123
148
|
);
|
package/src/auth/better-auth.ts
CHANGED
|
@@ -820,6 +820,19 @@ export function createBetterAuthInstance(
|
|
|
820
820
|
},
|
|
821
821
|
},
|
|
822
822
|
|
|
823
|
+
// PAY-1781 B: KEEP OAUTH STATE IN THE ENCRYPTED COOKIE, as it was on better-auth 1.5.6.
|
|
824
|
+
// 1.5.6 resolved the default as `options.database ? 'database' : 'cookie'` - we have no
|
|
825
|
+
// database, so: cookie. 1.6.13+ resolves it as `(database || secondaryStorage) ? 'database'
|
|
826
|
+
// : 'cookie'`, and we DO have secondaryStorage, so the upgrade alone would silently move every
|
|
827
|
+
// OAuth state record into Redis through the adapter above (its enrichment read-modify-write
|
|
828
|
+
// and key handling were built for session records, not state). Pinned so the upgrade changes
|
|
829
|
+
// the version, not the design. The cookie branch is where GHSA-wxw3-q3m9-c3jr lived; 1.6.2+
|
|
830
|
+
// compares the returned `state` with the nonce sealed in the cookie, and our provider is also
|
|
831
|
+
// pkce:true, which the advisory required to be false. Test: oauth-state-strategy-cookie.
|
|
832
|
+
account: {
|
|
833
|
+
storeStateStrategy: 'cookie',
|
|
834
|
+
},
|
|
835
|
+
|
|
823
836
|
// Cookie prefix must match slim-middleware expectations ({slug}.session-token)
|
|
824
837
|
advanced: {
|
|
825
838
|
cookiePrefix: appSlug,
|
|
@@ -1331,12 +1344,79 @@ export async function storeIdpTokensInSession(
|
|
|
1331
1344
|
}
|
|
1332
1345
|
}
|
|
1333
1346
|
|
|
1347
|
+
/**
|
|
1348
|
+
* The `?error=` code the login page receives when an OAuth sign-in is ABORTED because the IdP exchange did not
|
|
1349
|
+
* store tokens (PAY-1733). Hosts map it to ONE neutral line; the card's words are "That sign-in didn't work.
|
|
1350
|
+
* Try again or use another method." The cause stays in the server log, never on the page.
|
|
1351
|
+
*/
|
|
1352
|
+
export const OAUTH_SIGN_IN_FAILED_ERROR = 'OAUTH_SIGN_IN_FAILED';
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Re-emit each cookie the callback response SET as an immediate expiry, keeping the attributes that decide
|
|
1356
|
+
* WHICH cookie the browser overwrites (Path, Domain, Secure, SameSite, HttpOnly - a `__Secure-` name is only
|
|
1357
|
+
* accepted with Secure) and dropping Expires/Max-Age in favour of Max-Age=0.
|
|
1358
|
+
*/
|
|
1359
|
+
export function expireSetCookies(setCookies: readonly string[]): string[] {
|
|
1360
|
+
const out: string[] = [];
|
|
1361
|
+
for (const sc of setCookies) {
|
|
1362
|
+
const [pair, ...attrs] = sc.split(';').map((s) => s.trim());
|
|
1363
|
+
const eq = pair.indexOf('=');
|
|
1364
|
+
const name = eq > 0 ? pair.slice(0, eq) : '';
|
|
1365
|
+
if (!name) continue;
|
|
1366
|
+
const kept = attrs.filter((a) => /^(path|domain|secure|samesite|httponly)\b/i.test(a));
|
|
1367
|
+
out.push([`${name}=`, ...kept, 'Max-Age=0'].join('; '));
|
|
1368
|
+
}
|
|
1369
|
+
return out;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* PAY-1733 (Jon, live on :4797): ABORT an OAuth sign-in whose IdP exchange did not store tokens.
|
|
1374
|
+
*
|
|
1375
|
+
* Before this, a failed exchange was a console.error and the user kept the Better Auth session the callback had
|
|
1376
|
+
* just created: signed in, holding NO IdP tokens. The host then routed that session to 2FA, which painted the
|
|
1377
|
+
* user's email and failed every call - a red "sign in again" screen about an account the app could no longer
|
|
1378
|
+
* authenticate. The IdP's 401 was CORRECT (it fail-closes unverified assertions); keeping the session was the bug.
|
|
1379
|
+
*
|
|
1380
|
+
* So: no session without IdP tokens. The Better Auth session is destroyed (its own signOut, best effort, then the
|
|
1381
|
+
* `ba:<slug>:<token>` record directly - that record is what makes it resolvable), every cookie the callback set is
|
|
1382
|
+
* expired, and the user goes to the login page with OAUTH_SIGN_IN_FAILED. Relative Location, as for the state-error
|
|
1383
|
+
* branch above (328439: an absolute URL built from request.url is a dead tab in a container).
|
|
1384
|
+
*/
|
|
1385
|
+
async function abortOAuthSignIn(
|
|
1386
|
+
callbackResponse: Response,
|
|
1387
|
+
sessionToken: string | null,
|
|
1388
|
+
cookieHeader: string,
|
|
1389
|
+
loginPath: string,
|
|
1390
|
+
): Promise<Response> {
|
|
1391
|
+
const headers = new Headers();
|
|
1392
|
+
if (sessionToken) {
|
|
1393
|
+
try {
|
|
1394
|
+
const auth = await getBetterAuthInstance();
|
|
1395
|
+
const out = await auth?.api?.signOut?.({ headers: new Headers({ cookie: cookieHeader }), asResponse: true });
|
|
1396
|
+
for (const sc of (out as Response | undefined)?.headers?.getSetCookie?.() ?? []) headers.append('set-cookie', sc);
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
console.warn('[BETTER_AUTH] abort: Better Auth signOut failed, deleting the record directly:', err instanceof Error ? err.message : String(err));
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
const config = await getIDPClientConfig();
|
|
1402
|
+
await getRedis().del(`ba:${config.clientSlug || getAppSlug()}:${sessionToken}`);
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
console.error('[BETTER_AUTH] abort: could not delete the Better Auth session record', tokenLogId(sessionToken), err instanceof Error ? err.message : String(err));
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
for (const sc of expireSetCookies(callbackResponse.headers.getSetCookie?.() ?? [])) headers.append('set-cookie', sc);
|
|
1408
|
+
headers.set('location', `${loginPath}${loginPath.includes('?') ? '&' : '?'}error=${OAUTH_SIGN_IN_FAILED_ERROR}`);
|
|
1409
|
+
console.warn('[BETTER_AUTH] OAuth sign-in ABORTED: the IdP exchange stored no tokens, so no session is kept (PAY-1733)', sessionToken ? tokenLogId(sessionToken) : '(no session)');
|
|
1410
|
+
return new Response(null, { status: 302, headers });
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1334
1413
|
/**
|
|
1335
1414
|
* Create a production-ready GET handler for the auth catch-all route.
|
|
1336
1415
|
*
|
|
1337
1416
|
* Wraps better-auth's GET handler with:
|
|
1338
1417
|
* - OAuth state error recovery (redirects to login instead of error page)
|
|
1339
|
-
* - IDP token exchange after successful OAuth callback
|
|
1418
|
+
* - IDP token exchange after successful OAuth callback - and, when it stores no tokens, the sign-in is ABORTED
|
|
1419
|
+
* (session destroyed, cookies expired, login page with OAUTH_SIGN_IN_FAILED; PAY-1733)
|
|
1340
1420
|
*
|
|
1341
1421
|
* Usage in host app:
|
|
1342
1422
|
* ```ts
|
|
@@ -1408,24 +1488,28 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
|
|
|
1408
1488
|
}
|
|
1409
1489
|
}
|
|
1410
1490
|
|
|
1411
|
-
// After
|
|
1491
|
+
// After the OAuth callback: exchange the provider identity for IDP tokens. If that does NOT store tokens - the
|
|
1492
|
+
// IdP refused (401), no session was found, or anything threw - the sign-in is ABORTED (PAY-1733). A session is
|
|
1493
|
+
// kept only when it holds IdP tokens.
|
|
1412
1494
|
const url = new URL(request.url);
|
|
1413
1495
|
if (url.pathname.includes('/callback/') && response.status === 302) {
|
|
1496
|
+
const setCookies = response.headers.getSetCookie?.() || [];
|
|
1497
|
+
const cookieHeader = setCookies
|
|
1498
|
+
.map((c: string) => c.split(';')[0])
|
|
1499
|
+
.join('; ');
|
|
1500
|
+
let sessionToken: string | null = null;
|
|
1501
|
+
let exchanged = false;
|
|
1414
1502
|
try {
|
|
1415
1503
|
const auth = await getBetterAuthInstance();
|
|
1416
1504
|
if (auth?.api?.getSession) {
|
|
1417
|
-
const setCookies = response.headers.getSetCookie?.() || [];
|
|
1418
|
-
const cookieHeader = setCookies
|
|
1419
|
-
.map((c: string) => c.split(';')[0])
|
|
1420
|
-
.join('; ');
|
|
1421
|
-
|
|
1422
1505
|
const headers = new Headers();
|
|
1423
1506
|
headers.set('cookie', cookieHeader);
|
|
1424
1507
|
|
|
1425
1508
|
const session = await auth.api.getSession({ headers });
|
|
1426
1509
|
if (session?.session?.token) {
|
|
1510
|
+
sessionToken = session.session.token;
|
|
1427
1511
|
console.log('[BETTER_AUTH] Got session token from callback:', tokenLogId(session.session.token), '| email:', session.user?.email);
|
|
1428
|
-
await exchangeOAuthForIdpTokens(session.session.token);
|
|
1512
|
+
exchanged = await exchangeOAuthForIdpTokens(session.session.token);
|
|
1429
1513
|
} else {
|
|
1430
1514
|
console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
|
|
1431
1515
|
}
|
|
@@ -1433,6 +1517,7 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
|
|
|
1433
1517
|
} catch (err: any) {
|
|
1434
1518
|
console.error('[BETTER_AUTH] IDP token exchange failed:', err.message);
|
|
1435
1519
|
}
|
|
1520
|
+
if (!exchanged) return abortOAuthSignIn(response, sessionToken, cookieHeader, loginPath);
|
|
1436
1521
|
}
|
|
1437
1522
|
|
|
1438
1523
|
return response;
|
package/src/config/env.ts
CHANGED
|
@@ -29,8 +29,6 @@ export const API_ENDPOINTS = {
|
|
|
29
29
|
profile: '/api/account/profile',
|
|
30
30
|
updateProfile: '/api/account/profile',
|
|
31
31
|
maskedInfo: '/api/account/masked-info',
|
|
32
|
-
sendResetCode: '/api/account/send-reset-code',
|
|
33
|
-
verifyResetCode: '/api/account/verify-reset-code',
|
|
34
32
|
resetPassword: '/api/account/reset-password',
|
|
35
33
|
changePassword: '/api/account/change-password',
|
|
36
34
|
validatePassword: '/api/account/validate-password'
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,10 @@ export type { AuthConfig } from './types/auth';
|
|
|
23
23
|
// Route configuration (client-safe)
|
|
24
24
|
export { makeAuthDecision } from './auth/auth-decision';
|
|
25
25
|
export { isUnauthenticatedRoute, configurePublicRoutes, getRouteConfig } from './auth/route-config';
|
|
26
|
+
|
|
27
|
+
// The ONE callback-target guard (N7 / PAY-1989): apps import this instead of
|
|
28
|
+
// copying it. Exposed from the main index AND as the subpath ./auth/safe-callback.
|
|
29
|
+
export { safeCallbackPath, isSafeCallbackPath, resolveCallbackDestination, DEFAULT_CALLBACK } from './auth/safe-callback';
|
|
26
30
|
export { createMvpMiddleware } from './middleware/create-middleware';
|
|
27
31
|
|
|
28
32
|
// Account Components
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
import 'server-only';
|
|
44
44
|
import { createHash, randomUUID } from 'crypto';
|
|
45
|
+
import { markNeverInResponse } from './never-in-response';
|
|
45
46
|
// REDIS IS IMPORTED LAZILY, AND THAT IS LOAD-BEARING.
|
|
46
47
|
// A module-scope `import redis from './redis'` drags ioredis into EVERY bundle that
|
|
47
48
|
// touches this file - including instrumentation.ts. Next compiles the instrumentation
|
|
@@ -211,6 +212,10 @@ function resolveAuthSecret(fromConfig: string | undefined): { value: string; sou
|
|
|
211
212
|
function applyEnvSecrets(config: IDPClientConfig): IDPClientConfig {
|
|
212
213
|
const auth = resolveAuthSecret(config.authSecret);
|
|
213
214
|
logSecretSource('authSecret', auth.source);
|
|
215
|
+
// PAY-1529: every secret this module resolves is registered as never-in-response HERE, the one place
|
|
216
|
+
// both fetch paths and the Redis read pass through, so the runtime backstop knows the value whichever
|
|
217
|
+
// handler later tries to serialize it.
|
|
218
|
+
markNeverInResponse(auth.value);
|
|
214
219
|
return {
|
|
215
220
|
...config,
|
|
216
221
|
authSecret: auth.value,
|
|
@@ -219,7 +224,9 @@ function applyEnvSecrets(config: IDPClientConfig): IDPClientConfig {
|
|
|
219
224
|
const fromEnv = process.env[envVar];
|
|
220
225
|
const source: SecretSource = fromEnv ? `env:${envVar}` : p.clientSecret ? 'idp-config' : 'none';
|
|
221
226
|
logSecretSource(`${p.provider} clientSecret`, source);
|
|
222
|
-
|
|
227
|
+
const clientSecret = fromEnv || p.clientSecret;
|
|
228
|
+
markNeverInResponse(clientSecret);
|
|
229
|
+
return { ...p, clientSecret };
|
|
223
230
|
}),
|
|
224
231
|
};
|
|
225
232
|
}
|