@adcp/sdk 7.8.0 → 7.9.0
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/AGENTS.md +2 -0
- package/bin/adcp.js +3 -1
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts +8 -0
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +15 -0
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +8 -7
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/protocols/mcp.d.ts.map +1 -1
- package/dist/lib/protocols/mcp.js +63 -4
- package/dist/lib/protocols/mcp.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts +12 -3
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/pg.js +59 -23
- package/dist/lib/server/ctx-metadata/backends/pg.js.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/redis.d.ts +163 -0
- package/dist/lib/server/ctx-metadata/backends/redis.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/redis.js +204 -0
- package/dist/lib/server/ctx-metadata/backends/redis.js.map +1 -0
- package/dist/lib/server/ctx-metadata/index.d.ts +2 -0
- package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -1
- package/dist/lib/server/ctx-metadata/index.js +3 -1
- package/dist/lib/server/ctx-metadata/index.js.map +1 -1
- package/dist/lib/server/idempotency/backends/redis.d.ts +171 -0
- package/dist/lib/server/idempotency/backends/redis.d.ts.map +1 -0
- package/dist/lib/server/idempotency/backends/redis.js +253 -0
- package/dist/lib/server/idempotency/backends/redis.js.map +1 -0
- package/dist/lib/server/idempotency/index.d.ts +2 -0
- package/dist/lib/server/idempotency/index.d.ts.map +1 -1
- package/dist/lib/server/idempotency/index.js +3 -1
- package/dist/lib/server/idempotency/index.js.map +1 -1
- package/dist/lib/server/idempotency/store.d.ts +44 -0
- package/dist/lib/server/idempotency/store.d.ts.map +1 -1
- package/dist/lib/server/idempotency/store.js.map +1 -1
- package/dist/lib/server/index.d.ts +4 -4
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +3 -1
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/signing/redis-replay-store.d.ts +180 -0
- package/dist/lib/signing/redis-replay-store.d.ts.map +1 -0
- package/dist/lib/signing/redis-replay-store.js +270 -0
- package/dist/lib/signing/redis-replay-store.js.map +1 -0
- package/dist/lib/signing/server.d.ts +1 -0
- package/dist/lib/signing/server.d.ts.map +1 -1
- package/dist/lib/signing/server.js +4 -2
- package/dist/lib/signing/server.js.map +1 -1
- package/dist/lib/utils/redis-default-prefix-warn.d.ts +60 -0
- package/dist/lib/utils/redis-default-prefix-warn.d.ts.map +1 -0
- package/dist/lib/utils/redis-default-prefix-warn.js +95 -0
- package/dist/lib/utils/redis-default-prefix-warn.js.map +1 -0
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/package.json +8 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis backend for `CtxMetadataStore`.
|
|
3
|
+
*
|
|
4
|
+
* Stores one key per `(account_id, kind, id)` carrying the JSON payload
|
|
5
|
+
* `{ value, resource?, expiresAt? }`. Entries with no `expiresAt` have
|
|
6
|
+
* no Redis TTL — ctx-metadata lifetimes can be months (a media buy can
|
|
7
|
+
* run all year), and silent eviction would produce "package not found"
|
|
8
|
+
* errors that look like publisher bugs and run for weeks.
|
|
9
|
+
*
|
|
10
|
+
* **TTL semantics.** When `entry.expiresAt` is set, the backend stores
|
|
11
|
+
* the key with `EX = expiresAt - now + expiredGraceSeconds` (default
|
|
12
|
+
* 60s grace) so the store layer's own expiry check (`entry.expiresAt <
|
|
13
|
+
* now`) can still run on values within the grace window. When
|
|
14
|
+
* `entry.expiresAt` is absent, the key is stored with no TTL — durable
|
|
15
|
+
* by default, matches the pg backend's `expires_at NULL` semantic.
|
|
16
|
+
*
|
|
17
|
+
* **`bulkGet` uses `MGET`.** Single round trip for any batch size,
|
|
18
|
+
* unlike the looped-`GET` fallback. Adopters using an escape-hatch
|
|
19
|
+
* `RedisLikeClient` adapter MUST implement `mGet` for batch shapes
|
|
20
|
+
* (`get_products` with N products, `create_media_buy` carrying a
|
|
21
|
+
* package list referencing products by ID).
|
|
22
|
+
*
|
|
23
|
+
* **`clearAll` intentionally omitted.** Same rationale as the
|
|
24
|
+
* idempotency Redis backend — a shared Redis is a production resource,
|
|
25
|
+
* and a compliance-reset `FLUSHDB` would nuke unrelated keys. Tests
|
|
26
|
+
* against a dedicated db index (`REDIS_URL=…/15`) call `FLUSHDB`
|
|
27
|
+
* themselves.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { createClient } from 'redis';
|
|
32
|
+
* import { createCtxMetadataStore, redisCtxMetadataStore } from '@adcp/sdk/server';
|
|
33
|
+
*
|
|
34
|
+
* const client = createClient({ url: process.env.REDIS_URL });
|
|
35
|
+
* client.on('error', (err) => console.error('redis error', err));
|
|
36
|
+
* await client.connect();
|
|
37
|
+
*
|
|
38
|
+
* const ctxMetadata = createCtxMetadataStore({
|
|
39
|
+
* backend: redisCtxMetadataStore(client),
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
import type { CtxMetadataBackend } from '../store';
|
|
44
|
+
import type { RedisClientType } from 'redis';
|
|
45
|
+
/**
|
|
46
|
+
* Escape-hatch interface for adopters not using the official `redis`
|
|
47
|
+
* client (node-redis v4/v5). Mirrors the methods this backend calls.
|
|
48
|
+
*
|
|
49
|
+
* `mGet` matches node-redis's batch-get shape (single round trip).
|
|
50
|
+
* `ioredis` exposes `mget(keys)` which returns `(string | null)[]` in
|
|
51
|
+
* the same order — the shim is one line.
|
|
52
|
+
*
|
|
53
|
+
* @example ioredis adapter
|
|
54
|
+
* ```typescript
|
|
55
|
+
* import Redis from 'ioredis';
|
|
56
|
+
* const ioredis = new Redis(process.env.REDIS_URL!);
|
|
57
|
+
*
|
|
58
|
+
* const client: CtxMetadataRedisLikeClient = {
|
|
59
|
+
* get: (k) => ioredis.get(k),
|
|
60
|
+
* mGet: (keys) => ioredis.mget(keys),
|
|
61
|
+
* set: (k, v, opts) =>
|
|
62
|
+
* opts?.EX !== undefined
|
|
63
|
+
* ? ioredis.set(k, v, 'EX', opts.EX)
|
|
64
|
+
* : ioredis.set(k, v),
|
|
65
|
+
* del: (k) => ioredis.del(k as string),
|
|
66
|
+
* ping: () => ioredis.ping(),
|
|
67
|
+
* };
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export interface CtxMetadataRedisLikeClient {
|
|
71
|
+
get(key: string): Promise<string | null>;
|
|
72
|
+
mGet(keys: string[]): Promise<(string | null)[]>;
|
|
73
|
+
set(key: string, value: string, options?: {
|
|
74
|
+
EX?: number;
|
|
75
|
+
}): Promise<string | null>;
|
|
76
|
+
del(key: string | string[]): Promise<number>;
|
|
77
|
+
ping(): Promise<string>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Accepted client shape: either a real `redis` (node-redis v4/v5)
|
|
81
|
+
* `RedisClientType` (the typical path — pass `createClient(...)`
|
|
82
|
+
* straight in) or an adapter conforming to `CtxMetadataRedisLikeClient`
|
|
83
|
+
* for non-node-redis clients (ioredis, Upstash, test doubles).
|
|
84
|
+
*/
|
|
85
|
+
export type CtxMetadataRedisBackendClient = RedisClientType<any, any, any> | CtxMetadataRedisLikeClient;
|
|
86
|
+
export interface RedisCtxMetadataBackendOptions {
|
|
87
|
+
/**
|
|
88
|
+
* Key prefix prepended to every scoped key written to Redis. Defaults
|
|
89
|
+
* to `"adcp:ctx_meta:"`.
|
|
90
|
+
*
|
|
91
|
+
* **Sharing a Redis db across deployments? Override this.** The
|
|
92
|
+
* default is fine for a dedicated Redis (or a dedicated db index).
|
|
93
|
+
* Two AdCP servers sharing the same db with the same default prefix
|
|
94
|
+
* collide on any overlapping `accountId` — the per-tenant scope
|
|
95
|
+
* segment can't carry deployment isolation on its own. Set a
|
|
96
|
+
* deployment-unique prefix (`"adcp:ctx_meta:prod-eu:"`, etc.) or use
|
|
97
|
+
* separate Redis dbs.
|
|
98
|
+
*/
|
|
99
|
+
keyPrefix?: string;
|
|
100
|
+
/**
|
|
101
|
+
* How many seconds past `entry.expiresAt` to keep the key alive in
|
|
102
|
+
* Redis so the store layer's expiry check (`entry.expiresAt < now`)
|
|
103
|
+
* can still observe the value within a clock-skew window. Defaults
|
|
104
|
+
* to 60s.
|
|
105
|
+
*
|
|
106
|
+
* Only applies to entries with an `expiresAt`; entries without one
|
|
107
|
+
* are stored with no TTL (durable by design).
|
|
108
|
+
*/
|
|
109
|
+
expiredGraceSeconds?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Suppress the one-time `console.warn` emitted at construction when
|
|
112
|
+
* the default `keyPrefix` is used against a node-redis client on db
|
|
113
|
+
* 0. Set to `true` if you know your Redis is dedicated to this
|
|
114
|
+
* deployment. The recommended fix is to set `keyPrefix` explicitly,
|
|
115
|
+
* not to suppress.
|
|
116
|
+
*/
|
|
117
|
+
suppressDefaultPrefixWarning?: boolean;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create a Redis-backed ctx-metadata cache.
|
|
121
|
+
*
|
|
122
|
+
* **Startup probe.** Call `store.probe()` before serving traffic to
|
|
123
|
+
* catch a bad `REDIS_URL` at boot rather than on the first
|
|
124
|
+
* ctx_metadata write.
|
|
125
|
+
*
|
|
126
|
+
* **Client error handling.** node-redis emits errors on the client
|
|
127
|
+
* itself for transient connection drops. Without a listener, Node's
|
|
128
|
+
* `EventEmitter` default-throws and crashes the process. Add one in
|
|
129
|
+
* your bootstrap:
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* client.on('error', (err) => console.error('redis error', err));
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* **Redis memory policy — set this on the deployment.** ctx_metadata
|
|
136
|
+
* entries default to durable (no TTL) when no `expiresAt` is provided,
|
|
137
|
+
* matching the pg sibling's `expires_at = NULL` semantic. Without a
|
|
138
|
+
* memory policy, an adopter who writes many durable entries can
|
|
139
|
+
* pressure Redis memory; once `maxmemory` is hit, default `noeviction`
|
|
140
|
+
* makes new writes fail. Choose deliberately:
|
|
141
|
+
*
|
|
142
|
+
* - **`maxmemory-policy allkeys-lru`** on a Redis db dedicated to AdCP
|
|
143
|
+
* ctx_metadata — evicts the oldest entries to make room. Acceptable
|
|
144
|
+
* for ctx_metadata because the next call referencing the same
|
|
145
|
+
* resource will write a fresh entry; evicted entries are recoverable
|
|
146
|
+
* as publisher traffic re-derives them, not lost permanently.
|
|
147
|
+
* - **`maxmemory-policy volatile-lru`** if you mostly use `expiresAt`
|
|
148
|
+
* on entries — bounds growth to TTL'd keys only.
|
|
149
|
+
* - **`maxmemory-policy noeviction`** (Redis default) — fail-closed:
|
|
150
|
+
* writes start erroring at the limit, mutating tools fail. Pages
|
|
151
|
+
* you instead of silently evicting.
|
|
152
|
+
*
|
|
153
|
+
* For ctx_metadata specifically, evicting cached entries is safer than
|
|
154
|
+
* for the idempotency cache (which uses eviction as a feature) because
|
|
155
|
+
* publisher re-derivation refills the cache on the next reference.
|
|
156
|
+
* Note that this isn't synchronous "re-hydrate on miss" — the
|
|
157
|
+
* framework reads `null` for a missing entry and falls through to the
|
|
158
|
+
* publisher's adapter, which may or may not produce a fresh
|
|
159
|
+
* `ctx_metadata` value on that path. `allkeys-lru` is the recommended
|
|
160
|
+
* default on a dedicated db.
|
|
161
|
+
*/
|
|
162
|
+
export declare function redisCtxMetadataStore(client: CtxMetadataRedisBackendClient, options?: RedisCtxMetadataBackendOptions): CtxMetadataBackend;
|
|
163
|
+
//# sourceMappingURL=redis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../../../../src/lib/server/ctx-metadata/backends/redis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,UAAU,CAAC;AAIrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAG7C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,6BAA6B,GAAG,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,0BAA0B,CAAC;AAExG,MAAM,WAAW,8BAA8B;IAC7C;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;CACxC;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,6BAA6B,EACrC,OAAO,GAAE,8BAAmC,GAC3C,kBAAkB,CAwHpB"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis backend for `CtxMetadataStore`.
|
|
4
|
+
*
|
|
5
|
+
* Stores one key per `(account_id, kind, id)` carrying the JSON payload
|
|
6
|
+
* `{ value, resource?, expiresAt? }`. Entries with no `expiresAt` have
|
|
7
|
+
* no Redis TTL — ctx-metadata lifetimes can be months (a media buy can
|
|
8
|
+
* run all year), and silent eviction would produce "package not found"
|
|
9
|
+
* errors that look like publisher bugs and run for weeks.
|
|
10
|
+
*
|
|
11
|
+
* **TTL semantics.** When `entry.expiresAt` is set, the backend stores
|
|
12
|
+
* the key with `EX = expiresAt - now + expiredGraceSeconds` (default
|
|
13
|
+
* 60s grace) so the store layer's own expiry check (`entry.expiresAt <
|
|
14
|
+
* now`) can still run on values within the grace window. When
|
|
15
|
+
* `entry.expiresAt` is absent, the key is stored with no TTL — durable
|
|
16
|
+
* by default, matches the pg backend's `expires_at NULL` semantic.
|
|
17
|
+
*
|
|
18
|
+
* **`bulkGet` uses `MGET`.** Single round trip for any batch size,
|
|
19
|
+
* unlike the looped-`GET` fallback. Adopters using an escape-hatch
|
|
20
|
+
* `RedisLikeClient` adapter MUST implement `mGet` for batch shapes
|
|
21
|
+
* (`get_products` with N products, `create_media_buy` carrying a
|
|
22
|
+
* package list referencing products by ID).
|
|
23
|
+
*
|
|
24
|
+
* **`clearAll` intentionally omitted.** Same rationale as the
|
|
25
|
+
* idempotency Redis backend — a shared Redis is a production resource,
|
|
26
|
+
* and a compliance-reset `FLUSHDB` would nuke unrelated keys. Tests
|
|
27
|
+
* against a dedicated db index (`REDIS_URL=…/15`) call `FLUSHDB`
|
|
28
|
+
* themselves.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { createClient } from 'redis';
|
|
33
|
+
* import { createCtxMetadataStore, redisCtxMetadataStore } from '@adcp/sdk/server';
|
|
34
|
+
*
|
|
35
|
+
* const client = createClient({ url: process.env.REDIS_URL });
|
|
36
|
+
* client.on('error', (err) => console.error('redis error', err));
|
|
37
|
+
* await client.connect();
|
|
38
|
+
*
|
|
39
|
+
* const ctxMetadata = createCtxMetadataStore({
|
|
40
|
+
* backend: redisCtxMetadataStore(client),
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.redisCtxMetadataStore = redisCtxMetadataStore;
|
|
46
|
+
const redis_default_prefix_warn_1 = require("../../../utils/redis-default-prefix-warn");
|
|
47
|
+
const DEFAULT_KEY_PREFIX = 'adcp:ctx_meta:';
|
|
48
|
+
const DEFAULT_EXPIRED_GRACE_SECONDS = 60;
|
|
49
|
+
/**
|
|
50
|
+
* Create a Redis-backed ctx-metadata cache.
|
|
51
|
+
*
|
|
52
|
+
* **Startup probe.** Call `store.probe()` before serving traffic to
|
|
53
|
+
* catch a bad `REDIS_URL` at boot rather than on the first
|
|
54
|
+
* ctx_metadata write.
|
|
55
|
+
*
|
|
56
|
+
* **Client error handling.** node-redis emits errors on the client
|
|
57
|
+
* itself for transient connection drops. Without a listener, Node's
|
|
58
|
+
* `EventEmitter` default-throws and crashes the process. Add one in
|
|
59
|
+
* your bootstrap:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* client.on('error', (err) => console.error('redis error', err));
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* **Redis memory policy — set this on the deployment.** ctx_metadata
|
|
66
|
+
* entries default to durable (no TTL) when no `expiresAt` is provided,
|
|
67
|
+
* matching the pg sibling's `expires_at = NULL` semantic. Without a
|
|
68
|
+
* memory policy, an adopter who writes many durable entries can
|
|
69
|
+
* pressure Redis memory; once `maxmemory` is hit, default `noeviction`
|
|
70
|
+
* makes new writes fail. Choose deliberately:
|
|
71
|
+
*
|
|
72
|
+
* - **`maxmemory-policy allkeys-lru`** on a Redis db dedicated to AdCP
|
|
73
|
+
* ctx_metadata — evicts the oldest entries to make room. Acceptable
|
|
74
|
+
* for ctx_metadata because the next call referencing the same
|
|
75
|
+
* resource will write a fresh entry; evicted entries are recoverable
|
|
76
|
+
* as publisher traffic re-derives them, not lost permanently.
|
|
77
|
+
* - **`maxmemory-policy volatile-lru`** if you mostly use `expiresAt`
|
|
78
|
+
* on entries — bounds growth to TTL'd keys only.
|
|
79
|
+
* - **`maxmemory-policy noeviction`** (Redis default) — fail-closed:
|
|
80
|
+
* writes start erroring at the limit, mutating tools fail. Pages
|
|
81
|
+
* you instead of silently evicting.
|
|
82
|
+
*
|
|
83
|
+
* For ctx_metadata specifically, evicting cached entries is safer than
|
|
84
|
+
* for the idempotency cache (which uses eviction as a feature) because
|
|
85
|
+
* publisher re-derivation refills the cache on the next reference.
|
|
86
|
+
* Note that this isn't synchronous "re-hydrate on miss" — the
|
|
87
|
+
* framework reads `null` for a missing entry and falls through to the
|
|
88
|
+
* publisher's adapter, which may or may not produce a fresh
|
|
89
|
+
* `ctx_metadata` value on that path. `allkeys-lru` is the recommended
|
|
90
|
+
* default on a dedicated db.
|
|
91
|
+
*/
|
|
92
|
+
function redisCtxMetadataStore(client, options = {}) {
|
|
93
|
+
// The function calls only the methods on CtxMetadataRedisLikeClient.
|
|
94
|
+
// The wider RedisClientType union covers the node-redis happy path
|
|
95
|
+
// without forcing a cast at the call site; internally we narrow.
|
|
96
|
+
const c = client;
|
|
97
|
+
const keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
98
|
+
const expiredGraceSeconds = options.expiredGraceSeconds ?? DEFAULT_EXPIRED_GRACE_SECONDS;
|
|
99
|
+
if (!Number.isFinite(expiredGraceSeconds) || expiredGraceSeconds < 0) {
|
|
100
|
+
throw new Error(`redisCtxMetadataStore: expiredGraceSeconds must be a non-negative finite number. Got ${expiredGraceSeconds}.`);
|
|
101
|
+
}
|
|
102
|
+
(0, redis_default_prefix_warn_1.maybeWarnOnSharedRedisPrefix)({
|
|
103
|
+
client,
|
|
104
|
+
callerKeyPrefix: options.keyPrefix,
|
|
105
|
+
defaultKeyPrefix: DEFAULT_KEY_PREFIX,
|
|
106
|
+
suppress: options.suppressDefaultPrefixWarning,
|
|
107
|
+
backendName: 'redisCtxMetadataStore',
|
|
108
|
+
});
|
|
109
|
+
function prefixed(scopedKey) {
|
|
110
|
+
return `${keyPrefix}${scopedKey}`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Compute the Redis TTL when `expiresAt` is set. Returns `undefined`
|
|
114
|
+
* when no TTL should be applied (entry is durable). Throws if the
|
|
115
|
+
* resulting TTL would be non-positive — a logic bug at the caller.
|
|
116
|
+
*/
|
|
117
|
+
function ttlFor(expiresAt) {
|
|
118
|
+
if (expiresAt === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
121
|
+
const ttl = Math.floor(expiresAt - nowSeconds + expiredGraceSeconds);
|
|
122
|
+
if (ttl <= 0) {
|
|
123
|
+
throw new Error(`redisCtxMetadataStore: refusing to write an entry whose expiresAt (${expiresAt}) is already past — ` +
|
|
124
|
+
`the substrate-level TTL would be ${ttl}s. Caller logic error.`);
|
|
125
|
+
}
|
|
126
|
+
return ttl;
|
|
127
|
+
}
|
|
128
|
+
function parseEntry(raw, scopedKey) {
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(raw);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// The scoped key contains the account id — omit from the public
|
|
135
|
+
// message; attach the parse error as Error.cause so server logs
|
|
136
|
+
// retain the detail without leaking via response bodies.
|
|
137
|
+
void scopedKey;
|
|
138
|
+
throw new Error('redisCtxMetadataStore: corrupt cache entry — not valid JSON. See server logs for key + parse error.', {
|
|
139
|
+
cause: err,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const entry = { value: parsed.value };
|
|
143
|
+
if (parsed.resource !== undefined)
|
|
144
|
+
entry.resource = parsed.resource;
|
|
145
|
+
if (parsed.expiresAt !== undefined)
|
|
146
|
+
entry.expiresAt = parsed.expiresAt;
|
|
147
|
+
return entry;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
async probe() {
|
|
151
|
+
try {
|
|
152
|
+
await c.ping();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
throw new Error(`ctx_metadata backend probe failed: Redis is unreachable or misconfigured. ` +
|
|
156
|
+
`Check REDIS_URL and that the instance is up. See server logs for the underlying cause.`, { cause: err });
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
async get(scopedKey) {
|
|
160
|
+
const raw = await c.get(prefixed(scopedKey));
|
|
161
|
+
if (raw === null)
|
|
162
|
+
return null;
|
|
163
|
+
return parseEntry(raw, scopedKey);
|
|
164
|
+
},
|
|
165
|
+
async bulkGet(scopedKeys) {
|
|
166
|
+
if (scopedKeys.length === 0)
|
|
167
|
+
return new Map();
|
|
168
|
+
const prefixedKeys = scopedKeys.map(k => prefixed(k));
|
|
169
|
+
const raws = await c.mGet(prefixedKeys);
|
|
170
|
+
const out = new Map();
|
|
171
|
+
for (let i = 0; i < scopedKeys.length; i++) {
|
|
172
|
+
const raw = raws[i];
|
|
173
|
+
if (raw === null || raw === undefined)
|
|
174
|
+
continue;
|
|
175
|
+
const scopedKey = scopedKeys[i];
|
|
176
|
+
if (scopedKey === undefined)
|
|
177
|
+
continue;
|
|
178
|
+
out.set(scopedKey, parseEntry(raw, scopedKey));
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
},
|
|
182
|
+
async put(scopedKey, entry) {
|
|
183
|
+
const serialized = { value: entry.value };
|
|
184
|
+
if (entry.resource !== undefined)
|
|
185
|
+
serialized.resource = entry.resource;
|
|
186
|
+
if (entry.expiresAt !== undefined)
|
|
187
|
+
serialized.expiresAt = entry.expiresAt;
|
|
188
|
+
const ttl = ttlFor(entry.expiresAt);
|
|
189
|
+
const body = JSON.stringify(serialized);
|
|
190
|
+
if (ttl !== undefined) {
|
|
191
|
+
await c.set(prefixed(scopedKey), body, { EX: ttl });
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// No TTL: durable entry. Pass undefined / empty options so the
|
|
195
|
+
// call site is symmetric with the pg backend's `expires_at = NULL`.
|
|
196
|
+
await c.set(prefixed(scopedKey), body);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
async delete(scopedKey) {
|
|
200
|
+
await c.del(prefixed(scopedKey));
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../../../src/lib/server/ctx-metadata/backends/redis.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,8 @@ export { memoryCtxMetadataStore } from './backends/memory';
|
|
|
10
10
|
export type { MemoryCtxMetadataStoreOptions } from './backends/memory';
|
|
11
11
|
export { pgCtxMetadataStore, getCtxMetadataMigration, cleanupExpiredCtxMetadata, CTX_METADATA_MIGRATION, } from './backends/pg';
|
|
12
12
|
export type { PgCtxMetadataBackendOptions } from './backends/pg';
|
|
13
|
+
export { redisCtxMetadataStore } from './backends/redis';
|
|
14
|
+
export type { RedisCtxMetadataBackendOptions, CtxMetadataRedisBackendClient, CtxMetadataRedisLikeClient, } from './backends/redis';
|
|
13
15
|
export { stripCtxMetadata, hasCtxMetadata, stripImplementationConfig, hasImplementationConfig } from './wire-shape';
|
|
14
16
|
export type { WireShape } from './wire-shape';
|
|
15
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/ctx-metadata/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,iBAAiB,EACjB,uBAAuB,EACvB,eAAe,GAChB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,YAAY,GACb,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAEvE,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACpH,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/ctx-metadata/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,iBAAiB,EACjB,uBAAuB,EACvB,eAAe,GAChB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,YAAY,GACb,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAEvE,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EACV,8BAA8B,EAC9B,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACpH,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @public
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.hasImplementationConfig = exports.stripImplementationConfig = exports.hasCtxMetadata = exports.stripCtxMetadata = exports.CTX_METADATA_MIGRATION = exports.cleanupExpiredCtxMetadata = exports.getCtxMetadataMigration = exports.pgCtxMetadataStore = exports.memoryCtxMetadataStore = exports.MAX_TTL_SECONDS = exports.DEFAULT_MAX_VALUE_BYTES = exports.ADCP_INTERNAL_TAG = exports.CtxMetadataValidationError = exports.scopeCtxMetadataKey = exports.ctxMetadataResultKey = exports.createCtxMetadataStore = void 0;
|
|
9
|
+
exports.hasImplementationConfig = exports.stripImplementationConfig = exports.hasCtxMetadata = exports.stripCtxMetadata = exports.redisCtxMetadataStore = exports.CTX_METADATA_MIGRATION = exports.cleanupExpiredCtxMetadata = exports.getCtxMetadataMigration = exports.pgCtxMetadataStore = exports.memoryCtxMetadataStore = exports.MAX_TTL_SECONDS = exports.DEFAULT_MAX_VALUE_BYTES = exports.ADCP_INTERNAL_TAG = exports.CtxMetadataValidationError = exports.scopeCtxMetadataKey = exports.ctxMetadataResultKey = exports.createCtxMetadataStore = void 0;
|
|
10
10
|
var store_1 = require("./store");
|
|
11
11
|
Object.defineProperty(exports, "createCtxMetadataStore", { enumerable: true, get: function () { return store_1.createCtxMetadataStore; } });
|
|
12
12
|
Object.defineProperty(exports, "ctxMetadataResultKey", { enumerable: true, get: function () { return store_1.ctxMetadataResultKey; } });
|
|
@@ -22,6 +22,8 @@ Object.defineProperty(exports, "pgCtxMetadataStore", { enumerable: true, get: fu
|
|
|
22
22
|
Object.defineProperty(exports, "getCtxMetadataMigration", { enumerable: true, get: function () { return pg_1.getCtxMetadataMigration; } });
|
|
23
23
|
Object.defineProperty(exports, "cleanupExpiredCtxMetadata", { enumerable: true, get: function () { return pg_1.cleanupExpiredCtxMetadata; } });
|
|
24
24
|
Object.defineProperty(exports, "CTX_METADATA_MIGRATION", { enumerable: true, get: function () { return pg_1.CTX_METADATA_MIGRATION; } });
|
|
25
|
+
var redis_1 = require("./backends/redis");
|
|
26
|
+
Object.defineProperty(exports, "redisCtxMetadataStore", { enumerable: true, get: function () { return redis_1.redisCtxMetadataStore; } });
|
|
25
27
|
var wire_shape_1 = require("./wire-shape");
|
|
26
28
|
Object.defineProperty(exports, "stripCtxMetadata", { enumerable: true, get: function () { return wire_shape_1.stripCtxMetadata; } });
|
|
27
29
|
Object.defineProperty(exports, "hasCtxMetadata", { enumerable: true, get: function () { return wire_shape_1.hasCtxMetadata; } });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/server/ctx-metadata/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,iCAQiB;AAPf,+GAAA,sBAAsB,OAAA;AACtB,6GAAA,oBAAoB,OAAA;AACpB,4GAAA,mBAAmB,OAAA;AACnB,mHAAA,0BAA0B,OAAA;AAC1B,0GAAA,iBAAiB,OAAA;AACjB,gHAAA,uBAAuB,OAAA;AACvB,wGAAA,eAAe,OAAA;AAYjB,4CAA2D;AAAlD,gHAAA,sBAAsB,OAAA;AAG/B,oCAKuB;AAJrB,wGAAA,kBAAkB,OAAA;AAClB,6GAAA,uBAAuB,OAAA;AACvB,+GAAA,yBAAyB,OAAA;AACzB,4GAAA,sBAAsB,OAAA;AAIxB,2CAAoH;AAA3G,8GAAA,gBAAgB,OAAA;AAAE,4GAAA,cAAc,OAAA;AAAE,uHAAA,yBAAyB,OAAA;AAAE,qHAAA,uBAAuB,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/server/ctx-metadata/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,iCAQiB;AAPf,+GAAA,sBAAsB,OAAA;AACtB,6GAAA,oBAAoB,OAAA;AACpB,4GAAA,mBAAmB,OAAA;AACnB,mHAAA,0BAA0B,OAAA;AAC1B,0GAAA,iBAAiB,OAAA;AACjB,gHAAA,uBAAuB,OAAA;AACvB,wGAAA,eAAe,OAAA;AAYjB,4CAA2D;AAAlD,gHAAA,sBAAsB,OAAA;AAG/B,oCAKuB;AAJrB,wGAAA,kBAAkB,OAAA;AAClB,6GAAA,uBAAuB,OAAA;AACvB,+GAAA,yBAAyB,OAAA;AACzB,4GAAA,sBAAsB,OAAA;AAIxB,0CAAyD;AAAhD,8GAAA,qBAAqB,OAAA;AAO9B,2CAAoH;AAA3G,8GAAA,gBAAgB,OAAA;AAAE,4GAAA,cAAc,OAAA;AAAE,uHAAA,yBAAyB,OAAA;AAAE,qHAAA,uBAAuB,OAAA"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis backend for the idempotency store.
|
|
3
|
+
*
|
|
4
|
+
* Stores one key per `(principal, key, [extraScope])` carrying the JSON
|
|
5
|
+
* payload `{ payloadHash, response, expiresAt }`. Expiry is enforced by
|
|
6
|
+
* Redis itself via the key TTL — no sweeper job required.
|
|
7
|
+
*
|
|
8
|
+
* **Reclaim semantics.** The `putIfAbsent` claim maps to `SET … NX EX`:
|
|
9
|
+
* because Redis auto-deletes expired keys, a crashed in-flight claim is
|
|
10
|
+
* naturally reclaimable on retry without the explicit `WHERE expires_at <
|
|
11
|
+
* NOW()` dance the Postgres backend needs.
|
|
12
|
+
*
|
|
13
|
+
* **`expired` vs `miss` parity.** The store layer distinguishes `expired`
|
|
14
|
+
* (cached key past TTL within clock-skew window) from `miss` (no cached
|
|
15
|
+
* key) — that affects whether the buyer sees `IDEMPOTENCY_EXPIRED` or a
|
|
16
|
+
* fresh execution. Postgres rows linger past `expires_at` until cleanup;
|
|
17
|
+
* Redis would evict them at the second they expire, collapsing `expired`
|
|
18
|
+
* into `miss`. We hold the key alive for an extra `expiredGraceSeconds`
|
|
19
|
+
* (defaults to 120s — covers the store's default 60s clock skew plus a
|
|
20
|
+
* margin) so the store layer can read `expiresAt` from the value and
|
|
21
|
+
* return `expired` correctly within the skew window.
|
|
22
|
+
*
|
|
23
|
+
* **`clearAll` intentionally omitted.** A shared Redis instance is a
|
|
24
|
+
* production resource — accidentally calling `FLUSHDB` from a compliance
|
|
25
|
+
* reset hook would nuke unrelated keys. Test setups that want a clean
|
|
26
|
+
* slate should run against a dedicated Redis db (`REDIS_URL=…/15`) and
|
|
27
|
+
* call `FLUSHDB` themselves.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { createClient } from 'redis';
|
|
32
|
+
* import { createIdempotencyStore, redisBackend } from '@adcp/sdk/server';
|
|
33
|
+
*
|
|
34
|
+
* const client = createClient({ url: process.env.REDIS_URL });
|
|
35
|
+
* client.on('error', (err) => console.error('redis error', err));
|
|
36
|
+
* await client.connect();
|
|
37
|
+
*
|
|
38
|
+
* const store = createIdempotencyStore({
|
|
39
|
+
* backend: redisBackend(client),
|
|
40
|
+
* ttlSeconds: 86400,
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
import type { IdempotencyBackend } from '../store';
|
|
45
|
+
import type { RedisClientType } from 'redis';
|
|
46
|
+
/**
|
|
47
|
+
* Escape-hatch interface for adopters not using the official `redis`
|
|
48
|
+
* client (node-redis v4/v5) — e.g., `ioredis`, Upstash, a test double.
|
|
49
|
+
*
|
|
50
|
+
* Mirrors the four methods this backend actually calls. The `set`
|
|
51
|
+
* signature follows node-redis's options-object form; `ioredis` users
|
|
52
|
+
* pass a thin shim that maps to its positional API.
|
|
53
|
+
*
|
|
54
|
+
* @example ioredis adapter
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import Redis from 'ioredis';
|
|
57
|
+
* const ioredis = new Redis(process.env.REDIS_URL!);
|
|
58
|
+
*
|
|
59
|
+
* const client: RedisLikeClient = {
|
|
60
|
+
* get: (k) => ioredis.get(k),
|
|
61
|
+
* set: (k, v, { EX, NX }) =>
|
|
62
|
+
* NX
|
|
63
|
+
* ? ioredis.set(k, v, 'EX', EX, 'NX').then(r => (r === 'OK' ? 'OK' : null))
|
|
64
|
+
* : ioredis.set(k, v, 'EX', EX),
|
|
65
|
+
* del: (k) => ioredis.del(k as string),
|
|
66
|
+
* ping: () => ioredis.ping(),
|
|
67
|
+
* };
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export interface RedisLikeClient {
|
|
71
|
+
get(key: string): Promise<string | null>;
|
|
72
|
+
set(key: string, value: string, options: {
|
|
73
|
+
EX: number;
|
|
74
|
+
NX?: boolean;
|
|
75
|
+
}): Promise<string | null>;
|
|
76
|
+
del(key: string | string[]): Promise<number>;
|
|
77
|
+
ping(): Promise<string>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Accepted client shape: either a real `redis` (node-redis v4/v5)
|
|
81
|
+
* `RedisClientType` (the typical path — pass `createClient(...)` straight
|
|
82
|
+
* in) or an adapter that conforms to `RedisLikeClient` (for `ioredis`,
|
|
83
|
+
* Upstash, or test doubles). The union avoids forcing node-redis users
|
|
84
|
+
* to write `as unknown as RedisLikeClient` casts on the documented path.
|
|
85
|
+
*/
|
|
86
|
+
export type RedisBackendClient = RedisClientType<any, any, any> | RedisLikeClient;
|
|
87
|
+
export interface RedisBackendOptions {
|
|
88
|
+
/**
|
|
89
|
+
* Key prefix prepended to every scoped key written to Redis. Defaults
|
|
90
|
+
* to `"adcp:idem:"`.
|
|
91
|
+
*
|
|
92
|
+
* **Sharing a Redis db across deployments? Override this.** The default
|
|
93
|
+
* is fine for a dedicated Redis (or a dedicated db index) and for
|
|
94
|
+
* coexisting with non-AdCP applications. But two AdCP servers sharing
|
|
95
|
+
* the *same* db with the *same* default prefix will collide on any
|
|
96
|
+
* overlapping principal namespace (e.g., both deployments having a
|
|
97
|
+
* tenant called `acme`) — the principal segment is per-tenant, not
|
|
98
|
+
* per-deployment, so it's the wrong layer to do deployment isolation.
|
|
99
|
+
* Set a deployment-unique prefix (`"adcp:idem:prod-eu:"`, etc.) or use
|
|
100
|
+
* separate Redis dbs.
|
|
101
|
+
*/
|
|
102
|
+
keyPrefix?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Suppress the one-time `console.warn` emitted at construction when the
|
|
105
|
+
* default `keyPrefix` is used against a node-redis client that appears
|
|
106
|
+
* to be on db 0 (the most likely signal of a shared, non-dedicated
|
|
107
|
+
* Redis). Set to `true` if you know your Redis is dedicated to this
|
|
108
|
+
* AdCP deployment and don't want the warning noise. The recommended
|
|
109
|
+
* fix is to set `keyPrefix` explicitly, not to suppress.
|
|
110
|
+
*/
|
|
111
|
+
suppressDefaultPrefixWarning?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* How many seconds past `expiresAt` to keep the key alive in Redis, so
|
|
114
|
+
* the store layer can still read it during the clock-skew window and
|
|
115
|
+
* return `IDEMPOTENCY_EXPIRED` (rather than treating it as a fresh
|
|
116
|
+
* miss). Defaults to 120s — covers the store's default 60s skew with
|
|
117
|
+
* margin. Set to 0 to collapse `expired` into `miss` (not recommended
|
|
118
|
+
* — buyers lose the explicit expired signal).
|
|
119
|
+
*/
|
|
120
|
+
expiredGraceSeconds?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Test-only escape hatch to reset the once-warn flag between test runs.
|
|
124
|
+
* Not exported through any index — adopters can't reach it from outside
|
|
125
|
+
* this file.
|
|
126
|
+
*/
|
|
127
|
+
export declare function __resetDefaultPrefixWarningForTests(): void;
|
|
128
|
+
/**
|
|
129
|
+
* Create a Redis-backed idempotency cache.
|
|
130
|
+
*
|
|
131
|
+
* **Startup probe.** Call `store.probe()` (or `probeIdempotencyStore(store)`)
|
|
132
|
+
* before serving traffic to catch a bad `REDIS_URL` or unreachable
|
|
133
|
+
* instance at boot rather than on the first mutating request. Wire it via:
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* serve(createAgent, { readinessCheck: () => store.probe() });
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* **Client error handling.** node-redis emits errors on the client itself
|
|
140
|
+
* for transient connection drops. Without a listener, Node's
|
|
141
|
+
* `EventEmitter` default-throws and crashes the process. Add one in your
|
|
142
|
+
* bootstrap:
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* client.on('error', (err) => console.error('redis error', err));
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* **Redis memory policy — set this on the deployment.** A buyer with a
|
|
149
|
+
* valid principal can mint unbounded distinct `idempotency_key` values
|
|
150
|
+
* and hit any mutating tool; each write adds a key to Redis with the
|
|
151
|
+
* configured `ttlSeconds` (default 24h). A sufficient rate can pressure
|
|
152
|
+
* Redis memory before TTLs evict naturally. Configure your Redis with:
|
|
153
|
+
*
|
|
154
|
+
* - **`maxmemory-policy volatile-lru`** (recommended) — evicts only
|
|
155
|
+
* TTL'd keys, containing blast radius to AdCP's keyspace if the
|
|
156
|
+
* instance is shared with other apps. All keys this backend writes
|
|
157
|
+
* carry TTL, so this is safe.
|
|
158
|
+
* - **`maxmemory-policy allkeys-lru`** — only on a Redis db dedicated
|
|
159
|
+
* to AdCP. Will evict your other keys if shared.
|
|
160
|
+
* - **`maxmemory-policy noeviction`** (Redis default) — fail-closed:
|
|
161
|
+
* the backend's writes will start erroring once memory fills, and
|
|
162
|
+
* mutating requests will fail. Operationally noisy but never serves
|
|
163
|
+
* stale data; choose this only if you'd rather page than evict.
|
|
164
|
+
*
|
|
165
|
+
* Pair with alerting on a per-principal `VALIDATION_ERROR` rate — a
|
|
166
|
+
* drifted handler hit by a retrying buyer writes 10s-TTL entries on
|
|
167
|
+
* every fresh key, amplifying the rate of cache fill. Steady-state
|
|
168
|
+
* `VALIDATION_ERROR` should be zero.
|
|
169
|
+
*/
|
|
170
|
+
export declare function redisBackend(client: RedisBackendClient, options?: RedisBackendOptions): IdempotencyBackend;
|
|
171
|
+
//# sourceMappingURL=redis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../../../../src/lib/server/idempotency/backends/redis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAyB,MAAM,UAAU,CAAC;AAK1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/F,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,eAAe,CAAC;AAElF,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAkDD;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,GAAE,mBAAwB,GAAG,kBAAkB,CA+H9G"}
|