@payez/next-mvp 4.4.5 → 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.
Files changed (219) hide show
  1. package/README.md +10 -3
  2. package/dist/api/auth-handler.js +5 -3
  3. package/dist/api-handlers/account/change-password.js +33 -8
  4. package/dist/api-handlers/account/masked-info.js +2 -1
  5. package/dist/api-handlers/account/recovery/forward.d.ts +14 -0
  6. package/dist/api-handlers/account/recovery/forward.js +105 -0
  7. package/dist/api-handlers/account/recovery/initiate.js +4 -0
  8. package/dist/api-handlers/account/recovery/send-code.d.ts +12 -2
  9. package/dist/api-handlers/account/recovery/send-code.js +11 -84
  10. package/dist/api-handlers/account/recovery/verify-code.d.ts +11 -2
  11. package/dist/api-handlers/account/recovery/verify-code.js +10 -84
  12. package/dist/api-handlers/account/reset-password.js +24 -1
  13. package/dist/api-handlers/account/send-code.d.ts +1 -15
  14. package/dist/api-handlers/account/send-code.js +3 -6
  15. package/dist/api-handlers/account/update-phone.js +6 -1
  16. package/dist/api-handlers/account/validate-password.js +3 -1
  17. package/dist/api-handlers/account/verify-email.d.ts +1 -17
  18. package/dist/api-handlers/account/verify-email.js +4 -6
  19. package/dist/api-handlers/account/verify-sms.d.ts +1 -17
  20. package/dist/api-handlers/account/verify-sms.js +4 -6
  21. package/dist/api-handlers/admin/analytics.js +12 -4
  22. package/dist/api-handlers/admin/audit.js +12 -4
  23. package/dist/api-handlers/admin/redis-sessions.js +16 -8
  24. package/dist/api-handlers/admin/sessions.js +14 -6
  25. package/dist/api-handlers/admin/site-logs.js +39 -16
  26. package/dist/api-handlers/admin/stats.js +14 -4
  27. package/dist/api-handlers/admin/users.js +14 -6
  28. package/dist/api-handlers/admin/vibe-data.js +19 -11
  29. package/dist/api-handlers/auth/jwks.js +2 -1
  30. package/dist/api-handlers/auth/login.js +13 -4
  31. package/dist/api-handlers/auth/passkey-signin.js +6 -0
  32. package/dist/api-handlers/auth/refresh.js +38 -11
  33. package/dist/api-handlers/auth/signout.js +2 -1
  34. package/dist/api-handlers/auth/status.js +4 -1
  35. package/dist/api-handlers/auth/update-session.d.ts +25 -4
  36. package/dist/api-handlers/auth/update-session.js +38 -11
  37. package/dist/api-handlers/auth/validate.js +4 -2
  38. package/dist/api-handlers/auth/verify-code.js +3 -2
  39. package/dist/api-handlers/mfa/backup-code-verify.d.ts +1 -18
  40. package/dist/api-handlers/mfa/backup-code-verify.js +4 -6
  41. package/dist/api-handlers/mfa/backup-codes.js +2 -1
  42. package/dist/api-handlers/mfa/methods.js +2 -1
  43. package/dist/api-handlers/mfa/totp-confirm.js +2 -1
  44. package/dist/api-handlers/mfa/totp-enroll.js +2 -1
  45. package/dist/api-handlers/mfa/totp-verify.d.ts +1 -19
  46. package/dist/api-handlers/mfa/totp-verify.js +4 -6
  47. package/dist/api-handlers/passkeys/list.js +2 -1
  48. package/dist/api-handlers/passkeys/manage.js +2 -1
  49. package/dist/api-handlers/passkeys/register-options.js +2 -1
  50. package/dist/api-handlers/passkeys/register-verify.js +2 -1
  51. package/dist/api-handlers/session/refresh-viability.d.ts +4 -2
  52. package/dist/api-handlers/session/refresh-viability.js +10 -7
  53. package/dist/api-handlers/session/viability.js +4 -3
  54. package/dist/api-handlers/test/force-expire.js +5 -3
  55. package/dist/auth/auth-decision.js +2 -1
  56. package/dist/auth/better-auth.d.ts +58 -14
  57. package/dist/auth/better-auth.js +125 -54
  58. package/dist/auth/utils/idp-client.js +2 -1
  59. package/dist/client/better-auth-client.d.ts +101 -956
  60. package/dist/components/admin/DataBrowserTab.js +4 -1
  61. package/dist/components/admin/SessionsTab.js +9 -3
  62. package/dist/components/recovery/RequestedStep.d.ts +11 -0
  63. package/dist/components/recovery/RequestedStep.js +14 -0
  64. package/dist/components/recovery/SetPasswordStep.js +10 -1
  65. package/dist/config/env.d.ts +0 -2
  66. package/dist/config/env.js +0 -2
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +8 -1
  69. package/dist/lib/app-slug.d.ts +21 -0
  70. package/dist/lib/app-slug.js +51 -0
  71. package/dist/lib/ensure-fresh-access-token.d.ts +26 -0
  72. package/dist/lib/ensure-fresh-access-token.js +105 -4
  73. package/dist/lib/idp-client-config.d.ts +20 -0
  74. package/dist/lib/idp-client-config.js +73 -45
  75. package/dist/lib/idp-fetch.js +1 -1
  76. package/dist/lib/never-in-response.d.ts +30 -0
  77. package/dist/lib/never-in-response.js +71 -0
  78. package/dist/lib/password-rules.d.ts +22 -0
  79. package/dist/lib/password-rules.js +43 -0
  80. package/dist/lib/safe-error-response.d.ts +67 -0
  81. package/dist/lib/safe-error-response.js +130 -0
  82. package/dist/lib/session-cookie-shape.d.ts +22 -19
  83. package/dist/lib/session-cookie-shape.js +14 -18
  84. package/dist/lib/session-store.d.ts +8 -1
  85. package/dist/lib/session-store.js +170 -24
  86. package/dist/lib/startup-init.d.ts +6 -0
  87. package/dist/lib/startup-init.js +30 -4
  88. package/dist/lib/token-lifecycle.js +2 -1
  89. package/dist/lib/token-log-id.d.ts +18 -0
  90. package/dist/lib/token-log-id.js +84 -0
  91. package/dist/lib/two-factor-verify-failure.d.ts +78 -0
  92. package/dist/lib/two-factor-verify-failure.js +220 -0
  93. package/dist/middleware/create-middleware.d.ts +4 -1
  94. package/dist/middleware/create-middleware.js +79 -8
  95. package/dist/pages/login/page.js +5 -0
  96. package/dist/pages/recovery/page.js +14 -4
  97. package/dist/pages/test-env/JwtInspectPage.js +7 -149
  98. package/dist/pages/test-env/RefreshTokenPage.js +4 -1
  99. package/dist/pages/test-env/redact-credentials.d.ts +6 -0
  100. package/dist/pages/test-env/redact-credentials.js +101 -0
  101. package/dist/pages/verify-code/page.js +6 -3
  102. package/dist/routes/auth/logout.d.ts +2 -1
  103. package/dist/routes/auth/logout.js +4 -2
  104. package/dist/routes/auth/session.d.ts +0 -3
  105. package/dist/routes/auth/session.js +16 -10
  106. package/dist/routes/auth/settings.js +4 -1
  107. package/dist/routes/auth/viability.d.ts +3 -2
  108. package/dist/routes/auth/viability.js +11 -7
  109. package/dist/server/auth.d.ts +36 -294
  110. package/dist/server/auth.js +32 -31
  111. package/dist/server/decode-session.js +15 -26
  112. package/dist/server/verified-session-token.d.ts +13 -0
  113. package/dist/server/verified-session-token.js +103 -0
  114. package/dist/stores/authStore.js +14 -3
  115. package/dist/types/recovery.d.ts +1 -1
  116. package/dist/utils/api.d.ts +18 -0
  117. package/dist/utils/api.js +51 -9
  118. package/dist/vibe/client.d.ts +132 -16
  119. package/dist/vibe/client.js +288 -82
  120. package/dist/vibe/document-unwrap.d.ts +50 -0
  121. package/dist/vibe/document-unwrap.js +78 -0
  122. package/dist/vibe/errors.d.ts +17 -0
  123. package/dist/vibe/errors.js +21 -1
  124. package/dist/vibe/generic.d.ts +2 -49
  125. package/dist/vibe/generic.js +17 -78
  126. package/dist/vibe/index.d.ts +1 -1
  127. package/dist/vibe/index.js +2 -1
  128. package/dist/vibe/sessions.js +2 -1
  129. package/package.json +13 -2
  130. package/scripts/verify-release-provenance.mjs +410 -0
  131. package/src/api/auth-handler.ts +5 -3
  132. package/src/api-handlers/account/change-password.ts +36 -8
  133. package/src/api-handlers/account/masked-info.ts +2 -1
  134. package/src/api-handlers/account/recovery/forward.ts +117 -0
  135. package/src/api-handlers/account/recovery/initiate.ts +3 -0
  136. package/src/api-handlers/account/recovery/send-code.ts +13 -99
  137. package/src/api-handlers/account/recovery/verify-code.ts +12 -99
  138. package/src/api-handlers/account/reset-password.ts +30 -1
  139. package/src/api-handlers/account/send-code.ts +3 -9
  140. package/src/api-handlers/account/update-phone.ts +6 -1
  141. package/src/api-handlers/account/validate-password.ts +3 -1
  142. package/src/api-handlers/account/verify-email.ts +4 -9
  143. package/src/api-handlers/account/verify-sms.ts +4 -9
  144. package/src/api-handlers/admin/analytics.ts +13 -5
  145. package/src/api-handlers/admin/audit.ts +13 -5
  146. package/src/api-handlers/admin/redis-sessions.ts +16 -8
  147. package/src/api-handlers/admin/sessions.ts +15 -7
  148. package/src/api-handlers/admin/site-logs.ts +39 -16
  149. package/src/api-handlers/admin/stats.ts +15 -5
  150. package/src/api-handlers/admin/users.ts +15 -7
  151. package/src/api-handlers/admin/vibe-data.ts +20 -12
  152. package/src/api-handlers/auth/jwks.ts +2 -1
  153. package/src/api-handlers/auth/login.ts +13 -4
  154. package/src/api-handlers/auth/passkey-signin.ts +4 -0
  155. package/src/api-handlers/auth/refresh.ts +38 -11
  156. package/src/api-handlers/auth/signout.ts +2 -1
  157. package/src/api-handlers/auth/status.ts +4 -1
  158. package/src/api-handlers/auth/update-session.ts +45 -11
  159. package/src/api-handlers/auth/validate.ts +4 -2
  160. package/src/api-handlers/auth/verify-code.ts +3 -2
  161. package/src/api-handlers/mfa/backup-code-verify.ts +4 -9
  162. package/src/api-handlers/mfa/backup-codes.ts +2 -1
  163. package/src/api-handlers/mfa/methods.ts +2 -1
  164. package/src/api-handlers/mfa/totp-confirm.ts +2 -1
  165. package/src/api-handlers/mfa/totp-enroll.ts +2 -1
  166. package/src/api-handlers/mfa/totp-verify.ts +4 -9
  167. package/src/api-handlers/passkeys/list.ts +2 -1
  168. package/src/api-handlers/passkeys/manage.ts +2 -1
  169. package/src/api-handlers/passkeys/register-options.ts +2 -1
  170. package/src/api-handlers/passkeys/register-verify.ts +2 -1
  171. package/src/api-handlers/session/refresh-viability.ts +10 -7
  172. package/src/api-handlers/session/viability.ts +4 -3
  173. package/src/api-handlers/test/force-expire.ts +5 -3
  174. package/src/auth/auth-decision.ts +2 -1
  175. package/src/auth/better-auth.ts +128 -61
  176. package/src/auth/utils/idp-client.ts +2 -1
  177. package/src/components/admin/DataBrowserTab.tsx +4 -1
  178. package/src/components/admin/SessionsTab.tsx +10 -3
  179. package/src/components/recovery/RequestedStep.tsx +30 -0
  180. package/src/components/recovery/SetPasswordStep.tsx +24 -3
  181. package/src/config/env.ts +0 -2
  182. package/src/hooks/useViabilitySession.ts +2 -1
  183. package/src/index.ts +4 -0
  184. package/src/lib/app-slug.ts +50 -0
  185. package/src/lib/ensure-fresh-access-token.ts +133 -4
  186. package/src/lib/idp-client-config.ts +98 -45
  187. package/src/lib/idp-fetch.ts +1 -1
  188. package/src/lib/never-in-response.ts +64 -0
  189. package/src/lib/password-rules.ts +50 -0
  190. package/src/lib/safe-error-response.ts +126 -0
  191. package/src/lib/session-cookie-shape.ts +23 -22
  192. package/src/lib/session-store.ts +167 -24
  193. package/src/lib/startup-init.ts +29 -4
  194. package/src/lib/token-lifecycle.ts +2 -1
  195. package/src/lib/token-log-id.ts +69 -0
  196. package/src/lib/two-factor-verify-failure.ts +248 -0
  197. package/src/middleware/create-middleware.ts +97 -9
  198. package/src/pages/login/page.tsx +4 -0
  199. package/src/pages/recovery/page.tsx +21 -4
  200. package/src/pages/test-env/JwtInspectPage.tsx +11 -233
  201. package/src/pages/test-env/RefreshTokenPage.tsx +9 -4
  202. package/src/pages/test-env/redact-credentials.ts +96 -0
  203. package/src/pages/verify-code/page.tsx +6 -3
  204. package/src/routes/auth/logout.ts +4 -2
  205. package/src/routes/auth/session.ts +16 -10
  206. package/src/routes/auth/settings.ts +5 -1
  207. package/src/routes/auth/viability.ts +11 -7
  208. package/src/server/auth.ts +33 -35
  209. package/src/server/decode-session.ts +16 -26
  210. package/src/server/verified-session-token.ts +103 -0
  211. package/src/stores/authStore.ts +16 -5
  212. package/src/types/recovery.ts +1 -1
  213. package/src/utils/api.ts +47 -8
  214. package/src/vibe/client.ts +378 -91
  215. package/src/vibe/document-unwrap.ts +103 -0
  216. package/src/vibe/errors.ts +24 -0
  217. package/src/vibe/generic.ts +8 -100
  218. package/src/vibe/index.ts +1 -0
  219. package/src/vibe/sessions.ts +2 -1
