@dbx-tools/cli-tunnel 0.6.51 → 0.6.52

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.
@@ -31,11 +31,11 @@
31
31
  *
32
32
  * ## Forcing every session to end
33
33
  *
34
- * {@link resolveSessionEpoch} reads a date from `TUNNEL_AUTH_SESSION_EPOCH` (or
35
- * `--session-epoch`), and that date is part of the cache KEY. Moving it forward
34
+ * {@link resolveSessionCutoff} reads a date from `TUNNEL_AUTH_SESSION_CUTOFF` (or
35
+ * `--session-cutoff`), and that date is part of the cache KEY. Moving it forward
36
36
  * makes every prior key unreachable, so every cookie signed against it stops
37
37
  * verifying - the log-everyone-out switch, without having to find and flush a
38
- * cache entry. The epoch is also asserted against each token's `iat`, so a
38
+ * cache entry. The cutoff is also asserted against each token's `iat`, so a
39
39
  * cookie that predates it is refused even if it was signed with the key that is
40
40
  * somehow still current.
41
41
  *
@@ -44,8 +44,8 @@
44
44
 
45
45
  import { randomBytes } from "node:crypto";
46
46
  import { CacheManager } from "@databricks/appkit";
47
- import { env, log } from "@dbx-tools/shared-core";
48
- import { JWT_SECRET_ENV, SESSION_EPOCH_ENV } from "./env.ts";
47
+ import { env, log, object } from "@dbx-tools/shared-core";
48
+ import { JWT_SECRET_ENV, SESSION_CUTOFF_ENV } from "./env.ts";
49
49
 
50
50
  const logger = log.logger("tunnel:signing-key");
51
51
 
@@ -73,59 +73,48 @@ interface StoredKey {
73
73
  }
74
74
 
75
75
  /**
76
- * Resolve the force-clear epoch as epoch MILLISECONDS, or `0` when unset.
76
+ * Resolve the force-clear cutoff as epoch MILLISECONDS, or `0` when unset.
77
+ *
78
+ * Accepts whatever `object.toDate` accepts - a `Date`, `2026-08-02`, an ISO
79
+ * instant, epoch seconds or millis from `date +%s`, or a relative duration
80
+ * (`-30d`, `7 days ago`), which is the spelling an operator reaching for this
81
+ * usually wants: sign out everyone who signed in more than a month ago.
77
82
  *
78
- * Accepts anything `Date` parses (`2026-08-02`, an ISO timestamp) plus a bare
79
- * epoch-seconds / epoch-millis number, so a value pasted from `date +%s` works.
80
83
  * An UNPARSEABLE value is ignored with a warning rather than throwing: this is
81
84
  * the switch that logs a fleet back in, and failing to boot over a typo is worse
82
85
  * than not rotating.
83
86
  */
