@koolbase/core 12.14.0 → 12.16.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/index.d.ts +3 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/network.d.ts +14 -0
- package/dist/cjs/network.js +60 -0
- package/dist/cjs/storage-errors.d.ts +39 -0
- package/dist/cjs/storage-errors.js +75 -1
- package/dist/cjs/storage-files.d.ts +53 -0
- package/dist/cjs/storage-files.js +61 -0
- package/dist/cjs/storage-transfer.d.ts +88 -0
- package/dist/cjs/storage-transfer.js +214 -0
- package/dist/cjs/storage.d.ts +41 -0
- package/dist/cjs/storage.js +101 -18
- package/dist/cjs/sync-engine.js +3 -3
- package/dist/cjs/types.d.ts +19 -1
- 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/index.d.ts +3 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/network.d.ts +14 -0
- package/dist/esm/network.js +58 -0
- package/dist/esm/storage-errors.d.ts +39 -0
- package/dist/esm/storage-errors.js +69 -0
- package/dist/esm/storage-files.d.ts +53 -0
- package/dist/esm/storage-files.js +56 -0
- package/dist/esm/storage-transfer.d.ts +88 -0
- package/dist/esm/storage-transfer.js +208 -0
- package/dist/esm/storage.d.ts +41 -0
- package/dist/esm/storage.js +102 -19
- package/dist/esm/sync-engine.js +4 -4
- package/dist/esm/types.d.ts +19 -1
- package/package.json +1 -1
package/dist/cjs/storage.js
CHANGED
|
@@ -5,6 +5,8 @@ const storage_errors_js_1 = require("./storage-errors.js");
|
|
|
5
5
|
const network_js_1 = require("./network.js");
|
|
6
6
|
const errors_js_1 = require("./errors.js");
|
|
7
7
|
const storage_errors_js_2 = require("./storage-errors.js");
|
|
8
|
+
const storage_transfer_js_1 = require("./storage-transfer.js");
|
|
9
|
+
const storage_files_js_1 = require("./storage-files.js");
|
|
8
10
|
// --- Cloudflare image-transform URL helpers -------------------------------
|
|
9
11
|
// Module-private — callers use KoolbaseStorage.publicUrl / publicUrlForObject.
|
|
10
12
|
function clampInt(v, min, max) {
|
|
@@ -91,6 +93,46 @@ class KoolbaseStorage {
|
|
|
91
93
|
* overwriting silently, pass `overwrite: true` explicitly.
|
|
92
94
|
*/
|
|
93
95
|
async upload(options) {
|
|
96
|
+
const task = this.startUpload(options);
|
|
97
|
+
const signal = options.signal;
|
|
98
|
+
const onAbort = () => task.cancel();
|
|
99
|
+
if (signal) {
|
|
100
|
+
if (signal.aborted)
|
|
101
|
+
task.cancel();
|
|
102
|
+
else
|
|
103
|
+
signal.addEventListener('abort', onAbort);
|
|
104
|
+
}
|
|
105
|
+
const stop = options.onProgress
|
|
106
|
+
? task.onProgress((p) => options.onProgress?.(Math.round(p.fraction * 100), p))
|
|
107
|
+
: null;
|
|
108
|
+
try {
|
|
109
|
+
return await task.result;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
stop?.();
|
|
113
|
+
signal?.removeEventListener('abort', onAbort);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Starts an upload you can follow and cancel: the same upload as
|
|
118
|
+
* {@link upload}, returned at once as a {@link KoolbaseUploadTask}.
|
|
119
|
+
*
|
|
120
|
+
* The task goes pending -> uploading (bytes sent of total) -> verifying
|
|
121
|
+
* -> completed, or failed / cancelled. `cancel()` stops it until the
|
|
122
|
+
* server begins checking the bytes (verifying); nothing is stored. Only
|
|
123
|
+
* completed means the file can be referenced from a record.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* const task = Koolbase.storage.startUpload({ bucket: 'posters', path: 'gig.png', file });
|
|
127
|
+
* const stop = task.onProgress((p) => setPercent(Math.round(p.fraction * 100)));
|
|
128
|
+
* cancelButton.onclick = () => task.cancel();
|
|
129
|
+
* const { object } = await task.result;
|
|
130
|
+
*/
|
|
131
|
+
startUpload(options) {
|
|
132
|
+
return (0, storage_transfer_js_1.runUploadTask)((task) => this.runUpload(options, task));
|
|
133
|
+
}
|
|
134
|
+
async runUpload(options, task) {
|
|
135
|
+
task.checkCancelled();
|
|
94
136
|
const overwrite = options.overwrite ?? false;
|
|
95
137
|
// A Blob (browser File included) knows its own type; the React Native
|
|
96
138
|
// URI form carries it alongside. Falling back to octet-stream keeps a
|
|
@@ -100,8 +142,10 @@ class KoolbaseStorage {
|
|
|
100
142
|
const contentType = isBlob
|
|
101
143
|
? options.file.type || 'application/octet-stream'
|
|
102
144
|
: options.file.type;
|
|
145
|
+
if (isBlob)
|
|
146
|
+
task.setTotal(options.file.size);
|
|
103
147
|
// ─── Step 1: Get presigned upload URL ───
|
|
104
|
-
const urlRes = await (0, network_js_1.
|
|
148
|
+
const urlRes = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
|
|
105
149
|
method: 'POST',
|
|
106
150
|
headers: {
|
|
107
151
|
...(await this.buildHeaders()),
|
|
@@ -117,7 +161,8 @@ class KoolbaseStorage {
|
|
|
117
161
|
if (!urlRes.ok) {
|
|
118
162
|
throw await this.error(urlRes, 'Failed to get upload URL');
|
|
119
163
|
}
|
|
120
|
-
const { upload_url } = (await urlRes.json());
|
|
164
|
+
const { upload_url, upload_id } = (await urlRes.json());
|
|
165
|
+
task.checkCancelled();
|
|
121
166
|
// ─── Step 2: Upload directly to R2 ───
|
|
122
167
|
// R2 presigned URLs expect raw binary, NOT multipart/form-data.
|
|
123
168
|
//
|
|
@@ -127,19 +172,19 @@ class KoolbaseStorage {
|
|
|
127
172
|
// the two hosts differ here and nowhere else in this method.
|
|
128
173
|
const fileBlob = isBlob
|
|
129
174
|
? options.file
|
|
130
|
-
: await (await (0, network_js_1.
|
|
175
|
+
: await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
|
|
131
176
|
const fileSize = fileBlob.size;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
body: fileBlob,
|
|
136
|
-
});
|
|
137
|
-
if (!uploadRes.ok) {
|
|
177
|
+
task.setTotal(fileSize);
|
|
178
|
+
const uploadRes = await (0, storage_transfer_js_1.putWithProgress)(task, upload_url, fileBlob, contentType);
|
|
179
|
+
if (uploadRes.status < 200 || uploadRes.status >= 300) {
|
|
138
180
|
// R2 PUT errors don't follow the Koolbase error shape — surface as a
|
|
139
181
|
// generic storage error rather than trying to decode a Koolbase body.
|
|
140
182
|
throw new storage_errors_js_2.KoolbaseStorageError(`Upload to storage failed: ${uploadRes.status}`);
|
|
141
183
|
}
|
|
142
|
-
const etag = uploadRes.
|
|
184
|
+
const etag = uploadRes.etag;
|
|
185
|
+
// From here the server checks the bytes and puts them in place; the
|
|
186
|
+
// upload can no longer be cancelled.
|
|
187
|
+
task.verifying();
|
|
143
188
|
// ─── Step 3: Confirm upload ───
|
|
144
189
|
// Build the body conditionally so the `metadata` field is only sent
|
|
145
190
|
// when the caller passed it — keeps the wire shape clean for callers
|
|
@@ -153,10 +198,14 @@ class KoolbaseStorage {
|
|
|
153
198
|
etag,
|
|
154
199
|
overwrite,
|
|
155
200
|
};
|
|
201
|
+
// The confirm names its upload, so it takes exactly these bytes.
|
|
202
|
+
if (upload_id) {
|
|
203
|
+
confirmBody.upload_id = upload_id;
|
|
204
|
+
}
|
|
156
205
|
if (options.metadata !== undefined) {
|
|
157
206
|
confirmBody.metadata = options.metadata;
|
|
158
207
|
}
|
|
159
|
-
const confirmRes = await (0, network_js_1.
|
|
208
|
+
const confirmRes = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
|
|
160
209
|
method: 'POST',
|
|
161
210
|
headers: {
|
|
162
211
|
...(await this.buildHeaders()),
|
|
@@ -173,6 +222,40 @@ class KoolbaseStorage {
|
|
|
173
222
|
const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
|
|
174
223
|
return { object, downloadUrl };
|
|
175
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* One page of a bucket's files. The bucket's view rule is applied to every
|
|
227
|
+
* file: the page holds only files the signed-in person may view. Pass the
|
|
228
|
+
* page's `nextCursor` back as `cursor` for the next one; a page can hold
|
|
229
|
+
* fewer files than `limit` and still have a next.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* let cursor: string | undefined;
|
|
233
|
+
* do {
|
|
234
|
+
* const page = await Koolbase.storage.listFiles({ bucket: 'posters', prefix: 'events/', cursor });
|
|
235
|
+
* show(page.files);
|
|
236
|
+
* cursor = page.nextCursor ?? undefined;
|
|
237
|
+
* } while (cursor);
|
|
238
|
+
*/
|
|
239
|
+
async listFiles(options) {
|
|
240
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/files?${(0, storage_files_js_1.listFilesQuery)(options)}`, { headers: await this.buildHeaders() });
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
throw await this.error(res, 'Failed to list files');
|
|
243
|
+
}
|
|
244
|
+
return (0, storage_files_js_1.filePageFromJson)((await res.json()));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* One file's details -- size, type, metadata, who uploaded it, and its
|
|
248
|
+
* permanent link while its bucket is public. Throws
|
|
249
|
+
* `KoolbaseStorageNotFoundError` for a file that is not there or that
|
|
250
|
+
* the bucket's view rule hides from the caller.
|
|
251
|
+
*/
|
|
252
|
+
async getFileInfo(bucket, path) {
|
|
253
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/file?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`, { headers: await this.buildHeaders() });
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
throw await this.error(res, 'Failed to get the file');
|
|
256
|
+
}
|
|
257
|
+
return (0, storage_files_js_1.fileInfoFromJson)((await res.json()));
|
|
258
|
+
}
|
|
176
259
|
/**
|
|
177
260
|
* Apply a partial metadata update to an existing object. Returns the
|
|
178
261
|
* post-update {@link KoolbaseObject} with the merged metadata.
|
|
@@ -207,7 +290,7 @@ class KoolbaseStorage {
|
|
|
207
290
|
* // -> { category: 'landscape', tag: 'sunset' }
|
|
208
291
|
*/
|
|
209
292
|
async updateMetadata(bucket, path, metadata) {
|
|
210
|
-
const res = await (0, network_js_1.
|
|
293
|
+
const res = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/objects/metadata`, {
|
|
211
294
|
method: 'PATCH',
|
|
212
295
|
headers: {
|
|
213
296
|
...(await this.buildHeaders()),
|
|
@@ -230,7 +313,7 @@ class KoolbaseStorage {
|
|
|
230
313
|
if (versionId) {
|
|
231
314
|
url += `&version_id=${encodeURIComponent(versionId)}`;
|
|
232
315
|
}
|
|
233
|
-
const res = await (0, network_js_1.
|
|
316
|
+
const res = await (0, network_js_1.sessionFetch)(url, { headers: await this.buildHeaders() });
|
|
234
317
|
if (!res.ok) {
|
|
235
318
|
throw await this.error(res, 'Failed to get download URL');
|
|
236
319
|
}
|
|
@@ -342,7 +425,7 @@ class KoolbaseStorage {
|
|
|
342
425
|
const url = forcePurge
|
|
343
426
|
? `${this.config.baseUrl}/v1/sdk/storage/object?force_purge=true`
|
|
344
427
|
: `${this.config.baseUrl}/v1/sdk/storage/object`;
|
|
345
|
-
const res = await (0, network_js_1.
|
|
428
|
+
const res = await (0, network_js_1.sessionFetch)(url, {
|
|
346
429
|
method: 'DELETE',
|
|
347
430
|
headers: {
|
|
348
431
|
...(await this.buildHeaders()),
|
|
@@ -369,7 +452,7 @@ class KoolbaseStorage {
|
|
|
369
452
|
async listVersions(bucket, path) {
|
|
370
453
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions` +
|
|
371
454
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
372
|
-
const res = await (0, network_js_1.
|
|
455
|
+
const res = await (0, network_js_1.sessionFetch)(url, { headers: await this.buildHeaders() });
|
|
373
456
|
if (!res.ok) {
|
|
374
457
|
throw await this.error(res, 'Failed to list versions');
|
|
375
458
|
}
|
|
@@ -385,7 +468,7 @@ class KoolbaseStorage {
|
|
|
385
468
|
async getVersion(bucket, path, versionId) {
|
|
386
469
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
|
|
387
470
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
388
|
-
const res = await (0, network_js_1.
|
|
471
|
+
const res = await (0, network_js_1.sessionFetch)(url, { headers: await this.buildHeaders() });
|
|
389
472
|
if (!res.ok) {
|
|
390
473
|
throw await this.error(res, 'Failed to fetch version');
|
|
391
474
|
}
|
|
@@ -404,7 +487,7 @@ class KoolbaseStorage {
|
|
|
404
487
|
async restoreVersion(bucket, path, versionId) {
|
|
405
488
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}/restore` +
|
|
406
489
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
407
|
-
const res = await (0, network_js_1.
|
|
490
|
+
const res = await (0, network_js_1.sessionFetch)(url, {
|
|
408
491
|
method: 'POST',
|
|
409
492
|
headers: await this.buildHeaders(),
|
|
410
493
|
});
|
|
@@ -422,7 +505,7 @@ class KoolbaseStorage {
|
|
|
422
505
|
async purgeVersion(bucket, path, versionId) {
|
|
423
506
|
const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
|
|
424
507
|
`?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
|
|
425
|
-
const res = await (0, network_js_1.
|
|
508
|
+
const res = await (0, network_js_1.sessionFetch)(url, {
|
|
426
509
|
method: 'DELETE',
|
|
427
510
|
headers: await this.buildHeaders(),
|
|
428
511
|
});
|
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
|
|
@@ -362,7 +369,18 @@ export interface UploadOptions {
|
|
|
362
369
|
* for post-upload merge changes.
|
|
363
370
|
*/
|
|
364
371
|
metadata?: Record<string, string>;
|
|
365
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Called as the upload goes: `percent` is 0 to 100 of the bytes sent;
|
|
374
|
+
* `progress` says where the upload is (uploading, verifying, completed,
|
|
375
|
+
* ...). For a cancellable upload use `KoolbaseStorage.startUpload`.
|
|
376
|
+
*/
|
|
377
|
+
onProgress?: (percent: number, progress: import('./storage-transfer.js').KoolbaseUploadProgress) => void;
|
|
378
|
+
/**
|
|
379
|
+
* Aborting this signal cancels the upload, if the server has not begun
|
|
380
|
+
* checking it yet; the upload then rejects with
|
|
381
|
+
* `KoolbaseUploadCancelledError`.
|
|
382
|
+
*/
|
|
383
|
+
signal?: AbortSignal;
|
|
366
384
|
}
|
|
367
385
|
/**
|
|
368
386
|
* A stored object's server-side metadata. Field names are camelCase here
|
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) {
|