@@ -41,7 +41,11 @@ import {
41
41
  VibeError,
42
42
  VibeNotFoundError,
43
43
  VibeServiceError,
44
+ VibeValidationError,
45
+ VibeBatchCreateError,
44
46
  } from './errors';
47
+ import { unwrapVibeDocument, extractVibeDocuments } from './document-unwrap';
48
+ import type { VibeDocumentWrapper } from './document-unwrap';
45
49
  import type {
46
50
  VibeMeta,
47
51
  VibeResponse,
@@ -69,7 +73,13 @@ export interface VibeRetryConfig {
69
73
  }
70
74
 
71
75
  export interface VibeClientConfig {
72
- /** Base URL for the Vibe API (defaults to VIBE_API_URL env var) */
76
+ /**
77
+ * Base URL for the Vibe API (defaults to VIBE_API_URL env var).
78
+ *
79
+ * PAY-1767: proxy mode does NOT need this. Every request is sent to the IDP proxy as an endpoint
80
+ * PATH (`/v1/collections/...`), which is what the proxy validates and signs - it never builds a
81
+ * Vibe URL from this value. It is retained only for compatibility with consumers that still read it.
82
+ */
73
83
  baseUrl?: string;
74
84
  /** Vibe Client ID (defaults to VIBE_CLIENT_ID env var) */
75
85
  clientId?: string;
@@ -84,6 +94,15 @@ export interface VibeClientConfig {
84
94
  * When set, requests are routed through the IDP proxy instead of directly to Vibe.
85
95
  */
86
96
  appSigningKey?: string;
97
+ /**
98
+ * PAY-1767: which COLLECTION the table delegates address (defaults to VIBE_COLLECTION, then 'vibe_app').
99
+ *
100
+ * The Vibe API route is `/v1/collections/{collection}/tables/{table}`. This client used to hardcode
101
+ * `/api/v1/vibe_app/{table}` - a collection baked into the path with no route behind it. Pin the
102
+ * collection explicitly at the call site when your tables do not live in `vibe_app` (the UMI
103
+ * VIBE_COLLECTION-double-duty lesson, POST-INSTALL.md item 16).
104
+ */
105
+ collection?: string;
87
106
  /** Custom fetch function (for testing or custom auth) */
88
107
  fetch?: typeof fetch;
89
108
  /** Retry configuration for transient errors */
@@ -121,7 +140,13 @@ export interface FindManyOptions<T> {
121
140
  skip?: number;
122
141
  }
123
142
 
124
- /** Options for findUnique */
143
+ /**
144
+ * Options for findUnique.
145
+ *
146
+ * PAY-1767 (NightHawk 64534 MUST 2): `where.id` is the DOCUMENT id - the storage-layer primary key the
147
+ * route constrains (`{id:int}`), NOT a row's own `id` column. The unwrapped row exposes it as
148
+ * `_vibe_doc_id`; use THAT value for a subsequent update/delete.
149
+ */
125
150
  export interface FindUniqueOptions {
126
151
  where: { id: number };
127
152
  }
@@ -131,13 +156,15 @@ export interface CreateOptions<T> {
131
156
  data: Omit<T, 'id' | 'created_at' | 'updated_at'>;
132
157
  }
133
158
 
134
- /** Options for update */
159
+ /**
160
+ * Options for update. `where.id` is the DOCUMENT id (see FindUniqueOptions), not a row's own id column.
161
+ */
135
162
  export interface UpdateOptions<T> {
136
163
  where: { id: number };
137
164
  data: Partial<Omit<T, 'id' | 'created_at' | 'updated_at'>>;
138
165
  }
139
166
 
140
- /** Options for delete */
167
+ /** Options for delete. `where.id` is the DOCUMENT id (see FindUniqueOptions). */
141
168
  export interface DeleteOptions {
142
169
  where: { id: number };
143
170
  }
@@ -150,6 +177,11 @@ export interface CountOptions<T> {
150
177
  /** Result of findMany with metadata */
151
178
  export interface FindManyResult<T> {
152
179
  data: T[];
180
+ /**
181
+ * PAY-1767 (NightHawk 64552 item B): true when `data` is a PAGE of a larger population. Read from Core's
182
+ * `pagination.totalCount`; a caller must not treat `data.length` as the table's size.
183
+ */
184
+ hasMore?: boolean;
153
185
  meta: VibeMeta;
154
186
  }
155
187
 
@@ -164,6 +196,93 @@ const DEFAULT_RETRY_CONFIG: Required<VibeRetryConfig> = {
164
196
  retryOnRateLimit: true,
165
197
  };
166
198
 
199
+ /**
200
+ * PAY-1767: the most records `createMany` will create in one call. It is per-record and NOT atomic, so a
201
+ * ceiling bounds the blast radius of a partial failure. 50 is the house's only batch ceiling (Vibe's own
202
+ * DecryptController BATCH_TOO_LARGE = 50).
203
+ */
204
+ export const MAX_BATCH_CREATE_ROWS = 50;
205
+
206
+ /**
207
+ * PAY-1767: the default page size findMany asks the query route for. Core clamps pageSize/limit to 100
208
+ * (DocumentsController.Query), so this is the most one call returns.
209
+ */
210
+ export const MAX_FIND_MANY_PAGE_SIZE = 100;
211
+
212
+ /**
213
+ * PAY-1767: the body POST /v1/collections/{c}/tables/{t}/query binds (Core QueryRequest). `filter` is a
214
+ * VibeFilterNode OBJECT - `{ "<field>": { "<operator>": value } }` - NOT the array sessions.ts sends.
215
+ */
216
+ interface QueryRequestBody {
217
+ filter?: Record<string, unknown>;
218
+ orderBy?: Record<string, string>;
219
+ page: number;
220
+ pageSize: number;
221
+ /** QueryRequest.Limit is an alias for PageSize; findMany sets it so take:0 means count-only. */
222
+ limit?: number;
223
+ }
224
+
225
+ /** The operators Core's VibeQueryBuilder accepts, from VibeOperators.All. */
226
+ const VIBE_OPERATORS = new Set(['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'contains', 'startsWith', 'endsWith', 'in', 'notIn', 'isNull', 'isNotNull', 'ilike']);
227
+
228
+ /**
229
+ * PAY-1767: map a Prisma-style `where` to Core's VibeFilterNode OBJECT.
230
+ *
231
+ * { status: 'active' } -> { 'status': { eq: 'active' } }
232
+ * { score: { gt: 5 } } -> { 'score': { gt: 5 } }
233
+ * { status: 'a', tier: { in: [] } } -> { AND: [ {status:{eq:'a'}}, {tier:{in:[]}} ] }
234
+ *
235
+ * A field name is used as-is: a top-level document column (document_id, created_at...) or a JSONB path
236
+ * (Core treats an unknown name as data->>'name'; a nested path like 'data.x' is honoured explicitly).
237
+ * An unknown OPERATOR throws - dropping an operator would widen the result silently.
238
+ */
239
+ export function buildVibeFilter(where: Record<string, unknown>, table: string): Record<string, unknown> {
240
+ const leaves: Record<string, unknown>[] = [];
241
+
242
+ for (const [field, value] of Object.entries(where)) {
243
+ if (value === undefined) continue;
244
+
245
+ const conditions: Record<string, unknown> = {};
246
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
247
+ for (const [op, val] of Object.entries(value as Record<string, unknown>)) {
248
+ if (!VIBE_OPERATORS.has(op)) {
249
+ throw new VibeValidationError(
250
+ `where.${field} uses unsupported operator '${op}'. Core accepts: ${[...VIBE_OPERATORS].join(', ')}.`,
251
+ { table, field, operator: op },
252
+ );
253
+ }
254
+ conditions[op] = val;
255
+ }
256
+ if (Object.keys(conditions).length === 0) {
257
+ throw new VibeValidationError(`where.${field} has no operator`, { table, field });
258
+ }
259
+ } else if (Array.isArray(value)) {
260
+ // SHOULD (NightHawk 64610): a bare array is a set membership, NOT equality. `{ id: [1,2] }` as
261
+ // eq:[1,2] compares a scalar to an array and matches nothing; map it to Core's `in`.
262
+ conditions.in = value;
263
+ } else if (value === null) {
264
+ // SHOULD (NightHawk 64610): `{ x: null }` as eq:null compares against JSON null; Core has a dedicated
265
+ // isNull operator, so map it rather than emit an equality that silently misses.
266
+ conditions.isNull = true;
267
+ } else {
268
+ // A bare value is equality.
269
+ conditions.eq = value;
270
+ }
271
+ leaves.push({ [field]: conditions });
272
+ }
273
+
274
+ if (leaves.length === 0) return {};
275
+ if (leaves.length === 1) return leaves[0];
276
+ // Multiple fields default to AND (Core's VibeFilterNode AND group).
277
+ return { AND: leaves };
278
+ }
279
+
280
+ /** Read the total from whichever pagination shape Core returned (DocumentListResponse.pagination). */
281
+ function readPaginationTotal(response: VibeResponse<unknown>): number | undefined {
282
+ const r = response as unknown as { pagination?: { totalCount?: number; total?: number }; meta?: { total?: number } };
283
+ return r.pagination?.totalCount ?? r.pagination?.total ?? r.meta?.total;
284
+ }
285
+
167
286
  /** Sleep for a given number of milliseconds */
168
287
  function sleep(ms: number): Promise<void> {
169
288
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -196,63 +315,117 @@ export class VibeTableDelegate<T extends VibeTableName> {
196
315
  }
197
316
 
198
317
  /**
199
- * Find multiple records with optional filtering and pagination.
318
+ * PAY-1767: the Vibe API route for this table's document collection.
319
+ * `/v1/collections/{collection}/tables/{table}` (Core DocumentsController.cs:15) - the same shape
320
+ * generic.ts and sessions.ts already use. The OLD `/api/v1/vibe_app/{table}` had no route behind it.
321
+ */
322
+ private tableEndpoint(id?: number | string): string {
323
+ return this.client.tableEndpoint(this.tableName, id);
324
+ }
325
+
326
+ /**
327
+ * Find multiple records, WITH filtering, sorting and paging honoured.
328
+ *
329
+ * PAY-1767 (NightHawk 64534 MUST 1, QAPert 64530, BAPert 64535). The GET list route
330
+ * (`GET /v1/collections/{c}/tables/{t}`, Core DocumentsController.List :44-48) binds ONLY `page` and
331
+ * `pageSize` - it reads no filter, sort or offset, so sending where/orderBy/take/skip there DROPPED them
332
+ * silently. Through the proxy that is worse than loud: a proxied MVP service account is ADMIN mode
333
+ * (HasValidContext), so the ownership filter is bypassed and an unfiltered page is 20 ARBITRARY rows of
334
+ * the tenant - caller code doing `rows[0]` gets the WRONG record with no error.
335
+ *
336
+ * So this now goes to `POST .../tables/{t}/query`, which binds QueryRequest (Filter / OrderBy / Page /
337
+ * PageSize / Limit). The FILTER is a VibeFilterNode OBJECT - `{ "<field>": { "<op>": value } }` - NOT the
338
+ * filter ARRAY that sessions.ts sends: QueryRequest.Filter is typed VibeFilterNode, which has no array
339
+ * form (measured at Core origin/master; CliInitProjectService.cs:197 builds the same object shape). An
340
+ * unmappable option THROWS rather than being dropped.
200
341
  */
201
342
  async findMany(options?: FindManyOptions<VibeTableType<T>>): Promise<FindManyResult<VibeTableType<T>>> {
202
- const params = new URLSearchParams();
203
-
204
- // Build filter params
205
- if (options?.where) {
206
- for (const [key, value] of Object.entries(options.where)) {
207
- if (value !== undefined) {
208
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
209
- // Handle operator objects (e.g., { gt: 5 }, { in: [1,2,3] })
210
- for (const [op, val] of Object.entries(value)) {
211
- if (Array.isArray(val)) {
212
- // Handle array values for in/nin operators
213
- val.forEach(v => params.append(`filter[${key}][${op}][]`, String(v)));
214
- } else {
215
- params.append(`filter[${key}][${op}]`, String(val));
216
- }
217
- }
218
- } else if (Array.isArray(value)) {
219
- // Handle direct array values
220
- value.forEach(v => params.append(`filter[${key}][]`, String(v)));
221
- } else {
222
- params.append(`filter[${key}]`, String(value));
223
- }
224
- }
343
+ const body = this.buildQueryBody(options);
344
+ const endpoint = `${this.tableEndpoint()}/query`;
345
+
346
+ const response = await this.client.request<VibeDocumentWrapper[]>(endpoint, 'POST', body);
347
+ // MUST 2: the rows come back as document WRAPPERS; unwrap to the row's own fields.
348
+ const rows = extractVibeDocuments(response.data) as unknown as VibeTableType<T>[];
349
+ // item B (NightHawk 64552): the TRUE population is Core's pagination.totalCount, NOT a page size.
350
+ const total = readPaginationTotal(response) ?? rows.length;
351
+ const limit = body.limit ?? body.pageSize;
352
+
353
+ const offset = (body.page - 1) * limit;
354
+ return {
355
+ data: rows,
356
+ // item B: `hasMore` so a caller cannot mistake a page for the whole table.
357
+ // MUST 2 (NightHawk 64610): it must account for the OFFSET - on the last page `rows.length < total`
358
+ // was still true, so a caller paging until hasMore is false never stopped.
359
+ hasMore: offset + rows.length < total,
360
+ meta: { total, limit, offset },
361
+ };
362
+ }
363
+
364
+ /**
365
+ * PAY-1767: map the Prisma-style findMany options onto Core's QueryRequest body. THROWS on anything the
366
+ * query endpoint cannot express, so nothing is silently dropped.
367
+ */
368
+ private buildQueryBody(options?: FindManyOptions<VibeTableType<T>>): QueryRequestBody {
369
+ const body: QueryRequestBody = { page: 1, pageSize: MAX_FIND_MANY_PAGE_SIZE };
370
+
371
+ if (options?.where && Object.keys(options.where).length > 0) {
372
+ body.filter = buildVibeFilter(options.where, this.tableName);
373
+ }
374
+
375
+ if (options?.orderBy && Object.keys(options.orderBy).length > 0) {
376
+ const orderBy: Record<string, string> = {};
377
+ for (const [field, dir] of Object.entries(options.orderBy)) {
378
+ orderBy[field] = dir === 'desc' ? 'desc' : 'asc';
225
379
  }
380
+ body.orderBy = orderBy;
226
381
  }
227
382
 
228
- // Pagination
229
383
  if (options?.take !== undefined) {
230
- params.append('limit', String(options.take));
231
- }
232
- if (options?.skip !== undefined) {
233
- params.append('offset', String(options.skip));
384
+ if (!Number.isInteger(options.take) || options.take < 0) {
385
+ throw new VibeValidationError(`take must be a non-negative integer (got ${options.take})`, { table: this.tableName });
386
+ }
387
+ // MUST 1 (NightHawk 64610): Core CLAMPS pageSize to 100 (DocumentsController.Query:
388
+ // `if (pageSize > 100) pageSize = 100`), so take > 100 would return 100 rows while meta.limit claimed
389
+ // the larger size - and a skip computed from a 250 size would land on Core's page 2 of size 100
390
+ // (rows 100-199), not rows 250-499. Refuse it rather than return the wrong rows silently.
391
+ if (options.take > MAX_FIND_MANY_PAGE_SIZE) {
392
+ throw new VibeValidationError(
393
+ `take ${options.take} exceeds the maximum page size ${MAX_FIND_MANY_PAGE_SIZE}; the query endpoint ` +
394
+ `CLAMPS pageSize to ${MAX_FIND_MANY_PAGE_SIZE}, so a larger take returns a short page while ` +
395
+ `reporting the larger limit. Page with take ${MAX_FIND_MANY_PAGE_SIZE} and a skip multiple.`,
396
+ { table: this.tableName, take: options.take, max: MAX_FIND_MANY_PAGE_SIZE },
397
+ );
398
+ }
399
+ body.limit = options.take;
400
+ body.pageSize = options.take === 0 ? 0 : options.take;
234
401
  }
235
402
 
236
- // Sorting
237
- if (options?.orderBy) {
238
- const sortParts: string[] = [];
239
- for (const [key, dir] of Object.entries(options.orderBy)) {
240
- sortParts.push(dir === 'desc' ? `-${key}` : key);
403
+ if (options?.skip !== undefined) {
404
+ if (!Number.isInteger(options.skip) || options.skip < 0) {
405
+ throw new VibeValidationError(`skip must be a non-negative integer (got ${options.skip})`, { table: this.tableName });
241
406
  }
242
- if (sortParts.length > 0) {
243
- params.append('sort', sortParts.join(','));
407
+ // The query endpoint PAGES (page + pageSize); it cannot express a bare row offset. A skip that is a
408
+ // whole number of pages can be expressed exactly; anything else would return rows BEFORE the offset
409
+ // and look correct. Refuse it.
410
+ const size = body.limit ?? body.pageSize;
411
+ if (body.limit === undefined && options.take === undefined && options.skip % size !== 0) {
412
+ throw new VibeValidationError(
413
+ `skip ${options.skip} is not a multiple of the default page size ${size}; pass a take so the page ` +
414
+ `size is explicit, or use a multiple (the query endpoint pages, it has no bare offset)`,
415
+ { table: this.tableName, skip: options.skip },
416
+ );
244
417
  }
418
+ if (size <= 0 || options.skip % size !== 0) {
419
+ throw new VibeValidationError(
420
+ `skip ${options.skip} cannot be expressed as a page of size ${size} (the query endpoint pages; ` +
421
+ `skip must be a multiple of take)`,
422
+ { table: this.tableName, skip: options.skip, take: options.take },
423
+ );
424
+ }
425
+ body.page = options.skip / size + 1;
245
426
  }
246
427
 
247
- const queryString = params.toString();
248
- const url = `${this.client.getBaseUrl()}/api/v1/vibe_app/${this.tableName}${queryString ? `?${queryString}` : ''}`;
249
-
250
- const response = await this.client.request<VibeTableType<T>[]>(url, 'GET');
251
-
252
- return {
253
- data: response.data,
254
- meta: response.meta || { total: response.data.length, limit: options?.take || 50, offset: options?.skip || 0 },
255
- };
428
+ return body;
256
429
  }
257
430
 
258
431
  /**
@@ -260,9 +433,18 @@ export class VibeTableDelegate<T extends VibeTableName> {
260
433
  * Throws VibeNotFoundError if not found.
261
434
  */
262
435
  async findUnique(options: FindUniqueOptions): Promise<VibeTableType<T>> {
263
- const url = `${this.client.getBaseUrl()}/api/v1/vibe_app/${this.tableName}/${options.where.id}`;
264
- const response = await this.client.request<VibeTableType<T>>(url, 'GET');
265
- return response.data;
436
+ const endpoint = this.tableEndpoint(options.where.id);
437
+ const response = await this.client.request<VibeDocumentWrapper>(endpoint, 'GET');
438
+ // PAY-1767: unwrap the document wrapper so the caller gets the row's own fields.
439
+ const row = unwrapVibeDocument<VibeTableType<T>>(response.data);
440
+ if (!row) {
441
+ throw new VibeNotFoundError(
442
+ `Document ${options.where.id} not found in ${this.tableName}`,
443
+ 'NOT_FOUND',
444
+ { table: this.tableName, id: options.where.id },
445
+ );
446
+ }
447
+ return row as VibeTableType<T>;
266
448
  }
267
449
 
268
450
  /**
@@ -281,11 +463,15 @@ export class VibeTableDelegate<T extends VibeTableName> {
281
463
  }
282
464
 
283
465
  /**
284
- * Find the first record matching the filter.
466
+ * Find the first record.
285
467
  * Returns null if none found.
468
+ *
469
+ * PAY-1767: takes the first row of the (caller-owned, created_at-ordered) list. Any where/orderBy in
470
+ * `options` is passed through to findMany, which REFUSES them on this route rather than dropping them,
471
+ * so this cannot pretend to filter.
286
472
  */
287
473
  async findFirst(options?: FindManyOptions<VibeTableType<T>>): Promise<VibeTableType<T> | null> {
288
- const result = await this.findMany({ ...options, take: 1 });
474
+ const result = await this.findMany(options);
289
475
  return result.data[0] || null;
290
476
  }
291
477
 
@@ -293,9 +479,10 @@ export class VibeTableDelegate<T extends VibeTableName> {
293
479
  * Create a new record.
294
480
  */
295
481
  async create(options: CreateOptions<VibeTableType<T>>): Promise<VibeTableType<T>> {
296
- const url = `${this.client.getBaseUrl()}/api/v1/vibe_app/${this.tableName}`;
297
- const response = await this.client.request<VibeTableType<T>>(url, 'POST', options.data);
298
- return response.data;
482
+ const endpoint = this.tableEndpoint();
483
+ const response = await this.client.request<VibeDocumentWrapper>(endpoint, 'POST', options.data);
484
+ // SHOULD (NightHawk 64534): unwrap; a success with no data THROWS rather than returning {} typed as a row.
485
+ return this.requireRow(response.data, 'create');
299
486
  }
300
487
 
301
488
  /**
@@ -303,43 +490,93 @@ export class VibeTableDelegate<T extends VibeTableName> {
303
490
  * Throws VibeNotFoundError if not found.
304
491
  */
305
492
  async update(options: UpdateOptions<VibeTableType<T>>): Promise<VibeTableType<T>> {
306
- const url = `${this.client.getBaseUrl()}/api/v1/vibe_app/${this.tableName}/${options.where.id}`;
307
- const response = await this.client.request<VibeTableType<T>>(url, 'PUT', options.data);
308
- return response.data;
493
+ const endpoint = this.tableEndpoint(options.where.id);
494
+ const response = await this.client.request<VibeDocumentWrapper>(endpoint, 'PUT', options.data);
495
+ return this.requireRow(response.data, 'update');
309
496
  }
310
497
 
311
498
  /**
312
499
  * Delete a record by ID (soft delete).
313
500
  * Throws VibeNotFoundError if not found.
501
+ *
502
+ * PAY-1767 (NightHawk 64578 item 2): Core answers DELETE with **204 No Content** (DocumentsController
503
+ * Delete returns NoContent()), so a SUCCESSFUL delete carries NO document - and there is none to unwrap.
504
+ * This must therefore NOT go through requireRow (that is the create/update rule: "a success WITH no
505
+ * document throws"). A 2xx with no body is the success here; the request layer already resolves it to an
506
+ * empty success. Returns null when the server sent no document.
314
507
  */
315
- async delete(options: DeleteOptions): Promise<VibeTableType<T>> {
316
- const url = `${this.client.getBaseUrl()}/api/v1/vibe_app/${this.tableName}/${options.where.id}`;
317
- const response = await this.client.request<VibeTableType<T>>(url, 'DELETE');
318
- return response.data;
508
+ async delete(options: DeleteOptions): Promise<VibeTableType<T> | null> {
509
+ const endpoint = this.tableEndpoint(options.where.id);
510
+ const response = await this.client.request<VibeDocumentWrapper>(endpoint, 'DELETE');
511
+ return response?.data ? (unwrapVibeDocument<VibeTableType<T>>(response.data) as VibeTableType<T>) : null;
512
+ }
513
+
514
+ /** Unwrap a single-document response, or THROW - never return an empty object typed as a row. */
515
+ private requireRow(wire: VibeDocumentWrapper | undefined, op: string): VibeTableType<T> {
516
+ const row = wire ? unwrapVibeDocument<VibeTableType<T>>(wire) : null;
517
+ if (!row) {
518
+ throw new VibeNotFoundError(
519
+ `${op} on ${this.tableName} returned no document data`,
520
+ 'NOT_FOUND',
521
+ { table: this.tableName, op },
522
+ );
523
+ }
524
+ return row as VibeTableType<T>;
319
525
  }
320
526
 
321
527
  /**
322
528
  * Count records matching the filter.
529
+ *
530
+ * PAY-1767 (NightHawk 64534): goes to POST /query with limit:0, which is Core's COUNT-ONLY mode
531
+ * (DocumentsController.Query returns an empty Data array plus Pagination.TotalCount). So this is the
532
+ * true population of the filter, not the first page's size.
323
533
  */
324
534
  async count(options?: CountOptions<VibeTableType<T>>): Promise<number> {
325
- // Use findMany with limit 0 to get just the count
326
- const result = await this.findMany({ ...options, take: 0 });
327
- return result.meta.total;
535
+ const body = this.buildQueryBody({ where: options?.where, take: 0 } as FindManyOptions<VibeTableType<T>>);
536
+ const endpoint = `${this.tableEndpoint()}/query`;
537
+ const response = await this.client.request<VibeDocumentWrapper[]>(endpoint, 'POST', body);
538
+ return readPaginationTotal(response) ?? 0;
328
539
  }
329
540
 
330
541
  /**
331
- * Create multiple records in a batch.
542
+ * Create multiple records.
543
+ *
544
+ * PAY-1767: the old path `/api/v1/vibe_app/batch` has no route (there is no documents batch endpoint in
545
+ * Core's Vibe Public API), so this CANNOT be atomic here. Until a Core batch route exists, this creates
546
+ * each record against the REAL table endpoint in turn.
547
+ *
548
+ * BOUNDED ON PURPOSE (BAPert 64527). Non-atomic is tolerable only while the blast radius is small, so a
549
+ * request above MAX_BATCH_CREATE_ROWS is REFUSED before anything is sent. 50 matches the house's only
550
+ * batch ceiling (Vibe's DecryptController BATCH_TOO_LARGE).
551
+ *
552
+ * PARTIAL FAILURE IS REPORTED, NOT SWALLOWED (NightHawk 64534 condition (a)). A failure at item k throws
553
+ * a VibeBatchCreateError that CARRIES the count and the document ids already created - a cap cannot fix
554
+ * partial failure, only reporting it can. The created rows are unwrapped (MUST 2) so their own fields are
555
+ * present, not the wrapper's.
332
556
  */
333
- async createMany(options: { data: CreateOptions<VibeTableType<T>>['data'][] }): Promise<{ count: number }> {
334
- const url = `${this.client.getBaseUrl()}/api/v1/vibe_app/batch`;
335
- const response = await this.client.request<{ count: number }>(url, 'POST', {
336
- operations: options.data.map(item => ({
337
- table: this.tableName,
338
- operation: 'create',
339
- data: item,
340
- })),
341
- });
342
- return response.data;
557
+ async createMany(options: { data: CreateOptions<VibeTableType<T>>['data'][] }): Promise<{ count: number; ids: Array<number | string> }> {
558
+ if (options.data.length > MAX_BATCH_CREATE_ROWS) {
559
+ throw new VibeServiceError(
560
+ `createMany accepts at most ${MAX_BATCH_CREATE_ROWS} records per call (got ${options.data.length}); ` +
561
+ `it is NOT atomic, so a larger batch could leave a partial write`,
562
+ );
563
+ }
564
+ const endpoint = this.tableEndpoint();
565
+ const created: Array<number | string> = [];
566
+ for (let i = 0; i < options.data.length; i++) {
567
+ try {
568
+ const response = await this.client.request<VibeDocumentWrapper>(endpoint, 'POST', options.data[i]);
569
+ const row = unwrapVibeDocument<{ _vibe_doc_id?: number | string }>(response.data);
570
+ if (row?._vibe_doc_id != null) created.push(row._vibe_doc_id);
571
+ } catch (err) {
572
+ throw new VibeBatchCreateError(
573
+ `createMany created ${created.length} of ${options.data.length} records before failing at index ${i}`,
574
+ { createdCount: created.length, createdIds: created, failedIndex: i, total: options.data.length },
575
+ err,
576
+ );
577
+ }
578
+ }
579
+ return { count: created.length, ids: created };
343
580
  }
344
581
  }
345
582
 
@@ -357,6 +594,8 @@ export class VibeClient {
357
594
  clientId: string;
358
595
  idpUrl: string;
359
596
  appSigningKey: string;
597
+ /** PAY-1767: the collection the table delegates address. */
598
+ collection: string;
360
599
  fetch: typeof fetch;
361
600
  retry: Required<VibeRetryConfig>;
362
601
  };
@@ -379,6 +618,9 @@ export class VibeClient {
379
618
  clientId: config.clientId || process.env.VIBE_CLIENT_ID || '',
380
619
  idpUrl: config.idpUrl || process.env.IDP_URL || '',
381
620
  appSigningKey: config.appSigningKey || process.env.VIBE_HMAC_KEY || '',
621
+ // PAY-1767: which collection the table delegates address. Library default is correct ('vibe_app');
622
+ // pinning it per-call avoids the VIBE_COLLECTION-global-retarget trap (POST-INSTALL.md item 16).
623
+ collection: config.collection || process.env.VIBE_COLLECTION || 'vibe_app',
382
624
  fetch: config.fetch || globalThis.fetch,
383
625
  retry: { ...DEFAULT_RETRY_CONFIG, ...config.retry },
384
626
  };
@@ -431,20 +673,30 @@ export class VibeClient {
431
673
  }
432
674
  }
433
675
 
676
+ /**
677
+ * PAY-1767: the Vibe API endpoint PATH for a table in this client's collection.
678
+ * `/v1/collections/{collection}/tables/{table}[/{id}]` - what the IDP proxy validates, signs and forwards.
679
+ * Returned as a PATH, not a URL: proxy mode needs no VIBE_API_URL (the proxy owns the base).
680
+ */
681
+ tableEndpoint(table: string, id?: number | string): string {
682
+ const base = `/v1/collections/${this.config.collection}/tables/${table}`;
683
+ return id === undefined ? base : `${base}/${id}`;
684
+ }
685
+
434
686
  /**
435
687
  * Make a request through the IDP Vibe proxy.
436
688
  * The proxy handles credential injection securely.
689
+ *
690
+ * PAY-1767: `endpoint` is the RAW path (+query) to forward. It is what gets signed and sent, so this
691
+ * takes a path directly rather than parsing one out of a URL - proxy mode is complete without a Vibe
692
+ * base URL, which is exactly what an endpoint-path call needs.
437
693
  */
438
694
  private async requestViaProxy<T>(
439
- url: string,
695
+ endpoint: string,
440
696
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
441
697
  body?: unknown,
442
698
  authToken?: string
443
699
  ): Promise<VibeResponse<T>> {
444
- // Extract the endpoint path from the full URL
445
- const urlObj = new URL(url);
446
- const endpoint = urlObj.pathname + urlObj.search;
447
-
448
700
  const timestamp = Math.floor(Date.now() / 1000);
449
701
  const signature = await this.generateProxySignature(timestamp, method, endpoint);
450
702
 
@@ -462,10 +714,12 @@ export class VibeClient {
462
714
  headers['Authorization'] = `Bearer ${authToken}`;
463
715
  }
464
716
 
717
+ // PAY-1767: the proxy binds Endpoint / Method / Data (Core VibeProxyController :376-391), so a write
718
+ // goes out as `data` - sending `body` (as this used to) arrived as data:null.
465
719
  const proxyBody = {
466
720
  method,
467
721
  endpoint,
468
- ...(body !== undefined && { body }),
722
+ ...(body !== undefined && { data: body }),
469
723
  };
470
724
 
471
725
  const response = await this.config.fetch(proxyUrl, {
@@ -479,9 +733,30 @@ export class VibeClient {
479
733
  throw VibeError.fromResponse(errorBody, response.status);
480
734
  }
481
735
 
482
- const responseData = await response.json();
736
+ // PAY-1767 (NightHawk 64578 item 2): a 204 No Content (or any 2xx with an EMPTY body) is a SUCCESS with
737
+ // no payload - e.g. a successful DELETE. Parsing it as JSON throws, which surfaced a successful delete as
738
+ // an error. Return an empty success here; this is distinct from the SHOULD rule (that one throws when a
739
+ // create/update answers success but carries NO DOCUMENT), which lives in the delegates.
740
+ if (response.status === 204) {
741
+ return { success: true } as unknown as VibeResponse<T>;
742
+ }
743
+ const raw = await response.text();
744
+ if (raw.trim() === '') {
745
+ return { success: true } as unknown as VibeResponse<T>;
746
+ }
483
747
 
484
- if (!responseData.success) {
748
+ let responseData: unknown;
749
+ try {
750
+ responseData = JSON.parse(raw);
751
+ } catch {
752
+ // A non-empty 2xx body that is not JSON is not a shape we can trust - fail rather than pretend.
753
+ throw new VibeServiceError(
754
+ `Vibe API answered ${response.status} with a body that is not JSON`,
755
+ { status: response.status, bodyLength: raw.length },
756
+ );
757
+ }
758
+
759
+ if (!(responseData as VibeResponse<T>).success) {
485
760
  throw VibeError.fromResponse(responseData as VibeErrorResponse, response.status);
486
761
  }
487
762
 
@@ -501,6 +776,10 @@ export class VibeClient {
501
776
  * Routing:
502
777
  * - All requests route through the IDP proxy (requires VIBE_HMAC_KEY + IDP_URL)
503
778
  *
779
+ * PAY-1767: `urlOrPath` is EITHER a full URL or a bare Vibe path ('/v1/collections/...'). Proxy mode
780
+ * signs and forwards the PATH either way, so a caller with only a path (the table delegates) needs no
781
+ * Vibe base URL - VIBE_API_URL is not required to make a proxied call.
782
+ *
504
783
  * Retry behavior:
505
784
  * - 5xx errors: Retry up to maxRetries times with exponential backoff
506
785
  * - 429 errors: Retry once after Retry-After delay (if retryOnRateLimit enabled)
@@ -508,7 +787,7 @@ export class VibeClient {
508
787
  * - Network errors: Retry with backoff
509
788
  */
510
789
  async request<T>(
511
- url: string,
790
+ urlOrPath: string,
512
791
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
513
792
  body?: unknown,
514
793
  authToken?: string
@@ -525,8 +804,13 @@ export class VibeClient {
525
804
  if (!this.config.appSigningKey) {
526
805
  throw new VibeServiceError('VIBE_HMAC_KEY is not configured (required for proxy mode)');
527
806
  }
807
+ // PAY-1767: a bare path is used as-is; a full URL is reduced to its path+query. Both sign identically.
808
+ const endpoint = urlOrPath.startsWith('/') ? urlOrPath : (() => {
809
+ const u = new URL(urlOrPath);
810
+ return u.pathname + u.search;
811
+ })();
528
812
  // Route through IDP proxy
529
- return this.requestViaProxy<T>(url, method, body, authToken);
813
+ return this.requestViaProxy<T>(endpoint, method, body, authToken);
530
814
  }
531
815
 
532
816
  // MVP requires proxy mode - direct Vibe access is not supported
@@ -556,10 +840,13 @@ export class VibeClient {
556
840
 
557
841
  /**
558
842
  * Fetch the schema for type generation.
843
+ *
844
+ * PAY-1767: the old path `/api/v1/vibe_app/schema` has no route. The real per-collection schema route is
845
+ * `/v1/mvp/schemas/{collection}` (Core MvpSchemaController.cs:65). Returns the collection schema DTO.
559
846
  */
560
- async getSchema(): Promise<Record<string, { columns: Array<{ name: string; type: string; nullable: boolean }> }>> {
561
- const url = `${this.config.baseUrl}/api/v1/vibe_app/schema`;
562
- const response = await this.request<Record<string, { columns: Array<{ name: string; type: string; nullable: boolean }> }>>(url, 'GET');
847
+ async getSchema(): Promise<Record<string, unknown>> {
848
+ const endpoint = `/v1/mvp/schemas/${this.config.collection}`;
849
+ const response = await this.request<Record<string, unknown>>(endpoint, 'GET');
563
850
  return response.data;
564
851
  }
565
852
  }