84
- export function resolveSessionEpoch(configured?: string | number | Date): number {
85
- const raw = configured ?? env.text(SESSION_EPOCH_ENV) ?? undefined;
87
+ export function resolveSessionCutoff(configured?: string | number | Date): number {
88
+ const raw = configured ?? env.text(SESSION_CUTOFF_ENV) ?? undefined;
86
89
  if (raw === undefined || raw === null || raw === "") return 0;
87
- return clampToPast(parseEpoch(raw));
88
- }
89
90
 
90
- /** Parse the accepted epoch spellings to ms, or `0` when unusable. */
91
- function parseEpoch(raw: string | number | Date): number {
92
- if (raw instanceof Date) return Number.isNaN(raw.getTime()) ? 0 : raw.getTime();
93
-
94
- const text = String(raw).trim();
95
- // A bare number is a timestamp, not a date string: `Date.parse("1785...")`
96
- // would read it as a YEAR. Values below ~1e11 are seconds (any millis
97
- // timestamp since 1973 is larger), which is what `date +%s` prints.
98
- if (/^\d+$/.test(text)) {
99
- const numeric = Number(text);
100
- return numeric < 1e11 ? numeric * 1000 : numeric;
101
- }
102
- const parsed = Date.parse(text);
103
- if (Number.isNaN(parsed)) {
104
- logger.warn(`ignoring unparseable ${env.name(SESSION_EPOCH_ENV)}`, { value: text });
91
+ const date = object.toDate(raw);
92
+ if (!date) {
93
+ logger.warn(`ignoring unparseable ${env.name(SESSION_CUTOFF_ENV)}`, { value: String(raw) });
105
94
  return 0;
106
95
  }
107
- return parsed;
96
+ return clampToPast(date.getTime());
108
97
  }
109
98
 
110
99
  /**
111
- * Hold the cutoff at "now", because a FUTURE epoch would refuse the sessions it
100
+ * Hold the cutoff at "now", because a FUTURE cutoff would refuse the sessions it
112
101
  * is about to mint as well as the old ones - an app nobody can sign in to, from a
113
102
  * mistyped year, with the fix hidden behind understanding this flag. Clamped, a
114
103
  * future date means what an operator setting it always meant: clear everything
115
104
  * outstanding, then carry on.
116
105
  */
117
- function clampToPast(epochMs: number): number {
106
+ function clampToPast(cutoffMs: number): number {
118
107
  const now = Date.now();
119
- if (epochMs <= now) return epochMs;
120
- logger.warn(`${env.name(SESSION_EPOCH_ENV)} is in the future - clamping to now`, {
121
- configured: new Date(epochMs).toISOString(),
108
+ if (cutoffMs <= now) return cutoffMs;
109
+ logger.warn(`${env.name(SESSION_CUTOFF_ENV)} is in the future - clamping to now`, {
110
+ configured: new Date(cutoffMs).toISOString(),
122
111
  });
123
112
  return now;
124
113
  }
125
114
 
126
- /** The cache key for one epoch, so moving the epoch orphans every earlier key. */
127
- function cacheKey(epochMs: number): string {
128
- return `${KEY_PREFIX}${epochMs}`;
115
+ /** The cache key for one cutoff, so moving the cutoff orphans every earlier key. */
116
+ function cacheKey(cutoffMs: number): string {
117
+ return `${KEY_PREFIX}${cutoffMs}`;
129
118
  }
130
119
 
131
120
  function decode(stored: StoredKey): Uint8Array {
@@ -133,12 +122,12 @@ function decode(stored: StoredKey): Uint8Array {
133
122
  }
134
123
 
135
124
  /**
136
- * Load the signing key for `epochMs` from the cache, minting and storing one when
137
- * absent. See the module docs for why the write is followed by a re-read.
125
+ * Load the signing key for `cutoffMs` from the cache, minting and storing one
126
+ * when absent. See the module docs for why the write is followed by a re-read.
138
127
  */
139
- async function loadFromCache(epochMs: number): Promise<Uint8Array> {
128
+ async function loadFromCache(cutoffMs: number): Promise<Uint8Array> {
140
129
  const cache = CacheManager.getInstanceSync();
141
- const key = cacheKey(epochMs);
130
+ const key = cacheKey(cutoffMs);
142
131
 
143
132
  const existing = await cache.get<StoredKey>(key);
144
133
  if (existing?.secret) {
@@ -164,11 +153,11 @@ async function loadFromCache(epochMs: number): Promise<Uint8Array> {
164
153
  return decode(settled);
165
154
  }
166
155
 
167
- /** A resolved key plus the epoch it is scoped to. */
156
+ /** A resolved key plus the cutoff it is scoped to. */
168
157
  export interface SigningKey {
169
158
  key: Uint8Array;
170
- /** Force-clear epoch in ms; `0` when unset. Tokens older than this are refused. */
171
- epochMs: number;
159
+ /** Force-clear cutoff in ms; `0` when unset. Tokens older than this are refused. */
160
+ cutoffMs: number;
172
161
  }
173
162
 
174
163
  let pending: Promise<SigningKey> | undefined;
@@ -176,7 +165,7 @@ let pending: Promise<SigningKey> | undefined;
176
165
  /**
177
166
  * The signing key for this gate, resolved once per process.
178
167
  *
179
- * Resolved ONCE, so `configuredEpoch` is honoured only on the first call - the
168
+ * Resolved ONCE, so `configuredCutoff` is honoured only on the first call - the
180
169
  * plugin's `setup()` passes its resolved value there, before any request can
181
170
  * reach the lazy path.
182
171
  *
@@ -186,21 +175,21 @@ let pending: Promise<SigningKey> | undefined;
186
175
  * so losing it costs sessions, never admission - a caller still needs a code
187
176
  * delivered to an allow-listed address.
188
177
  */
189
- export function signingKey(configuredEpoch?: string | number | Date): Promise<SigningKey> {
178
+ export function signingKey(configuredCutoff?: string | number | Date): Promise<SigningKey> {
190
179
  pending ??= (async () => {
191
- const epochMs = resolveSessionEpoch(configuredEpoch);
180
+ const cutoffMs = resolveSessionCutoff(configuredCutoff);
192
181
  const configured = env.text(JWT_SECRET_ENV);
193
182
  if (configured) {
194
- return { key: new TextEncoder().encode(configured), epochMs };
183
+ return { key: new TextEncoder().encode(configured), cutoffMs };
195
184
  }
196
185
  try {
197
- return { key: await loadFromCache(epochMs), epochMs };
186
+ return { key: await loadFromCache(cutoffMs), cutoffMs };
198
187
  } catch (error) {
199
188
  logger.warn(
200
189
  `no ${env.name(JWT_SECRET_ENV)} and the cache is unavailable - using an ephemeral per-process key; sessions will not survive a restart`,
201
190
  { error },
202
191
  );
203
- return { key: new Uint8Array(randomBytes(KEY_BYTES)), epochMs };
192
+ return { key: new Uint8Array(randomBytes(KEY_BYTES)), cutoffMs };
204
193
  }
205
194
  })();
206
195
  return pending;