@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 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 revlm = new Revlm({ baseUrl: 'https://your-server.example.com' });
21
- const login = await revlm.login({ authId: 'user', password: 'secret' });
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.60",
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": "workspace:*",
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
- cookieStore;
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 || (typeof fetch !== "undefined" ? fetch : void 0);
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.cookieStore = opts.cookieStore;
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
- async applyCookieStore(headers, url) {
296
- const existing = headers.cookie || headers.Cookie;
297
- if (!this.cookieStore || existing) return;
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 storeSetCookies(res, url) {
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
- await this.cookieStore.setCookie(url, setCookie);
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
- await this.applyCookieStore(headers, url);
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.storeSetCookies(res, signedUrl);
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 refreshUrl = `${this.baseUrl}/refresh-token`;
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: typeof oldExp === "number" ? oldExp - now : void 0,
527
- newTtlSec: typeof newExp === "number" ? newExp - now : void 0
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({ secretMaster: this.provisionalAuthSecretMaster, authDomain: this.provisionalAuthDomain });
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
- const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
585
- this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
586
- if (first.ok) return;
587
- if (first.reason !== "cookie_missing") {
588
- throw new Error(`Cookie check failed: ${first.reason || first.error || "unknown_error"}`);
589
- }
590
- const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
591
- this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
592
- if (!second.ok) {
593
- throw new Error("Cookie support missing. Provide a cookie-aware fetch implementation for Node/RN.");
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
- cookieStore?: CookieStore;
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 cookieStore;
218
+ private refreshSecret;
219
+ private refreshMode;
220
+ private stateStore;
218
221
  constructor(baseUrl: string, opts?: RevlmOptions);
219
222
  private resolveSessionId;
220
- private applyCookieStore;
221
- private storeSetCookies;
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 CookieStore, 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, RevlmUser, type Update, type UpdateDescription, type UpdateEvent, type UpdateOptions, type UpdateResult, type User, type UserBase, type WatchOptionsFilter, type WatchOptionsIds };
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
- cookieStore?: CookieStore;
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 cookieStore;
218
+ private refreshSecret;
219
+ private refreshMode;
220
+ private stateStore;
218
221
  constructor(baseUrl: string, opts?: RevlmOptions);
219
222
  private resolveSessionId;
220
- private applyCookieStore;
221
- private storeSetCookies;
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 CookieStore, 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, RevlmUser, type Update, type UpdateDescription, type UpdateEvent, type UpdateOptions, type UpdateResult, type User, type UserBase, type WatchOptionsFilter, type WatchOptionsIds };
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.60",
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": "workspace:*",
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
- cookieStore;
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 || (typeof fetch !== "undefined" ? fetch : void 0);
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.cookieStore = opts.cookieStore;
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
- async applyCookieStore(headers, url) {
339
- const existing = headers.cookie || headers.Cookie;
340
- if (!this.cookieStore || existing) return;
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 storeSetCookies(res, url) {
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
- await this.cookieStore.setCookie(url, setCookie);
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
- await this.applyCookieStore(headers, url);
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.storeSetCookies(res, signedUrl);
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 refreshUrl = `${this.baseUrl}/refresh-token`;
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: typeof oldExp === "number" ? oldExp - now : void 0,
570
- newTtlSec: typeof newExp === "number" ? newExp - now : void 0
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({ secretMaster: this.provisionalAuthSecretMaster, authDomain: this.provisionalAuthDomain });
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
- const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
628
- this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
629
- if (first.ok) return;
630
- if (first.reason !== "cookie_missing") {
631
- throw new Error(`Cookie check failed: ${first.reason || first.error || "unknown_error"}`);
632
- }
633
- const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
634
- this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
635
- if (!second.ok) {
636
- throw new Error("Cookie support missing. Provide a cookie-aware fetch implementation for Node/RN.");
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
@@ -9,7 +9,7 @@ import {
9
9
  Revlm,
10
10
  RevlmDBDatabase,
11
11
  RevlmUser
12
- } from "./chunk-367MFQ4K.mjs";
12
+ } from "./chunk-OBJS4AOD.mjs";
13
13
  import "./chunk-EBO3CZXG.mjs";
14
14
  export {
15
15
  App,
@@ -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, CookieStore, 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, RevlmUser, UpdateEvent, User, UserBase } from './index.mjs';
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';
@@ -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, CookieStore, 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, RevlmUser, UpdateEvent, User, UserBase } from './index.js';
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';
@@ -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.60",
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": "workspace:*",
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
- cookieStore;
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 || (typeof fetch !== "undefined" ? fetch : void 0);
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.cookieStore = opts.cookieStore;
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
- async applyCookieStore(headers, url) {
337
- const existing = headers.cookie || headers.Cookie;
338
- if (!this.cookieStore || existing) return;
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 storeSetCookies(res, url) {
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
- await this.cookieStore.setCookie(url, setCookie);
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
- await this.applyCookieStore(headers, url);
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.storeSetCookies(res, signedUrl);
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 refreshUrl = `${this.baseUrl}/refresh-token`;
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: typeof oldExp === "number" ? oldExp - now : void 0,
568
- newTtlSec: typeof newExp === "number" ? newExp - now : void 0
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({ secretMaster: this.provisionalAuthSecretMaster, authDomain: this.provisionalAuthDomain });
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
- const first = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
626
- this.logDebug("### cookie check", { step: "first", ok: first.ok, reason: first.reason, status: first.status });
627
- if (first.ok) return;
628
- if (first.reason !== "cookie_missing") {
629
- throw new Error(`Cookie check failed: ${first.reason || first.error || "unknown_error"}`);
630
- }
631
- const second = await this.requestWithRetry("/cookie-check", "POST", void 0, { allowAuthRetry: false, retrying: false });
632
- this.logDebug("### cookie check", { step: "second", ok: second.ok, reason: second.reason, status: second.status });
633
- if (!second.ok) {
634
- throw new Error("Cookie support missing. Provide a cookie-aware fetch implementation for Node/RN.");
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;
@@ -7,7 +7,7 @@ import {
7
7
  ObjectId,
8
8
  Revlm,
9
9
  RevlmUser
10
- } from "./chunk-367MFQ4K.mjs";
10
+ } from "./chunk-OBJS4AOD.mjs";
11
11
  import "./chunk-EBO3CZXG.mjs";
12
12
  export {
13
13
  App,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kedaruma/revlm-client",
3
- "version": "1.0.60",
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"