@koolbase/core 11.4.0 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { AuthStateListener, KoolbaseConfig, KoolbaseSession, KoolbaseUser, LinkPhoneParams, LoginParams, OtpSendResult, PhoneVerifyResult, RegisterParams, RestoreResult, ResendVerificationResult, SendOtpParams, SignUpResult, SignInWithAppleParams, VerifyOtpParams } from './types.js';
1
+ import { AuthStateListener, KoolbaseConfig, KoolbaseSession, KoolbaseUser, LinkPhoneParams, LoginParams, OtpSendResult, PhoneVerifyResult, RegisterParams, RestoreResult, KoolbaseAuditPage, KoolbaseSessionInfo, ResendVerificationResult, SendOtpParams, SignUpResult, SignInWithAppleParams, VerifyOtpParams } from './types.js';
2
2
  import type { SignInWithGoogleParams } from './types.js';
3
3
  export declare class KoolbaseAuth {
4
4
  private config;
@@ -55,6 +55,18 @@ export declare class KoolbaseAuth {
55
55
  * delegates to {@link authRequest} with includeAuth=true.
56
56
  */
57
57
  private authedRequest;
58
+ /**
59
+ * Replace the user inside the current session and persist it.
60
+ *
61
+ * A profile update or a fresh fetch changes the user, not the tokens.
62
+ * Without this the new values live only in the returned object: the cached
63
+ * `currentUser` stays stale, anything bound to `onAuthStateChange` never
64
+ * hears about the change, and a reload restores the old name.
65
+ *
66
+ * No session means nobody is signed in, and there is nothing to update —
67
+ * the caller's own request would have failed first.
68
+ */
69
+ private updateUserAndPersist;
58
70
  private setSessionInternal;
59
71
  /**
60
72
  * Discards the stored session without contacting the server.
@@ -191,6 +203,92 @@ export declare class KoolbaseAuth {
191
203
  * taking an email address — otherwise this would be an open mail relay.
192
204
  */
193
205
  resendVerificationEmail(): Promise<ResendVerificationResult>;
206
+ /**
207
+ * The signed-in user, fetched fresh from the server.
208
+ *
209
+ * `currentUser` returns what was cached at sign-in; this asks. Use it when
210
+ * something may have changed the user server-side — a verification
211
+ * completing, a profile updated from another device — and persist the
212
+ * result so the cached copy stops being stale.
213
+ */
214
+ getCurrentUser(): Promise<KoolbaseUser>;
215
+ /**
216
+ * Update the signed-in user's profile.
217
+ *
218
+ * Only the fields given are sent, so passing one leaves the other
219
+ * untouched. The updated user is persisted, so `currentUser` reflects it
220
+ * without a refetch.
221
+ */
222
+ updateProfile(params: {
223
+ fullName?: string;
224
+ avatarUrl?: string;
225
+ }): Promise<KoolbaseUser>;
226
+ /**
227
+ * Change the signed-in user's password.
228
+ *
229
+ * The current password is required: a stolen session should not be enough
230
+ * to lock the real owner out. A wrong one throws
231
+ * CurrentPasswordIncorrectError — the server returns `invalid_password`,
232
+ * which reads like a rejected new password and means the opposite.
233
+ *
234
+ * An account that signed up through Google or Apple has no password to
235
+ * change and gets the same error, deliberately, so a caller cannot probe
236
+ * which sign-in methods an account has.
237
+ */
238
+ changePassword(params: {
239
+ currentPassword: string;
240
+ newPassword: string;
241
+ }): Promise<void>;
242
+ /**
243
+ * Delete the signed-in user's account, and sign out.
244
+ *
245
+ * Deletes the caller only — there is no user id to pass, which is the
246
+ * security property. The server removes the auth record and every session
247
+ * on every device, then fires an `auth.user.deleted` trigger.
248
+ *
249
+ * It does NOT delete the account's records in your collections. Koolbase
250
+ * cannot know whether a row should be deleted, anonymised, or kept for a
251
+ * statutory retention period. Subscribe a Function to `auth.user.deleted`
252
+ * and clean up there — it runs server-side, so a tab closing mid-flow
253
+ * cannot strand anything.
254
+ */
255
+ deleteAccount(): Promise<void>;
256
+ /**
257
+ * Every session the signed-in user has — a "where you're signed in" list.
258
+ *
259
+ * Newest first is not guaranteed; sort by createdAt if the order matters.
260
+ * The entry with `isCurrent` is this device.
261
+ */
262
+ listSessions(): Promise<KoolbaseSessionInfo[]>;
263
+ /**
264
+ * Sign out one session by id.
265
+ *
266
+ * Revoking the current one ends this session too — the server does not
267
+ * refuse it, and the local session is cleared here so the app does not keep
268
+ * a token the server has dropped. Use logout() when that is what you mean.
269
+ */
270
+ revokeSession(sessionId: string): Promise<void>;
271
+ /**
272
+ * Sign out every OTHER session, keeping this one.
273
+ *
274
+ * What "sign out my other devices" means after a lost phone. Returns how
275
+ * many were ended, so an app can say so rather than guess.
276
+ */
277
+ revokeAllOtherSessions(): Promise<number>;
278
+ /**
279
+ * What has happened to this account — sign-ins, failures, lockouts,
280
+ * password changes.
281
+ *
282
+ * For a "recent security activity" screen. The server sanitizes each
283
+ * event, so this carries what that event type is allowed to say and
284
+ * nothing more.
285
+ *
286
+ * @param limit up to 200; the server caps it there.
287
+ */
288
+ auditLog(options?: {
289
+ limit?: number;
290
+ offset?: number;
291
+ }): Promise<KoolbaseAuditPage>;
194
292
  unlock(token: string): Promise<void>;
195
293
  get currentUser(): KoolbaseUser | null;
196
294
  get accessToken(): string | null;
@@ -204,12 +302,10 @@ export declare class KoolbaseAuth {
204
302
  validAccessToken(): Promise<string | null>;
205
303
  setSession(session: KoolbaseSession | null): Promise<void>;
206
304
  /**
207
- * @deprecated v1.9.0: Server endpoint /v1/sdk/auth/oauth not yet
208
- * shipped. This method previously routed to /v1/auth/oauth (dashboard
209
- * developer OAuth) which never created project-scoped end-user
210
- * sessions. Properly implemented in v1.10.0 with provider-specific
211
- * server endpoints under /v1/sdk/auth/oauth/{apple,google,github}.
212
- * Use email/password sign-in for now.
305
+ * @deprecated Never implemented; kept only so existing code compiles.
306
+ * Google and Apple sign-in are supported — use signInWithGoogle() or
307
+ * signInWithApple(). This used to say OAuth was not yet shipped and to
308
+ * use email and password instead, which stopped being true long ago.
213
309
  *
214
310
  * @throws Always throws KoolbaseAuthError('not_implemented').
215
311
  */
@@ -220,6 +316,33 @@ export declare class KoolbaseAuth {
220
316
  name?: string;
221
317
  avatarUrl?: string;
222
318
  }): Promise<never>;
319
+ /**
320
+ * Emails a six-digit sign-in code.
321
+ *
322
+ * Resolves the same way whether or not the address has an account, so it
323
+ * cannot be used to check who is registered. A new address receives a code
324
+ * only while the project accepts sign-ups; a disabled account receives
325
+ * nothing.
326
+ *
327
+ * @throws EmailCodeDisabledError if the project has switched this off
328
+ * @throws RateLimitError after too many requests for one address
329
+ */
330
+ requestEmailCode(params: {
331
+ email: string;
332
+ }): Promise<void>;
333
+ /**
334
+ * Signs in with a code from requestEmailCode(), storing the session exactly
335
+ * as login() does. An unknown address becomes an account only if the
336
+ * project still accepts sign-ups at this moment. Each code works once and
337
+ * allows three attempts.
338
+ *
339
+ * @throws OtpInvalidError, OtpExpiredError, OtpMaxAttemptsError
340
+ * @throws EmailCodeDisabledError if the project has switched this off
341
+ */
342
+ signInWithEmailCode(params: {
343
+ email: string;
344
+ code: string;
345
+ }): Promise<KoolbaseSession>;
223
346
  sendOtp(params: SendOtpParams): Promise<OtpSendResult>;
224
347
  verifyOtp(params: VerifyOtpParams): Promise<PhoneVerifyResult>;
225
348
  linkPhone(params: LinkPhoneParams): Promise<void>;
package/dist/esm/auth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RestoreResult, } from './types.js';
2
- import { AccountLockedError, EmailAlreadyInUseError, InvalidCredentialsError, InvalidPhoneNumberError, KoolbaseAuthError, OtpExpiredError, OtpInvalidError, OtpMaxAttemptsError, OtpRateLimitError, PhoneAlreadyLinkedError, IdentityNotFoundError, InsufficientScopeError, MalformedSessionResponseError, ProviderIdentityAlreadyLinkedError, AccountExistsError, ContactNotVerifiedError, CurrentPasswordIncorrectError, HideRequiresVerificationError, InsufficientAuthorityError, LastCredentialError, OAuthOnlyAccountError, SessionRequiredError, SignupsDisabledError, TokenAlreadyUsedError, TokenExpiredError, UnsupportedOAuthProviderError, RateLimitError, VerificationResendCooldownError, VerificationResendDailyCapError, SessionExpiredError, SmsConfigMissingError, UnlockTokenInvalidError, UserDisabledError, WeakPasswordError, AppleEmailRequiredError, AppleSignInNotConfiguredError, InvalidAppleTokenError, OAuthEmailConflictError, GoogleEmailRequiredError, GoogleSignInNotConfiguredError, InvalidGoogleTokenError, } from './auth-errors.js';
2
+ import { AccountLockedError, EmailAlreadyInUseError, InvalidCredentialsError, InvalidPhoneNumberError, KoolbaseAuthError, OtpExpiredError, OtpInvalidError, OtpMaxAttemptsError, OtpRateLimitError, PhoneAlreadyLinkedError, IdentityNotFoundError, InsufficientScopeError, MalformedSessionResponseError, ProviderIdentityAlreadyLinkedError, AccountExistsError, ContactNotVerifiedError, CurrentPasswordIncorrectError, HideRequiresVerificationError, InsufficientAuthorityError, LastCredentialError, OAuthOnlyAccountError, SessionRequiredError, SignupsDisabledError, TokenAlreadyUsedError, TokenExpiredError, UnsupportedOAuthProviderError, RateLimitError, VerificationResendCooldownError, VerificationResendDailyCapError, SessionExpiredError, SmsConfigMissingError, UnlockTokenInvalidError, UserDisabledError, WeakPasswordError, AppleEmailRequiredError, AppleSignInNotConfiguredError, InvalidAppleTokenError, OAuthEmailConflictError, GoogleEmailRequiredError, GoogleSignInNotConfiguredError, InvalidGoogleTokenError, EmailCodeDisabledError, } from './auth-errors.js';
3
3
  import { getPlatform } from './platform.js';
4
4
  import { DeviceMetadata } from './device-metadata.js';
5
5
  export class KoolbaseAuth {
@@ -130,6 +130,22 @@ export class KoolbaseAuth {
130
130
  return this.authRequest(path, { ...options, includeAuth: true });
131
131
  }
132
132
  // ─── Internal session lifecycle ─────────────────────────────────────────
133
+ /**
134
+ * Replace the user inside the current session and persist it.
135
+ *
136
+ * A profile update or a fresh fetch changes the user, not the tokens.
137
+ * Without this the new values live only in the returned object: the cached
138
+ * `currentUser` stays stale, anything bound to `onAuthStateChange` never
139
+ * hears about the change, and a reload restores the old name.
140
+ *
141
+ * No session means nobody is signed in, and there is nothing to update —
142
+ * the caller's own request would have failed first.
143
+ */
144
+ async updateUserAndPersist(user) {
145
+ if (!this.session)
146
+ return;
147
+ await this.setSessionInternal({ ...this.session, user });
148
+ }
133
149
  async setSessionInternal(session) {
134
150
  this.session = session;
135
151
  if (this.storage) {
@@ -583,6 +599,188 @@ export class KoolbaseAuth {
583
599
  cooldownUntil: body.cooldown_until ? new Date(body.cooldown_until) : null,
584
600
  };
585
601
  }
602
+ /**
603
+ * The signed-in user, fetched fresh from the server.
604
+ *
605
+ * `currentUser` returns what was cached at sign-in; this asks. Use it when
606
+ * something may have changed the user server-side — a verification
607
+ * completing, a profile updated from another device — and persist the
608
+ * result so the cached copy stops being stale.
609
+ */
610
+ async getCurrentUser() {
611
+ const res = await this.authRequest('/v1/sdk/auth/me', {
612
+ method: 'GET',
613
+ includeAuth: true,
614
+ });
615
+ await this.checkResponse(res);
616
+ const user = this.mapUser(await res.json());
617
+ await this.updateUserAndPersist(user);
618
+ return user;
619
+ }
620
+ /**
621
+ * Update the signed-in user's profile.
622
+ *
623
+ * Only the fields given are sent, so passing one leaves the other
624
+ * untouched. The updated user is persisted, so `currentUser` reflects it
625
+ * without a refetch.
626
+ */
627
+ async updateProfile(params) {
628
+ const body = {};
629
+ if (params.fullName !== undefined)
630
+ body.full_name = params.fullName;
631
+ if (params.avatarUrl !== undefined)
632
+ body.avatar_url = params.avatarUrl;
633
+ const res = await this.authRequest('/v1/sdk/auth/me', {
634
+ method: 'PATCH',
635
+ body,
636
+ includeAuth: true,
637
+ });
638
+ await this.checkResponse(res);
639
+ const user = this.mapUser(await res.json());
640
+ await this.updateUserAndPersist(user);
641
+ return user;
642
+ }
643
+ /**
644
+ * Change the signed-in user's password.
645
+ *
646
+ * The current password is required: a stolen session should not be enough
647
+ * to lock the real owner out. A wrong one throws
648
+ * CurrentPasswordIncorrectError — the server returns `invalid_password`,
649
+ * which reads like a rejected new password and means the opposite.
650
+ *
651
+ * An account that signed up through Google or Apple has no password to
652
+ * change and gets the same error, deliberately, so a caller cannot probe
653
+ * which sign-in methods an account has.
654
+ */
655
+ async changePassword(params) {
656
+ if (params.newPassword.length < 8)
657
+ throw new WeakPasswordError();
658
+ const res = await this.authRequest('/v1/sdk/auth/me/password', {
659
+ method: 'PATCH',
660
+ body: {
661
+ current_password: params.currentPassword,
662
+ new_password: params.newPassword,
663
+ },
664
+ includeAuth: true,
665
+ });
666
+ await this.checkResponse(res);
667
+ }
668
+ /**
669
+ * Delete the signed-in user's account, and sign out.
670
+ *
671
+ * Deletes the caller only — there is no user id to pass, which is the
672
+ * security property. The server removes the auth record and every session
673
+ * on every device, then fires an `auth.user.deleted` trigger.
674
+ *
675
+ * It does NOT delete the account's records in your collections. Koolbase
676
+ * cannot know whether a row should be deleted, anonymised, or kept for a
677
+ * statutory retention period. Subscribe a Function to `auth.user.deleted`
678
+ * and clean up there — it runs server-side, so a tab closing mid-flow
679
+ * cannot strand anything.
680
+ */
681
+ async deleteAccount() {
682
+ const res = await this.authRequest('/v1/sdk/auth/me', {
683
+ method: 'DELETE',
684
+ includeAuth: true,
685
+ });
686
+ await this.checkResponse(res);
687
+ await this.clearSessionInternal();
688
+ }
689
+ /**
690
+ * Every session the signed-in user has — a "where you're signed in" list.
691
+ *
692
+ * Newest first is not guaranteed; sort by createdAt if the order matters.
693
+ * The entry with `isCurrent` is this device.
694
+ */
695
+ async listSessions() {
696
+ const res = await this.authRequest('/v1/sdk/auth/sessions', {
697
+ method: 'GET',
698
+ includeAuth: true,
699
+ });
700
+ await this.checkResponse(res);
701
+ const body = (await res.json());
702
+ return (body.sessions ?? []).map((s) => {
703
+ const r = s;
704
+ return {
705
+ id: String(r.id),
706
+ ip: r.ip,
707
+ userAgent: r.user_agent,
708
+ deviceLabel: r.device_label,
709
+ createdAt: String(r.created_at),
710
+ expiresAt: String(r.expires_at),
711
+ isCurrent: Boolean(r.is_current),
712
+ };
713
+ });
714
+ }
715
+ /**
716
+ * Sign out one session by id.
717
+ *
718
+ * Revoking the current one ends this session too — the server does not
719
+ * refuse it, and the local session is cleared here so the app does not keep
720
+ * a token the server has dropped. Use logout() when that is what you mean.
721
+ */
722
+ async revokeSession(sessionId) {
723
+ const res = await this.authRequest(`/v1/sdk/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, { method: 'POST', includeAuth: true });
724
+ await this.checkResponse(res);
725
+ // Revoking your own session leaves a token the server has dropped. The
726
+ // SDK cannot tell from the response which one it was, and re-listing to
727
+ // find out would cost a request on every revoke — so it asks the only
728
+ // party that already knows: the next request. If it was this session,
729
+ // that request 401s and the normal refresh path clears it.
730
+ //
731
+ // A UI listing sessions knows which row is current from isCurrent, and
732
+ // should call logout() rather than revokeSession() for that one, which
733
+ // is both clearer to the user and one fewer round trip.
734
+ }
735
+ /**
736
+ * Sign out every OTHER session, keeping this one.
737
+ *
738
+ * What "sign out my other devices" means after a lost phone. Returns how
739
+ * many were ended, so an app can say so rather than guess.
740
+ */
741
+ async revokeAllOtherSessions() {
742
+ const res = await this.authRequest('/v1/sdk/auth/sessions/revoke-all', {
743
+ method: 'POST',
744
+ includeAuth: true,
745
+ });
746
+ await this.checkResponse(res);
747
+ const body = (await res.json().catch(() => ({})));
748
+ return body.revoked_count ?? 0;
749
+ }
750
+ /**
751
+ * What has happened to this account — sign-ins, failures, lockouts,
752
+ * password changes.
753
+ *
754
+ * For a "recent security activity" screen. The server sanitizes each
755
+ * event, so this carries what that event type is allowed to say and
756
+ * nothing more.
757
+ *
758
+ * @param limit up to 200; the server caps it there.
759
+ */
760
+ async auditLog(options) {
761
+ const params = new URLSearchParams();
762
+ if (options?.limit !== undefined)
763
+ params.set('limit', String(options.limit));
764
+ if (options?.offset !== undefined)
765
+ params.set('offset', String(options.offset));
766
+ const qs = params.toString();
767
+ const res = await this.authRequest(`/v1/sdk/auth/audit${qs ? `?${qs}` : ''}`, { method: 'GET', includeAuth: true });
768
+ await this.checkResponse(res);
769
+ const body = (await res.json());
770
+ return {
771
+ events: (body.events ?? []).map((e) => ({
772
+ id: String(e.id),
773
+ eventType: String(e.event_type),
774
+ occurredAt: String(e.occurred_at),
775
+ ip: e.ip,
776
+ userAgent: e.user_agent,
777
+ eventData: e.event_data ?? {},
778
+ })),
779
+ total: Number(body.total ?? 0),
780
+ limit: Number(body.limit ?? 0),
781
+ offset: Number(body.offset ?? 0),
782
+ };
783
+ }
586
784
  async unlock(token) {
587
785
  const res = await this.authRequest('/v1/sdk/auth/unlock', {
588
786
  method: 'POST',
@@ -623,20 +821,54 @@ export class KoolbaseAuth {
623
821
  }
624
822
  // ─── OAuth (DEPRECATED — see v1.10.0) ───────────────────────────────────
625
823
  /**
626
- * @deprecated v1.9.0: Server endpoint /v1/sdk/auth/oauth not yet
627
- * shipped. This method previously routed to /v1/auth/oauth (dashboard
628
- * developer OAuth) which never created project-scoped end-user
629
- * sessions. Properly implemented in v1.10.0 with provider-specific
630
- * server endpoints under /v1/sdk/auth/oauth/{apple,google,github}.
631
- * Use email/password sign-in for now.
824
+ * @deprecated Never implemented; kept only so existing code compiles.
825
+ * Google and Apple sign-in are supported — use signInWithGoogle() or
826
+ * signInWithApple(). This used to say OAuth was not yet shipped and to
827
+ * use email and password instead, which stopped being true long ago.
632
828
  *
633
829
  * @throws Always throws KoolbaseAuthError('not_implemented').
634
830
  */
635
831
  async oauthLogin(_params) {
636
- throw new KoolbaseAuthError('OAuth sign-in is not yet implemented for the Koolbase SDK. ' +
637
- 'Planned for v1.10.0 (server-side endpoints under ' +
638
- '/v1/sdk/auth/oauth/{provider}). Use email/password authentication ' +
639
- 'in the meantime.', 'not_implemented');
832
+ throw new KoolbaseAuthError('oauthLogin() was never implemented and is kept only so existing ' +
833
+ 'code compiles. Use signInWithGoogle() or signInWithApple().', 'not_implemented');
834
+ }
835
+ // ─── Email code ─────────────────────────────────────────────────────────
836
+ /**
837
+ * Emails a six-digit sign-in code.
838
+ *
839
+ * Resolves the same way whether or not the address has an account, so it
840
+ * cannot be used to check who is registered. A new address receives a code
841
+ * only while the project accepts sign-ups; a disabled account receives
842
+ * nothing.
843
+ *
844
+ * @throws EmailCodeDisabledError if the project has switched this off
845
+ * @throws RateLimitError after too many requests for one address
846
+ */
847
+ async requestEmailCode(params) {
848
+ const res = await this.authRequest('/v1/sdk/auth/email/code', {
849
+ method: 'POST',
850
+ body: { email: params.email },
851
+ });
852
+ if (!res.ok)
853
+ await this.throwTypedError(res, false);
854
+ }
855
+ /**
856
+ * Signs in with a code from requestEmailCode(), storing the session exactly
857
+ * as login() does. An unknown address becomes an account only if the
858
+ * project still accepts sign-ups at this moment. Each code works once and
859
+ * allows three attempts.
860
+ *
861
+ * @throws OtpInvalidError, OtpExpiredError, OtpMaxAttemptsError
862
+ * @throws EmailCodeDisabledError if the project has switched this off
863
+ */
864
+ async signInWithEmailCode(params) {
865
+ const res = await this.authRequest('/v1/sdk/auth/email/code/verify', {
866
+ method: 'POST',
867
+ body: { email: params.email, code: params.code },
868
+ });
869
+ const session = await this.parseSessionResponse(res, false);
870
+ await this.setSessionInternal(session);
871
+ return session;
640
872
  }
641
873
  // ─── Phone OTP ──────────────────────────────────────────────────────────
642
874
  async sendOtp(params) {
@@ -829,6 +1061,16 @@ export class KoolbaseAuth {
829
1061
  case 'contact_not_verified':
830
1062
  case 'email_not_verified':
831
1063
  throw new ContactNotVerifiedError(msg || undefined);
1064
+ // Sign-in codes. The otp_* codes are shared with phone sign-in; mapped
1065
+ // here, every path that returns them throws the class an app catches.
1066
+ case 'email_code_disabled':
1067
+ throw new EmailCodeDisabledError();
1068
+ case 'otp_expired':
1069
+ throw new OtpExpiredError();
1070
+ case 'otp_invalid':
1071
+ throw new OtpInvalidError();
1072
+ case 'otp_max_attempts':
1073
+ throw new OtpMaxAttemptsError();
832
1074
  // Registration
833
1075
  case 'signups_disabled':
834
1076
  throw new SignupsDisabledError(msg || undefined);
@@ -195,6 +195,52 @@ export declare class KoolbaseConflictError extends KoolbaseDataError {
195
195
  field?: string;
196
196
  constructor(message?: string, field?: string);
197
197
  }
198
+ /**
199
+ * A reference field points at a record that does not exist, is deleted, or
200
+ * lives in another collection — the server responds 400 `reference_invalid`.
201
+ *
202
+ * Checked when the write commits, so it can surface from an insert, an update,
203
+ * an upsert or a batch. In a batch the whole transaction is refused: nothing
204
+ * in it was written.
205
+ */
206
+ export declare class KoolbaseReferenceInvalidError extends KoolbaseDataError {
207
+ constructor(message?: string);
208
+ }
209
+ /**
210
+ * A record could not be deleted because live records still reference it, under
211
+ * a reference declared `on_delete: restrict` — 409 `reference_in_use`.
212
+ *
213
+ * Delete the referencing records first, or in the same batch: the check runs at
214
+ * commit, so one batch may delete a parent and its children in any order.
215
+ */
216
+ export declare class KoolbaseReferenceInUseError extends KoolbaseDataError {
217
+ constructor(message?: string);
218
+ }
219
+ /**
220
+ * A reference could not be declared because existing records already point at
221
+ * records that do not exist — 409 `dangling_references`.
222
+ *
223
+ * `dangling` lists the offending records so they can be repaired; Koolbase
224
+ * never repairs them for you. It is capped at the first 50.
225
+ */
226
+ export declare class KoolbaseDanglingReferencesError extends KoolbaseDataError {
227
+ dangling: Array<{
228
+ record_id: string;
229
+ value: string;
230
+ }>;
231
+ constructor(message?: string, dangling?: Array<{
232
+ record_id: string;
233
+ value: string;
234
+ }>);
235
+ }
236
+ /**
237
+ * A collection could not be deleted because another collection has a reference
238
+ * field pointing at it — 409 `collection_referenced`. Remove that reference
239
+ * first.
240
+ */
241
+ export declare class KoolbaseCollectionReferencedError extends KoolbaseDataError {
242
+ constructor(message?: string);
243
+ }
198
244
  /**
199
245
  * Thrown when the requested record or collection does not exist — the server
200
246
  * responds with 404 and code `not_found` / `record_not_found` /
@@ -272,6 +272,62 @@ export class KoolbaseConflictError extends KoolbaseDataError {
272
272
  Object.setPrototypeOf(this, KoolbaseConflictError.prototype);
273
273
  }
274
274
  }
275
+ /**
276
+ * A reference field points at a record that does not exist, is deleted, or
277
+ * lives in another collection — the server responds 400 `reference_invalid`.
278
+ *
279
+ * Checked when the write commits, so it can surface from an insert, an update,
280
+ * an upsert or a batch. In a batch the whole transaction is refused: nothing
281
+ * in it was written.
282
+ */
283
+ export class KoolbaseReferenceInvalidError extends KoolbaseDataError {
284
+ constructor(message) {
285
+ super(message ?? 'A reference points at a record that does not exist', 'reference_invalid');
286
+ this.name = 'KoolbaseReferenceInvalidError';
287
+ Object.setPrototypeOf(this, KoolbaseReferenceInvalidError.prototype);
288
+ }
289
+ }
290
+ /**
291
+ * A record could not be deleted because live records still reference it, under
292
+ * a reference declared `on_delete: restrict` — 409 `reference_in_use`.
293
+ *
294
+ * Delete the referencing records first, or in the same batch: the check runs at
295
+ * commit, so one batch may delete a parent and its children in any order.
296
+ */
297
+ export class KoolbaseReferenceInUseError extends KoolbaseDataError {
298
+ constructor(message) {
299
+ super(message ?? 'This record is still referenced by other records', 'reference_in_use');
300
+ this.name = 'KoolbaseReferenceInUseError';
301
+ Object.setPrototypeOf(this, KoolbaseReferenceInUseError.prototype);
302
+ }
303
+ }
304
+ /**
305
+ * A reference could not be declared because existing records already point at
306
+ * records that do not exist — 409 `dangling_references`.
307
+ *
308
+ * `dangling` lists the offending records so they can be repaired; Koolbase
309
+ * never repairs them for you. It is capped at the first 50.
310
+ */
311
+ export class KoolbaseDanglingReferencesError extends KoolbaseDataError {
312
+ constructor(message, dangling) {
313
+ super(message ?? 'Existing records point at records that do not exist', 'dangling_references');
314
+ this.dangling = dangling ?? [];
315
+ this.name = 'KoolbaseDanglingReferencesError';
316
+ Object.setPrototypeOf(this, KoolbaseDanglingReferencesError.prototype);
317
+ }
318
+ }
319
+ /**
320
+ * A collection could not be deleted because another collection has a reference
321
+ * field pointing at it — 409 `collection_referenced`. Remove that reference
322
+ * first.
323
+ */
324
+ export class KoolbaseCollectionReferencedError extends KoolbaseDataError {
325
+ constructor(message) {
326
+ super(message ?? 'Another collection references this one', 'collection_referenced');
327
+ this.name = 'KoolbaseCollectionReferencedError';
328
+ Object.setPrototypeOf(this, KoolbaseCollectionReferencedError.prototype);
329
+ }
330
+ }
275
331
  /**
276
332
  * Thrown when the requested record or collection does not exist — the server
277
333
  * responds with 404 and code `not_found` / `record_not_found` /
@@ -396,6 +452,16 @@ export function koolbaseDataError(status, body, fallbackMessage = 'Request faile
396
452
  switch (code) {
397
453
  case 'unique_violation':
398
454
  return attach(new KoolbaseConflictError(message, field));
455
+ case 'reference_invalid':
456
+ return attach(new KoolbaseReferenceInvalidError(message));
457
+ case 'reference_in_use':
458
+ return attach(new KoolbaseReferenceInUseError(message));
459
+ case 'dangling_references': {
460
+ const d = (body?.details ?? {});
461
+ return attach(new KoolbaseDanglingReferencesError(message, d.dangling));
462
+ }
463
+ case 'collection_referenced':
464
+ return attach(new KoolbaseCollectionReferencedError(message));
399
465
  case 'plan_limit_reached': {
400
466
  const d = (body?.details ?? {});
401
467
  return new KoolbasePlanLimitError(message, d.resource, d.limit, d.plan);
@@ -1,3 +1,4 @@
1
+ import { KoolbaseAggregateRequest, KoolbaseAggregateResult } from './aggregate.js';
1
2
  import { KoolbaseConflict } from './conflict.js';
2
3
  import { PendingWrite } from './pending-write.js';
3
4
  import { KoolbaseConfig, KoolbaseRecord, QueryOptions, QueryResult, UpsertResult, BatchOp, BatchResult, KoolbaseVector, SemanticSearchResult, SearchMode } from './types.js';
@@ -127,6 +128,28 @@ export declare class KoolbaseDatabase {
127
128
  * moment matters. Snapshot, not a live handle; per-user.
128
129
  */
129
130
  pendingWrites(): Promise<PendingWrite[]>;
131
+ /**
132
+ * Count and total over a whole collection, with the read rule applied
133
+ * inside the query.
134
+ *
135
+ * Not a paged query you add up yourself: the server aggregates the entire
136
+ * authorized set. And never a bare number — collections are schemaless, so
137
+ * a sum that quietly skipped three malformed rows would be a wrong number
138
+ * that looks right. Check `accounting` for what contributed.
139
+ *
140
+ * ```ts
141
+ * const r = await db.aggregate({
142
+ * collection: 'orders',
143
+ * groupBy: { field: 'created_at', bucket: 'month', timezone: 'Africa/Accra' },
144
+ * measures: [{ aggregate: 'sum', field: 'total', as: 'revenue' }],
145
+ * });
146
+ * ```
147
+ *
148
+ * A calendar bucket requires a timezone and the types enforce it: midnight
149
+ * means nothing without one, and Accra and UTC disagree about which day a
150
+ * 23:30 sale belongs to.
151
+ */
152
+ aggregate(request: KoolbaseAggregateRequest): Promise<KoolbaseAggregateResult>;
130
153
  conflicts(): Promise<KoolbaseConflict[]>;
131
154
  /**
132
155
  * Resolves by id, reloading the stored conflict first.