@kedaruma/revlm-client 1.0.58 → 1.0.62

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