@kedaruma/revlm-client 1.0.58 → 1.0.62
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/README.md +8 -2
- package/dist/{chunk-YHUJ52AP.mjs → chunk-V2GBS6JY.mjs} +128 -34
- package/dist/index.d.mts +18 -9
- package/dist/index.d.ts +18 -9
- package/dist/index.js +128 -34
- package/dist/index.mjs +1 -1
- package/dist/revlm-compat.d.mts +1 -1
- package/dist/revlm-compat.d.ts +1 -1
- package/dist/revlm-compat.js +128 -34
- package/dist/revlm-compat.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,8 +17,14 @@ The package ships both CJS and ESM bundles plus typings (`types` and `exports` a
|
|
|
17
17
|
```ts
|
|
18
18
|
import { Revlm } from '@kedaruma/revlm-client';
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
20
|
+
const randomBytes = (length: number) => {
|
|
21
|
+
const out = new Uint8Array(length);
|
|
22
|
+
crypto.getRandomValues(out);
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const revlm = new Revlm('https://your-server.example.com', { randomBytes });
|
|
27
|
+
const login = await revlm.login('user', 'secret');
|
|
22
28
|
const db = revlm.db('db_name');
|
|
23
29
|
const coll = db.collection<any>('collection_name');
|
|
24
30
|
const all = await coll.find({});
|
|
@@ -8,7 +8,7 @@ var require_package = __commonJS({
|
|
|
8
8
|
"package.json"(exports, module) {
|
|
9
9
|
module.exports = {
|
|
10
10
|
name: "@kedaruma/revlm-client",
|
|
11
|
-
version: "1.0.
|
|
11
|
+
version: "1.0.62",
|
|
12
12
|
private: false,
|
|
13
13
|
description: "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
14
14
|
keywords: [
|
|
@@ -190,6 +190,9 @@ var LOG_LEVEL_RANK = {
|
|
|
190
190
|
debug: 3
|
|
191
191
|
};
|
|
192
192
|
var SESSION_HEADER_NAME = "x-revlm-session-id";
|
|
193
|
+
var STATE_STORE_KEYS = {
|
|
194
|
+
refreshSecret: "refreshSecret"
|
|
195
|
+
};
|
|
193
196
|
function normalizeLogLevel(value) {
|
|
194
197
|
if (!value) return "info";
|
|
195
198
|
const lowered = value.toLowerCase();
|
|
@@ -234,6 +237,14 @@ function getRevlmClientVersion() {
|
|
|
234
237
|
const globalVersion = globalThis?.REVLM_CLIENT_VERSION;
|
|
235
238
|
return typeof globalVersion === "string" ? globalVersion : "unknown";
|
|
236
239
|
}
|
|
240
|
+
function resolveFetchImpl(fetchImpl) {
|
|
241
|
+
if (fetchImpl) return fetchImpl;
|
|
242
|
+
const globalFetch = globalThis?.fetch;
|
|
243
|
+
if (typeof globalFetch === "function") {
|
|
244
|
+
return globalFetch.bind(globalThis);
|
|
245
|
+
}
|
|
246
|
+
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
247
|
+
}
|
|
237
248
|
var Revlm = class {
|
|
238
249
|
baseUrl;
|
|
239
250
|
fetchImpl;
|
|
@@ -249,11 +260,13 @@ var Revlm = class {
|
|
|
249
260
|
refreshPromise;
|
|
250
261
|
sessionId;
|
|
251
262
|
sessionIdProvider;
|
|
252
|
-
|
|
263
|
+
refreshSecret;
|
|
264
|
+
refreshMode = "unknown";
|
|
265
|
+
stateStore;
|
|
253
266
|
constructor(baseUrl, opts = {}) {
|
|
254
267
|
if (!baseUrl) throw new Error("baseUrl is required");
|
|
255
268
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
256
|
-
this.fetchImpl = opts.fetchImpl
|
|
269
|
+
this.fetchImpl = resolveFetchImpl(opts.fetchImpl);
|
|
257
270
|
this.defaultHeaders = opts.defaultHeaders || {};
|
|
258
271
|
this.provisionalEnabled = opts.provisionalEnabled || false;
|
|
259
272
|
this.provisionalAuthSecretMaster = opts.provisionalAuthSecretMaster || "";
|
|
@@ -263,10 +276,7 @@ var Revlm = class {
|
|
|
263
276
|
this.logLevel = normalizeLogLevel(opts.logLevel);
|
|
264
277
|
this.sessionId = opts.sessionId;
|
|
265
278
|
this.sessionIdProvider = opts.sessionIdProvider;
|
|
266
|
-
this.
|
|
267
|
-
if (!this.fetchImpl) {
|
|
268
|
-
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
269
|
-
}
|
|
279
|
+
this.stateStore = opts.stateStore;
|
|
270
280
|
this.logInfo("\u{1F680} Revlm Client Init", {
|
|
271
281
|
version: getRevlmClientVersion(),
|
|
272
282
|
baseUrl: this.baseUrl,
|
|
@@ -292,18 +302,33 @@ var Revlm = class {
|
|
|
292
302
|
this.sessionId = generateSessionId();
|
|
293
303
|
return this.sessionId;
|
|
294
304
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const cookieHeader = await this.cookieStore.getCookieHeader(url);
|
|
299
|
-
if (cookieHeader) headers.cookie = cookieHeader;
|
|
305
|
+
normalizeCookieValue(value) {
|
|
306
|
+
if (!value) return void 0;
|
|
307
|
+
return value.replace(/^"+|"+$/g, "");
|
|
300
308
|
}
|
|
301
|
-
async
|
|
302
|
-
if (!this.cookieStore) return;
|
|
309
|
+
async updateRefreshSecretFromResponse(res) {
|
|
303
310
|
const setCookies = getSetCookieHeaders(res);
|
|
304
311
|
if (!setCookies.length) return;
|
|
305
312
|
for (const setCookie of setCookies) {
|
|
306
|
-
|
|
313
|
+
const match = setCookie.match(/revlm_refresh=([^;]+)/);
|
|
314
|
+
if (match && match[1]) {
|
|
315
|
+
const normalized = this.normalizeCookieValue(match[1]);
|
|
316
|
+
this.refreshSecret = normalized;
|
|
317
|
+
if (this.stateStore && normalized) {
|
|
318
|
+
await this.stateStore.set(STATE_STORE_KEYS.refreshSecret, normalized);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async ensureRefreshSecretLoaded() {
|
|
324
|
+
if (this.refreshSecret || !this.stateStore) return;
|
|
325
|
+
const stored = await this.stateStore.get(STATE_STORE_KEYS.refreshSecret);
|
|
326
|
+
if (stored) this.refreshSecret = stored;
|
|
327
|
+
}
|
|
328
|
+
async clearRefreshSecret() {
|
|
329
|
+
this.refreshSecret = void 0;
|
|
330
|
+
if (this.stateStore) {
|
|
331
|
+
await this.stateStore.remove(STATE_STORE_KEYS.refreshSecret);
|
|
307
332
|
}
|
|
308
333
|
}
|
|
309
334
|
canLog(level) {
|
|
@@ -321,6 +346,25 @@ var Revlm = class {
|
|
|
321
346
|
logDebug(...args) {
|
|
322
347
|
if (this.canLog("debug")) console.log(...args);
|
|
323
348
|
}
|
|
349
|
+
maskSecret(value, head = 10, tail = 6) {
|
|
350
|
+
if (!value) return "<empty>";
|
|
351
|
+
if (value.length <= head + tail) return value;
|
|
352
|
+
return `${value.slice(0, head)}***${value.slice(-tail)}`;
|
|
353
|
+
}
|
|
354
|
+
logRefreshFailureClient(params) {
|
|
355
|
+
const recoverable = typeof params.code === "number" ? params.code >= 1e4 && params.code < 2e4 : params.status === 400 || params.status === 401;
|
|
356
|
+
const line1 = `[refresh-token][${recoverable ? "debug" : "error"}][${recoverable ? "recoverable" : "fatal"}] cause=${params.cause}`;
|
|
357
|
+
const line2 = `session=${this.maskSecret(params.sessionId)} refresh=${this.maskSecret(params.refreshSecret)}`;
|
|
358
|
+
const line3 = `details=${JSON.stringify({
|
|
359
|
+
status: params.status,
|
|
360
|
+
reason: params.reason,
|
|
361
|
+
code: params.code
|
|
362
|
+
})}`;
|
|
363
|
+
this.logError(line1);
|
|
364
|
+
this.logError(line2);
|
|
365
|
+
this.logError(line3);
|
|
366
|
+
this.logError("");
|
|
367
|
+
}
|
|
324
368
|
setToken(token) {
|
|
325
369
|
this._token = token;
|
|
326
370
|
}
|
|
@@ -437,7 +481,12 @@ var Revlm = class {
|
|
|
437
481
|
const headers = this.makeHeaders(hasBody);
|
|
438
482
|
const sessionId = await this.resolveSessionId();
|
|
439
483
|
if (sessionId) headers[SESSION_HEADER_NAME] = sessionId;
|
|
440
|
-
|
|
484
|
+
if (url.includes("/refresh-token") && this.refreshMode === "header") {
|
|
485
|
+
await this.ensureRefreshSecretLoaded();
|
|
486
|
+
if (this.refreshSecret) {
|
|
487
|
+
headers["x-revlm-refresh"] = this.refreshSecret;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
441
490
|
let serializedBody;
|
|
442
491
|
if (hasBody) {
|
|
443
492
|
serializedBody = EJSON.stringify(body);
|
|
@@ -447,9 +496,10 @@ var Revlm = class {
|
|
|
447
496
|
const res = await this.fetchImpl(signedUrl, {
|
|
448
497
|
method,
|
|
449
498
|
headers: signedHeaders,
|
|
450
|
-
body: serializedBody
|
|
499
|
+
body: serializedBody,
|
|
500
|
+
credentials: url.includes("/refresh-token") && this.refreshMode === "header" ? "omit" : "include"
|
|
451
501
|
});
|
|
452
|
-
await this.
|
|
502
|
+
await this.updateRefreshSecretFromResponse(res);
|
|
453
503
|
const parsed = await this.parseResponse(res);
|
|
454
504
|
const out = parsed && typeof parsed === "object" ? parsed : { ok: res.ok, result: parsed };
|
|
455
505
|
out.status = res.status;
|
|
@@ -466,12 +516,26 @@ var Revlm = class {
|
|
|
466
516
|
const refreshFailed = {
|
|
467
517
|
reason: refreshRes.reason,
|
|
468
518
|
status: refreshRes.status,
|
|
469
|
-
error: refreshRes.error
|
|
519
|
+
error: refreshRes.error,
|
|
520
|
+
code: refreshRes.code
|
|
470
521
|
};
|
|
471
522
|
this.logDebug("### refresh failed:", refreshFailed, JSON.stringify(refreshFailed));
|
|
523
|
+
const refreshSecret = this.refreshSecret;
|
|
524
|
+
const refreshFailureLog = {
|
|
525
|
+
cause: refreshRes.reason || "refresh_failed",
|
|
526
|
+
reason: refreshFailed.reason,
|
|
527
|
+
status: refreshFailed.status,
|
|
528
|
+
code: refreshFailed.code
|
|
529
|
+
};
|
|
530
|
+
if (sessionId) refreshFailureLog.sessionId = sessionId;
|
|
531
|
+
if (refreshSecret) refreshFailureLog.refreshSecret = refreshSecret;
|
|
532
|
+
this.logRefreshFailureClient(refreshFailureLog);
|
|
472
533
|
if (refreshRes.reason === "no_refresh_secret") {
|
|
534
|
+
await this.clearRefreshSecret();
|
|
473
535
|
const missingError = new Error("Refresh cookie missing. Provide a cookie-aware fetch implementation for Node/RN.");
|
|
474
536
|
missingError.revlmReason = "no_refresh_secret";
|
|
537
|
+
missingError.revlmCode = refreshRes.code;
|
|
538
|
+
missingError.httpStatus = refreshRes.status;
|
|
475
539
|
throw missingError;
|
|
476
540
|
}
|
|
477
541
|
}
|
|
@@ -480,12 +544,17 @@ var Revlm = class {
|
|
|
480
544
|
const now = Math.floor(Date.now() / 1e3);
|
|
481
545
|
const oldExp = beforePayload?.exp;
|
|
482
546
|
const newExp = afterPayload?.exp;
|
|
547
|
+
const oldTtlSec = typeof oldExp === "number" ? oldExp - now : void 0;
|
|
548
|
+
const newTtlSec = typeof newExp === "number" ? newExp - now : void 0;
|
|
549
|
+
const oldExpDisplay = this.formatUnixSeconds(oldExp);
|
|
550
|
+
const newExpDisplay = this.formatUnixSeconds(newExp);
|
|
551
|
+
const ttlDisplay = this.formatTtlDetailed(newTtlSec);
|
|
483
552
|
this.logDebug("### refresh success", {
|
|
484
553
|
path,
|
|
485
|
-
oldExp,
|
|
486
|
-
newExp,
|
|
487
|
-
oldTtlSec
|
|
488
|
-
newTtlSec: typeof
|
|
554
|
+
oldExp: oldExpDisplay ? `${oldExp} (${oldExpDisplay})` : oldExp,
|
|
555
|
+
newExp: newExpDisplay ? `${newExp} (${newExpDisplay})` : newExp,
|
|
556
|
+
oldTtlSec,
|
|
557
|
+
newTtlSec: typeof newTtlSec === "number" && ttlDisplay ? `${newTtlSec} (${ttlDisplay})` : newTtlSec
|
|
489
558
|
});
|
|
490
559
|
return this.requestWithRetry(path, method, body, { allowAuthRetry: false, retrying: true });
|
|
491
560
|
}
|
|
@@ -514,7 +583,10 @@ var Revlm = class {
|
|
|
514
583
|
}
|
|
515
584
|
await this.ensureCookieSupport();
|
|
516
585
|
if (!authId) throw new Error("authId is required");
|
|
517
|
-
const provisionalClient = new AuthClient({
|
|
586
|
+
const provisionalClient = new AuthClient({
|
|
587
|
+
secretMaster: this.provisionalAuthSecretMaster,
|
|
588
|
+
authDomain: this.provisionalAuthDomain
|
|
589
|
+
});
|
|
518
590
|
const provisionalPassword = await provisionalClient.producePassword(String(Date.now() * 1e3));
|
|
519
591
|
const res = await this.request("/provisional-login", "POST", { authId, password: provisionalPassword });
|
|
520
592
|
if (this.autoSetToken && res && res.ok && res.token) {
|
|
@@ -522,6 +594,22 @@ var Revlm = class {
|
|
|
522
594
|
}
|
|
523
595
|
return res;
|
|
524
596
|
}
|
|
597
|
+
formatUnixSeconds(epochSeconds) {
|
|
598
|
+
if (typeof epochSeconds !== "number" || !Number.isFinite(epochSeconds)) return void 0;
|
|
599
|
+
const date = new Date(epochSeconds * 1e3);
|
|
600
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
601
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
602
|
+
}
|
|
603
|
+
formatTtlDetailed(seconds) {
|
|
604
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return void 0;
|
|
605
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
606
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
607
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
608
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
609
|
+
const secs = totalSeconds % 60;
|
|
610
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
611
|
+
return `${days} ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
612
|
+
}
|
|
525
613
|
async registerUser(user, password) {
|
|
526
614
|
if (!user) throw new Error("user is required");
|
|
527
615
|
if (!password) throw new Error("password is required");
|
|
@@ -542,16 +630,22 @@ var Revlm = class {
|
|
|
542
630
|
async ensureCookieSupport() {
|
|
543
631
|
if (this.cookieCheckPromise) return this.cookieCheckPromise;
|
|
544
632
|
this.cookieCheckPromise = (async () => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
633
|
+
try {
|
|
634
|
+
const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
635
|
+
this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
|
|
636
|
+
if (first.ok) {
|
|
637
|
+
this.refreshMode = "cookie";
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (first.reason !== "cookie_missing") {
|
|
641
|
+
this.refreshMode = "header";
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
645
|
+
this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
|
|
646
|
+
this.refreshMode = second.ok ? "cookie" : "header";
|
|
647
|
+
} catch {
|
|
648
|
+
this.refreshMode = "header";
|
|
555
649
|
}
|
|
556
650
|
})();
|
|
557
651
|
return this.cookieCheckPromise;
|
package/dist/index.d.mts
CHANGED
|
@@ -174,13 +174,10 @@ type UserInput = Omit<User, 'userType'> & {
|
|
|
174
174
|
userType: User['userType'] | string;
|
|
175
175
|
};
|
|
176
176
|
type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
177
|
-
type CookieStore = {
|
|
178
|
-
getCookieHeader: (url: string) => string | undefined | Promise<string | undefined>;
|
|
179
|
-
setCookie: (url: string, setCookieHeader: string) => void | Promise<void>;
|
|
180
|
-
};
|
|
181
177
|
type RevlmOptions = {
|
|
182
178
|
fetchImpl?: typeof fetch;
|
|
183
179
|
defaultHeaders?: Record<string, string>;
|
|
180
|
+
stateStore?: RevlmStateStore;
|
|
184
181
|
provisionalEnabled?: boolean;
|
|
185
182
|
provisionalAuthSecretMaster?: string;
|
|
186
183
|
provisionalAuthDomain?: string;
|
|
@@ -189,7 +186,11 @@ type RevlmOptions = {
|
|
|
189
186
|
logLevel?: LogLevel;
|
|
190
187
|
sessionId?: string;
|
|
191
188
|
sessionIdProvider?: () => string | Promise<string>;
|
|
192
|
-
|
|
189
|
+
};
|
|
190
|
+
type RevlmStateStore = {
|
|
191
|
+
get: (key: string) => Promise<string | undefined>;
|
|
192
|
+
set: (key: string, value: string) => Promise<void>;
|
|
193
|
+
remove: (key: string) => Promise<void>;
|
|
193
194
|
};
|
|
194
195
|
type RevlmResponse<T = any> = {
|
|
195
196
|
ok: boolean;
|
|
@@ -214,16 +215,22 @@ declare class Revlm {
|
|
|
214
215
|
private refreshPromise;
|
|
215
216
|
private sessionId;
|
|
216
217
|
private sessionIdProvider;
|
|
217
|
-
private
|
|
218
|
+
private refreshSecret;
|
|
219
|
+
private refreshMode;
|
|
220
|
+
private stateStore;
|
|
218
221
|
constructor(baseUrl: string, opts?: RevlmOptions);
|
|
219
222
|
private resolveSessionId;
|
|
220
|
-
private
|
|
221
|
-
private
|
|
223
|
+
private normalizeCookieValue;
|
|
224
|
+
private updateRefreshSecretFromResponse;
|
|
225
|
+
private ensureRefreshSecretLoaded;
|
|
226
|
+
private clearRefreshSecret;
|
|
222
227
|
private canLog;
|
|
223
228
|
logError(...args: any[]): void;
|
|
224
229
|
logWarn(...args: any[]): void;
|
|
225
230
|
logInfo(...args: any[]): void;
|
|
226
231
|
logDebug(...args: any[]): void;
|
|
232
|
+
private maskSecret;
|
|
233
|
+
private logRefreshFailureClient;
|
|
227
234
|
setToken(token: string): void;
|
|
228
235
|
getToken(): string | undefined;
|
|
229
236
|
clearToken(): void;
|
|
@@ -241,6 +248,8 @@ declare class Revlm {
|
|
|
241
248
|
private requestWithRetry;
|
|
242
249
|
login(authId: string, password: string): Promise<LoginResponse>;
|
|
243
250
|
provisionalLogin(authId: string): Promise<ProvisionalLoginResponse>;
|
|
251
|
+
private formatUnixSeconds;
|
|
252
|
+
private formatTtlDetailed;
|
|
244
253
|
registerUser(user: UserInput, password: string): Promise<RegisterUserResponse>;
|
|
245
254
|
deleteUser(params: {
|
|
246
255
|
_id?: any;
|
|
@@ -300,4 +309,4 @@ declare const BSON: typeof bson & {
|
|
|
300
309
|
ObjectID: typeof bson.ObjectId;
|
|
301
310
|
};
|
|
302
311
|
|
|
303
|
-
export { type AggregatePipelineStage, App, BSON, type BaseChangeEvent, type ChangeEvent, type ChangeEventId, type
|
|
312
|
+
export { type AggregatePipelineStage, App, BSON, type BaseChangeEvent, type ChangeEvent, type ChangeEventId, type CountOptions, Credentials, type DeleteEvent, type DeleteResult, type DeleteUserResponse, type DeleteUserSuccess, type Document, type DocumentKey, type DocumentNamespace, type DropDatabaseEvent, type DropEvent, type Filter, type FindOneAndModifyOptions, type FindOneOptions, type FindOptions, type InsertEvent, type InsertManyResult, type InsertOneResult, type InvalidateEvent, type LoginResponse, type LoginSuccess, MdbCollection, MongoDBService, type NewDocument, ObjectID, ObjectId, type OperationType, type ProvisionalLoginResponse, type ProvisionalLoginSuccess, type RegisterUserResponse, type RegisterUserSuccess, type RenameEvent, type ReplaceEvent, Revlm, RevlmDBDatabase, type RevlmErrorResponse, type RevlmOptions, type RevlmResponse, type RevlmStateStore, RevlmUser, type Update, type UpdateDescription, type UpdateEvent, type UpdateOptions, type UpdateResult, type User, type UserBase, type WatchOptionsFilter, type WatchOptionsIds };
|
package/dist/index.d.ts
CHANGED
|
@@ -174,13 +174,10 @@ type UserInput = Omit<User, 'userType'> & {
|
|
|
174
174
|
userType: User['userType'] | string;
|
|
175
175
|
};
|
|
176
176
|
type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
177
|
-
type CookieStore = {
|
|
178
|
-
getCookieHeader: (url: string) => string | undefined | Promise<string | undefined>;
|
|
179
|
-
setCookie: (url: string, setCookieHeader: string) => void | Promise<void>;
|
|
180
|
-
};
|
|
181
177
|
type RevlmOptions = {
|
|
182
178
|
fetchImpl?: typeof fetch;
|
|
183
179
|
defaultHeaders?: Record<string, string>;
|
|
180
|
+
stateStore?: RevlmStateStore;
|
|
184
181
|
provisionalEnabled?: boolean;
|
|
185
182
|
provisionalAuthSecretMaster?: string;
|
|
186
183
|
provisionalAuthDomain?: string;
|
|
@@ -189,7 +186,11 @@ type RevlmOptions = {
|
|
|
189
186
|
logLevel?: LogLevel;
|
|
190
187
|
sessionId?: string;
|
|
191
188
|
sessionIdProvider?: () => string | Promise<string>;
|
|
192
|
-
|
|
189
|
+
};
|
|
190
|
+
type RevlmStateStore = {
|
|
191
|
+
get: (key: string) => Promise<string | undefined>;
|
|
192
|
+
set: (key: string, value: string) => Promise<void>;
|
|
193
|
+
remove: (key: string) => Promise<void>;
|
|
193
194
|
};
|
|
194
195
|
type RevlmResponse<T = any> = {
|
|
195
196
|
ok: boolean;
|
|
@@ -214,16 +215,22 @@ declare class Revlm {
|
|
|
214
215
|
private refreshPromise;
|
|
215
216
|
private sessionId;
|
|
216
217
|
private sessionIdProvider;
|
|
217
|
-
private
|
|
218
|
+
private refreshSecret;
|
|
219
|
+
private refreshMode;
|
|
220
|
+
private stateStore;
|
|
218
221
|
constructor(baseUrl: string, opts?: RevlmOptions);
|
|
219
222
|
private resolveSessionId;
|
|
220
|
-
private
|
|
221
|
-
private
|
|
223
|
+
private normalizeCookieValue;
|
|
224
|
+
private updateRefreshSecretFromResponse;
|
|
225
|
+
private ensureRefreshSecretLoaded;
|
|
226
|
+
private clearRefreshSecret;
|
|
222
227
|
private canLog;
|
|
223
228
|
logError(...args: any[]): void;
|
|
224
229
|
logWarn(...args: any[]): void;
|
|
225
230
|
logInfo(...args: any[]): void;
|
|
226
231
|
logDebug(...args: any[]): void;
|
|
232
|
+
private maskSecret;
|
|
233
|
+
private logRefreshFailureClient;
|
|
227
234
|
setToken(token: string): void;
|
|
228
235
|
getToken(): string | undefined;
|
|
229
236
|
clearToken(): void;
|
|
@@ -241,6 +248,8 @@ declare class Revlm {
|
|
|
241
248
|
private requestWithRetry;
|
|
242
249
|
login(authId: string, password: string): Promise<LoginResponse>;
|
|
243
250
|
provisionalLogin(authId: string): Promise<ProvisionalLoginResponse>;
|
|
251
|
+
private formatUnixSeconds;
|
|
252
|
+
private formatTtlDetailed;
|
|
244
253
|
registerUser(user: UserInput, password: string): Promise<RegisterUserResponse>;
|
|
245
254
|
deleteUser(params: {
|
|
246
255
|
_id?: any;
|
|
@@ -300,4 +309,4 @@ declare const BSON: typeof bson & {
|
|
|
300
309
|
ObjectID: typeof bson.ObjectId;
|
|
301
310
|
};
|
|
302
311
|
|
|
303
|
-
export { type AggregatePipelineStage, App, BSON, type BaseChangeEvent, type ChangeEvent, type ChangeEventId, type
|
|
312
|
+
export { type AggregatePipelineStage, App, BSON, type BaseChangeEvent, type ChangeEvent, type ChangeEventId, type CountOptions, Credentials, type DeleteEvent, type DeleteResult, type DeleteUserResponse, type DeleteUserSuccess, type Document, type DocumentKey, type DocumentNamespace, type DropDatabaseEvent, type DropEvent, type Filter, type FindOneAndModifyOptions, type FindOneOptions, type FindOptions, type InsertEvent, type InsertManyResult, type InsertOneResult, type InvalidateEvent, type LoginResponse, type LoginSuccess, MdbCollection, MongoDBService, type NewDocument, ObjectID, ObjectId, type OperationType, type ProvisionalLoginResponse, type ProvisionalLoginSuccess, type RegisterUserResponse, type RegisterUserSuccess, type RenameEvent, type ReplaceEvent, Revlm, RevlmDBDatabase, type RevlmErrorResponse, type RevlmOptions, type RevlmResponse, type RevlmStateStore, RevlmUser, type Update, type UpdateDescription, type UpdateEvent, type UpdateOptions, type UpdateResult, type User, type UserBase, type WatchOptionsFilter, type WatchOptionsIds };
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ var require_package = __commonJS({
|
|
|
35
35
|
"package.json"(exports2, module2) {
|
|
36
36
|
module2.exports = {
|
|
37
37
|
name: "@kedaruma/revlm-client",
|
|
38
|
-
version: "1.0.
|
|
38
|
+
version: "1.0.62",
|
|
39
39
|
private: false,
|
|
40
40
|
description: "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
41
41
|
keywords: [
|
|
@@ -233,6 +233,9 @@ var LOG_LEVEL_RANK = {
|
|
|
233
233
|
debug: 3
|
|
234
234
|
};
|
|
235
235
|
var SESSION_HEADER_NAME = "x-revlm-session-id";
|
|
236
|
+
var STATE_STORE_KEYS = {
|
|
237
|
+
refreshSecret: "refreshSecret"
|
|
238
|
+
};
|
|
236
239
|
function normalizeLogLevel(value) {
|
|
237
240
|
if (!value) return "info";
|
|
238
241
|
const lowered = value.toLowerCase();
|
|
@@ -277,6 +280,14 @@ function getRevlmClientVersion() {
|
|
|
277
280
|
const globalVersion = globalThis?.REVLM_CLIENT_VERSION;
|
|
278
281
|
return typeof globalVersion === "string" ? globalVersion : "unknown";
|
|
279
282
|
}
|
|
283
|
+
function resolveFetchImpl(fetchImpl) {
|
|
284
|
+
if (fetchImpl) return fetchImpl;
|
|
285
|
+
const globalFetch = globalThis?.fetch;
|
|
286
|
+
if (typeof globalFetch === "function") {
|
|
287
|
+
return globalFetch.bind(globalThis);
|
|
288
|
+
}
|
|
289
|
+
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
290
|
+
}
|
|
280
291
|
var Revlm = class {
|
|
281
292
|
baseUrl;
|
|
282
293
|
fetchImpl;
|
|
@@ -292,11 +303,13 @@ var Revlm = class {
|
|
|
292
303
|
refreshPromise;
|
|
293
304
|
sessionId;
|
|
294
305
|
sessionIdProvider;
|
|
295
|
-
|
|
306
|
+
refreshSecret;
|
|
307
|
+
refreshMode = "unknown";
|
|
308
|
+
stateStore;
|
|
296
309
|
constructor(baseUrl, opts = {}) {
|
|
297
310
|
if (!baseUrl) throw new Error("baseUrl is required");
|
|
298
311
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
299
|
-
this.fetchImpl = opts.fetchImpl
|
|
312
|
+
this.fetchImpl = resolveFetchImpl(opts.fetchImpl);
|
|
300
313
|
this.defaultHeaders = opts.defaultHeaders || {};
|
|
301
314
|
this.provisionalEnabled = opts.provisionalEnabled || false;
|
|
302
315
|
this.provisionalAuthSecretMaster = opts.provisionalAuthSecretMaster || "";
|
|
@@ -306,10 +319,7 @@ var Revlm = class {
|
|
|
306
319
|
this.logLevel = normalizeLogLevel(opts.logLevel);
|
|
307
320
|
this.sessionId = opts.sessionId;
|
|
308
321
|
this.sessionIdProvider = opts.sessionIdProvider;
|
|
309
|
-
this.
|
|
310
|
-
if (!this.fetchImpl) {
|
|
311
|
-
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
312
|
-
}
|
|
322
|
+
this.stateStore = opts.stateStore;
|
|
313
323
|
this.logInfo("\u{1F680} Revlm Client Init", {
|
|
314
324
|
version: getRevlmClientVersion(),
|
|
315
325
|
baseUrl: this.baseUrl,
|
|
@@ -335,18 +345,33 @@ var Revlm = class {
|
|
|
335
345
|
this.sessionId = generateSessionId();
|
|
336
346
|
return this.sessionId;
|
|
337
347
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const cookieHeader = await this.cookieStore.getCookieHeader(url);
|
|
342
|
-
if (cookieHeader) headers.cookie = cookieHeader;
|
|
348
|
+
normalizeCookieValue(value) {
|
|
349
|
+
if (!value) return void 0;
|
|
350
|
+
return value.replace(/^"+|"+$/g, "");
|
|
343
351
|
}
|
|
344
|
-
async
|
|
345
|
-
if (!this.cookieStore) return;
|
|
352
|
+
async updateRefreshSecretFromResponse(res) {
|
|
346
353
|
const setCookies = getSetCookieHeaders(res);
|
|
347
354
|
if (!setCookies.length) return;
|
|
348
355
|
for (const setCookie of setCookies) {
|
|
349
|
-
|
|
356
|
+
const match = setCookie.match(/revlm_refresh=([^;]+)/);
|
|
357
|
+
if (match && match[1]) {
|
|
358
|
+
const normalized = this.normalizeCookieValue(match[1]);
|
|
359
|
+
this.refreshSecret = normalized;
|
|
360
|
+
if (this.stateStore && normalized) {
|
|
361
|
+
await this.stateStore.set(STATE_STORE_KEYS.refreshSecret, normalized);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async ensureRefreshSecretLoaded() {
|
|
367
|
+
if (this.refreshSecret || !this.stateStore) return;
|
|
368
|
+
const stored = await this.stateStore.get(STATE_STORE_KEYS.refreshSecret);
|
|
369
|
+
if (stored) this.refreshSecret = stored;
|
|
370
|
+
}
|
|
371
|
+
async clearRefreshSecret() {
|
|
372
|
+
this.refreshSecret = void 0;
|
|
373
|
+
if (this.stateStore) {
|
|
374
|
+
await this.stateStore.remove(STATE_STORE_KEYS.refreshSecret);
|
|
350
375
|
}
|
|
351
376
|
}
|
|
352
377
|
canLog(level) {
|
|
@@ -364,6 +389,25 @@ var Revlm = class {
|
|
|
364
389
|
logDebug(...args) {
|
|
365
390
|
if (this.canLog("debug")) console.log(...args);
|
|
366
391
|
}
|
|
392
|
+
maskSecret(value, head = 10, tail = 6) {
|
|
393
|
+
if (!value) return "<empty>";
|
|
394
|
+
if (value.length <= head + tail) return value;
|
|
395
|
+
return `${value.slice(0, head)}***${value.slice(-tail)}`;
|
|
396
|
+
}
|
|
397
|
+
logRefreshFailureClient(params) {
|
|
398
|
+
const recoverable = typeof params.code === "number" ? params.code >= 1e4 && params.code < 2e4 : params.status === 400 || params.status === 401;
|
|
399
|
+
const line1 = `[refresh-token][${recoverable ? "debug" : "error"}][${recoverable ? "recoverable" : "fatal"}] cause=${params.cause}`;
|
|
400
|
+
const line2 = `session=${this.maskSecret(params.sessionId)} refresh=${this.maskSecret(params.refreshSecret)}`;
|
|
401
|
+
const line3 = `details=${JSON.stringify({
|
|
402
|
+
status: params.status,
|
|
403
|
+
reason: params.reason,
|
|
404
|
+
code: params.code
|
|
405
|
+
})}`;
|
|
406
|
+
this.logError(line1);
|
|
407
|
+
this.logError(line2);
|
|
408
|
+
this.logError(line3);
|
|
409
|
+
this.logError("");
|
|
410
|
+
}
|
|
367
411
|
setToken(token) {
|
|
368
412
|
this._token = token;
|
|
369
413
|
}
|
|
@@ -480,7 +524,12 @@ var Revlm = class {
|
|
|
480
524
|
const headers = this.makeHeaders(hasBody);
|
|
481
525
|
const sessionId = await this.resolveSessionId();
|
|
482
526
|
if (sessionId) headers[SESSION_HEADER_NAME] = sessionId;
|
|
483
|
-
|
|
527
|
+
if (url.includes("/refresh-token") && this.refreshMode === "header") {
|
|
528
|
+
await this.ensureRefreshSecretLoaded();
|
|
529
|
+
if (this.refreshSecret) {
|
|
530
|
+
headers["x-revlm-refresh"] = this.refreshSecret;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
484
533
|
let serializedBody;
|
|
485
534
|
if (hasBody) {
|
|
486
535
|
serializedBody = import_bson.EJSON.stringify(body);
|
|
@@ -490,9 +539,10 @@ var Revlm = class {
|
|
|
490
539
|
const res = await this.fetchImpl(signedUrl, {
|
|
491
540
|
method,
|
|
492
541
|
headers: signedHeaders,
|
|
493
|
-
body: serializedBody
|
|
542
|
+
body: serializedBody,
|
|
543
|
+
credentials: url.includes("/refresh-token") && this.refreshMode === "header" ? "omit" : "include"
|
|
494
544
|
});
|
|
495
|
-
await this.
|
|
545
|
+
await this.updateRefreshSecretFromResponse(res);
|
|
496
546
|
const parsed = await this.parseResponse(res);
|
|
497
547
|
const out = parsed && typeof parsed === "object" ? parsed : { ok: res.ok, result: parsed };
|
|
498
548
|
out.status = res.status;
|
|
@@ -509,12 +559,26 @@ var Revlm = class {
|
|
|
509
559
|
const refreshFailed = {
|
|
510
560
|
reason: refreshRes.reason,
|
|
511
561
|
status: refreshRes.status,
|
|
512
|
-
error: refreshRes.error
|
|
562
|
+
error: refreshRes.error,
|
|
563
|
+
code: refreshRes.code
|
|
513
564
|
};
|
|
514
565
|
this.logDebug("### refresh failed:", refreshFailed, JSON.stringify(refreshFailed));
|
|
566
|
+
const refreshSecret = this.refreshSecret;
|
|
567
|
+
const refreshFailureLog = {
|
|
568
|
+
cause: refreshRes.reason || "refresh_failed",
|
|
569
|
+
reason: refreshFailed.reason,
|
|
570
|
+
status: refreshFailed.status,
|
|
571
|
+
code: refreshFailed.code
|
|
572
|
+
};
|
|
573
|
+
if (sessionId) refreshFailureLog.sessionId = sessionId;
|
|
574
|
+
if (refreshSecret) refreshFailureLog.refreshSecret = refreshSecret;
|
|
575
|
+
this.logRefreshFailureClient(refreshFailureLog);
|
|
515
576
|
if (refreshRes.reason === "no_refresh_secret") {
|
|
577
|
+
await this.clearRefreshSecret();
|
|
516
578
|
const missingError = new Error("Refresh cookie missing. Provide a cookie-aware fetch implementation for Node/RN.");
|
|
517
579
|
missingError.revlmReason = "no_refresh_secret";
|
|
580
|
+
missingError.revlmCode = refreshRes.code;
|
|
581
|
+
missingError.httpStatus = refreshRes.status;
|
|
518
582
|
throw missingError;
|
|
519
583
|
}
|
|
520
584
|
}
|
|
@@ -523,12 +587,17 @@ var Revlm = class {
|
|
|
523
587
|
const now = Math.floor(Date.now() / 1e3);
|
|
524
588
|
const oldExp = beforePayload?.exp;
|
|
525
589
|
const newExp = afterPayload?.exp;
|
|
590
|
+
const oldTtlSec = typeof oldExp === "number" ? oldExp - now : void 0;
|
|
591
|
+
const newTtlSec = typeof newExp === "number" ? newExp - now : void 0;
|
|
592
|
+
const oldExpDisplay = this.formatUnixSeconds(oldExp);
|
|
593
|
+
const newExpDisplay = this.formatUnixSeconds(newExp);
|
|
594
|
+
const ttlDisplay = this.formatTtlDetailed(newTtlSec);
|
|
526
595
|
this.logDebug("### refresh success", {
|
|
527
596
|
path,
|
|
528
|
-
oldExp,
|
|
529
|
-
newExp,
|
|
530
|
-
oldTtlSec
|
|
531
|
-
newTtlSec: typeof
|
|
597
|
+
oldExp: oldExpDisplay ? `${oldExp} (${oldExpDisplay})` : oldExp,
|
|
598
|
+
newExp: newExpDisplay ? `${newExp} (${newExpDisplay})` : newExp,
|
|
599
|
+
oldTtlSec,
|
|
600
|
+
newTtlSec: typeof newTtlSec === "number" && ttlDisplay ? `${newTtlSec} (${ttlDisplay})` : newTtlSec
|
|
532
601
|
});
|
|
533
602
|
return this.requestWithRetry(path, method, body, { allowAuthRetry: false, retrying: true });
|
|
534
603
|
}
|
|
@@ -557,7 +626,10 @@ var Revlm = class {
|
|
|
557
626
|
}
|
|
558
627
|
await this.ensureCookieSupport();
|
|
559
628
|
if (!authId) throw new Error("authId is required");
|
|
560
|
-
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
629
|
+
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
630
|
+
secretMaster: this.provisionalAuthSecretMaster,
|
|
631
|
+
authDomain: this.provisionalAuthDomain
|
|
632
|
+
});
|
|
561
633
|
const provisionalPassword = await provisionalClient.producePassword(String(Date.now() * 1e3));
|
|
562
634
|
const res = await this.request("/provisional-login", "POST", { authId, password: provisionalPassword });
|
|
563
635
|
if (this.autoSetToken && res && res.ok && res.token) {
|
|
@@ -565,6 +637,22 @@ var Revlm = class {
|
|
|
565
637
|
}
|
|
566
638
|
return res;
|
|
567
639
|
}
|
|
640
|
+
formatUnixSeconds(epochSeconds) {
|
|
641
|
+
if (typeof epochSeconds !== "number" || !Number.isFinite(epochSeconds)) return void 0;
|
|
642
|
+
const date = new Date(epochSeconds * 1e3);
|
|
643
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
644
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
645
|
+
}
|
|
646
|
+
formatTtlDetailed(seconds) {
|
|
647
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return void 0;
|
|
648
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
649
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
650
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
651
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
652
|
+
const secs = totalSeconds % 60;
|
|
653
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
654
|
+
return `${days} ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
655
|
+
}
|
|
568
656
|
async registerUser(user, password) {
|
|
569
657
|
if (!user) throw new Error("user is required");
|
|
570
658
|
if (!password) throw new Error("password is required");
|
|
@@ -585,16 +673,22 @@ var Revlm = class {
|
|
|
585
673
|
async ensureCookieSupport() {
|
|
586
674
|
if (this.cookieCheckPromise) return this.cookieCheckPromise;
|
|
587
675
|
this.cookieCheckPromise = (async () => {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
676
|
+
try {
|
|
677
|
+
const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
678
|
+
this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
|
|
679
|
+
if (first.ok) {
|
|
680
|
+
this.refreshMode = "cookie";
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (first.reason !== "cookie_missing") {
|
|
684
|
+
this.refreshMode = "header";
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
688
|
+
this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
|
|
689
|
+
this.refreshMode = second.ok ? "cookie" : "header";
|
|
690
|
+
} catch {
|
|
691
|
+
this.refreshMode = "header";
|
|
598
692
|
}
|
|
599
693
|
})();
|
|
600
694
|
return this.cookieCheckPromise;
|
package/dist/index.mjs
CHANGED
package/dist/revlm-compat.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MdbCollection, Document, NewDocument, Filter, Update, AggregatePipelineStage, FindOneOptions, FindOptions, FindOneAndModifyOptions, CountOptions, UpdateOptions, InsertOneResult, InsertManyResult, DeleteResult, UpdateResult, ChangeEvent, UpdateDescription, WatchOptionsIds, WatchOptionsFilter } from './index.mjs';
|
|
2
|
-
export { App, BSON, BaseChangeEvent, ChangeEventId,
|
|
2
|
+
export { App, BSON, BaseChangeEvent, ChangeEventId, Credentials, DeleteEvent, DeleteUserResponse, DeleteUserSuccess, DocumentKey, DocumentNamespace, DropDatabaseEvent, DropEvent, InsertEvent, InvalidateEvent, LoginResponse, LoginSuccess, MongoDBService, ObjectID, ObjectId, OperationType, ProvisionalLoginResponse, ProvisionalLoginSuccess, RegisterUserResponse, RegisterUserSuccess, RenameEvent, ReplaceEvent, Revlm, RevlmErrorResponse, RevlmOptions, RevlmResponse, RevlmStateStore, RevlmUser, UpdateEvent, User, UserBase } from './index.mjs';
|
|
3
3
|
import '@kedaruma/revlm-shared/models/mongo-doc-base-types';
|
|
4
4
|
import 'bson';
|
|
5
5
|
import '@kedaruma/revlm-shared/models/user-types';
|
package/dist/revlm-compat.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MdbCollection, Document, NewDocument, Filter, Update, AggregatePipelineStage, FindOneOptions, FindOptions, FindOneAndModifyOptions, CountOptions, UpdateOptions, InsertOneResult, InsertManyResult, DeleteResult, UpdateResult, ChangeEvent, UpdateDescription, WatchOptionsIds, WatchOptionsFilter } from './index.js';
|
|
2
|
-
export { App, BSON, BaseChangeEvent, ChangeEventId,
|
|
2
|
+
export { App, BSON, BaseChangeEvent, ChangeEventId, Credentials, DeleteEvent, DeleteUserResponse, DeleteUserSuccess, DocumentKey, DocumentNamespace, DropDatabaseEvent, DropEvent, InsertEvent, InvalidateEvent, LoginResponse, LoginSuccess, MongoDBService, ObjectID, ObjectId, OperationType, ProvisionalLoginResponse, ProvisionalLoginSuccess, RegisterUserResponse, RegisterUserSuccess, RenameEvent, ReplaceEvent, Revlm, RevlmErrorResponse, RevlmOptions, RevlmResponse, RevlmStateStore, RevlmUser, UpdateEvent, User, UserBase } from './index.js';
|
|
3
3
|
import '@kedaruma/revlm-shared/models/mongo-doc-base-types';
|
|
4
4
|
import 'bson';
|
|
5
5
|
import '@kedaruma/revlm-shared/models/user-types';
|
package/dist/revlm-compat.js
CHANGED
|
@@ -35,7 +35,7 @@ var require_package = __commonJS({
|
|
|
35
35
|
"package.json"(exports2, module2) {
|
|
36
36
|
module2.exports = {
|
|
37
37
|
name: "@kedaruma/revlm-client",
|
|
38
|
-
version: "1.0.
|
|
38
|
+
version: "1.0.62",
|
|
39
39
|
private: false,
|
|
40
40
|
description: "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
41
41
|
keywords: [
|
|
@@ -231,6 +231,9 @@ var LOG_LEVEL_RANK = {
|
|
|
231
231
|
debug: 3
|
|
232
232
|
};
|
|
233
233
|
var SESSION_HEADER_NAME = "x-revlm-session-id";
|
|
234
|
+
var STATE_STORE_KEYS = {
|
|
235
|
+
refreshSecret: "refreshSecret"
|
|
236
|
+
};
|
|
234
237
|
function normalizeLogLevel(value) {
|
|
235
238
|
if (!value) return "info";
|
|
236
239
|
const lowered = value.toLowerCase();
|
|
@@ -275,6 +278,14 @@ function getRevlmClientVersion() {
|
|
|
275
278
|
const globalVersion = globalThis?.REVLM_CLIENT_VERSION;
|
|
276
279
|
return typeof globalVersion === "string" ? globalVersion : "unknown";
|
|
277
280
|
}
|
|
281
|
+
function resolveFetchImpl(fetchImpl) {
|
|
282
|
+
if (fetchImpl) return fetchImpl;
|
|
283
|
+
const globalFetch = globalThis?.fetch;
|
|
284
|
+
if (typeof globalFetch === "function") {
|
|
285
|
+
return globalFetch.bind(globalThis);
|
|
286
|
+
}
|
|
287
|
+
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
288
|
+
}
|
|
278
289
|
var Revlm = class {
|
|
279
290
|
baseUrl;
|
|
280
291
|
fetchImpl;
|
|
@@ -290,11 +301,13 @@ var Revlm = class {
|
|
|
290
301
|
refreshPromise;
|
|
291
302
|
sessionId;
|
|
292
303
|
sessionIdProvider;
|
|
293
|
-
|
|
304
|
+
refreshSecret;
|
|
305
|
+
refreshMode = "unknown";
|
|
306
|
+
stateStore;
|
|
294
307
|
constructor(baseUrl, opts = {}) {
|
|
295
308
|
if (!baseUrl) throw new Error("baseUrl is required");
|
|
296
309
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
297
|
-
this.fetchImpl = opts.fetchImpl
|
|
310
|
+
this.fetchImpl = resolveFetchImpl(opts.fetchImpl);
|
|
298
311
|
this.defaultHeaders = opts.defaultHeaders || {};
|
|
299
312
|
this.provisionalEnabled = opts.provisionalEnabled || false;
|
|
300
313
|
this.provisionalAuthSecretMaster = opts.provisionalAuthSecretMaster || "";
|
|
@@ -304,10 +317,7 @@ var Revlm = class {
|
|
|
304
317
|
this.logLevel = normalizeLogLevel(opts.logLevel);
|
|
305
318
|
this.sessionId = opts.sessionId;
|
|
306
319
|
this.sessionIdProvider = opts.sessionIdProvider;
|
|
307
|
-
this.
|
|
308
|
-
if (!this.fetchImpl) {
|
|
309
|
-
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
310
|
-
}
|
|
320
|
+
this.stateStore = opts.stateStore;
|
|
311
321
|
this.logInfo("\u{1F680} Revlm Client Init", {
|
|
312
322
|
version: getRevlmClientVersion(),
|
|
313
323
|
baseUrl: this.baseUrl,
|
|
@@ -333,18 +343,33 @@ var Revlm = class {
|
|
|
333
343
|
this.sessionId = generateSessionId();
|
|
334
344
|
return this.sessionId;
|
|
335
345
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const cookieHeader = await this.cookieStore.getCookieHeader(url);
|
|
340
|
-
if (cookieHeader) headers.cookie = cookieHeader;
|
|
346
|
+
normalizeCookieValue(value) {
|
|
347
|
+
if (!value) return void 0;
|
|
348
|
+
return value.replace(/^"+|"+$/g, "");
|
|
341
349
|
}
|
|
342
|
-
async
|
|
343
|
-
if (!this.cookieStore) return;
|
|
350
|
+
async updateRefreshSecretFromResponse(res) {
|
|
344
351
|
const setCookies = getSetCookieHeaders(res);
|
|
345
352
|
if (!setCookies.length) return;
|
|
346
353
|
for (const setCookie of setCookies) {
|
|
347
|
-
|
|
354
|
+
const match = setCookie.match(/revlm_refresh=([^;]+)/);
|
|
355
|
+
if (match && match[1]) {
|
|
356
|
+
const normalized = this.normalizeCookieValue(match[1]);
|
|
357
|
+
this.refreshSecret = normalized;
|
|
358
|
+
if (this.stateStore && normalized) {
|
|
359
|
+
await this.stateStore.set(STATE_STORE_KEYS.refreshSecret, normalized);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async ensureRefreshSecretLoaded() {
|
|
365
|
+
if (this.refreshSecret || !this.stateStore) return;
|
|
366
|
+
const stored = await this.stateStore.get(STATE_STORE_KEYS.refreshSecret);
|
|
367
|
+
if (stored) this.refreshSecret = stored;
|
|
368
|
+
}
|
|
369
|
+
async clearRefreshSecret() {
|
|
370
|
+
this.refreshSecret = void 0;
|
|
371
|
+
if (this.stateStore) {
|
|
372
|
+
await this.stateStore.remove(STATE_STORE_KEYS.refreshSecret);
|
|
348
373
|
}
|
|
349
374
|
}
|
|
350
375
|
canLog(level) {
|
|
@@ -362,6 +387,25 @@ var Revlm = class {
|
|
|
362
387
|
logDebug(...args) {
|
|
363
388
|
if (this.canLog("debug")) console.log(...args);
|
|
364
389
|
}
|
|
390
|
+
maskSecret(value, head = 10, tail = 6) {
|
|
391
|
+
if (!value) return "<empty>";
|
|
392
|
+
if (value.length <= head + tail) return value;
|
|
393
|
+
return `${value.slice(0, head)}***${value.slice(-tail)}`;
|
|
394
|
+
}
|
|
395
|
+
logRefreshFailureClient(params) {
|
|
396
|
+
const recoverable = typeof params.code === "number" ? params.code >= 1e4 && params.code < 2e4 : params.status === 400 || params.status === 401;
|
|
397
|
+
const line1 = `[refresh-token][${recoverable ? "debug" : "error"}][${recoverable ? "recoverable" : "fatal"}] cause=${params.cause}`;
|
|
398
|
+
const line2 = `session=${this.maskSecret(params.sessionId)} refresh=${this.maskSecret(params.refreshSecret)}`;
|
|
399
|
+
const line3 = `details=${JSON.stringify({
|
|
400
|
+
status: params.status,
|
|
401
|
+
reason: params.reason,
|
|
402
|
+
code: params.code
|
|
403
|
+
})}`;
|
|
404
|
+
this.logError(line1);
|
|
405
|
+
this.logError(line2);
|
|
406
|
+
this.logError(line3);
|
|
407
|
+
this.logError("");
|
|
408
|
+
}
|
|
365
409
|
setToken(token) {
|
|
366
410
|
this._token = token;
|
|
367
411
|
}
|
|
@@ -478,7 +522,12 @@ var Revlm = class {
|
|
|
478
522
|
const headers = this.makeHeaders(hasBody);
|
|
479
523
|
const sessionId = await this.resolveSessionId();
|
|
480
524
|
if (sessionId) headers[SESSION_HEADER_NAME] = sessionId;
|
|
481
|
-
|
|
525
|
+
if (url.includes("/refresh-token") && this.refreshMode === "header") {
|
|
526
|
+
await this.ensureRefreshSecretLoaded();
|
|
527
|
+
if (this.refreshSecret) {
|
|
528
|
+
headers["x-revlm-refresh"] = this.refreshSecret;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
482
531
|
let serializedBody;
|
|
483
532
|
if (hasBody) {
|
|
484
533
|
serializedBody = import_bson.EJSON.stringify(body);
|
|
@@ -488,9 +537,10 @@ var Revlm = class {
|
|
|
488
537
|
const res = await this.fetchImpl(signedUrl, {
|
|
489
538
|
method,
|
|
490
539
|
headers: signedHeaders,
|
|
491
|
-
body: serializedBody
|
|
540
|
+
body: serializedBody,
|
|
541
|
+
credentials: url.includes("/refresh-token") && this.refreshMode === "header" ? "omit" : "include"
|
|
492
542
|
});
|
|
493
|
-
await this.
|
|
543
|
+
await this.updateRefreshSecretFromResponse(res);
|
|
494
544
|
const parsed = await this.parseResponse(res);
|
|
495
545
|
const out = parsed && typeof parsed === "object" ? parsed : { ok: res.ok, result: parsed };
|
|
496
546
|
out.status = res.status;
|
|
@@ -507,12 +557,26 @@ var Revlm = class {
|
|
|
507
557
|
const refreshFailed = {
|
|
508
558
|
reason: refreshRes.reason,
|
|
509
559
|
status: refreshRes.status,
|
|
510
|
-
error: refreshRes.error
|
|
560
|
+
error: refreshRes.error,
|
|
561
|
+
code: refreshRes.code
|
|
511
562
|
};
|
|
512
563
|
this.logDebug("### refresh failed:", refreshFailed, JSON.stringify(refreshFailed));
|
|
564
|
+
const refreshSecret = this.refreshSecret;
|
|
565
|
+
const refreshFailureLog = {
|
|
566
|
+
cause: refreshRes.reason || "refresh_failed",
|
|
567
|
+
reason: refreshFailed.reason,
|
|
568
|
+
status: refreshFailed.status,
|
|
569
|
+
code: refreshFailed.code
|
|
570
|
+
};
|
|
571
|
+
if (sessionId) refreshFailureLog.sessionId = sessionId;
|
|
572
|
+
if (refreshSecret) refreshFailureLog.refreshSecret = refreshSecret;
|
|
573
|
+
this.logRefreshFailureClient(refreshFailureLog);
|
|
513
574
|
if (refreshRes.reason === "no_refresh_secret") {
|
|
575
|
+
await this.clearRefreshSecret();
|
|
514
576
|
const missingError = new Error("Refresh cookie missing. Provide a cookie-aware fetch implementation for Node/RN.");
|
|
515
577
|
missingError.revlmReason = "no_refresh_secret";
|
|
578
|
+
missingError.revlmCode = refreshRes.code;
|
|
579
|
+
missingError.httpStatus = refreshRes.status;
|
|
516
580
|
throw missingError;
|
|
517
581
|
}
|
|
518
582
|
}
|
|
@@ -521,12 +585,17 @@ var Revlm = class {
|
|
|
521
585
|
const now = Math.floor(Date.now() / 1e3);
|
|
522
586
|
const oldExp = beforePayload?.exp;
|
|
523
587
|
const newExp = afterPayload?.exp;
|
|
588
|
+
const oldTtlSec = typeof oldExp === "number" ? oldExp - now : void 0;
|
|
589
|
+
const newTtlSec = typeof newExp === "number" ? newExp - now : void 0;
|
|
590
|
+
const oldExpDisplay = this.formatUnixSeconds(oldExp);
|
|
591
|
+
const newExpDisplay = this.formatUnixSeconds(newExp);
|
|
592
|
+
const ttlDisplay = this.formatTtlDetailed(newTtlSec);
|
|
524
593
|
this.logDebug("### refresh success", {
|
|
525
594
|
path,
|
|
526
|
-
oldExp,
|
|
527
|
-
newExp,
|
|
528
|
-
oldTtlSec
|
|
529
|
-
newTtlSec: typeof
|
|
595
|
+
oldExp: oldExpDisplay ? `${oldExp} (${oldExpDisplay})` : oldExp,
|
|
596
|
+
newExp: newExpDisplay ? `${newExp} (${newExpDisplay})` : newExp,
|
|
597
|
+
oldTtlSec,
|
|
598
|
+
newTtlSec: typeof newTtlSec === "number" && ttlDisplay ? `${newTtlSec} (${ttlDisplay})` : newTtlSec
|
|
530
599
|
});
|
|
531
600
|
return this.requestWithRetry(path, method, body, { allowAuthRetry: false, retrying: true });
|
|
532
601
|
}
|
|
@@ -555,7 +624,10 @@ var Revlm = class {
|
|
|
555
624
|
}
|
|
556
625
|
await this.ensureCookieSupport();
|
|
557
626
|
if (!authId) throw new Error("authId is required");
|
|
558
|
-
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
627
|
+
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
628
|
+
secretMaster: this.provisionalAuthSecretMaster,
|
|
629
|
+
authDomain: this.provisionalAuthDomain
|
|
630
|
+
});
|
|
559
631
|
const provisionalPassword = await provisionalClient.producePassword(String(Date.now() * 1e3));
|
|
560
632
|
const res = await this.request("/provisional-login", "POST", { authId, password: provisionalPassword });
|
|
561
633
|
if (this.autoSetToken && res && res.ok && res.token) {
|
|
@@ -563,6 +635,22 @@ var Revlm = class {
|
|
|
563
635
|
}
|
|
564
636
|
return res;
|
|
565
637
|
}
|
|
638
|
+
formatUnixSeconds(epochSeconds) {
|
|
639
|
+
if (typeof epochSeconds !== "number" || !Number.isFinite(epochSeconds)) return void 0;
|
|
640
|
+
const date = new Date(epochSeconds * 1e3);
|
|
641
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
642
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
643
|
+
}
|
|
644
|
+
formatTtlDetailed(seconds) {
|
|
645
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return void 0;
|
|
646
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
647
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
648
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
649
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
650
|
+
const secs = totalSeconds % 60;
|
|
651
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
652
|
+
return `${days} ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
653
|
+
}
|
|
566
654
|
async registerUser(user, password) {
|
|
567
655
|
if (!user) throw new Error("user is required");
|
|
568
656
|
if (!password) throw new Error("password is required");
|
|
@@ -583,16 +671,22 @@ var Revlm = class {
|
|
|
583
671
|
async ensureCookieSupport() {
|
|
584
672
|
if (this.cookieCheckPromise) return this.cookieCheckPromise;
|
|
585
673
|
this.cookieCheckPromise = (async () => {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
674
|
+
try {
|
|
675
|
+
const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
676
|
+
this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
|
|
677
|
+
if (first.ok) {
|
|
678
|
+
this.refreshMode = "cookie";
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (first.reason !== "cookie_missing") {
|
|
682
|
+
this.refreshMode = "header";
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
686
|
+
this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
|
|
687
|
+
this.refreshMode = second.ok ? "cookie" : "header";
|
|
688
|
+
} catch {
|
|
689
|
+
this.refreshMode = "header";
|
|
596
690
|
}
|
|
597
691
|
})();
|
|
598
692
|
return this.cookieCheckPromise;
|
package/dist/revlm-compat.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kedaruma/revlm-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.62",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
6
6
|
"keywords": [
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"bson": "^6.10.4",
|
|
60
60
|
"dotenv": "^17.2.3",
|
|
61
|
-
"@kedaruma/revlm-shared": "1.0.
|
|
61
|
+
"@kedaruma/revlm-shared": "1.0.12"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"tsup": "^8.5.1"
|