@koolbase/core 12.1.0 → 12.3.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,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KoolbaseStorage = void 0;
4
+ const network_js_1 = require("./network.js");
4
5
  const errors_js_1 = require("./errors.js");
5
6
  const storage_errors_js_1 = require("./storage-errors.js");
6
7
  // --- Cloudflare image-transform URL helpers -------------------------------
@@ -97,7 +98,7 @@ class KoolbaseStorage {
97
98
  ? options.file.type || 'application/octet-stream'
98
99
  : options.file.type;
99
100
  // ─── Step 1: Get presigned upload URL ───
100
- const urlRes = await fetch(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
101
+ const urlRes = await (0, network_js_1.koolbaseFetch)(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
101
102
  method: 'POST',
102
103
  headers: {
103
104
  ...(await this.buildHeaders()),
@@ -123,9 +124,9 @@ class KoolbaseStorage {
123
124
  // the two hosts differ here and nowhere else in this method.
124
125
  const fileBlob = isBlob
125
126
  ? options.file
126
- : await (await fetch(options.file.uri)).blob();
127
+ : await (await (0, network_js_1.koolbaseFetch)(options.file.uri)).blob();
127
128
  const fileSize = fileBlob.size;
128
- const uploadRes = await fetch(upload_url, {
129
+ const uploadRes = await (0, network_js_1.koolbaseFetch)(upload_url, {
129
130
  method: 'PUT',
130
131
  headers: { 'Content-Type': contentType },
131
132
  body: fileBlob,
@@ -152,7 +153,7 @@ class KoolbaseStorage {
152
153
  if (options.metadata !== undefined) {
153
154
  confirmBody.metadata = options.metadata;
154
155
  }
155
- const confirmRes = await fetch(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
156
+ const confirmRes = await (0, network_js_1.koolbaseFetch)(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
156
157
  method: 'POST',
157
158
  headers: {
158
159
  ...(await this.buildHeaders()),
@@ -203,7 +204,7 @@ class KoolbaseStorage {
203
204
  * // -> { category: 'landscape', tag: 'sunset' }
204
205
  */
205
206
  async updateMetadata(bucket, path, metadata) {
206
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/objects/metadata`, {
207
+ const res = await (0, network_js_1.koolbaseFetch)(`${this.config.baseUrl}/v1/sdk/storage/objects/metadata`, {
207
208
  method: 'PATCH',
208
209
  headers: {
209
210
  ...(await this.buildHeaders()),
@@ -226,7 +227,7 @@ class KoolbaseStorage {
226
227
  if (versionId) {
227
228
  url += `&version_id=${encodeURIComponent(versionId)}`;
228
229
  }
229
- const res = await fetch(url, { headers: await this.buildHeaders() });
230
+ const res = await (0, network_js_1.koolbaseFetch)(url, { headers: await this.buildHeaders() });
230
231
  if (!res.ok) {
231
232
  throw await this.error(res, 'Failed to get download URL');
232
233
  }
@@ -321,7 +322,7 @@ class KoolbaseStorage {
321
322
  const url = forcePurge
322
323
  ? `${this.config.baseUrl}/v1/sdk/storage/object?force_purge=true`
323
324
  : `${this.config.baseUrl}/v1/sdk/storage/object`;
324
- const res = await fetch(url, {
325
+ const res = await (0, network_js_1.koolbaseFetch)(url, {
325
326
  method: 'DELETE',
326
327
  headers: {
327
328
  ...(await this.buildHeaders()),
@@ -348,7 +349,7 @@ class KoolbaseStorage {
348
349
  async listVersions(bucket, path) {
349
350
  const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions` +
350
351
  `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
351
- const res = await fetch(url, { headers: await this.buildHeaders() });
352
+ const res = await (0, network_js_1.koolbaseFetch)(url, { headers: await this.buildHeaders() });
352
353
  if (!res.ok) {
353
354
  throw await this.error(res, 'Failed to list versions');
354
355
  }
@@ -364,7 +365,7 @@ class KoolbaseStorage {
364
365
  async getVersion(bucket, path, versionId) {
365
366
  const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
366
367
  `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
367
- const res = await fetch(url, { headers: await this.buildHeaders() });
368
+ const res = await (0, network_js_1.koolbaseFetch)(url, { headers: await this.buildHeaders() });
368
369
  if (!res.ok) {
369
370
  throw await this.error(res, 'Failed to fetch version');
370
371
  }
@@ -383,7 +384,7 @@ class KoolbaseStorage {
383
384
  async restoreVersion(bucket, path, versionId) {
384
385
  const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}/restore` +
385
386
  `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
386
- const res = await fetch(url, {
387
+ const res = await (0, network_js_1.koolbaseFetch)(url, {
387
388
  method: 'POST',
388
389
  headers: await this.buildHeaders(),
389
390
  });
@@ -401,7 +402,7 @@ class KoolbaseStorage {
401
402
  async purgeVersion(bucket, path, versionId) {
402
403
  const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
403
404
  `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
404
- const res = await fetch(url, {
405
+ const res = await (0, network_js_1.koolbaseFetch)(url, {
405
406
  method: 'DELETE',
406
407
  headers: await this.buildHeaders(),
407
408
  });
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SyncEngine = void 0;
4
+ const network_js_1 = require("./network.js");
4
5
  const offline_state_js_1 = require("./offline-state.js");
5
6
  const errors_js_1 = require("./errors.js");
6
7
  const platform_js_1 = require("./platform.js");
@@ -245,7 +246,7 @@ class SyncEngine {
245
246
  : `${this.config.baseUrl}/v1/sdk/db/records/${write.recordId}`;
246
247
  let res;
247
248
  if (write.operation === 'insert') {
248
- res = await fetch(url, {
249
+ res = await (0, network_js_1.koolbaseFetch)(url, {
249
250
  method: 'POST',
250
251
  headers,
251
252
  // The write's own id: generated at enqueue, identical on every retry.
@@ -259,7 +260,7 @@ class SyncEngine {
259
260
  });
260
261
  }
261
262
  else if (write.operation === 'update') {
262
- res = await fetch(url, {
263
+ res = await (0, network_js_1.koolbaseFetch)(url, {
263
264
  method: 'PATCH',
264
265
  headers,
265
266
  // The revision the change was composed against. The server applies it
@@ -278,7 +279,7 @@ class SyncEngine {
278
279
  const q = write.baseRevision !== undefined
279
280
  ? `?expected_revision=${write.baseRevision}`
280
281
  : '';
281
- res = await fetch(`${url}${q}`, { method: 'DELETE', headers });
282
+ res = await (0, network_js_1.koolbaseFetch)(`${url}${q}`, { method: 'DELETE', headers });
282
283
  }
283
284
  if (res.status === 401)
284
285
  throw new errors_js_1.KoolbaseUnauthenticatedError('unauthorized');
@@ -239,6 +239,13 @@ export interface QueryOptions {
239
239
  orderBy?: string;
240
240
  orderDesc?: boolean;
241
241
  populate?: string[];
242
+ /**
243
+ * How to read: 'default' is cache-first, refreshed in the background;
244
+ * 'network-only' skips the cache and waits for the server (the result still
245
+ * refreshes the cache). Use 'network-only' after a write this client did not
246
+ * make, by a Function or another user, when you need the server's state now.
247
+ */
248
+ cache?: 'default' | 'network-only';
242
249
  }
243
250
  export interface QueryResult {
244
251
  records: KoolbaseRecord[];
@@ -1,3 +1,4 @@
1
+ import { koolbaseFetch } from './network.js';
1
2
  import { getPlatform } from './platform.js';
2
3
  import { getOrCreateDeviceId } from './device-id.js';
3
4
  // ─── KoolbaseAnalytics ───────────────────────────────────────────────────────
@@ -99,7 +100,7 @@ export class KoolbaseAnalytics {
99
100
  const batch = [...this.queue];
100
101
  this.queue = [];
101
102
  try {
102
- const response = await fetch(`${this.config.baseUrl}/v1/analytics/events`, {
103
+ const response = await koolbaseFetch(`${this.config.baseUrl}/v1/analytics/events`, {
103
104
  method: 'POST',
104
105
  headers: {
105
106
  'Content-Type': 'application/json',
@@ -257,3 +257,29 @@ export declare class InvalidGoogleTokenError extends KoolbaseAuthError {
257
257
  export declare class GoogleEmailRequiredError extends KoolbaseAuthError {
258
258
  constructor();
259
259
  }
260
+ /**
261
+ * The first factor passed and this account needs a second: sign-in is not
262
+ * finished. Pass `challengeToken` to `verifyMfa` with a code from the
263
+ * person's authenticator app, or to `verifyRecoveryCode`. Apps that never
264
+ * enable MFA never see this.
265
+ */
266
+ export declare class MfaRequiredError extends KoolbaseAuthError {
267
+ readonly challengeToken: string;
268
+ readonly expiresAt?: string;
269
+ constructor(challengeToken: string, expiresAt?: string);
270
+ }
271
+ export declare class RecentAuthRequiredError extends KoolbaseAuthError {
272
+ constructor();
273
+ }
274
+ export declare class RecentMfaRequiredError extends KoolbaseAuthError {
275
+ constructor();
276
+ }
277
+ export declare class MfaAlreadyEnabledError extends KoolbaseAuthError {
278
+ constructor();
279
+ }
280
+ export declare class MfaEnrollmentNotFoundError extends KoolbaseAuthError {
281
+ constructor();
282
+ }
283
+ export declare class MfaNotEnabledError extends KoolbaseAuthError {
284
+ constructor();
285
+ }
@@ -438,3 +438,54 @@ export class GoogleEmailRequiredError extends KoolbaseAuthError {
438
438
  Object.setPrototypeOf(this, GoogleEmailRequiredError.prototype);
439
439
  }
440
440
  }
441
+ // ─── Two-step sign-in (MFA) ─────────────────────────────────────────────────
442
+ /**
443
+ * The first factor passed and this account needs a second: sign-in is not
444
+ * finished. Pass `challengeToken` to `verifyMfa` with a code from the
445
+ * person's authenticator app, or to `verifyRecoveryCode`. Apps that never
446
+ * enable MFA never see this.
447
+ */
448
+ export class MfaRequiredError extends KoolbaseAuthError {
449
+ constructor(challengeToken, expiresAt) {
450
+ super('Two-step sign-in required', 'mfa_required');
451
+ this.name = 'MfaRequiredError';
452
+ this.challengeToken = challengeToken;
453
+ this.expiresAt = expiresAt;
454
+ Object.setPrototypeOf(this, MfaRequiredError.prototype);
455
+ }
456
+ }
457
+ export class RecentAuthRequiredError extends KoolbaseAuthError {
458
+ constructor() {
459
+ super('Sign in again to change two-step sign-in', 'recent_auth_required');
460
+ this.name = 'RecentAuthRequiredError';
461
+ Object.setPrototypeOf(this, RecentAuthRequiredError.prototype);
462
+ }
463
+ }
464
+ export class RecentMfaRequiredError extends KoolbaseAuthError {
465
+ constructor() {
466
+ super('Confirm with your authenticator first', 'recent_mfa_required');
467
+ this.name = 'RecentMfaRequiredError';
468
+ Object.setPrototypeOf(this, RecentMfaRequiredError.prototype);
469
+ }
470
+ }
471
+ export class MfaAlreadyEnabledError extends KoolbaseAuthError {
472
+ constructor() {
473
+ super('Two-step sign-in is already on for this account', 'mfa_already_enabled');
474
+ this.name = 'MfaAlreadyEnabledError';
475
+ Object.setPrototypeOf(this, MfaAlreadyEnabledError.prototype);
476
+ }
477
+ }
478
+ export class MfaEnrollmentNotFoundError extends KoolbaseAuthError {
479
+ constructor() {
480
+ super('No enrolment in progress; start again', 'mfa_enrollment_not_found');
481
+ this.name = 'MfaEnrollmentNotFoundError';
482
+ Object.setPrototypeOf(this, MfaEnrollmentNotFoundError.prototype);
483
+ }
484
+ }
485
+ export class MfaNotEnabledError extends KoolbaseAuthError {
486
+ constructor() {
487
+ super('Two-step sign-in is not on for this account', 'mfa_not_enabled');
488
+ this.name = 'MfaNotEnabledError';
489
+ Object.setPrototypeOf(this, MfaNotEnabledError.prototype);
490
+ }
491
+ }
@@ -343,6 +343,47 @@ export declare class KoolbaseAuth {
343
343
  email: string;
344
344
  code: string;
345
345
  }): Promise<KoolbaseSession>;
346
+ private mfaRequired;
347
+ /** Finishes sign-in with a code from the person's authenticator app. */
348
+ verifyMfa(params: {
349
+ challengeToken: string;
350
+ code: string;
351
+ }): Promise<KoolbaseSession>;
352
+ /**
353
+ * Finishes sign-in with a recovery code. Each works once; prompt the person
354
+ * to regenerate when recoveryCodesRemaining reaches zero.
355
+ */
356
+ verifyRecoveryCode(params: {
357
+ challengeToken: string;
358
+ code: string;
359
+ }): Promise<{
360
+ session: KoolbaseSession;
361
+ recoveryCodesRemaining: number;
362
+ }>;
363
+ /**
364
+ * Starts adding an authenticator; needs a sign-in within ten minutes. Show
365
+ * otpauthUri as a QR code, then call confirmMfaEnrollment with the first
366
+ * code the app shows.
367
+ */
368
+ enrollMfa(): Promise<{
369
+ otpauthUri: string;
370
+ secret: string;
371
+ }>;
372
+ /** Turns MFA on and returns ten recovery codes, shown this once only. Other devices are signed out. */
373
+ confirmMfaEnrollment(code: string): Promise<string[]>;
374
+ mfaStatus(): Promise<{
375
+ enabled: boolean;
376
+ recoveryCodesRemaining: number;
377
+ }>;
378
+ /** Re-confirms the second factor, opening ten minutes to change MFA. */
379
+ stepUpMfa(params: {
380
+ code?: string;
381
+ recoveryCode?: string;
382
+ }): Promise<void>;
383
+ /** Turns MFA off. Needs stepUpMfa() within the last ten minutes. */
384
+ disableMfa(): Promise<void>;
385
+ /** Replaces all recovery codes; the old ones stop working. Needs stepUpMfa() within ten minutes. */
386
+ regenerateRecoveryCodes(): Promise<string[]>;
346
387
  sendOtp(params: SendOtpParams): Promise<OtpSendResult>;
347
388
  verifyOtp(params: VerifyOtpParams): Promise<PhoneVerifyResult>;
348
389
  linkPhone(params: LinkPhoneParams): Promise<void>;
package/dist/esm/auth.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { koolbaseFetch } from './network.js';
1
2
  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, EmailCodeDisabledError, } from './auth-errors.js';
3
+ 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, MfaRequiredError, RecentAuthRequiredError, RecentMfaRequiredError, MfaAlreadyEnabledError, MfaEnrollmentNotFoundError, MfaNotEnabledError, } from './auth-errors.js';
3
4
  import { getPlatform } from './platform.js';
4
5
  import { DeviceMetadata } from './device-metadata.js';
5
6
  export class KoolbaseAuth {
@@ -9,7 +10,7 @@ export class KoolbaseAuth {
9
10
  this.listeners = new Set();
10
11
  this.config = config;
11
12
  this.metadata = new DeviceMetadata(config.appVersion);
12
- this.fetchFn = config.fetch ?? ((url, init) => fetch(url, init));
13
+ this.fetchFn = config.fetch ?? ((url, init) => koolbaseFetch(url, init));
13
14
  this.timeoutMs = config.authTimeout ?? 10000;
14
15
  if (config.authStorage) {
15
16
  this.storage = config.authStorage;
@@ -385,6 +386,8 @@ export class KoolbaseAuth {
385
386
  const errorMessage = body?.error ?? '';
386
387
  // ─── code-first ───
387
388
  switch (code) {
389
+ case 'mfa_required':
390
+ throw this.mfaRequired(body);
388
391
  case 'oauth_not_configured':
389
392
  throw new GoogleSignInNotConfiguredError();
390
393
  case 'invalid_oauth_token':
@@ -439,6 +442,8 @@ export class KoolbaseAuth {
439
442
  const errorMessage = body?.error ?? '';
440
443
  // ─── code-first ───
441
444
  switch (code) {
445
+ case 'mfa_required':
446
+ throw this.mfaRequired(body);
442
447
  case 'oauth_not_configured':
443
448
  throw new AppleSignInNotConfiguredError();
444
449
  case 'invalid_oauth_token':
@@ -870,6 +875,102 @@ export class KoolbaseAuth {
870
875
  await this.setSessionInternal(session);
871
876
  return session;
872
877
  }
878
+ // ─── Two-step sign-in (MFA) ─────────────────────────────────────────────
879
+ //
880
+ // When an account has MFA on, every sign-in method throws MfaRequiredError.
881
+ // Finish with verifyMfa() or verifyRecoveryCode().
882
+ // One builder for every parser, so the four cannot drift apart in how the
883
+ // challenge reaches the app.
884
+ mfaRequired(body) {
885
+ const d = body?.details ?? {};
886
+ return new MfaRequiredError(String(d.challenge_token ?? ''), d.expires_at);
887
+ }
888
+ /** Finishes sign-in with a code from the person's authenticator app. */
889
+ async verifyMfa(params) {
890
+ const res = await this.authRequest('/v1/sdk/auth/mfa/verify', {
891
+ method: 'POST',
892
+ body: { challenge_token: params.challengeToken, code: params.code },
893
+ });
894
+ const session = await this.parseSessionResponse(res, false);
895
+ await this.setSessionInternal(session);
896
+ return session;
897
+ }
898
+ /**
899
+ * Finishes sign-in with a recovery code. Each works once; prompt the person
900
+ * to regenerate when recoveryCodesRemaining reaches zero.
901
+ */
902
+ async verifyRecoveryCode(params) {
903
+ const res = await this.authRequest('/v1/sdk/auth/mfa/verify-recovery', {
904
+ method: 'POST',
905
+ body: { challenge_token: params.challengeToken, code: params.code },
906
+ });
907
+ if (!res.ok)
908
+ await this.throwTypedError(res, false);
909
+ // Read once: the session and the remaining count come from one body.
910
+ const data = await res.json();
911
+ if (!data?.access_token)
912
+ throw new MalformedSessionResponseError('an access token');
913
+ if (!data?.refresh_token)
914
+ throw new MalformedSessionResponseError('a refresh token');
915
+ const session = {
916
+ accessToken: data.access_token,
917
+ refreshToken: data.refresh_token,
918
+ expiresAt: data.expires_at,
919
+ user: this.mapUser(data.user),
920
+ };
921
+ await this.setSessionInternal(session);
922
+ return { session, recoveryCodesRemaining: Number(data.recovery_codes_remaining ?? 0) };
923
+ }
924
+ /**
925
+ * Starts adding an authenticator; needs a sign-in within ten minutes. Show
926
+ * otpauthUri as a QR code, then call confirmMfaEnrollment with the first
927
+ * code the app shows.
928
+ */
929
+ async enrollMfa() {
930
+ const res = await this.authRequest('/v1/sdk/auth/mfa/enroll', { method: 'POST', includeAuth: true });
931
+ await this.checkResponse(res);
932
+ const d = await res.json();
933
+ return { otpauthUri: d.otpauth_uri, secret: d.secret };
934
+ }
935
+ /** Turns MFA on and returns ten recovery codes, shown this once only. Other devices are signed out. */
936
+ async confirmMfaEnrollment(code) {
937
+ const res = await this.authRequest('/v1/sdk/auth/mfa/enroll/confirm', {
938
+ method: 'POST',
939
+ body: { code },
940
+ includeAuth: true,
941
+ });
942
+ await this.checkResponse(res);
943
+ return (await res.json()).recovery_codes;
944
+ }
945
+ async mfaStatus() {
946
+ const res = await this.authRequest('/v1/sdk/auth/mfa', { method: 'GET', includeAuth: true });
947
+ await this.checkResponse(res);
948
+ const d = await res.json();
949
+ return { enabled: !!d.enabled, recoveryCodesRemaining: Number(d.recovery_codes_remaining ?? 0) };
950
+ }
951
+ /** Re-confirms the second factor, opening ten minutes to change MFA. */
952
+ async stepUpMfa(params) {
953
+ const res = await this.authRequest('/v1/sdk/auth/mfa/step-up', {
954
+ method: 'POST',
955
+ body: {
956
+ ...(params.code ? { code: params.code } : {}),
957
+ ...(params.recoveryCode ? { recovery_code: params.recoveryCode } : {}),
958
+ },
959
+ includeAuth: true,
960
+ });
961
+ await this.checkResponse(res);
962
+ }
963
+ /** Turns MFA off. Needs stepUpMfa() within the last ten minutes. */
964
+ async disableMfa() {
965
+ const res = await this.authRequest('/v1/sdk/auth/mfa/disable', { method: 'POST', includeAuth: true });
966
+ await this.checkResponse(res);
967
+ }
968
+ /** Replaces all recovery codes; the old ones stop working. Needs stepUpMfa() within ten minutes. */
969
+ async regenerateRecoveryCodes() {
970
+ const res = await this.authRequest('/v1/sdk/auth/mfa/recovery-codes', { method: 'POST', includeAuth: true });
971
+ await this.checkResponse(res);
972
+ return (await res.json()).recovery_codes;
973
+ }
873
974
  // ─── Phone OTP ──────────────────────────────────────────────────────────
874
975
  async sendOtp(params) {
875
976
  this.validatePhone(params.phoneNumber);
@@ -1037,6 +1138,18 @@ export class KoolbaseAuth {
1037
1138
  const msg = body.error ?? '';
1038
1139
  // ─── code-first ───
1039
1140
  switch (code) {
1141
+ case 'mfa_required':
1142
+ throw this.mfaRequired(body);
1143
+ case 'recent_auth_required':
1144
+ throw new RecentAuthRequiredError();
1145
+ case 'recent_mfa_required':
1146
+ throw new RecentMfaRequiredError();
1147
+ case 'mfa_already_enabled':
1148
+ throw new MfaAlreadyEnabledError();
1149
+ case 'mfa_enrollment_not_found':
1150
+ throw new MfaEnrollmentNotFoundError();
1151
+ case 'mfa_not_enabled':
1152
+ throw new MfaNotEnabledError();
1040
1153
  case 'invalid_credentials':
1041
1154
  throw new InvalidCredentialsError();
1042
1155
  case 'email_in_use':
@@ -1157,6 +1270,8 @@ export class KoolbaseAuth {
1157
1270
  const msg = body.error ?? '';
1158
1271
  // ─── code-first ───
1159
1272
  switch (code) {
1273
+ case 'mfa_required':
1274
+ throw this.mfaRequired(body);
1160
1275
  case 'invalid_phone':
1161
1276
  throw new InvalidPhoneNumberError();
1162
1277
  case 'otp_expired':
@@ -47,6 +47,12 @@ export declare class KoolbaseDatabase {
47
47
  * store for `update`/`delete`.
48
48
  */
49
49
  query(collection: string, options?: QueryOptions): Promise<QueryResult>;
50
+ /**
51
+ * Forget this client's cached query results for a collection, so the next
52
+ * `query` waits for the server. For data changed elsewhere: by a Function,
53
+ * a server, or another user. The SDK's own writes already do this.
54
+ */
55
+ invalidate(collection: string): Promise<void>;
50
56
  /**
51
57
  * Insert a new record into a collection.
52
58
  *
@@ -1,3 +1,4 @@
1
+ import { koolbaseFetch } from './network.js';
1
2
  import { KoolbaseError, KoolbaseOfflineBaselineUnavailableError, KoolbaseUnauthenticatedError, } from './errors.js';
2
3
  import { cacheRecord, getCachedRecord, removeCachedRecord } from './cache-store.js';
3
4
  import { readOfflineState, mutateOfflineState, queueWrite, } from './offline-state.js';
@@ -85,7 +86,7 @@ export class KoolbaseDatabase {
85
86
  };
86
87
  }
87
88
  async request(method, path, body) {
88
- const res = await fetch(`${this.config.baseUrl}${path}`, {
89
+ const res = await koolbaseFetch(`${this.config.baseUrl}${path}`, {
89
90
  method,
90
91
  headers: await this.buildHeaders(),
91
92
  body: body ? JSON.stringify(body) : undefined,
@@ -123,7 +124,7 @@ export class KoolbaseDatabase {
123
124
  * none of them clearing a rejected session. One path, two shapes of result.
124
125
  */
125
126
  async requestWithStatus(method, path, body) {
126
- const res = await fetch(`${this.config.baseUrl}${path}`, {
127
+ const res = await koolbaseFetch(`${this.config.baseUrl}${path}`, {
127
128
  method,
128
129
  headers: await this.buildHeaders(),
129
130
  body: body ? JSON.stringify(body) : undefined,
@@ -194,20 +195,31 @@ export class KoolbaseDatabase {
194
195
  */
195
196
  async query(collection, options = {}) {
196
197
  const userId = this.getUserId() ?? 'anonymous';
197
- const queryHash = hashQuery(collection, options);
198
- const cached = await getCached(userId, collection, queryHash);
198
+ // The cache policy is how to read, not what to read: it stays out of the
199
+ // query's identity, so both policies share one cache entry.
200
+ const { cache: cachePolicy, ...queryOptions } = options;
201
+ const queryHash = hashQuery(collection, queryOptions);
202
+ const cached = cachePolicy === 'network-only' ? null : await getCached(userId, collection, queryHash);
199
203
  if (cached) {
200
- this.runQuery(collection, options)
204
+ this.runQuery(collection, queryOptions)
201
205
  .then(result => setCached(userId, collection, queryHash, result))
202
206
  .catch(() => {
203
207
  // Network unavailable — cached data already returned
204
208
  });
205
209
  return { ...cached, isFromCache: true };
206
210
  }
207
- const result = await this.runQuery(collection, options);
211
+ const result = await this.runQuery(collection, queryOptions);
208
212
  await setCached(userId, collection, queryHash, result);
209
213
  return { ...result, isFromCache: false };
210
214
  }
215
+ /**
216
+ * Forget this client's cached query results for a collection, so the next
217
+ * `query` waits for the server. For data changed elsewhere: by a Function,
218
+ * a server, or another user. The SDK's own writes already do this.
219
+ */
220
+ async invalidate(collection) {
221
+ await invalidateCache(this.getUserId() ?? 'anonymous', collection);
222
+ }
211
223
  // ─── Insert (online-first with offline fallback) ───────────────────────────
212
224
  /**
213
225
  * Insert a new record into a collection.
@@ -1,3 +1,4 @@
1
+ import { koolbaseFetch } from './network.js';
1
2
  import { KoolbaseError, KoolbaseUnauthenticatedError } from './errors.js';
2
3
  const KNOWN_STATUSES = [
3
4
  'created', 'blocked', 'queued', 'submitting',
@@ -105,7 +106,7 @@ export class KoolbaseFiscal {
105
106
  const controller = new AbortController();
106
107
  const timer = setTimeout(() => controller.abort(), params.timeoutMs ?? 30000);
107
108
  try {
108
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/fiscal/submit`, {
109
+ const res = await koolbaseFetch(`${this.config.baseUrl}/v1/sdk/fiscal/submit`, {
109
110
  method: 'POST',
110
111
  headers: await this.buildHeaders(),
111
112
  signal: controller.signal,
@@ -143,7 +144,7 @@ export class KoolbaseFiscal {
143
144
  device_id: params.deviceId,
144
145
  client_ref: params.clientRef,
145
146
  });
146
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/fiscal/status?${q.toString()}`, { method: 'GET', headers: await this.buildHeaders(), signal: controller.signal });
147
+ const res = await koolbaseFetch(`${this.config.baseUrl}/v1/sdk/fiscal/status?${q.toString()}`, { method: 'GET', headers: await this.buildHeaders(), signal: controller.signal });
147
148
  return await this.decode(res);
148
149
  }
149
150
  catch (e) {
@@ -4,7 +4,7 @@ export declare class KoolbaseFlags {
4
4
  private payload;
5
5
  private deviceId;
6
6
  constructor(config: KoolbaseConfig, deviceId: string);
7
- fetch(appVersion: string, platform: string): Promise<void>;
7
+ koolbaseFetch(appVersion: string, platform: string): Promise<void>;
8
8
  isEnabled(key: string): boolean;
9
9
  getString(key: string, fallback?: string): string;
10
10
  getNumber(key: string, fallback?: number): number;
package/dist/esm/flags.js CHANGED
@@ -1,12 +1,13 @@
1
+ import { koolbaseFetch } from './network.js';
1
2
  export class KoolbaseFlags {
2
3
  constructor(config, deviceId) {
3
4
  this.payload = null;
4
5
  this.config = config;
5
6
  this.deviceId = deviceId;
6
7
  }
7
- async fetch(appVersion, platform) {
8
+ async koolbaseFetch(appVersion, platform) {
8
9
  try {
9
- const res = await fetch(`${this.config.baseUrl}/v1/bootstrap?public_key=${this.config.publicKey}&device_id=${this.deviceId}&app_version=${appVersion}&platform=${platform}`);
10
+ const res = await koolbaseFetch(`${this.config.baseUrl}/v1/bootstrap?public_key=${this.config.publicKey}&device_id=${this.deviceId}&app_version=${appVersion}&platform=${platform}`);
10
11
  if (res.ok) {
11
12
  this.payload = await res.json();
12
13
  }
@@ -1,3 +1,4 @@
1
+ import { koolbaseFetch } from './network.js';
1
2
  import { functionInvokeError } from './function-errors.js';
2
3
  import { KoolbaseUnauthenticatedError } from './errors.js';
3
4
  import { FunctionRuntime, } from './types.js';
@@ -10,7 +11,7 @@ export class KoolbaseFunctions {
10
11
  // ─── Deploy ────────────────────────────────────────────────────────────────
11
12
  async deploy(options) {
12
13
  const runtime = options.runtime ?? FunctionRuntime.Deno;
13
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/functions/deploy`, {
14
+ const res = await koolbaseFetch(`${this.config.baseUrl}/v1/sdk/functions/deploy`, {
14
15
  method: 'POST',
15
16
  headers: {
16
17
  'Content-Type': 'application/json',
@@ -54,7 +55,7 @@ export class KoolbaseFunctions {
54
55
  if (userToken) {
55
56
  headers['Authorization'] = `Bearer ${userToken}`;
56
57
  }
57
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/functions/${name}`, {
58
+ const res = await koolbaseFetch(`${this.config.baseUrl}/v1/sdk/functions/${name}`, {
58
59
  method: 'POST',
59
60
  headers,
60
61
  body: JSON.stringify({ body: body ?? {} }),
@@ -1,5 +1,6 @@
1
1
  export * from './types.js';
2
2
  export * from './errors.js';
3
+ export * from './network.js';
3
4
  export * from './aggregate.js';
4
5
  export * from './conflict.js';
5
6
  export * from './pending-write.js';
package/dist/esm/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // code installs the platform package, never this one directly.
6
6
  export * from './types.js';
7
7
  export * from './errors.js';
8
+ export * from './network.js';
8
9
  export * from './aggregate.js';
9
10
  export * from './conflict.js';
10
11
  export * from './pending-write.js';
@@ -1,3 +1,4 @@
1
+ import { koolbaseFetch } from './network.js';
1
2
  // ─── KoolbaseMessaging ────────────────────────────────────────────────────────
2
3
  export class KoolbaseMessaging {
3
4
  constructor(config) {
@@ -10,7 +11,7 @@ export class KoolbaseMessaging {
10
11
  // ─── Register token ───────────────────────────────────────────────────────
11
12
  async registerToken(options) {
12
13
  try {
13
- const response = await fetch(`${this.config.baseUrl}/v1/messaging/register`, {
14
+ const response = await koolbaseFetch(`${this.config.baseUrl}/v1/messaging/register`, {
14
15
  method: 'POST',
15
16
  headers: {
16
17
  'Content-Type': 'application/json',