@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.
Files changed (57) hide show
  1. package/AGENTS.md +2 -0
  2. package/bin/adcp.js +3 -1
  3. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  4. package/dist/lib/core/SingleAgentClient.d.ts +8 -0
  5. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  6. package/dist/lib/core/SingleAgentClient.js +15 -0
  7. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  8. package/dist/lib/index.d.ts +2 -2
  9. package/dist/lib/index.d.ts.map +1 -1
  10. package/dist/lib/index.js +8 -7
  11. package/dist/lib/index.js.map +1 -1
  12. package/dist/lib/protocols/mcp.d.ts.map +1 -1
  13. package/dist/lib/protocols/mcp.js +63 -4
  14. package/dist/lib/protocols/mcp.js.map +1 -1
  15. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  16. package/dist/lib/server/ctx-metadata/backends/pg.d.ts +12 -3
  17. package/dist/lib/server/ctx-metadata/backends/pg.d.ts.map +1 -1
  18. package/dist/lib/server/ctx-metadata/backends/pg.js +59 -23
  19. package/dist/lib/server/ctx-metadata/backends/pg.js.map +1 -1
  20. package/dist/lib/server/ctx-metadata/backends/redis.d.ts +163 -0
  21. package/dist/lib/server/ctx-metadata/backends/redis.d.ts.map +1 -0
  22. package/dist/lib/server/ctx-metadata/backends/redis.js +204 -0
  23. package/dist/lib/server/ctx-metadata/backends/redis.js.map +1 -0
  24. package/dist/lib/server/ctx-metadata/index.d.ts +2 -0
  25. package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -1
  26. package/dist/lib/server/ctx-metadata/index.js +3 -1
  27. package/dist/lib/server/ctx-metadata/index.js.map +1 -1
  28. package/dist/lib/server/idempotency/backends/redis.d.ts +171 -0
  29. package/dist/lib/server/idempotency/backends/redis.d.ts.map +1 -0
  30. package/dist/lib/server/idempotency/backends/redis.js +253 -0
  31. package/dist/lib/server/idempotency/backends/redis.js.map +1 -0
  32. package/dist/lib/server/idempotency/index.d.ts +2 -0
  33. package/dist/lib/server/idempotency/index.d.ts.map +1 -1
  34. package/dist/lib/server/idempotency/index.js +3 -1
  35. package/dist/lib/server/idempotency/index.js.map +1 -1
  36. package/dist/lib/server/idempotency/store.d.ts +44 -0
  37. package/dist/lib/server/idempotency/store.d.ts.map +1 -1
  38. package/dist/lib/server/idempotency/store.js.map +1 -1
  39. package/dist/lib/server/index.d.ts +4 -4
  40. package/dist/lib/server/index.d.ts.map +1 -1
  41. package/dist/lib/server/index.js +3 -1
  42. package/dist/lib/server/index.js.map +1 -1
  43. package/dist/lib/signing/redis-replay-store.d.ts +180 -0
  44. package/dist/lib/signing/redis-replay-store.d.ts.map +1 -0
  45. package/dist/lib/signing/redis-replay-store.js +270 -0
  46. package/dist/lib/signing/redis-replay-store.js.map +1 -0
  47. package/dist/lib/signing/server.d.ts +1 -0
  48. package/dist/lib/signing/server.d.ts.map +1 -1
  49. package/dist/lib/signing/server.js +4 -2
  50. package/dist/lib/signing/server.js.map +1 -1
  51. package/dist/lib/utils/redis-default-prefix-warn.d.ts +60 -0
  52. package/dist/lib/utils/redis-default-prefix-warn.d.ts.map +1 -0
  53. package/dist/lib/utils/redis-default-prefix-warn.js +95 -0
  54. package/dist/lib/utils/redis-default-prefix-warn.js.map +1 -0
  55. package/dist/lib/version.d.ts +3 -3
  56. package/dist/lib/version.js +3 -3
  57. 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"}