@koolbase/core 12.14.0 → 12.15.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.
- package/dist/cjs/auth.d.ts +31 -1
- package/dist/cjs/auth.js +167 -20
- package/dist/cjs/database.js +2 -2
- package/dist/cjs/fiscal.js +2 -2
- package/dist/cjs/functions.js +2 -2
- package/dist/cjs/network.d.ts +14 -0
- package/dist/cjs/network.js +60 -0
- package/dist/cjs/storage.js +11 -11
- package/dist/cjs/sync-engine.js +3 -3
- package/dist/cjs/types.d.ts +7 -0
- package/dist/esm/auth.d.ts +31 -1
- package/dist/esm/auth.js +168 -21
- package/dist/esm/database.js +3 -3
- package/dist/esm/fiscal.js +3 -3
- package/dist/esm/functions.js +3 -3
- package/dist/esm/network.d.ts +14 -0
- package/dist/esm/network.js +58 -0
- package/dist/esm/storage.js +12 -12
- package/dist/esm/sync-engine.js +4 -4
- package/dist/esm/types.d.ts +7 -0
- package/package.json +1 -1
package/dist/cjs/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthStateListener, KoolbaseConfig, KoolbaseSession, KoolbaseUser, LinkPhoneParams, LoginParams, OtpSendResult, PhoneVerifyResult, RegisterParams, RestoreResult, KoolbaseAuditPage, KoolbaseSessionInfo, ResendVerificationResult, SendOtpParams, SignUpResult, SignInWithAppleParams, VerifyOtpParams } from './types.js';
|
|
2
|
-
import type { SignInWithGoogleParams } from './types.js';
|
|
2
|
+
import type { SignInWithGoogleParams, SignOutReason } from './types.js';
|
|
3
3
|
export declare class KoolbaseAuth {
|
|
4
4
|
private config;
|
|
5
5
|
private storage;
|
|
@@ -9,6 +9,10 @@ export declare class KoolbaseAuth {
|
|
|
9
9
|
private timeoutMs;
|
|
10
10
|
private ongoingRefresh;
|
|
11
11
|
private listeners;
|
|
12
|
+
/** Why the last session ended (#104); null while signed in or never signed in. */
|
|
13
|
+
private signOutReason;
|
|
14
|
+
/** The last renewal failed for want of a connection, not because the server refused it (#96). */
|
|
15
|
+
private renewalUnavailable;
|
|
12
16
|
constructor(config: KoolbaseConfig);
|
|
13
17
|
/**
|
|
14
18
|
* Subscribe to authentication state changes. The listener fires:
|
|
@@ -82,6 +86,29 @@ export declare class KoolbaseAuth {
|
|
|
82
86
|
* need to.
|
|
83
87
|
*/
|
|
84
88
|
clearStoredSession(): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Why the last session ended (#104): `'signed_out'` when the person signed
|
|
91
|
+
* out (or deleted their account), `'session_ended'` when it ended on its own
|
|
92
|
+
* -- revoked, past renewal, signed out on another device. null while signed
|
|
93
|
+
* in. Read it in an onAuthStateChange listener to say "You were signed out"
|
|
94
|
+
* only when they did not ask to be.
|
|
95
|
+
*/
|
|
96
|
+
get lastSignOutReason(): SignOutReason | null;
|
|
97
|
+
/**
|
|
98
|
+
* A data request carrying `refused` came back unauthenticated. Renews the
|
|
99
|
+
* session and returns the token to send again with, or null when there is
|
|
100
|
+
* none -- nobody is signed in, or the server refused the renewal too, which
|
|
101
|
+
* ends the session (#96).
|
|
102
|
+
*/
|
|
103
|
+
renewRejected(refused: string): Promise<string | null>;
|
|
104
|
+
/**
|
|
105
|
+
* A data request was refused as unauthenticated and renewing could not
|
|
106
|
+
* help: the session is over, so it ends here and every listener hears it.
|
|
107
|
+
* Unless the renewal failed only for want of a connection -- then the
|
|
108
|
+
* session may well be good, and signing someone out for a dropped network
|
|
109
|
+
* would be wrong (#96).
|
|
110
|
+
*/
|
|
111
|
+
sessionRejected(): Promise<void>;
|
|
85
112
|
private clearSessionInternal;
|
|
86
113
|
restoreSession(): Promise<RestoreResult>;
|
|
87
114
|
/**
|
|
@@ -164,6 +191,9 @@ export declare class KoolbaseAuth {
|
|
|
164
191
|
private parseAppleSessionResponse;
|
|
165
192
|
refresh(refreshToken?: string): Promise<KoolbaseSession>;
|
|
166
193
|
private _doRefresh;
|
|
194
|
+
private readStored;
|
|
195
|
+
/** A newer session another tab stored after renewing `spent`, made current; null if none. */
|
|
196
|
+
private adoptNewer;
|
|
167
197
|
logout(): Promise<boolean>;
|
|
168
198
|
forgotPassword(email: string): Promise<void>;
|
|
169
199
|
/**
|
package/dist/cjs/auth.js
CHANGED
|
@@ -11,6 +11,10 @@ class KoolbaseAuth {
|
|
|
11
11
|
this.session = null;
|
|
12
12
|
this.ongoingRefresh = null;
|
|
13
13
|
this.listeners = new Set();
|
|
14
|
+
/** Why the last session ended (#104); null while signed in or never signed in. */
|
|
15
|
+
this.signOutReason = null;
|
|
16
|
+
/** The last renewal failed for want of a connection, not because the server refused it (#96). */
|
|
17
|
+
this.renewalUnavailable = false;
|
|
14
18
|
this.config = config;
|
|
15
19
|
this.metadata = new device_metadata_js_1.DeviceMetadata(config.appVersion);
|
|
16
20
|
this.fetchFn = config.fetch ?? ((url, init) => (0, network_js_1.koolbaseFetch)(url, init));
|
|
@@ -152,6 +156,7 @@ class KoolbaseAuth {
|
|
|
152
156
|
}
|
|
153
157
|
async setSessionInternal(session) {
|
|
154
158
|
this.session = session;
|
|
159
|
+
this.signOutReason = null;
|
|
155
160
|
if (this.storage) {
|
|
156
161
|
try {
|
|
157
162
|
await this.storage.saveSession(session);
|
|
@@ -178,9 +183,52 @@ class KoolbaseAuth {
|
|
|
178
183
|
* need to.
|
|
179
184
|
*/
|
|
180
185
|
async clearStoredSession() {
|
|
181
|
-
await this.clearSessionInternal();
|
|
186
|
+
await this.clearSessionInternal('session_ended');
|
|
182
187
|
}
|
|
183
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Why the last session ended (#104): `'signed_out'` when the person signed
|
|
190
|
+
* out (or deleted their account), `'session_ended'` when it ended on its own
|
|
191
|
+
* -- revoked, past renewal, signed out on another device. null while signed
|
|
192
|
+
* in. Read it in an onAuthStateChange listener to say "You were signed out"
|
|
193
|
+
* only when they did not ask to be.
|
|
194
|
+
*/
|
|
195
|
+
get lastSignOutReason() {
|
|
196
|
+
return this.signOutReason;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A data request carrying `refused` came back unauthenticated. Renews the
|
|
200
|
+
* session and returns the token to send again with, or null when there is
|
|
201
|
+
* none -- nobody is signed in, or the server refused the renewal too, which
|
|
202
|
+
* ends the session (#96).
|
|
203
|
+
*/
|
|
204
|
+
async renewRejected(refused) {
|
|
205
|
+
if (!this.session)
|
|
206
|
+
return null;
|
|
207
|
+
// Another request renewed it while this one was out.
|
|
208
|
+
if (this.session.accessToken !== refused)
|
|
209
|
+
return this.session.accessToken;
|
|
210
|
+
try {
|
|
211
|
+
return (await this.refresh()).accessToken;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* A data request was refused as unauthenticated and renewing could not
|
|
219
|
+
* help: the session is over, so it ends here and every listener hears it.
|
|
220
|
+
* Unless the renewal failed only for want of a connection -- then the
|
|
221
|
+
* session may well be good, and signing someone out for a dropped network
|
|
222
|
+
* would be wrong (#96).
|
|
223
|
+
*/
|
|
224
|
+
async sessionRejected() {
|
|
225
|
+
if (!this.session || this.renewalUnavailable)
|
|
226
|
+
return;
|
|
227
|
+
await this.clearSessionInternal('session_ended');
|
|
228
|
+
}
|
|
229
|
+
async clearSessionInternal(reason = 'session_ended') {
|
|
230
|
+
if (this.session)
|
|
231
|
+
this.signOutReason = reason;
|
|
184
232
|
this.session = null;
|
|
185
233
|
if (this.storage) {
|
|
186
234
|
try {
|
|
@@ -208,7 +256,28 @@ class KoolbaseAuth {
|
|
|
208
256
|
: 0;
|
|
209
257
|
const oneMinuteMs = 60 * 1000;
|
|
210
258
|
if (expiresAt > Date.now() + oneMinuteMs) {
|
|
211
|
-
|
|
259
|
+
// Valid by the clock is not valid: the session may have been signed out
|
|
260
|
+
// on another device, revoked, or the account disabled since. Ask the
|
|
261
|
+
// server before saying it is restored (#104). No answer -- offline, a
|
|
262
|
+
// timeout, a server error -- keeps the optimistic session: Offline.
|
|
263
|
+
try {
|
|
264
|
+
const res = await this.authRequest('/v1/sdk/auth/me', { method: 'GET', includeAuth: true });
|
|
265
|
+
if (res.ok) {
|
|
266
|
+
try {
|
|
267
|
+
await this.updateUserAndPersist(this.mapUser(await res.json()));
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// The session is good; a user that will not parse keeps the stored one.
|
|
271
|
+
}
|
|
272
|
+
return types_js_1.RestoreResult.Restored;
|
|
273
|
+
}
|
|
274
|
+
if (res.status !== 401)
|
|
275
|
+
return types_js_1.RestoreResult.Offline;
|
|
276
|
+
// Refused: the access token is done. The refresh token decides.
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return types_js_1.RestoreResult.Offline;
|
|
280
|
+
}
|
|
212
281
|
}
|
|
213
282
|
try {
|
|
214
283
|
await this.refresh(persisted.refreshToken);
|
|
@@ -224,8 +293,8 @@ class KoolbaseAuth {
|
|
|
224
293
|
// key or the request rather than the user's session — and deleting the
|
|
225
294
|
// refresh token on that reading signs someone out with no way back.
|
|
226
295
|
// Nothing reaches here with it today; it stays out so nothing does.
|
|
227
|
-
if (e
|
|
228
|
-
await this.clearSessionInternal();
|
|
296
|
+
if (isRejection(e)) {
|
|
297
|
+
await this.clearSessionInternal('session_ended');
|
|
229
298
|
return types_js_1.RestoreResult.Expired;
|
|
230
299
|
}
|
|
231
300
|
return types_js_1.RestoreResult.Offline;
|
|
@@ -498,17 +567,77 @@ class KoolbaseAuth {
|
|
|
498
567
|
return promise;
|
|
499
568
|
}
|
|
500
569
|
async _doRefresh(refreshToken) {
|
|
501
|
-
|
|
570
|
+
// The newest refresh token is the stored one: another tab of the same app
|
|
571
|
+
// may have renewed the session, and its in-memory copy here is spent.
|
|
572
|
+
const stored = refreshToken ? null : await this.readStored();
|
|
573
|
+
const token = refreshToken ?? stored?.refreshToken ?? this.session?.refreshToken;
|
|
502
574
|
if (!token) {
|
|
503
575
|
throw new auth_errors_js_1.SessionExpiredError();
|
|
504
576
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
577
|
+
try {
|
|
578
|
+
const res = await this.authRequest('/v1/sdk/auth/refresh', {
|
|
579
|
+
method: 'POST',
|
|
580
|
+
body: { refresh_token: token },
|
|
581
|
+
});
|
|
582
|
+
const session = await this.parseSessionResponse(res, true);
|
|
583
|
+
this.renewalUnavailable = false;
|
|
584
|
+
await this.setSessionInternal(session);
|
|
585
|
+
return session;
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
if (!isRejection(e)) {
|
|
589
|
+
// No answer, or a server error: the session may be fine. Keep it.
|
|
590
|
+
this.renewalUnavailable = true;
|
|
591
|
+
throw e;
|
|
592
|
+
}
|
|
593
|
+
this.renewalUnavailable = false;
|
|
594
|
+
// Two tabs renewing at the same moment send the same token; the
|
|
595
|
+
// server renews one and refuses the other. The winner's session is in
|
|
596
|
+
// storage: take it rather than signing out (#96).
|
|
597
|
+
const adopted = await this.adoptNewer(token);
|
|
598
|
+
if (adopted)
|
|
599
|
+
return adopted;
|
|
600
|
+
await this.clearSessionInternal('session_ended');
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async readStored() {
|
|
605
|
+
if (!this.storage)
|
|
606
|
+
return null;
|
|
607
|
+
try {
|
|
608
|
+
return await this.storage.readSession();
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/** A newer session another tab stored after renewing `spent`, made current; null if none. */
|
|
615
|
+
async adoptNewer(spent) {
|
|
616
|
+
for (const wait of [0, 400]) {
|
|
617
|
+
if (wait)
|
|
618
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
619
|
+
const stored = await this.readStored();
|
|
620
|
+
if (!stored || stored.refreshToken === spent)
|
|
621
|
+
continue;
|
|
622
|
+
const expiresAt = stored.expiresAt ? new Date(stored.expiresAt).getTime() : 0;
|
|
623
|
+
if (expiresAt > Date.now() + 60 * 1000) {
|
|
624
|
+
await this.setSessionInternal(stored);
|
|
625
|
+
return stored;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
const res = await this.authRequest('/v1/sdk/auth/refresh', {
|
|
629
|
+
method: 'POST',
|
|
630
|
+
body: { refresh_token: stored.refreshToken },
|
|
631
|
+
});
|
|
632
|
+
const session = await this.parseSessionResponse(res, true);
|
|
633
|
+
await this.setSessionInternal(session);
|
|
634
|
+
return session;
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return null;
|
|
512
641
|
}
|
|
513
642
|
async logout() {
|
|
514
643
|
let serverSucceeded = true;
|
|
@@ -529,7 +658,7 @@ class KoolbaseAuth {
|
|
|
529
658
|
serverSucceeded = false;
|
|
530
659
|
}
|
|
531
660
|
finally {
|
|
532
|
-
await this.clearSessionInternal();
|
|
661
|
+
await this.clearSessionInternal('signed_out');
|
|
533
662
|
}
|
|
534
663
|
return serverSucceeded;
|
|
535
664
|
}
|
|
@@ -692,7 +821,7 @@ class KoolbaseAuth {
|
|
|
692
821
|
includeAuth: true,
|
|
693
822
|
});
|
|
694
823
|
await this.checkResponse(res);
|
|
695
|
-
await this.clearSessionInternal();
|
|
824
|
+
await this.clearSessionInternal('signed_out');
|
|
696
825
|
}
|
|
697
826
|
/**
|
|
698
827
|
* Every session the signed-in user has — a "where you're signed in" list.
|
|
@@ -815,8 +944,14 @@ class KoolbaseAuth {
|
|
|
815
944
|
try {
|
|
816
945
|
return await this._ensureValidToken();
|
|
817
946
|
}
|
|
818
|
-
catch {
|
|
819
|
-
|
|
947
|
+
catch (e) {
|
|
948
|
+
// Refused: the session has ended (and been cleared); go api-key-only.
|
|
949
|
+
if (isRejection(e))
|
|
950
|
+
return null;
|
|
951
|
+
// No answer: the session may be fine, so the request must not go out
|
|
952
|
+
// as a signed-out visitor (whose refusal would end it). It fails as
|
|
953
|
+
// the network failure it is, and the caller retries (#96).
|
|
954
|
+
throw e;
|
|
820
955
|
}
|
|
821
956
|
}
|
|
822
957
|
async setSession(session) {
|
|
@@ -1064,9 +1199,9 @@ class KoolbaseAuth {
|
|
|
1064
1199
|
return session.accessToken;
|
|
1065
1200
|
}
|
|
1066
1201
|
catch (e) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
throw
|
|
1202
|
+
// A refusal stays a refusal; a network failure stays one too, rather
|
|
1203
|
+
// than being reported as an ended session (#96).
|
|
1204
|
+
throw e;
|
|
1070
1205
|
}
|
|
1071
1206
|
}
|
|
1072
1207
|
mapUser(raw) {
|
|
@@ -1314,3 +1449,15 @@ class KoolbaseAuth {
|
|
|
1314
1449
|
}
|
|
1315
1450
|
}
|
|
1316
1451
|
exports.KoolbaseAuth = KoolbaseAuth;
|
|
1452
|
+
/**
|
|
1453
|
+
* Whether an error is the server refusing the session -- it ended -- rather
|
|
1454
|
+
* than no answer at all (#96). Only a refusal signs anyone out.
|
|
1455
|
+
*/
|
|
1456
|
+
function isRejection(e) {
|
|
1457
|
+
if (e instanceof auth_errors_js_1.SessionExpiredError || e instanceof auth_errors_js_1.TokenRevokedError || e instanceof auth_errors_js_1.UserDisabledError)
|
|
1458
|
+
return true;
|
|
1459
|
+
if (e instanceof auth_errors_js_1.KoolbaseAuthError) {
|
|
1460
|
+
return ['invalid_refresh_token', 'token_revoked', 'account_disabled', 'mfa_session_insufficient', 'unauthenticated'].includes(e.code ?? '');
|
|
1461
|
+
}
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
package/dist/cjs/database.js
CHANGED
|
@@ -97,7 +97,7 @@ class KoolbaseDatabase {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
async request(method, path, body) {
|
|
100
|
-
const res = await (0, network_js_1.
|
|
100
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}${path}`, {
|
|
101
101
|
method,
|
|
102
102
|
headers: await this.buildHeaders(),
|
|
103
103
|
body: body ? JSON.stringify(body) : undefined,
|
|
@@ -135,7 +135,7 @@ class KoolbaseDatabase {
|
|
|
135
135
|
* none of them clearing a rejected session. One path, two shapes of result.
|
|
136
136
|
*/
|
|
137
137
|
async requestWithStatus(method, path, body) {
|
|
138
|
-
const res = await (0, network_js_1.
|
|
138
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}${path}`, {
|
|
139
139
|
method,
|
|
140
140
|
headers: await this.buildHeaders(),
|
|
141
141
|
body: body ? JSON.stringify(body) : undefined,
|
package/dist/cjs/fiscal.js
CHANGED
|
@@ -113,7 +113,7 @@ class KoolbaseFiscal {
|
|
|
113
113
|
const controller = new AbortController();
|
|
114
114
|
const timer = setTimeout(() => controller.abort(), params.timeoutMs ?? 30000);
|
|
115
115
|
try {
|
|
116
|
-
const res = await (0, network_js_1.
|
|
116
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/fiscal/submit`, {
|
|
117
117
|
method: 'POST',
|
|
118
118
|
headers: await this.buildHeaders(),
|
|
119
119
|
signal: controller.signal,
|
|
@@ -151,7 +151,7 @@ class KoolbaseFiscal {
|
|
|
151
151
|
device_id: params.deviceId,
|
|
152
152
|
client_ref: params.clientRef,
|
|
153
153
|
});
|
|
154
|
-
const res = await (0, network_js_1.
|
|
154
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/fiscal/status?${q.toString()}`, { method: 'GET', headers: await this.buildHeaders(), signal: controller.signal });
|
|
155
155
|
return await this.decode(res);
|
|
156
156
|
}
|
|
157
157
|
catch (e) {
|
package/dist/cjs/functions.js
CHANGED
|
@@ -14,7 +14,7 @@ class KoolbaseFunctions {
|
|
|
14
14
|
// ─── Deploy ────────────────────────────────────────────────────────────────
|
|
15
15
|
async deploy(options) {
|
|
16
16
|
const runtime = options.runtime ?? types_js_1.FunctionRuntime.Deno;
|
|
17
|
-
const res = await (0, network_js_1.
|
|
17
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/functions/deploy`, {
|
|
18
18
|
method: 'POST',
|
|
19
19
|
headers: {
|
|
20
20
|
'Content-Type': 'application/json',
|
|
@@ -58,7 +58,7 @@ class KoolbaseFunctions {
|
|
|
58
58
|
if (userToken) {
|
|
59
59
|
headers['Authorization'] = `Bearer ${userToken}`;
|
|
60
60
|
}
|
|
61
|
-
const res = await (0, network_js_1.
|
|
61
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/functions/${name}`, {
|
|
62
62
|
method: 'POST',
|
|
63
63
|
headers,
|
|
64
64
|
body: JSON.stringify({ body: body ?? {} }),
|
package/dist/cjs/network.d.ts
CHANGED
|
@@ -19,3 +19,17 @@ export declare class KoolbaseNetworkError extends TypeError {
|
|
|
19
19
|
}
|
|
20
20
|
/** fetch, with network failures turned into a KoolbaseNetworkError. */
|
|
21
21
|
export declare function koolbaseFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
22
|
+
type Renewal = (refusedToken: string) => Promise<string | null>;
|
|
23
|
+
/** Installed by Koolbase.initialize: renews the session a request was refused with. */
|
|
24
|
+
export declare function setSessionRenewal(fn: Renewal | null): void;
|
|
25
|
+
/**
|
|
26
|
+
* koolbaseFetch for a data request. A request refused as unauthenticated
|
|
27
|
+
* (401) while carrying the signed-in person's token is the session needing
|
|
28
|
+
* renewal, not ending: the access token expired early, the clock is off, or
|
|
29
|
+
* another tab renewed it. The session is renewed once and the request sent
|
|
30
|
+
* again with the new token, so the caller never sees the first refusal.
|
|
31
|
+
* Only when renewal is refused does the 401 reach the caller, and with it the
|
|
32
|
+
* client's onSessionExpired, which ends the session.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sessionFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
35
|
+
export {};
|
package/dist/cjs/network.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.KoolbaseNetworkError = void 0;
|
|
4
4
|
exports.koolbaseFetch = koolbaseFetch;
|
|
5
|
+
exports.setSessionRenewal = setSessionRenewal;
|
|
6
|
+
exports.sessionFetch = sessionFetch;
|
|
5
7
|
/**
|
|
6
8
|
* Network failures, made actionable.
|
|
7
9
|
*
|
|
@@ -52,3 +54,61 @@ async function koolbaseFetch(input, init) {
|
|
|
52
54
|
throw new KoolbaseNetworkError(input, e);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
57
|
+
let renewal = null;
|
|
58
|
+
/** Installed by Koolbase.initialize: renews the session a request was refused with. */
|
|
59
|
+
function setSessionRenewal(fn) {
|
|
60
|
+
renewal = fn;
|
|
61
|
+
}
|
|
62
|
+
function headerOf(headers, name) {
|
|
63
|
+
if (!headers)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (typeof headers.get === 'function')
|
|
66
|
+
return headers.get(name) ?? undefined;
|
|
67
|
+
if (Array.isArray(headers))
|
|
68
|
+
return headers.find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
|
|
69
|
+
const rec = headers;
|
|
70
|
+
const key = Object.keys(rec).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
71
|
+
return key ? rec[key] : undefined;
|
|
72
|
+
}
|
|
73
|
+
function withHeader(headers, name, value) {
|
|
74
|
+
const out = {};
|
|
75
|
+
if (headers && typeof headers.forEach === 'function' && !Array.isArray(headers)) {
|
|
76
|
+
headers.forEach((v, k) => { out[k] = v; });
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(headers)) {
|
|
79
|
+
for (const [k, v] of headers)
|
|
80
|
+
out[k] = v;
|
|
81
|
+
}
|
|
82
|
+
else if (headers) {
|
|
83
|
+
Object.assign(out, headers);
|
|
84
|
+
}
|
|
85
|
+
for (const k of Object.keys(out))
|
|
86
|
+
if (k.toLowerCase() === name.toLowerCase())
|
|
87
|
+
delete out[k];
|
|
88
|
+
out[name] = value;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* koolbaseFetch for a data request. A request refused as unauthenticated
|
|
93
|
+
* (401) while carrying the signed-in person's token is the session needing
|
|
94
|
+
* renewal, not ending: the access token expired early, the clock is off, or
|
|
95
|
+
* another tab renewed it. The session is renewed once and the request sent
|
|
96
|
+
* again with the new token, so the caller never sees the first refusal.
|
|
97
|
+
* Only when renewal is refused does the 401 reach the caller, and with it the
|
|
98
|
+
* client's onSessionExpired, which ends the session.
|
|
99
|
+
*/
|
|
100
|
+
async function sessionFetch(input, init) {
|
|
101
|
+
const res = await koolbaseFetch(input, init);
|
|
102
|
+
if (res.status !== 401 || !renewal)
|
|
103
|
+
return res;
|
|
104
|
+
const auth = headerOf(init?.headers, 'Authorization');
|
|
105
|
+
if (!auth || !auth.startsWith('Bearer '))
|
|
106
|
+
return res;
|
|
107
|
+
// Only a body that can be sent twice. Data requests send JSON text.
|
|
108
|
+
if (init?.body !== undefined && init.body !== null && typeof init.body !== 'string')
|
|
109
|
+
return res;
|
|
110
|
+
const fresh = await renewal(auth.slice('Bearer '.length));
|
|
111
|
+
if (!fresh)
|
|
112
|
+
return res;
|
|
113
|
+
return koolbaseFetch(input, { ...init, headers: withHeader(init?.headers, 'Authorization', `Bearer ${fresh}`) });
|
|
114
|
+
}
|
package/dist/cjs/storage.js
CHANGED
|
@@ -101,7 +101,7 @@ class KoolbaseStorage {
|
|
|
101
101
|
? options.file.type || 'application/octet-stream'
|
|
102
102
|
: options.file.type;
|
|
103
103
|
// ─── Step 1: Get presigned upload URL ───
|
|
104
|
-
const urlRes = await (0, network_js_1.
|
|
104
|
+
const urlRes = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
|
|
105
105
|
method: 'POST',
|
|
106
106
|
headers: {
|
|
107
107
|
...(await this.buildHeaders()),
|
|
@@ -127,9 +127,9 @@ class KoolbaseStorage {
|
|
|
127
127
|
// the two hosts differ here and nowhere else in this method.
|
|
128
128
|
const fileBlob = isBlob
|
|
129
129
|
? options.file
|
|
130
|
-
: await (await (0, network_js_1.
|
|
130
|
+
: await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
|
|
131
131
|
const fileSize = fileBlob.size;
|
|
132
|
-
const uploadRes = await (0, network_js_1.
|
|
132
|
+
const uploadRes = await (0, network_js_1.sessionFetch)(upload_url, {
|
|
133
133
|
method: 'PUT',
|
|
134
134
|
headers: { 'Content-Type': contentType },
|
|
135
135
|
body: fileBlob,
|
|
@@ -156,7 +156,7 @@ class KoolbaseStorage {
|
|
|
156
156
|
if (options.metadata !== undefined) {
|
|
157
157
|
confirmBody.metadata = options.metadata;
|
|
158
158
|
}
|
|
159
|
-
const confirmRes = await (0, network_js_1.
|
|
159
|
+
const confirmRes = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
|
|
160
160
|
method: 'POST',
|
|
161
161
|
headers: {
|
|
162
162
|
...(await this.buildHeaders()),
|
|
@@ -207,7 +207,7 @@ class KoolbaseStorage {
|
|
|
207
207
|
* // -> { category: 'landscape', tag: 'sunset' }
|
|
208
208
|
*/
|
|
209
209
|
async updateMetadata(bucket, path, metadata) {
|
|
210
|
-
const res = await (0, network_js_1.
|
|
210
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/objects/metadata`, {
|
|
211
211
|
method: 'PATCH',
|
|
212
212
|
headers: {
|
|
213
213
|
...(await this.buildHeaders()),
|
|
@@ -230,7 +230,7 @@ class KoolbaseStorage {
|
|
|
230
230
|
if (versionId) {
|
|
231
231
|
url += `&version_id=${encodeURIComponent(versionId)}`;
|
|
232
232
|
}
|
|
233
|
-
const res = await (0, network_js_1.
|
|
233
|
+
const res = await (0, network_js_1.sessionFetch)(url, { headers: await this.buildHeaders() });
|
|
234
234
|
if (!res.ok) {
|
|
235
235
|
throw await this.error(res, 'Failed to get download URL');
|
|
236
236
|
}
|
|
@@ -342,7 +342,7 @@ class KoolbaseStorage {
|
|
|
342
342
|
const url = forcePurge
|
|
343
343
|
? `${this.config.baseUrl}/v1/sdk/storage/object?force_purge=true`
|
|
344
344
|
: `${this.config.baseUrl}/v1/sdk/storage/object`;
|
|
345
|
-
const res = await (0, network_js_1.
|
|
345
|
+
const res = await (0, network_js_1.sessionFetch)(url, {
|
|
346
346
|
method: 'DELETE',
|
|
347
347
|
headers: {
|
|
348
348
|
...(await this.buildHeaders()),
|
|
@@ -369,7 +369,7 @@ class KoolbaseStorage {
|
|
|
369
369
|
async listVersions(bucket, path) {
|
|
370
370
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions` +
|
|
371
371
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
372
|
-
const res = await (0, network_js_1.
|
|
372
|
+
const res = await (0, network_js_1.sessionFetch)(url, { headers: await this.buildHeaders() });
|
|
373
373
|
if (!res.ok) {
|
|
374
374
|
throw await this.error(res, 'Failed to list versions');
|
|
375
375
|
}
|
|
@@ -385,7 +385,7 @@ class KoolbaseStorage {
|
|
|
385
385
|
async getVersion(bucket, path, versionId) {
|
|
386
386
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
|
|
387
387
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
388
|
-
const res = await (0, network_js_1.
|
|
388
|
+
const res = await (0, network_js_1.sessionFetch)(url, { headers: await this.buildHeaders() });
|
|
389
389
|
if (!res.ok) {
|
|
390
390
|
throw await this.error(res, 'Failed to fetch version');
|
|
391
391
|
}
|
|
@@ -404,7 +404,7 @@ class KoolbaseStorage {
|
|
|
404
404
|
async restoreVersion(bucket, path, versionId) {
|
|
405
405
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}/restore` +
|
|
406
406
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
407
|
-
const res = await (0, network_js_1.
|
|
407
|
+
const res = await (0, network_js_1.sessionFetch)(url, {
|
|
408
408
|
method: 'POST',
|
|
409
409
|
headers: await this.buildHeaders(),
|
|
410
410
|
});
|
|
@@ -422,7 +422,7 @@ class KoolbaseStorage {
|
|
|
422
422
|
async purgeVersion(bucket, path, versionId) {
|
|
423
423
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
|
|
424
424
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
425
|
-
const res = await (0, network_js_1.
|
|
425
|
+
const res = await (0, network_js_1.sessionFetch)(url, {
|
|
426
426
|
method: 'DELETE',
|
|
427
427
|
headers: await this.buildHeaders(),
|
|
428
428
|
});
|
package/dist/cjs/sync-engine.js
CHANGED
|
@@ -259,7 +259,7 @@ class SyncEngine {
|
|
|
259
259
|
: `${this.config.baseUrl}/v1/sdk/db/records/${write.recordId}`;
|
|
260
260
|
let res;
|
|
261
261
|
if (write.operation === 'insert') {
|
|
262
|
-
res = await (0, network_js_1.
|
|
262
|
+
res = await (0, network_js_1.sessionFetch)(url, {
|
|
263
263
|
method: 'POST',
|
|
264
264
|
headers,
|
|
265
265
|
// The write's own id: generated at enqueue, identical on every retry.
|
|
@@ -273,7 +273,7 @@ class SyncEngine {
|
|
|
273
273
|
});
|
|
274
274
|
}
|
|
275
275
|
else if (write.operation === 'update') {
|
|
276
|
-
res = await (0, network_js_1.
|
|
276
|
+
res = await (0, network_js_1.sessionFetch)(url, {
|
|
277
277
|
method: 'PATCH',
|
|
278
278
|
headers,
|
|
279
279
|
// The revision the change was composed against. The server applies it
|
|
@@ -292,7 +292,7 @@ class SyncEngine {
|
|
|
292
292
|
const q = write.baseRevision !== undefined
|
|
293
293
|
? `?expected_revision=${write.baseRevision}`
|
|
294
294
|
: '';
|
|
295
|
-
res = await (0, network_js_1.
|
|
295
|
+
res = await (0, network_js_1.sessionFetch)(`${url}${q}`, { method: 'DELETE', headers });
|
|
296
296
|
}
|
|
297
297
|
if (res.status === 401)
|
|
298
298
|
throw new errors_js_1.KoolbaseUnauthenticatedError('unauthorized');
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -180,6 +180,13 @@ export declare enum RestoreResult {
|
|
|
180
180
|
* broken listener cannot break propagation to others.
|
|
181
181
|
*/
|
|
182
182
|
export type AuthStateListener = (user: KoolbaseUser | null) => void;
|
|
183
|
+
/**
|
|
184
|
+
* Why the last session ended (#104): the person signed out, or the session
|
|
185
|
+
* ended on its own -- revoked, expired past renewal, signed out elsewhere.
|
|
186
|
+
* An app tells them apart to say "You were signed out" only when they did
|
|
187
|
+
* not ask to be.
|
|
188
|
+
*/
|
|
189
|
+
export type SignOutReason = 'signed_out' | 'session_ended';
|
|
183
190
|
/**
|
|
184
191
|
* Drop-in replacement for the global `fetch` function. Inject via
|
|
185
192
|
* KoolbaseConfig.fetch for testing (mock fetch), proxying, or
|
package/dist/esm/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthStateListener, KoolbaseConfig, KoolbaseSession, KoolbaseUser, LinkPhoneParams, LoginParams, OtpSendResult, PhoneVerifyResult, RegisterParams, RestoreResult, KoolbaseAuditPage, KoolbaseSessionInfo, ResendVerificationResult, SendOtpParams, SignUpResult, SignInWithAppleParams, VerifyOtpParams } from './types.js';
|
|
2
|
-
import type { SignInWithGoogleParams } from './types.js';
|
|
2
|
+
import type { SignInWithGoogleParams, SignOutReason } from './types.js';
|
|
3
3
|
export declare class KoolbaseAuth {
|
|
4
4
|
private config;
|
|
5
5
|
private storage;
|
|
@@ -9,6 +9,10 @@ export declare class KoolbaseAuth {
|
|
|
9
9
|
private timeoutMs;
|
|
10
10
|
private ongoingRefresh;
|
|
11
11
|
private listeners;
|
|
12
|
+
/** Why the last session ended (#104); null while signed in or never signed in. */
|
|
13
|
+
private signOutReason;
|
|
14
|
+
/** The last renewal failed for want of a connection, not because the server refused it (#96). */
|
|
15
|
+
private renewalUnavailable;
|
|
12
16
|
constructor(config: KoolbaseConfig);
|
|
13
17
|
/**
|
|
14
18
|
* Subscribe to authentication state changes. The listener fires:
|
|
@@ -82,6 +86,29 @@ export declare class KoolbaseAuth {
|
|
|
82
86
|
* need to.
|
|
83
87
|
*/
|
|
84
88
|
clearStoredSession(): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Why the last session ended (#104): `'signed_out'` when the person signed
|
|
91
|
+
* out (or deleted their account), `'session_ended'` when it ended on its own
|
|
92
|
+
* -- revoked, past renewal, signed out on another device. null while signed
|
|
93
|
+
* in. Read it in an onAuthStateChange listener to say "You were signed out"
|
|
94
|
+
* only when they did not ask to be.
|
|
95
|
+
*/
|
|
96
|
+
get lastSignOutReason(): SignOutReason | null;
|
|
97
|
+
/**
|
|
98
|
+
* A data request carrying `refused` came back unauthenticated. Renews the
|
|
99
|
+
* session and returns the token to send again with, or null when there is
|
|
100
|
+
* none -- nobody is signed in, or the server refused the renewal too, which
|
|
101
|
+
* ends the session (#96).
|
|
102
|
+
*/
|
|
103
|
+
renewRejected(refused: string): Promise<string | null>;
|
|
104
|
+
/**
|
|
105
|
+
* A data request was refused as unauthenticated and renewing could not
|
|
106
|
+
* help: the session is over, so it ends here and every listener hears it.
|
|
107
|
+
* Unless the renewal failed only for want of a connection -- then the
|
|
108
|
+
* session may well be good, and signing someone out for a dropped network
|
|
109
|
+
* would be wrong (#96).
|
|
110
|
+
*/
|
|
111
|
+
sessionRejected(): Promise<void>;
|
|
85
112
|
private clearSessionInternal;
|
|
86
113
|
restoreSession(): Promise<RestoreResult>;
|
|
87
114
|
/**
|
|
@@ -164,6 +191,9 @@ export declare class KoolbaseAuth {
|
|
|
164
191
|
private parseAppleSessionResponse;
|
|
165
192
|
refresh(refreshToken?: string): Promise<KoolbaseSession>;
|
|
166
193
|
private _doRefresh;
|
|
194
|
+
private readStored;
|
|
195
|
+
/** A newer session another tab stored after renewing `spent`, made current; null if none. */
|
|
196
|
+
private adoptNewer;
|
|
167
197
|
logout(): Promise<boolean>;
|
|
168
198
|
forgotPassword(email: string): Promise<void>;
|
|
169
199
|
/**
|
package/dist/esm/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { koolbaseFetch } from './network.js';
|
|
2
2
|
import { RestoreResult, } from './types.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
|
+
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, TokenRevokedError, UnlockTokenInvalidError, UserDisabledError, WeakPasswordError, AppleEmailRequiredError, AppleSignInNotConfiguredError, InvalidAppleTokenError, OAuthEmailConflictError, GoogleEmailRequiredError, GoogleSignInNotConfiguredError, InvalidGoogleTokenError, EmailCodeDisabledError, MfaRequiredError, RecentAuthRequiredError, RecentMfaRequiredError, MfaAlreadyEnabledError, MfaEnrollmentNotFoundError, MfaNotEnabledError, } from './auth-errors.js';
|
|
4
4
|
import { getPlatform } from './platform.js';
|
|
5
5
|
import { DeviceMetadata } from './device-metadata.js';
|
|
6
6
|
export class KoolbaseAuth {
|
|
@@ -8,6 +8,10 @@ export class KoolbaseAuth {
|
|
|
8
8
|
this.session = null;
|
|
9
9
|
this.ongoingRefresh = null;
|
|
10
10
|
this.listeners = new Set();
|
|
11
|
+
/** Why the last session ended (#104); null while signed in or never signed in. */
|
|
12
|
+
this.signOutReason = null;
|
|
13
|
+
/** The last renewal failed for want of a connection, not because the server refused it (#96). */
|
|
14
|
+
this.renewalUnavailable = false;
|
|
11
15
|
this.config = config;
|
|
12
16
|
this.metadata = new DeviceMetadata(config.appVersion);
|
|
13
17
|
this.fetchFn = config.fetch ?? ((url, init) => koolbaseFetch(url, init));
|
|
@@ -149,6 +153,7 @@ export class KoolbaseAuth {
|
|
|
149
153
|
}
|
|
150
154
|
async setSessionInternal(session) {
|
|
151
155
|
this.session = session;
|
|
156
|
+
this.signOutReason = null;
|
|
152
157
|
if (this.storage) {
|
|
153
158
|
try {
|
|
154
159
|
await this.storage.saveSession(session);
|
|
@@ -175,9 +180,52 @@ export class KoolbaseAuth {
|
|
|
175
180
|
* need to.
|
|
176
181
|
*/
|
|
177
182
|
async clearStoredSession() {
|
|
178
|
-
await this.clearSessionInternal();
|
|
183
|
+
await this.clearSessionInternal('session_ended');
|
|
179
184
|
}
|
|
180
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Why the last session ended (#104): `'signed_out'` when the person signed
|
|
187
|
+
* out (or deleted their account), `'session_ended'` when it ended on its own
|
|
188
|
+
* -- revoked, past renewal, signed out on another device. null while signed
|
|
189
|
+
* in. Read it in an onAuthStateChange listener to say "You were signed out"
|
|
190
|
+
* only when they did not ask to be.
|
|
191
|
+
*/
|
|
192
|
+
get lastSignOutReason() {
|
|
193
|
+
return this.signOutReason;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A data request carrying `refused` came back unauthenticated. Renews the
|
|
197
|
+
* session and returns the token to send again with, or null when there is
|
|
198
|
+
* none -- nobody is signed in, or the server refused the renewal too, which
|
|
199
|
+
* ends the session (#96).
|
|
200
|
+
*/
|
|
201
|
+
async renewRejected(refused) {
|
|
202
|
+
if (!this.session)
|
|
203
|
+
return null;
|
|
204
|
+
// Another request renewed it while this one was out.
|
|
205
|
+
if (this.session.accessToken !== refused)
|
|
206
|
+
return this.session.accessToken;
|
|
207
|
+
try {
|
|
208
|
+
return (await this.refresh()).accessToken;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A data request was refused as unauthenticated and renewing could not
|
|
216
|
+
* help: the session is over, so it ends here and every listener hears it.
|
|
217
|
+
* Unless the renewal failed only for want of a connection -- then the
|
|
218
|
+
* session may well be good, and signing someone out for a dropped network
|
|
219
|
+
* would be wrong (#96).
|
|
220
|
+
*/
|
|
221
|
+
async sessionRejected() {
|
|
222
|
+
if (!this.session || this.renewalUnavailable)
|
|
223
|
+
return;
|
|
224
|
+
await this.clearSessionInternal('session_ended');
|
|
225
|
+
}
|
|
226
|
+
async clearSessionInternal(reason = 'session_ended') {
|
|
227
|
+
if (this.session)
|
|
228
|
+
this.signOutReason = reason;
|
|
181
229
|
this.session = null;
|
|
182
230
|
if (this.storage) {
|
|
183
231
|
try {
|
|
@@ -205,7 +253,28 @@ export class KoolbaseAuth {
|
|
|
205
253
|
: 0;
|
|
206
254
|
const oneMinuteMs = 60 * 1000;
|
|
207
255
|
if (expiresAt > Date.now() + oneMinuteMs) {
|
|
208
|
-
|
|
256
|
+
// Valid by the clock is not valid: the session may have been signed out
|
|
257
|
+
// on another device, revoked, or the account disabled since. Ask the
|
|
258
|
+
// server before saying it is restored (#104). No answer -- offline, a
|
|
259
|
+
// timeout, a server error -- keeps the optimistic session: Offline.
|
|
260
|
+
try {
|
|
261
|
+
const res = await this.authRequest('/v1/sdk/auth/me', { method: 'GET', includeAuth: true });
|
|
262
|
+
if (res.ok) {
|
|
263
|
+
try {
|
|
264
|
+
await this.updateUserAndPersist(this.mapUser(await res.json()));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// The session is good; a user that will not parse keeps the stored one.
|
|
268
|
+
}
|
|
269
|
+
return RestoreResult.Restored;
|
|
270
|
+
}
|
|
271
|
+
if (res.status !== 401)
|
|
272
|
+
return RestoreResult.Offline;
|
|
273
|
+
// Refused: the access token is done. The refresh token decides.
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return RestoreResult.Offline;
|
|
277
|
+
}
|
|
209
278
|
}
|
|
210
279
|
try {
|
|
211
280
|
await this.refresh(persisted.refreshToken);
|
|
@@ -221,8 +290,8 @@ export class KoolbaseAuth {
|
|
|
221
290
|
// key or the request rather than the user's session — and deleting the
|
|
222
291
|
// refresh token on that reading signs someone out with no way back.
|
|
223
292
|
// Nothing reaches here with it today; it stays out so nothing does.
|
|
224
|
-
if (e
|
|
225
|
-
await this.clearSessionInternal();
|
|
293
|
+
if (isRejection(e)) {
|
|
294
|
+
await this.clearSessionInternal('session_ended');
|
|
226
295
|
return RestoreResult.Expired;
|
|
227
296
|
}
|
|
228
297
|
return RestoreResult.Offline;
|
|
@@ -495,17 +564,77 @@ export class KoolbaseAuth {
|
|
|
495
564
|
return promise;
|
|
496
565
|
}
|
|
497
566
|
async _doRefresh(refreshToken) {
|
|
498
|
-
|
|
567
|
+
// The newest refresh token is the stored one: another tab of the same app
|
|
568
|
+
// may have renewed the session, and its in-memory copy here is spent.
|
|
569
|
+
const stored = refreshToken ? null : await this.readStored();
|
|
570
|
+
const token = refreshToken ?? stored?.refreshToken ?? this.session?.refreshToken;
|
|
499
571
|
if (!token) {
|
|
500
572
|
throw new SessionExpiredError();
|
|
501
573
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
574
|
+
try {
|
|
575
|
+
const res = await this.authRequest('/v1/sdk/auth/refresh', {
|
|
576
|
+
method: 'POST',
|
|
577
|
+
body: { refresh_token: token },
|
|
578
|
+
});
|
|
579
|
+
const session = await this.parseSessionResponse(res, true);
|
|
580
|
+
this.renewalUnavailable = false;
|
|
581
|
+
await this.setSessionInternal(session);
|
|
582
|
+
return session;
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
if (!isRejection(e)) {
|
|
586
|
+
// No answer, or a server error: the session may be fine. Keep it.
|
|
587
|
+
this.renewalUnavailable = true;
|
|
588
|
+
throw e;
|
|
589
|
+
}
|
|
590
|
+
this.renewalUnavailable = false;
|
|
591
|
+
// Two tabs renewing at the same moment send the same token; the
|
|
592
|
+
// server renews one and refuses the other. The winner's session is in
|
|
593
|
+
// storage: take it rather than signing out (#96).
|
|
594
|
+
const adopted = await this.adoptNewer(token);
|
|
595
|
+
if (adopted)
|
|
596
|
+
return adopted;
|
|
597
|
+
await this.clearSessionInternal('session_ended');
|
|
598
|
+
throw e;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async readStored() {
|
|
602
|
+
if (!this.storage)
|
|
603
|
+
return null;
|
|
604
|
+
try {
|
|
605
|
+
return await this.storage.readSession();
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/** A newer session another tab stored after renewing `spent`, made current; null if none. */
|
|
612
|
+
async adoptNewer(spent) {
|
|
613
|
+
for (const wait of [0, 400]) {
|
|
614
|
+
if (wait)
|
|
615
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
616
|
+
const stored = await this.readStored();
|
|
617
|
+
if (!stored || stored.refreshToken === spent)
|
|
618
|
+
continue;
|
|
619
|
+
const expiresAt = stored.expiresAt ? new Date(stored.expiresAt).getTime() : 0;
|
|
620
|
+
if (expiresAt > Date.now() + 60 * 1000) {
|
|
621
|
+
await this.setSessionInternal(stored);
|
|
622
|
+
return stored;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
const res = await this.authRequest('/v1/sdk/auth/refresh', {
|
|
626
|
+
method: 'POST',
|
|
627
|
+
body: { refresh_token: stored.refreshToken },
|
|
628
|
+
});
|
|
629
|
+
const session = await this.parseSessionResponse(res, true);
|
|
630
|
+
await this.setSessionInternal(session);
|
|
631
|
+
return session;
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
509
638
|
}
|
|
510
639
|
async logout() {
|
|
511
640
|
let serverSucceeded = true;
|
|
@@ -526,7 +655,7 @@ export class KoolbaseAuth {
|
|
|
526
655
|
serverSucceeded = false;
|
|
527
656
|
}
|
|
528
657
|
finally {
|
|
529
|
-
await this.clearSessionInternal();
|
|
658
|
+
await this.clearSessionInternal('signed_out');
|
|
530
659
|
}
|
|
531
660
|
return serverSucceeded;
|
|
532
661
|
}
|
|
@@ -689,7 +818,7 @@ export class KoolbaseAuth {
|
|
|
689
818
|
includeAuth: true,
|
|
690
819
|
});
|
|
691
820
|
await this.checkResponse(res);
|
|
692
|
-
await this.clearSessionInternal();
|
|
821
|
+
await this.clearSessionInternal('signed_out');
|
|
693
822
|
}
|
|
694
823
|
/**
|
|
695
824
|
* Every session the signed-in user has — a "where you're signed in" list.
|
|
@@ -812,8 +941,14 @@ export class KoolbaseAuth {
|
|
|
812
941
|
try {
|
|
813
942
|
return await this._ensureValidToken();
|
|
814
943
|
}
|
|
815
|
-
catch {
|
|
816
|
-
|
|
944
|
+
catch (e) {
|
|
945
|
+
// Refused: the session has ended (and been cleared); go api-key-only.
|
|
946
|
+
if (isRejection(e))
|
|
947
|
+
return null;
|
|
948
|
+
// No answer: the session may be fine, so the request must not go out
|
|
949
|
+
// as a signed-out visitor (whose refusal would end it). It fails as
|
|
950
|
+
// the network failure it is, and the caller retries (#96).
|
|
951
|
+
throw e;
|
|
817
952
|
}
|
|
818
953
|
}
|
|
819
954
|
async setSession(session) {
|
|
@@ -1061,9 +1196,9 @@ export class KoolbaseAuth {
|
|
|
1061
1196
|
return session.accessToken;
|
|
1062
1197
|
}
|
|
1063
1198
|
catch (e) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
throw
|
|
1199
|
+
// A refusal stays a refusal; a network failure stays one too, rather
|
|
1200
|
+
// than being reported as an ended session (#96).
|
|
1201
|
+
throw e;
|
|
1067
1202
|
}
|
|
1068
1203
|
}
|
|
1069
1204
|
mapUser(raw) {
|
|
@@ -1310,3 +1445,15 @@ export class KoolbaseAuth {
|
|
|
1310
1445
|
throw new KoolbaseAuthError(msg || 'An unexpected error occurred', code || undefined);
|
|
1311
1446
|
}
|
|
1312
1447
|
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Whether an error is the server refusing the session -- it ended -- rather
|
|
1450
|
+
* than no answer at all (#96). Only a refusal signs anyone out.
|
|
1451
|
+
*/
|
|
1452
|
+
function isRejection(e) {
|
|
1453
|
+
if (e instanceof SessionExpiredError || e instanceof TokenRevokedError || e instanceof UserDisabledError)
|
|
1454
|
+
return true;
|
|
1455
|
+
if (e instanceof KoolbaseAuthError) {
|
|
1456
|
+
return ['invalid_refresh_token', 'token_revoked', 'account_disabled', 'mfa_session_insufficient', 'unauthenticated'].includes(e.code ?? '');
|
|
1457
|
+
}
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
package/dist/esm/database.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sessionFetch } from './network.js';
|
|
2
2
|
import { refreshCollectionQueries } from './query-refresh.js';
|
|
3
3
|
import { KoolbaseRevisionMismatchError } from './database-errors.js';
|
|
4
4
|
import { KoolbaseError, KoolbaseOfflineBaselineUnavailableError, KoolbaseUnauthenticatedError, } from './errors.js';
|
|
@@ -94,7 +94,7 @@ export class KoolbaseDatabase {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
async request(method, path, body) {
|
|
97
|
-
const res = await
|
|
97
|
+
const res = await sessionFetch(`${this.config.baseUrl}${path}`, {
|
|
98
98
|
method,
|
|
99
99
|
headers: await this.buildHeaders(),
|
|
100
100
|
body: body ? JSON.stringify(body) : undefined,
|
|
@@ -132,7 +132,7 @@ export class KoolbaseDatabase {
|
|
|
132
132
|
* none of them clearing a rejected session. One path, two shapes of result.
|
|
133
133
|
*/
|
|
134
134
|
async requestWithStatus(method, path, body) {
|
|
135
|
-
const res = await
|
|
135
|
+
const res = await sessionFetch(`${this.config.baseUrl}${path}`, {
|
|
136
136
|
method,
|
|
137
137
|
headers: await this.buildHeaders(),
|
|
138
138
|
body: body ? JSON.stringify(body) : undefined,
|
package/dist/esm/fiscal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sessionFetch } from './network.js';
|
|
2
2
|
import { KoolbaseError, KoolbaseUnauthenticatedError } from './errors.js';
|
|
3
3
|
const KNOWN_STATUSES = [
|
|
4
4
|
'created', 'blocked', 'queued', 'submitting',
|
|
@@ -106,7 +106,7 @@ export class KoolbaseFiscal {
|
|
|
106
106
|
const controller = new AbortController();
|
|
107
107
|
const timer = setTimeout(() => controller.abort(), params.timeoutMs ?? 30000);
|
|
108
108
|
try {
|
|
109
|
-
const res = await
|
|
109
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/fiscal/submit`, {
|
|
110
110
|
method: 'POST',
|
|
111
111
|
headers: await this.buildHeaders(),
|
|
112
112
|
signal: controller.signal,
|
|
@@ -144,7 +144,7 @@ export class KoolbaseFiscal {
|
|
|
144
144
|
device_id: params.deviceId,
|
|
145
145
|
client_ref: params.clientRef,
|
|
146
146
|
});
|
|
147
|
-
const res = await
|
|
147
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/fiscal/status?${q.toString()}`, { method: 'GET', headers: await this.buildHeaders(), signal: controller.signal });
|
|
148
148
|
return await this.decode(res);
|
|
149
149
|
}
|
|
150
150
|
catch (e) {
|
package/dist/esm/functions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sessionFetch } from './network.js';
|
|
2
2
|
import { functionInvokeError } from './function-errors.js';
|
|
3
3
|
import { KoolbaseUnauthenticatedError } from './errors.js';
|
|
4
4
|
import { FunctionRuntime, } from './types.js';
|
|
@@ -11,7 +11,7 @@ export class KoolbaseFunctions {
|
|
|
11
11
|
// ─── Deploy ────────────────────────────────────────────────────────────────
|
|
12
12
|
async deploy(options) {
|
|
13
13
|
const runtime = options.runtime ?? FunctionRuntime.Deno;
|
|
14
|
-
const res = await
|
|
14
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/functions/deploy`, {
|
|
15
15
|
method: 'POST',
|
|
16
16
|
headers: {
|
|
17
17
|
'Content-Type': 'application/json',
|
|
@@ -55,7 +55,7 @@ export class KoolbaseFunctions {
|
|
|
55
55
|
if (userToken) {
|
|
56
56
|
headers['Authorization'] = `Bearer ${userToken}`;
|
|
57
57
|
}
|
|
58
|
-
const res = await
|
|
58
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/functions/${name}`, {
|
|
59
59
|
method: 'POST',
|
|
60
60
|
headers,
|
|
61
61
|
body: JSON.stringify({ body: body ?? {} }),
|
package/dist/esm/network.d.ts
CHANGED
|
@@ -19,3 +19,17 @@ export declare class KoolbaseNetworkError extends TypeError {
|
|
|
19
19
|
}
|
|
20
20
|
/** fetch, with network failures turned into a KoolbaseNetworkError. */
|
|
21
21
|
export declare function koolbaseFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
22
|
+
type Renewal = (refusedToken: string) => Promise<string | null>;
|
|
23
|
+
/** Installed by Koolbase.initialize: renews the session a request was refused with. */
|
|
24
|
+
export declare function setSessionRenewal(fn: Renewal | null): void;
|
|
25
|
+
/**
|
|
26
|
+
* koolbaseFetch for a data request. A request refused as unauthenticated
|
|
27
|
+
* (401) while carrying the signed-in person's token is the session needing
|
|
28
|
+
* renewal, not ending: the access token expired early, the clock is off, or
|
|
29
|
+
* another tab renewed it. The session is renewed once and the request sent
|
|
30
|
+
* again with the new token, so the caller never sees the first refusal.
|
|
31
|
+
* Only when renewal is refused does the 401 reach the caller, and with it the
|
|
32
|
+
* client's onSessionExpired, which ends the session.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sessionFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
35
|
+
export {};
|
package/dist/esm/network.js
CHANGED
|
@@ -47,3 +47,61 @@ export async function koolbaseFetch(input, init) {
|
|
|
47
47
|
throw new KoolbaseNetworkError(input, e);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
let renewal = null;
|
|
51
|
+
/** Installed by Koolbase.initialize: renews the session a request was refused with. */
|
|
52
|
+
export function setSessionRenewal(fn) {
|
|
53
|
+
renewal = fn;
|
|
54
|
+
}
|
|
55
|
+
function headerOf(headers, name) {
|
|
56
|
+
if (!headers)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (typeof headers.get === 'function')
|
|
59
|
+
return headers.get(name) ?? undefined;
|
|
60
|
+
if (Array.isArray(headers))
|
|
61
|
+
return headers.find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
|
|
62
|
+
const rec = headers;
|
|
63
|
+
const key = Object.keys(rec).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
64
|
+
return key ? rec[key] : undefined;
|
|
65
|
+
}
|
|
66
|
+
function withHeader(headers, name, value) {
|
|
67
|
+
const out = {};
|
|
68
|
+
if (headers && typeof headers.forEach === 'function' && !Array.isArray(headers)) {
|
|
69
|
+
headers.forEach((v, k) => { out[k] = v; });
|
|
70
|
+
}
|
|
71
|
+
else if (Array.isArray(headers)) {
|
|
72
|
+
for (const [k, v] of headers)
|
|
73
|
+
out[k] = v;
|
|
74
|
+
}
|
|
75
|
+
else if (headers) {
|
|
76
|
+
Object.assign(out, headers);
|
|
77
|
+
}
|
|
78
|
+
for (const k of Object.keys(out))
|
|
79
|
+
if (k.toLowerCase() === name.toLowerCase())
|
|
80
|
+
delete out[k];
|
|
81
|
+
out[name] = value;
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* koolbaseFetch for a data request. A request refused as unauthenticated
|
|
86
|
+
* (401) while carrying the signed-in person's token is the session needing
|
|
87
|
+
* renewal, not ending: the access token expired early, the clock is off, or
|
|
88
|
+
* another tab renewed it. The session is renewed once and the request sent
|
|
89
|
+
* again with the new token, so the caller never sees the first refusal.
|
|
90
|
+
* Only when renewal is refused does the 401 reach the caller, and with it the
|
|
91
|
+
* client's onSessionExpired, which ends the session.
|
|
92
|
+
*/
|
|
93
|
+
export async function sessionFetch(input, init) {
|
|
94
|
+
const res = await koolbaseFetch(input, init);
|
|
95
|
+
if (res.status !== 401 || !renewal)
|
|
96
|
+
return res;
|
|
97
|
+
const auth = headerOf(init?.headers, 'Authorization');
|
|
98
|
+
if (!auth || !auth.startsWith('Bearer '))
|
|
99
|
+
return res;
|
|
100
|
+
// Only a body that can be sent twice. Data requests send JSON text.
|
|
101
|
+
if (init?.body !== undefined && init.body !== null && typeof init.body !== 'string')
|
|
102
|
+
return res;
|
|
103
|
+
const fresh = await renewal(auth.slice('Bearer '.length));
|
|
104
|
+
if (!fresh)
|
|
105
|
+
return res;
|
|
106
|
+
return koolbaseFetch(input, { ...init, headers: withHeader(init?.headers, 'Authorization', `Bearer ${fresh}`) });
|
|
107
|
+
}
|
package/dist/esm/storage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KoolbaseStorageProjectIdentityError } from './storage-errors.js';
|
|
2
|
-
import {
|
|
2
|
+
import { sessionFetch } from './network.js';
|
|
3
3
|
import { KoolbaseUnauthenticatedError } from './errors.js';
|
|
4
4
|
import { KoolbaseStorageError, koolbaseStorageErrorFromResponse, } from './storage-errors.js';
|
|
5
5
|
// --- Cloudflare image-transform URL helpers -------------------------------
|
|
@@ -98,7 +98,7 @@ export class KoolbaseStorage {
|
|
|
98
98
|
? options.file.type || 'application/octet-stream'
|
|
99
99
|
: options.file.type;
|
|
100
100
|
// ─── Step 1: Get presigned upload URL ───
|
|
101
|
-
const urlRes = await
|
|
101
|
+
const urlRes = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
|
|
102
102
|
method: 'POST',
|
|
103
103
|
headers: {
|
|
104
104
|
...(await this.buildHeaders()),
|
|
@@ -124,9 +124,9 @@ export class KoolbaseStorage {
|
|
|
124
124
|
// the two hosts differ here and nowhere else in this method.
|
|
125
125
|
const fileBlob = isBlob
|
|
126
126
|
? options.file
|
|
127
|
-
: await (await
|
|
127
|
+
: await (await sessionFetch(options.file.uri)).blob();
|
|
128
128
|
const fileSize = fileBlob.size;
|
|
129
|
-
const uploadRes = await
|
|
129
|
+
const uploadRes = await sessionFetch(upload_url, {
|
|
130
130
|
method: 'PUT',
|
|
131
131
|
headers: { 'Content-Type': contentType },
|
|
132
132
|
body: fileBlob,
|
|
@@ -153,7 +153,7 @@ export class KoolbaseStorage {
|
|
|
153
153
|
if (options.metadata !== undefined) {
|
|
154
154
|
confirmBody.metadata = options.metadata;
|
|
155
155
|
}
|
|
156
|
-
const confirmRes = await
|
|
156
|
+
const confirmRes = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
|
|
157
157
|
method: 'POST',
|
|
158
158
|
headers: {
|
|
159
159
|
...(await this.buildHeaders()),
|
|
@@ -204,7 +204,7 @@ export class KoolbaseStorage {
|
|
|
204
204
|
* // -> { category: 'landscape', tag: 'sunset' }
|
|
205
205
|
*/
|
|
206
206
|
async updateMetadata(bucket, path, metadata) {
|
|
207
|
-
const res = await
|
|
207
|
+
const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/objects/metadata`, {
|
|
208
208
|
method: 'PATCH',
|
|
209
209
|
headers: {
|
|
210
210
|
...(await this.buildHeaders()),
|
|
@@ -227,7 +227,7 @@ export class KoolbaseStorage {
|
|
|
227
227
|
if (versionId) {
|
|
228
228
|
url += `&version_id=${encodeURIComponent(versionId)}`;
|
|
229
229
|
}
|
|
230
|
-
const res = await
|
|
230
|
+
const res = await sessionFetch(url, { headers: await this.buildHeaders() });
|
|
231
231
|
if (!res.ok) {
|
|
232
232
|
throw await this.error(res, 'Failed to get download URL');
|
|
233
233
|
}
|
|
@@ -339,7 +339,7 @@ export class KoolbaseStorage {
|
|
|
339
339
|
const url = forcePurge
|
|
340
340
|
? `${this.config.baseUrl}/v1/sdk/storage/object?force_purge=true`
|
|
341
341
|
: `${this.config.baseUrl}/v1/sdk/storage/object`;
|
|
342
|
-
const res = await
|
|
342
|
+
const res = await sessionFetch(url, {
|
|
343
343
|
method: 'DELETE',
|
|
344
344
|
headers: {
|
|
345
345
|
...(await this.buildHeaders()),
|
|
@@ -366,7 +366,7 @@ export class KoolbaseStorage {
|
|
|
366
366
|
async listVersions(bucket, path) {
|
|
367
367
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions` +
|
|
368
368
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
369
|
-
const res = await
|
|
369
|
+
const res = await sessionFetch(url, { headers: await this.buildHeaders() });
|
|
370
370
|
if (!res.ok) {
|
|
371
371
|
throw await this.error(res, 'Failed to list versions');
|
|
372
372
|
}
|
|
@@ -382,7 +382,7 @@ export class KoolbaseStorage {
|
|
|
382
382
|
async getVersion(bucket, path, versionId) {
|
|
383
383
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
|
|
384
384
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
385
|
-
const res = await
|
|
385
|
+
const res = await sessionFetch(url, { headers: await this.buildHeaders() });
|
|
386
386
|
if (!res.ok) {
|
|
387
387
|
throw await this.error(res, 'Failed to fetch version');
|
|
388
388
|
}
|
|
@@ -401,7 +401,7 @@ export class KoolbaseStorage {
|
|
|
401
401
|
async restoreVersion(bucket, path, versionId) {
|
|
402
402
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}/restore` +
|
|
403
403
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
404
|
-
const res = await
|
|
404
|
+
const res = await sessionFetch(url, {
|
|
405
405
|
method: 'POST',
|
|
406
406
|
headers: await this.buildHeaders(),
|
|
407
407
|
});
|
|
@@ -419,7 +419,7 @@ export class KoolbaseStorage {
|
|
|
419
419
|
async purgeVersion(bucket, path, versionId) {
|
|
420
420
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
|
|
421
421
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
422
|
-
const res = await
|
|
422
|
+
const res = await sessionFetch(url, {
|
|
423
423
|
method: 'DELETE',
|
|
424
424
|
headers: await this.buildHeaders(),
|
|
425
425
|
});
|
package/dist/esm/sync-engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sessionFetch } from './network.js';
|
|
2
2
|
import { readOfflineState, mutateOfflineState, migrateLegacyQueue, flushLockName, } from './offline-state.js';
|
|
3
3
|
import { KoolbaseUnauthenticatedError } from './errors.js';
|
|
4
4
|
import { getPlatform } from './platform.js';
|
|
@@ -256,7 +256,7 @@ export class SyncEngine {
|
|
|
256
256
|
: `${this.config.baseUrl}/v1/sdk/db/records/${write.recordId}`;
|
|
257
257
|
let res;
|
|
258
258
|
if (write.operation === 'insert') {
|
|
259
|
-
res = await
|
|
259
|
+
res = await sessionFetch(url, {
|
|
260
260
|
method: 'POST',
|
|
261
261
|
headers,
|
|
262
262
|
// The write's own id: generated at enqueue, identical on every retry.
|
|
@@ -270,7 +270,7 @@ export class SyncEngine {
|
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
272
|
else if (write.operation === 'update') {
|
|
273
|
-
res = await
|
|
273
|
+
res = await sessionFetch(url, {
|
|
274
274
|
method: 'PATCH',
|
|
275
275
|
headers,
|
|
276
276
|
// The revision the change was composed against. The server applies it
|
|
@@ -289,7 +289,7 @@ export class SyncEngine {
|
|
|
289
289
|
const q = write.baseRevision !== undefined
|
|
290
290
|
? `?expected_revision=${write.baseRevision}`
|
|
291
291
|
: '';
|
|
292
|
-
res = await
|
|
292
|
+
res = await sessionFetch(`${url}${q}`, { method: 'DELETE', headers });
|
|
293
293
|
}
|
|
294
294
|
if (res.status === 401)
|
|
295
295
|
throw new KoolbaseUnauthenticatedError('unauthorized');
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -180,6 +180,13 @@ export declare enum RestoreResult {
|
|
|
180
180
|
* broken listener cannot break propagation to others.
|
|
181
181
|
*/
|
|
182
182
|
export type AuthStateListener = (user: KoolbaseUser | null) => void;
|
|
183
|
+
/**
|
|
184
|
+
* Why the last session ended (#104): the person signed out, or the session
|
|
185
|
+
* ended on its own -- revoked, expired past renewal, signed out elsewhere.
|
|
186
|
+
* An app tells them apart to say "You were signed out" only when they did
|
|
187
|
+
* not ask to be.
|
|
188
|
+
*/
|
|
189
|
+
export type SignOutReason = 'signed_out' | 'session_ended';
|
|
183
190
|
/**
|
|
184
191
|
* Drop-in replacement for the global `fetch` function. Inject via
|
|
185
192
|
* KoolbaseConfig.fetch for testing (mock fetch), proxying, or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koolbase/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.15.0",
|
|
4
4
|
"description": "Koolbase SDK core \u2014 shared behaviour behind @koolbase/react-native and @koolbase/js. Install one of those, not this.",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"types": "./dist/esm/index.d.ts",
|