@frontmcp/utils 1.9.6 → 1.9.7

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
@@ -1,10 +1,8 @@
1
1
  # @frontmcp/utils
2
2
 
3
- Shared utility functions for the FrontMCP ecosystem.
3
+ Shared helpers for the FrontMCP packages: names and URI templates, cross-platform crypto, key-value storage, file system access, environment detection and more. This is an internal package: `@frontmcp/sdk` and the other `@frontmcp/*` packages depend on it and install it for you. Import it directly only when your own code, like a plugin, needs one of these helpers.
4
4
 
5
- [![NPM](https://img.shields.io/npm/v/@frontmcp/utils.svg)](https://www.npmjs.com/package/@frontmcp/utils)
6
-
7
- > **Internal package.** Used by `@frontmcp/sdk` and other `@frontmcp/*` libraries — most users do not need to install this directly.
5
+ [![npm](https://img.shields.io/npm/v/@frontmcp/utils.svg)](https://www.npmjs.com/package/@frontmcp/utils) [![License](https://img.shields.io/npm/l/@frontmcp/utils.svg)](https://github.com/agentfront/frontmcp/blob/main/LICENSE)
8
6
 
9
7
  ## Install
10
8
 
@@ -12,40 +10,63 @@ Shared utility functions for the FrontMCP ecosystem.
12
10
  npm install @frontmcp/utils
13
11
  ```
14
12
 
15
- ## Features
16
-
17
- **Naming** — `splitWords`, `toCase`, `shortHash`, `ensureMaxLen`, `idFromString` for string manipulation and case conversion
18
-
19
- **URI** — `isValidMcpUri`, `extractUriScheme`, `parseUriTemplate`, `matchUriTemplate`, `expandUriTemplate` (RFC 3986 / RFC 6570)
20
-
21
- **Path** — `trimSlashes`, `joinPath` for URL path operations
13
+ It needs Node.js 24 or later. The crypto, async context, event emitter, environment and path helpers also have browser builds, picked through the package's `imports` map. `@frontmcp/lazy-zod` is a required peer dependency (it needs `zod` 4). The others are optional, one per storage backend:
22
14
 
23
- **Content** — `sanitizeToJson`, `inferMimeType` for safe serialization and MIME detection
15
+ | Optional peer | Enables |
16
+ | ---------------- | ------------------------------------------------------------ |
17
+ | `ioredis` | `createStorage({ type: 'redis' })` and `RedisStorageAdapter` |
18
+ | `@vercel/kv` | `createStorage({ type: 'vercel-kv' })` |
19
+ | `@upstash/redis` | `createStorage({ type: 'upstash' })` |
24
20
 
25
- **HTTP** — `validateBaseUrl` for URL validation and normalization
26
-
27
- **Crypto** — `sha256`, `sha256Hex`, `sha256Base64url`, `hkdfSha256`, `encryptAesGcm`, `decryptAesGcm`, `randomBytes`, `randomUUID`, `generateCodeVerifier`, `generateCodeChallenge`, `generatePkcePair`, `base64urlEncode`, `base64urlDecode` for cross-platform cryptography
28
-
29
- **File system** — `readFile`, `writeFile`, `mkdir`, `stat`, `fileExists`, `readJSON`, `writeJSON`, `ensureDir`, `isDirEmpty`, `runCmd` and more — lazy-loaded for Node.js environments
30
-
31
- ## Quick Example
21
+ ## Usage
32
22
 
33
23
  ```ts
34
- import { fileExists, matchUriTemplate, sha256Hex } from '@frontmcp/utils';
24
+ import { createStorage, expandUriTemplate, matchUriTemplate, sha256Hex, toCase } from '@frontmcp/utils';
35
25
 
36
- const params = matchUriTemplate('users/{id}/posts/{postId}', 'users/123/posts/456');
37
- // { id: '123', postId: '456' }
26
+ matchUriTemplate('tickets://{id}/comments/{commentId}', 'tickets://T-1/comments/42');
27
+ // { id: 'T-1', commentId: '42' }
38
28
 
39
- const hash = sha256Hex('hello world');
29
+ expandUriTemplate('tickets://{id}', { id: 'T-2' }); // 'tickets://T-2'
30
+ toCase(['create', 'ticket'], 'snake'); // 'create_ticket'
31
+ sha256Hex('hello world'); // 'b94d27b9...'
40
32
 
41
- const exists = await fileExists('/path/to/file');
33
+ const storage = await createStorage({ type: 'memory' });
34
+ await storage.set('ticket:T-1', 'open', { ttlSeconds: 60 });
35
+ await storage.get('ticket:T-1'); // 'open'
42
36
  ```
43
37
 
44
- ## Related Packages
45
-
46
- - [`@frontmcp/sdk`](../sdk) — core framework
47
- - [`@frontmcp/auth`](../auth) — uses crypto utilities for PKCE, encryption
38
+ ## What's in it
39
+
40
+ | Area | Exports |
41
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | Naming | `splitWords`, `toCase`, `shortHash`, `ensureMaxLen`, `idFromString` |
43
+ | URIs | `isValidMcpUri`, `extractUriScheme`, `parseUriTemplate`, `matchUriTemplate`, `expandUriTemplate`, `extractTemplateParams` (RFC 3986 URIs, RFC 6570 templates) |
44
+ | Paths | `trimSlashes`, `joinPath`, and cross-platform `pathJoin`, `pathResolve`, `basename`, `dirname`, `extname` |
45
+ | Content | `sanitizeToJson`, `inferMimeType`, `safeStringify` |
46
+ | HTTP | `validateBaseUrl`, `buildSetCookie`, `parseCookies`, `getCookie`, `isLocalhost`, `isSecureRequest` |
47
+ | Network | `classifyIpAddress`, `parseIpv4`, `expandIpv6`, `isRedirectResponse`, for SSRF (server-side request forgery) checks |
48
+ | Escaping | `escapeHtml`, `escapeHtmlAttr`, `escapeJsString`, `safeJsonForScript` |
49
+ | Crypto | `sha256`, `sha256Hex`, `hmacSha256`, `hkdfSha256`, `encryptAesGcm`, `decryptAesGcm`, `randomBytes`, `randomUUID`, `timingSafeEqual`, `base64urlEncode`, `base64urlDecode` |
50
+ | PKCE | `generateCodeVerifier`, `generateCodeChallenge`, `verifyCodeChallenge`, `generatePkcePair` (RFC 7636) |
51
+ | Encryption, keys | `encryptValue`, `decryptValue`, `signData`, `verifyData`, `KeyPersistence`, `createKeyPersistence` |
52
+ | Storage | `createStorage`, `createMemoryStorage`, `TypedStorage`, `EncryptedTypedStorage`, and adapters for memory, Redis, Vercel KV, Upstash and the file system |
53
+ | File system | `readFile`, `writeFile`, `mkdir`, `stat`, `fileExists`, `readJSON`, `writeJSON`, `ensureDir`, `isDirEmpty`, `runCmd` and more. Node.js only; `fs` loads on first use. |
54
+ | Environment | `getEnv`, `isProduction`, `isDevelopment`, `isEdgeRuntime`, `isServerless`, `getRuntimeContext`, `isNode`, `isBrowser` |
55
+ | Context, events | `AsyncLocalStorage`, `EventEmitter`, both with browser builds |
56
+ | Safe regex | `isPatternSafe`, `createSafeRegExp`, `safeTest`, `safeMatch`, `safeReplace`, against ReDoS (regular expression denial of service) |
57
+ | Model tool calls | `processPlatformToolCalls`, which passes OpenAI, Claude or Vercel AI tool calls to your `callTool` function and returns the results in that platform's format |
58
+ | Machine ID | `getMachineId`, `setMachineIdOverride`, `getHostname` |
59
+
60
+ ## Related packages
61
+
62
+ - [`@frontmcp/sdk`](https://www.npmjs.com/package/@frontmcp/sdk): the framework, built on these helpers.
63
+ - [`@frontmcp/auth`](https://www.npmjs.com/package/@frontmcp/auth): uses the crypto, PKCE and storage helpers.
64
+ - [`@frontmcp/storage-sqlite`](https://www.npmjs.com/package/@frontmcp/storage-sqlite): a SQLite storage adapter that implements this package's `StorageAdapter`.
65
+
66
+ ## Contributing
67
+
68
+ Source: [`libs/utils`](https://github.com/agentfront/frontmcp/tree/main/libs/utils). Issues and pull requests are welcome; see [CONTRIBUTING.md](https://github.com/agentfront/frontmcp/blob/main/CONTRIBUTING.md).
48
69
 
49
70
  ## License
50
71
 
51
- Apache-2.0 — see [LICENSE](../../LICENSE).
72
+ Apache-2.0. See [LICENSE](https://github.com/agentfront/frontmcp/blob/main/LICENSE).
package/esm/index.mjs CHANGED
@@ -1283,6 +1283,14 @@ var init_base = __esm({
1283
1283
  "Use the memory, Redis, Upstash or Vercel KV adapter for an atomic compare-and-delete."
1284
1284
  );
1285
1285
  }
1286
+ /** Default: not supported, for the same reason as {@link deleteIfEquals}. */
1287
+ async expireIfEquals(_key, _expectedValue, _ttlSeconds) {
1288
+ throw new StorageNotSupportedError(
1289
+ "expireIfEquals",
1290
+ this.backendName,
1291
+ "Use the memory, Redis, Upstash or Vercel KV adapter for an atomic compare-and-expire."
1292
+ );
1293
+ }
1286
1294
  // ============================================
1287
1295
  // Pub/Sub (default: not supported)
1288
1296
  // ============================================
@@ -1438,6 +1446,12 @@ var init_memory = __esm({
1438
1446
  if (!entry || isExpired(entry.expiresAt) || entry.value !== expectedValue) return false;
1439
1447
  return this.deleteEntry(key);
1440
1448
  }
1449
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
1450
+ this.ensureConnected();
1451
+ const entry = this.store.get(key);
1452
+ if (!entry || isExpired(entry.expiresAt) || entry.value !== expectedValue) return false;
1453
+ return this.expire(key, ttlSeconds);
1454
+ }
1441
1455
  async exists(key) {
1442
1456
  this.ensureConnected();
1443
1457
  const entry = this.store.get(key);
@@ -2951,11 +2965,12 @@ var init_utils = __esm({
2951
2965
  });
2952
2966
 
2953
2967
  // libs/utils/src/storage/utils/compare-and-delete.ts
2954
- var COMPARE_AND_DELETE_SCRIPT;
2968
+ var COMPARE_AND_DELETE_SCRIPT, COMPARE_AND_EXPIRE_SCRIPT;
2955
2969
  var init_compare_and_delete = __esm({
2956
2970
  "libs/utils/src/storage/utils/compare-and-delete.ts"() {
2957
2971
  "use strict";
2958
2972
  COMPARE_AND_DELETE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0`;
2973
+ COMPARE_AND_EXPIRE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('EXPIRE', KEYS[1], ARGV[2]) end return 0`;
2959
2974
  }
2960
2975
  });
2961
2976
 
@@ -3200,6 +3215,17 @@ var init_redis = __esm({
3200
3215
  );
3201
3216
  return deleted === 1;
3202
3217
  }
3218
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
3219
+ validateTTL(ttlSeconds);
3220
+ const extended = await this.getConnectedClient().eval(
3221
+ COMPARE_AND_EXPIRE_SCRIPT,
3222
+ 1,
3223
+ this.prefixKey(key),
3224
+ expectedValue,
3225
+ String(ttlSeconds)
3226
+ );
3227
+ return extended === 1;
3228
+ }
3203
3229
  async exists(key) {
3204
3230
  const result = await this.getConnectedClient().exists(this.prefixKey(key));
3205
3231
  return result > 0;
@@ -3533,6 +3559,15 @@ var init_vercel_kv = __esm({
3533
3559
  );
3534
3560
  return deleted === 1;
3535
3561
  }
3562
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
3563
+ validateTTL(ttlSeconds);
3564
+ const extended = await this.getConnectedClient().eval(
3565
+ COMPARE_AND_EXPIRE_SCRIPT,
3566
+ [this.prefixKey(key)],
3567
+ [expectedValue, String(ttlSeconds)]
3568
+ );
3569
+ return extended === 1;
3570
+ }
3536
3571
  async exists(key) {
3537
3572
  const result = await this.getConnectedClient().exists(this.prefixKey(key));
3538
3573
  return result > 0;
@@ -3768,6 +3803,15 @@ var init_upstash = __esm({
3768
3803
  );
3769
3804
  return deleted === 1;
3770
3805
  }
3806
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
3807
+ validateTTL(ttlSeconds);
3808
+ const extended = await this.getConnectedClient().eval(
3809
+ COMPARE_AND_EXPIRE_SCRIPT,
3810
+ [this.prefixKey(key)],
3811
+ [expectedValue, String(ttlSeconds)]
3812
+ );
3813
+ return extended === 1;
3814
+ }
3771
3815
  async exists(key) {
3772
3816
  const result = await this.getConnectedClient().exists(this.prefixKey(key));
3773
3817
  return result > 0;
@@ -5113,6 +5157,12 @@ var NamespacedStorageImpl = class _NamespacedStorageImpl {
5113
5157
  }
5114
5158
  return this.adapter.deleteIfEquals(this.prefixKey(key), expectedValue);
5115
5159
  }
5160
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
5161
+ if (!this.adapter.expireIfEquals) {
5162
+ throw new StorageNotSupportedError("expireIfEquals", "this storage adapter");
5163
+ }
5164
+ return this.adapter.expireIfEquals(this.prefixKey(key), expectedValue, ttlSeconds);
5165
+ }
5116
5166
  // ============================================
5117
5167
  // Pub/Sub (with channel prefixing)
5118
5168
  // ============================================
package/index.js CHANGED
@@ -1285,6 +1285,14 @@ var init_base = __esm({
1285
1285
  "Use the memory, Redis, Upstash or Vercel KV adapter for an atomic compare-and-delete."
1286
1286
  );
1287
1287
  }
1288
+ /** Default: not supported, for the same reason as {@link deleteIfEquals}. */
1289
+ async expireIfEquals(_key, _expectedValue, _ttlSeconds) {
1290
+ throw new StorageNotSupportedError(
1291
+ "expireIfEquals",
1292
+ this.backendName,
1293
+ "Use the memory, Redis, Upstash or Vercel KV adapter for an atomic compare-and-expire."
1294
+ );
1295
+ }
1288
1296
  // ============================================
1289
1297
  // Pub/Sub (default: not supported)
1290
1298
  // ============================================
@@ -1440,6 +1448,12 @@ var init_memory = __esm({
1440
1448
  if (!entry || isExpired(entry.expiresAt) || entry.value !== expectedValue) return false;
1441
1449
  return this.deleteEntry(key);
1442
1450
  }
1451
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
1452
+ this.ensureConnected();
1453
+ const entry = this.store.get(key);
1454
+ if (!entry || isExpired(entry.expiresAt) || entry.value !== expectedValue) return false;
1455
+ return this.expire(key, ttlSeconds);
1456
+ }
1443
1457
  async exists(key) {
1444
1458
  this.ensureConnected();
1445
1459
  const entry = this.store.get(key);
@@ -2953,11 +2967,12 @@ var init_utils = __esm({
2953
2967
  });
2954
2968
 
2955
2969
  // libs/utils/src/storage/utils/compare-and-delete.ts
2956
- var COMPARE_AND_DELETE_SCRIPT;
2970
+ var COMPARE_AND_DELETE_SCRIPT, COMPARE_AND_EXPIRE_SCRIPT;
2957
2971
  var init_compare_and_delete = __esm({
2958
2972
  "libs/utils/src/storage/utils/compare-and-delete.ts"() {
2959
2973
  "use strict";
2960
2974
  COMPARE_AND_DELETE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0`;
2975
+ COMPARE_AND_EXPIRE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('EXPIRE', KEYS[1], ARGV[2]) end return 0`;
2961
2976
  }
2962
2977
  });
2963
2978
 
@@ -3202,6 +3217,17 @@ var init_redis = __esm({
3202
3217
  );
3203
3218
  return deleted === 1;
3204
3219
  }
3220
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
3221
+ validateTTL(ttlSeconds);
3222
+ const extended = await this.getConnectedClient().eval(
3223
+ COMPARE_AND_EXPIRE_SCRIPT,
3224
+ 1,
3225
+ this.prefixKey(key),
3226
+ expectedValue,
3227
+ String(ttlSeconds)
3228
+ );
3229
+ return extended === 1;
3230
+ }
3205
3231
  async exists(key) {
3206
3232
  const result = await this.getConnectedClient().exists(this.prefixKey(key));
3207
3233
  return result > 0;
@@ -3535,6 +3561,15 @@ var init_vercel_kv = __esm({
3535
3561
  );
3536
3562
  return deleted === 1;
3537
3563
  }
3564
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
3565
+ validateTTL(ttlSeconds);
3566
+ const extended = await this.getConnectedClient().eval(
3567
+ COMPARE_AND_EXPIRE_SCRIPT,
3568
+ [this.prefixKey(key)],
3569
+ [expectedValue, String(ttlSeconds)]
3570
+ );
3571
+ return extended === 1;
3572
+ }
3538
3573
  async exists(key) {
3539
3574
  const result = await this.getConnectedClient().exists(this.prefixKey(key));
3540
3575
  return result > 0;
@@ -3770,6 +3805,15 @@ var init_upstash = __esm({
3770
3805
  );
3771
3806
  return deleted === 1;
3772
3807
  }
3808
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
3809
+ validateTTL(ttlSeconds);
3810
+ const extended = await this.getConnectedClient().eval(
3811
+ COMPARE_AND_EXPIRE_SCRIPT,
3812
+ [this.prefixKey(key)],
3813
+ [expectedValue, String(ttlSeconds)]
3814
+ );
3815
+ return extended === 1;
3816
+ }
3773
3817
  async exists(key) {
3774
3818
  const result = await this.getConnectedClient().exists(this.prefixKey(key));
3775
3819
  return result > 0;
@@ -5323,6 +5367,12 @@ var NamespacedStorageImpl = class _NamespacedStorageImpl {
5323
5367
  }
5324
5368
  return this.adapter.deleteIfEquals(this.prefixKey(key), expectedValue);
5325
5369
  }
5370
+ async expireIfEquals(key, expectedValue, ttlSeconds) {
5371
+ if (!this.adapter.expireIfEquals) {
5372
+ throw new StorageNotSupportedError("expireIfEquals", "this storage adapter");
5373
+ }
5374
+ return this.adapter.expireIfEquals(this.prefixKey(key), expectedValue, ttlSeconds);
5375
+ }
5326
5376
  // ============================================
5327
5377
  // Pub/Sub (with channel prefixing)
5328
5378
  // ============================================
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@frontmcp/utils",
3
- "version": "1.9.6",
4
- "description": "Shared utility functions for FrontMCP - string manipulation, URI handling, path utilities, and more",
3
+ "version": "1.9.7",
4
+ "description": "Shared helpers for FrontMCP: URI templates, naming, cross-platform crypto and PKCE, key-value storage, file system and environment detection",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
8
+ "frontmcp",
9
+ "mcp",
8
10
  "utils",
9
- "utilities",
10
- "uri",
11
- "url",
12
- "string",
13
- "path",
14
- "typescript",
15
- "mcp"
11
+ "uri-template",
12
+ "crypto",
13
+ "pkce",
14
+ "storage",
15
+ "redis",
16
+ "typescript"
16
17
  ],
17
18
  "repository": {
18
19
  "type": "git",
@@ -22,7 +23,7 @@
22
23
  "bugs": {
23
24
  "url": "https://github.com/agentfront/frontmcp/issues"
24
25
  },
25
- "homepage": "https://github.com/agentfront/frontmcp/blob/main/libs/utils/README.md",
26
+ "homepage": "https://frontmcp.dev",
26
27
  "engines": {
27
28
  "node": ">=24.0.0"
28
29
  },
@@ -32,7 +33,7 @@
32
33
  "@noble/hashes": "^2.0.1"
33
34
  },
34
35
  "peerDependencies": {
35
- "@frontmcp/lazy-zod": "1.9.6",
36
+ "@frontmcp/lazy-zod": "1.9.7",
36
37
  "@upstash/redis": "^1.34.0",
37
38
  "@vercel/kv": "^2.0.0 || ^3.0.0",
38
39
  "ioredis": "^5.0.0"
@@ -68,6 +68,8 @@ export declare abstract class BaseStorageAdapter implements StorageAdapter {
68
68
  * Override in adapters whose backend can compare and delete in one step.
69
69
  */
70
70
  deleteIfEquals(_key: string, _expectedValue: string): Promise<boolean>;
71
+ /** Default: not supported, for the same reason as {@link deleteIfEquals}. */
72
+ expireIfEquals(_key: string, _expectedValue: string, _ttlSeconds: number): Promise<boolean>;
71
73
  /**
72
74
  * Default: pub/sub not supported.
73
75
  * Override in adapters that support it.
@@ -43,6 +43,7 @@ export declare class MemoryStorageAdapter extends BaseStorageAdapter {
43
43
  protected doSet(key: string, value: string, options?: SetOptions): Promise<void>;
44
44
  delete(key: string): Promise<boolean>;
45
45
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
46
+ expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
46
47
  exists(key: string): Promise<boolean>;
47
48
  expire(key: string, ttlSeconds: number): Promise<boolean>;
48
49
  ttl(key: string): Promise<number | null>;
@@ -59,6 +59,7 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
59
59
  protected doSet(key: string, value: string, options?: SetOptions): Promise<void>;
60
60
  delete(key: string): Promise<boolean>;
61
61
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
62
+ expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
62
63
  exists(key: string): Promise<boolean>;
63
64
  mget(keys: string[]): Promise<(string | null)[]>;
64
65
  mset(entries: import('../types').SetEntry[]): Promise<void>;
@@ -53,6 +53,7 @@ export declare class UpstashStorageAdapter extends BaseStorageAdapter {
53
53
  protected doSet(key: string, value: string, options?: SetOptions): Promise<void>;
54
54
  delete(key: string): Promise<boolean>;
55
55
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
56
+ expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
56
57
  exists(key: string): Promise<boolean>;
57
58
  mget(keys: string[]): Promise<(string | null)[]>;
58
59
  mdelete(keys: string[]): Promise<number>;
@@ -88,6 +88,7 @@ export declare class VercelKvStorageAdapter extends BaseStorageAdapter {
88
88
  protected doSet(key: string, value: string, options?: SetOptions): Promise<void>;
89
89
  delete(key: string): Promise<boolean>;
90
90
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
91
+ expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
91
92
  exists(key: string): Promise<boolean>;
92
93
  mget(keys: string[]): Promise<(string | null)[]>;
93
94
  mdelete(keys: string[]): Promise<number>;
@@ -67,6 +67,7 @@ export declare class NamespacedStorageImpl implements NamespacedStorage {
67
67
  decr(key: string): Promise<number>;
68
68
  incrBy(key: string, amount: number): Promise<number>;
69
69
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
70
+ expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
70
71
  publish(channel: string, message: string): Promise<number>;
71
72
  subscribe(channel: string, handler: MessageHandler): Promise<Unsubscribe>;
72
73
  supportsPubSub(): boolean;
@@ -214,6 +214,16 @@ export interface StorageAdapter {
214
214
  * @throws StorageNotSupportedError if the backend has no atomic compare-and-delete
215
215
  */
216
216
  deleteIfEquals?(key: string, expectedValue: string): Promise<boolean>;
217
+ /**
218
+ * Atomically set a key's TTL only while it still holds `expectedValue` (compare-and-expire).
219
+ *
220
+ * @param key - Storage key
221
+ * @param expectedValue - The exact value the key must hold
222
+ * @param ttlSeconds - The new time to live, in seconds
223
+ * @returns true if this call set the TTL, false if the key was missing or held another value
224
+ * @throws StorageNotSupportedError if the backend has no atomic compare-and-expire
225
+ */
226
+ expireIfEquals?(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
217
227
  /**
218
228
  * Publish a message to a channel.
219
229
  * Not all adapters support pub/sub (e.g., Vercel KV doesn't).
@@ -278,6 +288,8 @@ export interface NamespacedStorage extends StorageAdapter {
278
288
  namespace(name: string, id?: string): NamespacedStorage;
279
289
  /** {@link StorageAdapter.deleteIfEquals}; throws StorageNotSupportedError when the adapter has none. */
280
290
  deleteIfEquals(key: string, expectedValue: string): Promise<boolean>;
291
+ /** {@link StorageAdapter.expireIfEquals}; throws StorageNotSupportedError when the adapter has none. */
292
+ expireIfEquals(key: string, expectedValue: string, ttlSeconds: number): Promise<boolean>;
281
293
  /**
282
294
  * Get the underlying root adapter (for advanced use).
283
295
  * Use with caution - operations bypass namespace prefixing.
@@ -1,2 +1,4 @@
1
1
  /** Redis Lua script for `deleteIfEquals`: deletes KEYS[1] only while it holds ARGV[1], returning 1 or 0. */
2
2
  export declare const COMPARE_AND_DELETE_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0";
3
+ /** Redis Lua script for `expireIfEquals`: sets KEYS[1]'s TTL to ARGV[2] seconds only while it holds ARGV[1]. */
4
+ export declare const COMPARE_AND_EXPIRE_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('EXPIRE', KEYS[1], ARGV[2]) end return 0";