@kedaruma/revlm-client 1.0.60 → 1.0.63
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-367MFQ4K.mjs → chunk-OBJS4AOD.mjs} +126 -42
- package/dist/index.d.mts +16 -10
- package/dist/index.d.ts +16 -10
- package/dist/index.js +126 -42
- 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 +126 -42
- package/dist/revlm-compat.mjs +1 -1
- package/package.json +3 -3
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.63",
|
|
12
12
|
private: false,
|
|
13
13
|
description: "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
14
14
|
keywords: [
|
|
@@ -69,7 +69,7 @@ var require_package = __commonJS({
|
|
|
69
69
|
test: "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ../../jest.config.cjs packages/revlm-client/src/__tests__/ --runInBand --watchman=false --verbose"
|
|
70
70
|
},
|
|
71
71
|
dependencies: {
|
|
72
|
-
"@kedaruma/revlm-shared": "
|
|
72
|
+
"@kedaruma/revlm-shared": "latest",
|
|
73
73
|
bson: "^6.10.4",
|
|
74
74
|
dotenv: "^17.2.3"
|
|
75
75
|
},
|
|
@@ -83,6 +83,7 @@ var require_package = __commonJS({
|
|
|
83
83
|
// src/Revlm.ts
|
|
84
84
|
import { EJSON } from "bson";
|
|
85
85
|
import { AuthClient } from "@kedaruma/revlm-shared";
|
|
86
|
+
import { initRandomBytes } from "@kedaruma/revlm-shared/random-bytes";
|
|
86
87
|
|
|
87
88
|
// src/MdbCollection.ts
|
|
88
89
|
var MdbCollection = class {
|
|
@@ -190,6 +191,36 @@ var LOG_LEVEL_RANK = {
|
|
|
190
191
|
debug: 3
|
|
191
192
|
};
|
|
192
193
|
var SESSION_HEADER_NAME = "x-revlm-session-id";
|
|
194
|
+
var STATE_STORE_KEYS = {
|
|
195
|
+
refreshSecret: "refreshSecret"
|
|
196
|
+
};
|
|
197
|
+
var randomBytesInitAttempted = false;
|
|
198
|
+
function resolvePlatformRandomBytes() {
|
|
199
|
+
const cryptoLike = globalThis?.crypto;
|
|
200
|
+
if (cryptoLike && typeof cryptoLike.getRandomValues === "function") {
|
|
201
|
+
return (length) => {
|
|
202
|
+
const out = new Uint8Array(length);
|
|
203
|
+
cryptoLike.getRandomValues(out);
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const nodeCrypto = __require("crypto");
|
|
209
|
+
if (typeof nodeCrypto?.randomBytes === "function") {
|
|
210
|
+
return (length) => new Uint8Array(nodeCrypto.randomBytes(length));
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
function ensureRandomBytesInitialized() {
|
|
217
|
+
if (randomBytesInitAttempted) return;
|
|
218
|
+
randomBytesInitAttempted = true;
|
|
219
|
+
const platformRandomBytes = resolvePlatformRandomBytes();
|
|
220
|
+
if (platformRandomBytes) {
|
|
221
|
+
initRandomBytes(platformRandomBytes);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
193
224
|
function normalizeLogLevel(value) {
|
|
194
225
|
if (!value) return "info";
|
|
195
226
|
const lowered = value.toLowerCase();
|
|
@@ -234,6 +265,14 @@ function getRevlmClientVersion() {
|
|
|
234
265
|
const globalVersion = globalThis?.REVLM_CLIENT_VERSION;
|
|
235
266
|
return typeof globalVersion === "string" ? globalVersion : "unknown";
|
|
236
267
|
}
|
|
268
|
+
function resolveFetchImpl(fetchImpl) {
|
|
269
|
+
if (fetchImpl) return fetchImpl;
|
|
270
|
+
const globalFetch = globalThis?.fetch;
|
|
271
|
+
if (typeof globalFetch === "function") {
|
|
272
|
+
return globalFetch.bind(globalThis);
|
|
273
|
+
}
|
|
274
|
+
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
275
|
+
}
|
|
237
276
|
var Revlm = class {
|
|
238
277
|
baseUrl;
|
|
239
278
|
fetchImpl;
|
|
@@ -249,11 +288,14 @@ var Revlm = class {
|
|
|
249
288
|
refreshPromise;
|
|
250
289
|
sessionId;
|
|
251
290
|
sessionIdProvider;
|
|
252
|
-
|
|
291
|
+
refreshSecret;
|
|
292
|
+
refreshMode = "unknown";
|
|
293
|
+
stateStore;
|
|
253
294
|
constructor(baseUrl, opts = {}) {
|
|
254
295
|
if (!baseUrl) throw new Error("baseUrl is required");
|
|
296
|
+
ensureRandomBytesInitialized();
|
|
255
297
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
256
|
-
this.fetchImpl = opts.fetchImpl
|
|
298
|
+
this.fetchImpl = resolveFetchImpl(opts.fetchImpl);
|
|
257
299
|
this.defaultHeaders = opts.defaultHeaders || {};
|
|
258
300
|
this.provisionalEnabled = opts.provisionalEnabled || false;
|
|
259
301
|
this.provisionalAuthSecretMaster = opts.provisionalAuthSecretMaster || "";
|
|
@@ -263,10 +305,7 @@ var Revlm = class {
|
|
|
263
305
|
this.logLevel = normalizeLogLevel(opts.logLevel);
|
|
264
306
|
this.sessionId = opts.sessionId;
|
|
265
307
|
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
|
-
}
|
|
308
|
+
this.stateStore = opts.stateStore;
|
|
270
309
|
this.logInfo("\u{1F680} Revlm Client Init", {
|
|
271
310
|
version: getRevlmClientVersion(),
|
|
272
311
|
baseUrl: this.baseUrl,
|
|
@@ -292,18 +331,33 @@ var Revlm = class {
|
|
|
292
331
|
this.sessionId = generateSessionId();
|
|
293
332
|
return this.sessionId;
|
|
294
333
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const cookieHeader = await this.cookieStore.getCookieHeader(url);
|
|
299
|
-
if (cookieHeader) headers.cookie = cookieHeader;
|
|
334
|
+
normalizeCookieValue(value) {
|
|
335
|
+
if (!value) return void 0;
|
|
336
|
+
return value.replace(/^"+|"+$/g, "");
|
|
300
337
|
}
|
|
301
|
-
async
|
|
302
|
-
if (!this.cookieStore) return;
|
|
338
|
+
async updateRefreshSecretFromResponse(res) {
|
|
303
339
|
const setCookies = getSetCookieHeaders(res);
|
|
304
340
|
if (!setCookies.length) return;
|
|
305
341
|
for (const setCookie of setCookies) {
|
|
306
|
-
|
|
342
|
+
const match = setCookie.match(/revlm_refresh=([^;]+)/);
|
|
343
|
+
if (match && match[1]) {
|
|
344
|
+
const normalized = this.normalizeCookieValue(match[1]);
|
|
345
|
+
this.refreshSecret = normalized;
|
|
346
|
+
if (this.stateStore && normalized) {
|
|
347
|
+
await this.stateStore.set(STATE_STORE_KEYS.refreshSecret, normalized);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async ensureRefreshSecretLoaded() {
|
|
353
|
+
if (this.refreshSecret || !this.stateStore) return;
|
|
354
|
+
const stored = await this.stateStore.get(STATE_STORE_KEYS.refreshSecret);
|
|
355
|
+
if (stored) this.refreshSecret = stored;
|
|
356
|
+
}
|
|
357
|
+
async clearRefreshSecret() {
|
|
358
|
+
this.refreshSecret = void 0;
|
|
359
|
+
if (this.stateStore) {
|
|
360
|
+
await this.stateStore.remove(STATE_STORE_KEYS.refreshSecret);
|
|
307
361
|
}
|
|
308
362
|
}
|
|
309
363
|
canLog(level) {
|
|
@@ -326,11 +380,6 @@ var Revlm = class {
|
|
|
326
380
|
if (value.length <= head + tail) return value;
|
|
327
381
|
return `${value.slice(0, head)}***${value.slice(-tail)}`;
|
|
328
382
|
}
|
|
329
|
-
extractRefreshSecret(cookieHeader) {
|
|
330
|
-
if (!cookieHeader) return void 0;
|
|
331
|
-
const match = cookieHeader.match(/(?:^|;\\s*)revlm_refresh=([^;]+)/);
|
|
332
|
-
return match?.[1];
|
|
333
|
-
}
|
|
334
383
|
logRefreshFailureClient(params) {
|
|
335
384
|
const recoverable = typeof params.code === "number" ? params.code >= 1e4 && params.code < 2e4 : params.status === 400 || params.status === 401;
|
|
336
385
|
const line1 = `[refresh-token][${recoverable ? "debug" : "error"}][${recoverable ? "recoverable" : "fatal"}] cause=${params.cause}`;
|
|
@@ -461,7 +510,12 @@ var Revlm = class {
|
|
|
461
510
|
const headers = this.makeHeaders(hasBody);
|
|
462
511
|
const sessionId = await this.resolveSessionId();
|
|
463
512
|
if (sessionId) headers[SESSION_HEADER_NAME] = sessionId;
|
|
464
|
-
|
|
513
|
+
if (url.includes("/refresh-token") && this.refreshMode === "header") {
|
|
514
|
+
await this.ensureRefreshSecretLoaded();
|
|
515
|
+
if (this.refreshSecret) {
|
|
516
|
+
headers["x-revlm-refresh"] = this.refreshSecret;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
465
519
|
let serializedBody;
|
|
466
520
|
if (hasBody) {
|
|
467
521
|
serializedBody = EJSON.stringify(body);
|
|
@@ -471,9 +525,10 @@ var Revlm = class {
|
|
|
471
525
|
const res = await this.fetchImpl(signedUrl, {
|
|
472
526
|
method,
|
|
473
527
|
headers: signedHeaders,
|
|
474
|
-
body: serializedBody
|
|
528
|
+
body: serializedBody,
|
|
529
|
+
credentials: url.includes("/refresh-token") && this.refreshMode === "header" ? "omit" : "include"
|
|
475
530
|
});
|
|
476
|
-
await this.
|
|
531
|
+
await this.updateRefreshSecretFromResponse(res);
|
|
477
532
|
const parsed = await this.parseResponse(res);
|
|
478
533
|
const out = parsed && typeof parsed === "object" ? parsed : { ok: res.ok, result: parsed };
|
|
479
534
|
out.status = res.status;
|
|
@@ -494,9 +549,7 @@ var Revlm = class {
|
|
|
494
549
|
code: refreshRes.code
|
|
495
550
|
};
|
|
496
551
|
this.logDebug("### refresh failed:", refreshFailed, JSON.stringify(refreshFailed));
|
|
497
|
-
const
|
|
498
|
-
const cookieHeader = await this.cookieStore?.getCookieHeader?.(refreshUrl);
|
|
499
|
-
const refreshSecret = this.extractRefreshSecret(cookieHeader);
|
|
552
|
+
const refreshSecret = this.refreshSecret;
|
|
500
553
|
const refreshFailureLog = {
|
|
501
554
|
cause: refreshRes.reason || "refresh_failed",
|
|
502
555
|
reason: refreshFailed.reason,
|
|
@@ -507,6 +560,7 @@ var Revlm = class {
|
|
|
507
560
|
if (refreshSecret) refreshFailureLog.refreshSecret = refreshSecret;
|
|
508
561
|
this.logRefreshFailureClient(refreshFailureLog);
|
|
509
562
|
if (refreshRes.reason === "no_refresh_secret") {
|
|
563
|
+
await this.clearRefreshSecret();
|
|
510
564
|
const missingError = new Error("Refresh cookie missing. Provide a cookie-aware fetch implementation for Node/RN.");
|
|
511
565
|
missingError.revlmReason = "no_refresh_secret";
|
|
512
566
|
missingError.revlmCode = refreshRes.code;
|
|
@@ -519,12 +573,17 @@ var Revlm = class {
|
|
|
519
573
|
const now = Math.floor(Date.now() / 1e3);
|
|
520
574
|
const oldExp = beforePayload?.exp;
|
|
521
575
|
const newExp = afterPayload?.exp;
|
|
576
|
+
const oldTtlSec = typeof oldExp === "number" ? oldExp - now : void 0;
|
|
577
|
+
const newTtlSec = typeof newExp === "number" ? newExp - now : void 0;
|
|
578
|
+
const oldExpDisplay = this.formatUnixSeconds(oldExp);
|
|
579
|
+
const newExpDisplay = this.formatUnixSeconds(newExp);
|
|
580
|
+
const ttlDisplay = this.formatTtlDetailed(newTtlSec);
|
|
522
581
|
this.logDebug("### refresh success", {
|
|
523
582
|
path,
|
|
524
|
-
oldExp,
|
|
525
|
-
newExp,
|
|
526
|
-
oldTtlSec
|
|
527
|
-
newTtlSec: typeof
|
|
583
|
+
oldExp: oldExpDisplay ? `${oldExp} (${oldExpDisplay})` : oldExp,
|
|
584
|
+
newExp: newExpDisplay ? `${newExp} (${newExpDisplay})` : newExp,
|
|
585
|
+
oldTtlSec,
|
|
586
|
+
newTtlSec: typeof newTtlSec === "number" && ttlDisplay ? `${newTtlSec} (${ttlDisplay})` : newTtlSec
|
|
528
587
|
});
|
|
529
588
|
return this.requestWithRetry(path, method, body, { allowAuthRetry: false, retrying: true });
|
|
530
589
|
}
|
|
@@ -553,7 +612,10 @@ var Revlm = class {
|
|
|
553
612
|
}
|
|
554
613
|
await this.ensureCookieSupport();
|
|
555
614
|
if (!authId) throw new Error("authId is required");
|
|
556
|
-
const provisionalClient = new AuthClient({
|
|
615
|
+
const provisionalClient = new AuthClient({
|
|
616
|
+
secretMaster: this.provisionalAuthSecretMaster,
|
|
617
|
+
authDomain: this.provisionalAuthDomain
|
|
618
|
+
});
|
|
557
619
|
const provisionalPassword = await provisionalClient.producePassword(String(Date.now() * 1e3));
|
|
558
620
|
const res = await this.request("/provisional-login", "POST", { authId, password: provisionalPassword });
|
|
559
621
|
if (this.autoSetToken && res && res.ok && res.token) {
|
|
@@ -561,6 +623,22 @@ var Revlm = class {
|
|
|
561
623
|
}
|
|
562
624
|
return res;
|
|
563
625
|
}
|
|
626
|
+
formatUnixSeconds(epochSeconds) {
|
|
627
|
+
if (typeof epochSeconds !== "number" || !Number.isFinite(epochSeconds)) return void 0;
|
|
628
|
+
const date = new Date(epochSeconds * 1e3);
|
|
629
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
630
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
631
|
+
}
|
|
632
|
+
formatTtlDetailed(seconds) {
|
|
633
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return void 0;
|
|
634
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
635
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
636
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
637
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
638
|
+
const secs = totalSeconds % 60;
|
|
639
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
640
|
+
return `${days} ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
641
|
+
}
|
|
564
642
|
async registerUser(user, password) {
|
|
565
643
|
if (!user) throw new Error("user is required");
|
|
566
644
|
if (!password) throw new Error("password is required");
|
|
@@ -581,16 +659,22 @@ var Revlm = class {
|
|
|
581
659
|
async ensureCookieSupport() {
|
|
582
660
|
if (this.cookieCheckPromise) return this.cookieCheckPromise;
|
|
583
661
|
this.cookieCheckPromise = (async () => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
662
|
+
try {
|
|
663
|
+
const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
664
|
+
this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
|
|
665
|
+
if (first.ok) {
|
|
666
|
+
this.refreshMode = "cookie";
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (first.reason !== "cookie_missing") {
|
|
670
|
+
this.refreshMode = "header";
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
674
|
+
this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
|
|
675
|
+
this.refreshMode = second.ok ? "cookie" : "header";
|
|
676
|
+
} catch {
|
|
677
|
+
this.refreshMode = "header";
|
|
594
678
|
}
|
|
595
679
|
})();
|
|
596
680
|
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,18 +215,21 @@ 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;
|
|
227
232
|
private maskSecret;
|
|
228
|
-
private extractRefreshSecret;
|
|
229
233
|
private logRefreshFailureClient;
|
|
230
234
|
setToken(token: string): void;
|
|
231
235
|
getToken(): string | undefined;
|
|
@@ -244,6 +248,8 @@ declare class Revlm {
|
|
|
244
248
|
private requestWithRetry;
|
|
245
249
|
login(authId: string, password: string): Promise<LoginResponse>;
|
|
246
250
|
provisionalLogin(authId: string): Promise<ProvisionalLoginResponse>;
|
|
251
|
+
private formatUnixSeconds;
|
|
252
|
+
private formatTtlDetailed;
|
|
247
253
|
registerUser(user: UserInput, password: string): Promise<RegisterUserResponse>;
|
|
248
254
|
deleteUser(params: {
|
|
249
255
|
_id?: any;
|
|
@@ -303,4 +309,4 @@ declare const BSON: typeof bson & {
|
|
|
303
309
|
ObjectID: typeof bson.ObjectId;
|
|
304
310
|
};
|
|
305
311
|
|
|
306
|
-
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,18 +215,21 @@ 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;
|
|
227
232
|
private maskSecret;
|
|
228
|
-
private extractRefreshSecret;
|
|
229
233
|
private logRefreshFailureClient;
|
|
230
234
|
setToken(token: string): void;
|
|
231
235
|
getToken(): string | undefined;
|
|
@@ -244,6 +248,8 @@ declare class Revlm {
|
|
|
244
248
|
private requestWithRetry;
|
|
245
249
|
login(authId: string, password: string): Promise<LoginResponse>;
|
|
246
250
|
provisionalLogin(authId: string): Promise<ProvisionalLoginResponse>;
|
|
251
|
+
private formatUnixSeconds;
|
|
252
|
+
private formatTtlDetailed;
|
|
247
253
|
registerUser(user: UserInput, password: string): Promise<RegisterUserResponse>;
|
|
248
254
|
deleteUser(params: {
|
|
249
255
|
_id?: any;
|
|
@@ -303,4 +309,4 @@ declare const BSON: typeof bson & {
|
|
|
303
309
|
ObjectID: typeof bson.ObjectId;
|
|
304
310
|
};
|
|
305
311
|
|
|
306
|
-
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.63",
|
|
39
39
|
private: false,
|
|
40
40
|
description: "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
41
41
|
keywords: [
|
|
@@ -96,7 +96,7 @@ var require_package = __commonJS({
|
|
|
96
96
|
test: "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ../../jest.config.cjs packages/revlm-client/src/__tests__/ --runInBand --watchman=false --verbose"
|
|
97
97
|
},
|
|
98
98
|
dependencies: {
|
|
99
|
-
"@kedaruma/revlm-shared": "
|
|
99
|
+
"@kedaruma/revlm-shared": "latest",
|
|
100
100
|
bson: "^6.10.4",
|
|
101
101
|
dotenv: "^17.2.3"
|
|
102
102
|
},
|
|
@@ -126,6 +126,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
126
126
|
// src/Revlm.ts
|
|
127
127
|
var import_bson = require("bson");
|
|
128
128
|
var import_revlm_shared = require("@kedaruma/revlm-shared");
|
|
129
|
+
var import_random_bytes = require("@kedaruma/revlm-shared/random-bytes");
|
|
129
130
|
|
|
130
131
|
// src/MdbCollection.ts
|
|
131
132
|
var MdbCollection = class {
|
|
@@ -233,6 +234,36 @@ var LOG_LEVEL_RANK = {
|
|
|
233
234
|
debug: 3
|
|
234
235
|
};
|
|
235
236
|
var SESSION_HEADER_NAME = "x-revlm-session-id";
|
|
237
|
+
var STATE_STORE_KEYS = {
|
|
238
|
+
refreshSecret: "refreshSecret"
|
|
239
|
+
};
|
|
240
|
+
var randomBytesInitAttempted = false;
|
|
241
|
+
function resolvePlatformRandomBytes() {
|
|
242
|
+
const cryptoLike = globalThis?.crypto;
|
|
243
|
+
if (cryptoLike && typeof cryptoLike.getRandomValues === "function") {
|
|
244
|
+
return (length) => {
|
|
245
|
+
const out = new Uint8Array(length);
|
|
246
|
+
cryptoLike.getRandomValues(out);
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const nodeCrypto = require("crypto");
|
|
252
|
+
if (typeof nodeCrypto?.randomBytes === "function") {
|
|
253
|
+
return (length) => new Uint8Array(nodeCrypto.randomBytes(length));
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
function ensureRandomBytesInitialized() {
|
|
260
|
+
if (randomBytesInitAttempted) return;
|
|
261
|
+
randomBytesInitAttempted = true;
|
|
262
|
+
const platformRandomBytes = resolvePlatformRandomBytes();
|
|
263
|
+
if (platformRandomBytes) {
|
|
264
|
+
(0, import_random_bytes.initRandomBytes)(platformRandomBytes);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
236
267
|
function normalizeLogLevel(value) {
|
|
237
268
|
if (!value) return "info";
|
|
238
269
|
const lowered = value.toLowerCase();
|
|
@@ -277,6 +308,14 @@ function getRevlmClientVersion() {
|
|
|
277
308
|
const globalVersion = globalThis?.REVLM_CLIENT_VERSION;
|
|
278
309
|
return typeof globalVersion === "string" ? globalVersion : "unknown";
|
|
279
310
|
}
|
|
311
|
+
function resolveFetchImpl(fetchImpl) {
|
|
312
|
+
if (fetchImpl) return fetchImpl;
|
|
313
|
+
const globalFetch = globalThis?.fetch;
|
|
314
|
+
if (typeof globalFetch === "function") {
|
|
315
|
+
return globalFetch.bind(globalThis);
|
|
316
|
+
}
|
|
317
|
+
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
318
|
+
}
|
|
280
319
|
var Revlm = class {
|
|
281
320
|
baseUrl;
|
|
282
321
|
fetchImpl;
|
|
@@ -292,11 +331,14 @@ var Revlm = class {
|
|
|
292
331
|
refreshPromise;
|
|
293
332
|
sessionId;
|
|
294
333
|
sessionIdProvider;
|
|
295
|
-
|
|
334
|
+
refreshSecret;
|
|
335
|
+
refreshMode = "unknown";
|
|
336
|
+
stateStore;
|
|
296
337
|
constructor(baseUrl, opts = {}) {
|
|
297
338
|
if (!baseUrl) throw new Error("baseUrl is required");
|
|
339
|
+
ensureRandomBytesInitialized();
|
|
298
340
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
299
|
-
this.fetchImpl = opts.fetchImpl
|
|
341
|
+
this.fetchImpl = resolveFetchImpl(opts.fetchImpl);
|
|
300
342
|
this.defaultHeaders = opts.defaultHeaders || {};
|
|
301
343
|
this.provisionalEnabled = opts.provisionalEnabled || false;
|
|
302
344
|
this.provisionalAuthSecretMaster = opts.provisionalAuthSecretMaster || "";
|
|
@@ -306,10 +348,7 @@ var Revlm = class {
|
|
|
306
348
|
this.logLevel = normalizeLogLevel(opts.logLevel);
|
|
307
349
|
this.sessionId = opts.sessionId;
|
|
308
350
|
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
|
-
}
|
|
351
|
+
this.stateStore = opts.stateStore;
|
|
313
352
|
this.logInfo("\u{1F680} Revlm Client Init", {
|
|
314
353
|
version: getRevlmClientVersion(),
|
|
315
354
|
baseUrl: this.baseUrl,
|
|
@@ -335,18 +374,33 @@ var Revlm = class {
|
|
|
335
374
|
this.sessionId = generateSessionId();
|
|
336
375
|
return this.sessionId;
|
|
337
376
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const cookieHeader = await this.cookieStore.getCookieHeader(url);
|
|
342
|
-
if (cookieHeader) headers.cookie = cookieHeader;
|
|
377
|
+
normalizeCookieValue(value) {
|
|
378
|
+
if (!value) return void 0;
|
|
379
|
+
return value.replace(/^"+|"+$/g, "");
|
|
343
380
|
}
|
|
344
|
-
async
|
|
345
|
-
if (!this.cookieStore) return;
|
|
381
|
+
async updateRefreshSecretFromResponse(res) {
|
|
346
382
|
const setCookies = getSetCookieHeaders(res);
|
|
347
383
|
if (!setCookies.length) return;
|
|
348
384
|
for (const setCookie of setCookies) {
|
|
349
|
-
|
|
385
|
+
const match = setCookie.match(/revlm_refresh=([^;]+)/);
|
|
386
|
+
if (match && match[1]) {
|
|
387
|
+
const normalized = this.normalizeCookieValue(match[1]);
|
|
388
|
+
this.refreshSecret = normalized;
|
|
389
|
+
if (this.stateStore && normalized) {
|
|
390
|
+
await this.stateStore.set(STATE_STORE_KEYS.refreshSecret, normalized);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async ensureRefreshSecretLoaded() {
|
|
396
|
+
if (this.refreshSecret || !this.stateStore) return;
|
|
397
|
+
const stored = await this.stateStore.get(STATE_STORE_KEYS.refreshSecret);
|
|
398
|
+
if (stored) this.refreshSecret = stored;
|
|
399
|
+
}
|
|
400
|
+
async clearRefreshSecret() {
|
|
401
|
+
this.refreshSecret = void 0;
|
|
402
|
+
if (this.stateStore) {
|
|
403
|
+
await this.stateStore.remove(STATE_STORE_KEYS.refreshSecret);
|
|
350
404
|
}
|
|
351
405
|
}
|
|
352
406
|
canLog(level) {
|
|
@@ -369,11 +423,6 @@ var Revlm = class {
|
|
|
369
423
|
if (value.length <= head + tail) return value;
|
|
370
424
|
return `${value.slice(0, head)}***${value.slice(-tail)}`;
|
|
371
425
|
}
|
|
372
|
-
extractRefreshSecret(cookieHeader) {
|
|
373
|
-
if (!cookieHeader) return void 0;
|
|
374
|
-
const match = cookieHeader.match(/(?:^|;\\s*)revlm_refresh=([^;]+)/);
|
|
375
|
-
return match?.[1];
|
|
376
|
-
}
|
|
377
426
|
logRefreshFailureClient(params) {
|
|
378
427
|
const recoverable = typeof params.code === "number" ? params.code >= 1e4 && params.code < 2e4 : params.status === 400 || params.status === 401;
|
|
379
428
|
const line1 = `[refresh-token][${recoverable ? "debug" : "error"}][${recoverable ? "recoverable" : "fatal"}] cause=${params.cause}`;
|
|
@@ -504,7 +553,12 @@ var Revlm = class {
|
|
|
504
553
|
const headers = this.makeHeaders(hasBody);
|
|
505
554
|
const sessionId = await this.resolveSessionId();
|
|
506
555
|
if (sessionId) headers[SESSION_HEADER_NAME] = sessionId;
|
|
507
|
-
|
|
556
|
+
if (url.includes("/refresh-token") && this.refreshMode === "header") {
|
|
557
|
+
await this.ensureRefreshSecretLoaded();
|
|
558
|
+
if (this.refreshSecret) {
|
|
559
|
+
headers["x-revlm-refresh"] = this.refreshSecret;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
508
562
|
let serializedBody;
|
|
509
563
|
if (hasBody) {
|
|
510
564
|
serializedBody = import_bson.EJSON.stringify(body);
|
|
@@ -514,9 +568,10 @@ var Revlm = class {
|
|
|
514
568
|
const res = await this.fetchImpl(signedUrl, {
|
|
515
569
|
method,
|
|
516
570
|
headers: signedHeaders,
|
|
517
|
-
body: serializedBody
|
|
571
|
+
body: serializedBody,
|
|
572
|
+
credentials: url.includes("/refresh-token") && this.refreshMode === "header" ? "omit" : "include"
|
|
518
573
|
});
|
|
519
|
-
await this.
|
|
574
|
+
await this.updateRefreshSecretFromResponse(res);
|
|
520
575
|
const parsed = await this.parseResponse(res);
|
|
521
576
|
const out = parsed && typeof parsed === "object" ? parsed : { ok: res.ok, result: parsed };
|
|
522
577
|
out.status = res.status;
|
|
@@ -537,9 +592,7 @@ var Revlm = class {
|
|
|
537
592
|
code: refreshRes.code
|
|
538
593
|
};
|
|
539
594
|
this.logDebug("### refresh failed:", refreshFailed, JSON.stringify(refreshFailed));
|
|
540
|
-
const
|
|
541
|
-
const cookieHeader = await this.cookieStore?.getCookieHeader?.(refreshUrl);
|
|
542
|
-
const refreshSecret = this.extractRefreshSecret(cookieHeader);
|
|
595
|
+
const refreshSecret = this.refreshSecret;
|
|
543
596
|
const refreshFailureLog = {
|
|
544
597
|
cause: refreshRes.reason || "refresh_failed",
|
|
545
598
|
reason: refreshFailed.reason,
|
|
@@ -550,6 +603,7 @@ var Revlm = class {
|
|
|
550
603
|
if (refreshSecret) refreshFailureLog.refreshSecret = refreshSecret;
|
|
551
604
|
this.logRefreshFailureClient(refreshFailureLog);
|
|
552
605
|
if (refreshRes.reason === "no_refresh_secret") {
|
|
606
|
+
await this.clearRefreshSecret();
|
|
553
607
|
const missingError = new Error("Refresh cookie missing. Provide a cookie-aware fetch implementation for Node/RN.");
|
|
554
608
|
missingError.revlmReason = "no_refresh_secret";
|
|
555
609
|
missingError.revlmCode = refreshRes.code;
|
|
@@ -562,12 +616,17 @@ var Revlm = class {
|
|
|
562
616
|
const now = Math.floor(Date.now() / 1e3);
|
|
563
617
|
const oldExp = beforePayload?.exp;
|
|
564
618
|
const newExp = afterPayload?.exp;
|
|
619
|
+
const oldTtlSec = typeof oldExp === "number" ? oldExp - now : void 0;
|
|
620
|
+
const newTtlSec = typeof newExp === "number" ? newExp - now : void 0;
|
|
621
|
+
const oldExpDisplay = this.formatUnixSeconds(oldExp);
|
|
622
|
+
const newExpDisplay = this.formatUnixSeconds(newExp);
|
|
623
|
+
const ttlDisplay = this.formatTtlDetailed(newTtlSec);
|
|
565
624
|
this.logDebug("### refresh success", {
|
|
566
625
|
path,
|
|
567
|
-
oldExp,
|
|
568
|
-
newExp,
|
|
569
|
-
oldTtlSec
|
|
570
|
-
newTtlSec: typeof
|
|
626
|
+
oldExp: oldExpDisplay ? `${oldExp} (${oldExpDisplay})` : oldExp,
|
|
627
|
+
newExp: newExpDisplay ? `${newExp} (${newExpDisplay})` : newExp,
|
|
628
|
+
oldTtlSec,
|
|
629
|
+
newTtlSec: typeof newTtlSec === "number" && ttlDisplay ? `${newTtlSec} (${ttlDisplay})` : newTtlSec
|
|
571
630
|
});
|
|
572
631
|
return this.requestWithRetry(path, method, body, { allowAuthRetry: false, retrying: true });
|
|
573
632
|
}
|
|
@@ -596,7 +655,10 @@ var Revlm = class {
|
|
|
596
655
|
}
|
|
597
656
|
await this.ensureCookieSupport();
|
|
598
657
|
if (!authId) throw new Error("authId is required");
|
|
599
|
-
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
658
|
+
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
659
|
+
secretMaster: this.provisionalAuthSecretMaster,
|
|
660
|
+
authDomain: this.provisionalAuthDomain
|
|
661
|
+
});
|
|
600
662
|
const provisionalPassword = await provisionalClient.producePassword(String(Date.now() * 1e3));
|
|
601
663
|
const res = await this.request("/provisional-login", "POST", { authId, password: provisionalPassword });
|
|
602
664
|
if (this.autoSetToken && res && res.ok && res.token) {
|
|
@@ -604,6 +666,22 @@ var Revlm = class {
|
|
|
604
666
|
}
|
|
605
667
|
return res;
|
|
606
668
|
}
|
|
669
|
+
formatUnixSeconds(epochSeconds) {
|
|
670
|
+
if (typeof epochSeconds !== "number" || !Number.isFinite(epochSeconds)) return void 0;
|
|
671
|
+
const date = new Date(epochSeconds * 1e3);
|
|
672
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
673
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
674
|
+
}
|
|
675
|
+
formatTtlDetailed(seconds) {
|
|
676
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return void 0;
|
|
677
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
678
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
679
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
680
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
681
|
+
const secs = totalSeconds % 60;
|
|
682
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
683
|
+
return `${days} ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
684
|
+
}
|
|
607
685
|
async registerUser(user, password) {
|
|
608
686
|
if (!user) throw new Error("user is required");
|
|
609
687
|
if (!password) throw new Error("password is required");
|
|
@@ -624,16 +702,22 @@ var Revlm = class {
|
|
|
624
702
|
async ensureCookieSupport() {
|
|
625
703
|
if (this.cookieCheckPromise) return this.cookieCheckPromise;
|
|
626
704
|
this.cookieCheckPromise = (async () => {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
705
|
+
try {
|
|
706
|
+
const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
707
|
+
this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
|
|
708
|
+
if (first.ok) {
|
|
709
|
+
this.refreshMode = "cookie";
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (first.reason !== "cookie_missing") {
|
|
713
|
+
this.refreshMode = "header";
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
717
|
+
this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
|
|
718
|
+
this.refreshMode = second.ok ? "cookie" : "header";
|
|
719
|
+
} catch {
|
|
720
|
+
this.refreshMode = "header";
|
|
637
721
|
}
|
|
638
722
|
})();
|
|
639
723
|
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.63",
|
|
39
39
|
private: false,
|
|
40
40
|
description: "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
41
41
|
keywords: [
|
|
@@ -96,7 +96,7 @@ var require_package = __commonJS({
|
|
|
96
96
|
test: "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ../../jest.config.cjs packages/revlm-client/src/__tests__/ --runInBand --watchman=false --verbose"
|
|
97
97
|
},
|
|
98
98
|
dependencies: {
|
|
99
|
-
"@kedaruma/revlm-shared": "
|
|
99
|
+
"@kedaruma/revlm-shared": "latest",
|
|
100
100
|
bson: "^6.10.4",
|
|
101
101
|
dotenv: "^17.2.3"
|
|
102
102
|
},
|
|
@@ -124,6 +124,7 @@ module.exports = __toCommonJS(revlm_compat_exports);
|
|
|
124
124
|
// src/Revlm.ts
|
|
125
125
|
var import_bson = require("bson");
|
|
126
126
|
var import_revlm_shared = require("@kedaruma/revlm-shared");
|
|
127
|
+
var import_random_bytes = require("@kedaruma/revlm-shared/random-bytes");
|
|
127
128
|
|
|
128
129
|
// src/MdbCollection.ts
|
|
129
130
|
var MdbCollection = class {
|
|
@@ -231,6 +232,36 @@ var LOG_LEVEL_RANK = {
|
|
|
231
232
|
debug: 3
|
|
232
233
|
};
|
|
233
234
|
var SESSION_HEADER_NAME = "x-revlm-session-id";
|
|
235
|
+
var STATE_STORE_KEYS = {
|
|
236
|
+
refreshSecret: "refreshSecret"
|
|
237
|
+
};
|
|
238
|
+
var randomBytesInitAttempted = false;
|
|
239
|
+
function resolvePlatformRandomBytes() {
|
|
240
|
+
const cryptoLike = globalThis?.crypto;
|
|
241
|
+
if (cryptoLike && typeof cryptoLike.getRandomValues === "function") {
|
|
242
|
+
return (length) => {
|
|
243
|
+
const out = new Uint8Array(length);
|
|
244
|
+
cryptoLike.getRandomValues(out);
|
|
245
|
+
return out;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const nodeCrypto = require("crypto");
|
|
250
|
+
if (typeof nodeCrypto?.randomBytes === "function") {
|
|
251
|
+
return (length) => new Uint8Array(nodeCrypto.randomBytes(length));
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
function ensureRandomBytesInitialized() {
|
|
258
|
+
if (randomBytesInitAttempted) return;
|
|
259
|
+
randomBytesInitAttempted = true;
|
|
260
|
+
const platformRandomBytes = resolvePlatformRandomBytes();
|
|
261
|
+
if (platformRandomBytes) {
|
|
262
|
+
(0, import_random_bytes.initRandomBytes)(platformRandomBytes);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
234
265
|
function normalizeLogLevel(value) {
|
|
235
266
|
if (!value) return "info";
|
|
236
267
|
const lowered = value.toLowerCase();
|
|
@@ -275,6 +306,14 @@ function getRevlmClientVersion() {
|
|
|
275
306
|
const globalVersion = globalThis?.REVLM_CLIENT_VERSION;
|
|
276
307
|
return typeof globalVersion === "string" ? globalVersion : "unknown";
|
|
277
308
|
}
|
|
309
|
+
function resolveFetchImpl(fetchImpl) {
|
|
310
|
+
if (fetchImpl) return fetchImpl;
|
|
311
|
+
const globalFetch = globalThis?.fetch;
|
|
312
|
+
if (typeof globalFetch === "function") {
|
|
313
|
+
return globalFetch.bind(globalThis);
|
|
314
|
+
}
|
|
315
|
+
throw new Error("No fetch implementation available. Provide fetchImpl in options or run in Node 18+ with global fetch.");
|
|
316
|
+
}
|
|
278
317
|
var Revlm = class {
|
|
279
318
|
baseUrl;
|
|
280
319
|
fetchImpl;
|
|
@@ -290,11 +329,14 @@ var Revlm = class {
|
|
|
290
329
|
refreshPromise;
|
|
291
330
|
sessionId;
|
|
292
331
|
sessionIdProvider;
|
|
293
|
-
|
|
332
|
+
refreshSecret;
|
|
333
|
+
refreshMode = "unknown";
|
|
334
|
+
stateStore;
|
|
294
335
|
constructor(baseUrl, opts = {}) {
|
|
295
336
|
if (!baseUrl) throw new Error("baseUrl is required");
|
|
337
|
+
ensureRandomBytesInitialized();
|
|
296
338
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
297
|
-
this.fetchImpl = opts.fetchImpl
|
|
339
|
+
this.fetchImpl = resolveFetchImpl(opts.fetchImpl);
|
|
298
340
|
this.defaultHeaders = opts.defaultHeaders || {};
|
|
299
341
|
this.provisionalEnabled = opts.provisionalEnabled || false;
|
|
300
342
|
this.provisionalAuthSecretMaster = opts.provisionalAuthSecretMaster || "";
|
|
@@ -304,10 +346,7 @@ var Revlm = class {
|
|
|
304
346
|
this.logLevel = normalizeLogLevel(opts.logLevel);
|
|
305
347
|
this.sessionId = opts.sessionId;
|
|
306
348
|
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
|
-
}
|
|
349
|
+
this.stateStore = opts.stateStore;
|
|
311
350
|
this.logInfo("\u{1F680} Revlm Client Init", {
|
|
312
351
|
version: getRevlmClientVersion(),
|
|
313
352
|
baseUrl: this.baseUrl,
|
|
@@ -333,18 +372,33 @@ var Revlm = class {
|
|
|
333
372
|
this.sessionId = generateSessionId();
|
|
334
373
|
return this.sessionId;
|
|
335
374
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const cookieHeader = await this.cookieStore.getCookieHeader(url);
|
|
340
|
-
if (cookieHeader) headers.cookie = cookieHeader;
|
|
375
|
+
normalizeCookieValue(value) {
|
|
376
|
+
if (!value) return void 0;
|
|
377
|
+
return value.replace(/^"+|"+$/g, "");
|
|
341
378
|
}
|
|
342
|
-
async
|
|
343
|
-
if (!this.cookieStore) return;
|
|
379
|
+
async updateRefreshSecretFromResponse(res) {
|
|
344
380
|
const setCookies = getSetCookieHeaders(res);
|
|
345
381
|
if (!setCookies.length) return;
|
|
346
382
|
for (const setCookie of setCookies) {
|
|
347
|
-
|
|
383
|
+
const match = setCookie.match(/revlm_refresh=([^;]+)/);
|
|
384
|
+
if (match && match[1]) {
|
|
385
|
+
const normalized = this.normalizeCookieValue(match[1]);
|
|
386
|
+
this.refreshSecret = normalized;
|
|
387
|
+
if (this.stateStore && normalized) {
|
|
388
|
+
await this.stateStore.set(STATE_STORE_KEYS.refreshSecret, normalized);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async ensureRefreshSecretLoaded() {
|
|
394
|
+
if (this.refreshSecret || !this.stateStore) return;
|
|
395
|
+
const stored = await this.stateStore.get(STATE_STORE_KEYS.refreshSecret);
|
|
396
|
+
if (stored) this.refreshSecret = stored;
|
|
397
|
+
}
|
|
398
|
+
async clearRefreshSecret() {
|
|
399
|
+
this.refreshSecret = void 0;
|
|
400
|
+
if (this.stateStore) {
|
|
401
|
+
await this.stateStore.remove(STATE_STORE_KEYS.refreshSecret);
|
|
348
402
|
}
|
|
349
403
|
}
|
|
350
404
|
canLog(level) {
|
|
@@ -367,11 +421,6 @@ var Revlm = class {
|
|
|
367
421
|
if (value.length <= head + tail) return value;
|
|
368
422
|
return `${value.slice(0, head)}***${value.slice(-tail)}`;
|
|
369
423
|
}
|
|
370
|
-
extractRefreshSecret(cookieHeader) {
|
|
371
|
-
if (!cookieHeader) return void 0;
|
|
372
|
-
const match = cookieHeader.match(/(?:^|;\\s*)revlm_refresh=([^;]+)/);
|
|
373
|
-
return match?.[1];
|
|
374
|
-
}
|
|
375
424
|
logRefreshFailureClient(params) {
|
|
376
425
|
const recoverable = typeof params.code === "number" ? params.code >= 1e4 && params.code < 2e4 : params.status === 400 || params.status === 401;
|
|
377
426
|
const line1 = `[refresh-token][${recoverable ? "debug" : "error"}][${recoverable ? "recoverable" : "fatal"}] cause=${params.cause}`;
|
|
@@ -502,7 +551,12 @@ var Revlm = class {
|
|
|
502
551
|
const headers = this.makeHeaders(hasBody);
|
|
503
552
|
const sessionId = await this.resolveSessionId();
|
|
504
553
|
if (sessionId) headers[SESSION_HEADER_NAME] = sessionId;
|
|
505
|
-
|
|
554
|
+
if (url.includes("/refresh-token") && this.refreshMode === "header") {
|
|
555
|
+
await this.ensureRefreshSecretLoaded();
|
|
556
|
+
if (this.refreshSecret) {
|
|
557
|
+
headers["x-revlm-refresh"] = this.refreshSecret;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
506
560
|
let serializedBody;
|
|
507
561
|
if (hasBody) {
|
|
508
562
|
serializedBody = import_bson.EJSON.stringify(body);
|
|
@@ -512,9 +566,10 @@ var Revlm = class {
|
|
|
512
566
|
const res = await this.fetchImpl(signedUrl, {
|
|
513
567
|
method,
|
|
514
568
|
headers: signedHeaders,
|
|
515
|
-
body: serializedBody
|
|
569
|
+
body: serializedBody,
|
|
570
|
+
credentials: url.includes("/refresh-token") && this.refreshMode === "header" ? "omit" : "include"
|
|
516
571
|
});
|
|
517
|
-
await this.
|
|
572
|
+
await this.updateRefreshSecretFromResponse(res);
|
|
518
573
|
const parsed = await this.parseResponse(res);
|
|
519
574
|
const out = parsed && typeof parsed === "object" ? parsed : { ok: res.ok, result: parsed };
|
|
520
575
|
out.status = res.status;
|
|
@@ -535,9 +590,7 @@ var Revlm = class {
|
|
|
535
590
|
code: refreshRes.code
|
|
536
591
|
};
|
|
537
592
|
this.logDebug("### refresh failed:", refreshFailed, JSON.stringify(refreshFailed));
|
|
538
|
-
const
|
|
539
|
-
const cookieHeader = await this.cookieStore?.getCookieHeader?.(refreshUrl);
|
|
540
|
-
const refreshSecret = this.extractRefreshSecret(cookieHeader);
|
|
593
|
+
const refreshSecret = this.refreshSecret;
|
|
541
594
|
const refreshFailureLog = {
|
|
542
595
|
cause: refreshRes.reason || "refresh_failed",
|
|
543
596
|
reason: refreshFailed.reason,
|
|
@@ -548,6 +601,7 @@ var Revlm = class {
|
|
|
548
601
|
if (refreshSecret) refreshFailureLog.refreshSecret = refreshSecret;
|
|
549
602
|
this.logRefreshFailureClient(refreshFailureLog);
|
|
550
603
|
if (refreshRes.reason === "no_refresh_secret") {
|
|
604
|
+
await this.clearRefreshSecret();
|
|
551
605
|
const missingError = new Error("Refresh cookie missing. Provide a cookie-aware fetch implementation for Node/RN.");
|
|
552
606
|
missingError.revlmReason = "no_refresh_secret";
|
|
553
607
|
missingError.revlmCode = refreshRes.code;
|
|
@@ -560,12 +614,17 @@ var Revlm = class {
|
|
|
560
614
|
const now = Math.floor(Date.now() / 1e3);
|
|
561
615
|
const oldExp = beforePayload?.exp;
|
|
562
616
|
const newExp = afterPayload?.exp;
|
|
617
|
+
const oldTtlSec = typeof oldExp === "number" ? oldExp - now : void 0;
|
|
618
|
+
const newTtlSec = typeof newExp === "number" ? newExp - now : void 0;
|
|
619
|
+
const oldExpDisplay = this.formatUnixSeconds(oldExp);
|
|
620
|
+
const newExpDisplay = this.formatUnixSeconds(newExp);
|
|
621
|
+
const ttlDisplay = this.formatTtlDetailed(newTtlSec);
|
|
563
622
|
this.logDebug("### refresh success", {
|
|
564
623
|
path,
|
|
565
|
-
oldExp,
|
|
566
|
-
newExp,
|
|
567
|
-
oldTtlSec
|
|
568
|
-
newTtlSec: typeof
|
|
624
|
+
oldExp: oldExpDisplay ? `${oldExp} (${oldExpDisplay})` : oldExp,
|
|
625
|
+
newExp: newExpDisplay ? `${newExp} (${newExpDisplay})` : newExp,
|
|
626
|
+
oldTtlSec,
|
|
627
|
+
newTtlSec: typeof newTtlSec === "number" && ttlDisplay ? `${newTtlSec} (${ttlDisplay})` : newTtlSec
|
|
569
628
|
});
|
|
570
629
|
return this.requestWithRetry(path, method, body, { allowAuthRetry: false, retrying: true });
|
|
571
630
|
}
|
|
@@ -594,7 +653,10 @@ var Revlm = class {
|
|
|
594
653
|
}
|
|
595
654
|
await this.ensureCookieSupport();
|
|
596
655
|
if (!authId) throw new Error("authId is required");
|
|
597
|
-
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
656
|
+
const provisionalClient = new import_revlm_shared.AuthClient({
|
|
657
|
+
secretMaster: this.provisionalAuthSecretMaster,
|
|
658
|
+
authDomain: this.provisionalAuthDomain
|
|
659
|
+
});
|
|
598
660
|
const provisionalPassword = await provisionalClient.producePassword(String(Date.now() * 1e3));
|
|
599
661
|
const res = await this.request("/provisional-login", "POST", { authId, password: provisionalPassword });
|
|
600
662
|
if (this.autoSetToken && res && res.ok && res.token) {
|
|
@@ -602,6 +664,22 @@ var Revlm = class {
|
|
|
602
664
|
}
|
|
603
665
|
return res;
|
|
604
666
|
}
|
|
667
|
+
formatUnixSeconds(epochSeconds) {
|
|
668
|
+
if (typeof epochSeconds !== "number" || !Number.isFinite(epochSeconds)) return void 0;
|
|
669
|
+
const date = new Date(epochSeconds * 1e3);
|
|
670
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
671
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
672
|
+
}
|
|
673
|
+
formatTtlDetailed(seconds) {
|
|
674
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return void 0;
|
|
675
|
+
const totalSeconds = Math.max(0, Math.floor(seconds));
|
|
676
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
677
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
678
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
679
|
+
const secs = totalSeconds % 60;
|
|
680
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
681
|
+
return `${days} ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
682
|
+
}
|
|
605
683
|
async registerUser(user, password) {
|
|
606
684
|
if (!user) throw new Error("user is required");
|
|
607
685
|
if (!password) throw new Error("password is required");
|
|
@@ -622,16 +700,22 @@ var Revlm = class {
|
|
|
622
700
|
async ensureCookieSupport() {
|
|
623
701
|
if (this.cookieCheckPromise) return this.cookieCheckPromise;
|
|
624
702
|
this.cookieCheckPromise = (async () => {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
703
|
+
try {
|
|
704
|
+
const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
705
|
+
this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
|
|
706
|
+
if (first.ok) {
|
|
707
|
+
this.refreshMode = "cookie";
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (first.reason !== "cookie_missing") {
|
|
711
|
+
this.refreshMode = "header";
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
|
|
715
|
+
this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
|
|
716
|
+
this.refreshMode = second.ok ? "cookie" : "header";
|
|
717
|
+
} catch {
|
|
718
|
+
this.refreshMode = "header";
|
|
635
719
|
}
|
|
636
720
|
})();
|
|
637
721
|
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.63",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "TypeScript client SDK for talking to the Revlm server replacement for MongoDB Realm.",
|
|
6
6
|
"keywords": [
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@kedaruma/revlm-shared": "latest",
|
|
59
60
|
"bson": "^6.10.4",
|
|
60
|
-
"dotenv": "^17.2.3"
|
|
61
|
-
"@kedaruma/revlm-shared": "1.0.11"
|
|
61
|
+
"dotenv": "^17.2.3"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"tsup": "^8.5.1"
|