@agentsbloom/sdk 0.4.0 → 0.5.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.
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Type declarations for `@agentsbloom/sdk/protocol` — the dependency-free
3
+ * wire-protocol primitives shared by every Node consumer (this SDK, the hosted
4
+ * gateway, and any language port checking itself against the reference).
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // RFC 8941 structured fields
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export class StructuredFieldError extends Error {
12
+ name: 'StructuredFieldError';
13
+ }
14
+
15
+ /** A Byte Sequence bare item (`:b64:`). */
16
+ export class ByteSequence {
17
+ constructor(bytes: Buffer);
18
+ bytes: Buffer;
19
+ }
20
+
21
+ /** A Token bare item (distinguishes `foo` from `"foo"`). */
22
+ export class Token {
23
+ constructor(value: string);
24
+ value: string;
25
+ toString(): string;
26
+ }
27
+
28
+ export type BareItem = string | number | boolean | ByteSequence | Token;
29
+ export type Parameters = Map<string, BareItem>;
30
+
31
+ export interface StructuredItem {
32
+ value: BareItem;
33
+ params: Parameters;
34
+ }
35
+
36
+ export interface DictionaryMember {
37
+ value: BareItem | StructuredItem[];
38
+ items?: StructuredItem[];
39
+ params: Parameters;
40
+ isInnerList: boolean;
41
+ /**
42
+ * The EXACT source substring of this member's value. RFC 9421 §2.5 requires
43
+ * `@signature-params` to reproduce the parameters as RECEIVED, so
44
+ * re-serializing from the parse tree would reject signers whose parameter
45
+ * ordering differs from ours.
46
+ */
47
+ raw: string;
48
+ }
49
+
50
+ export function parseDictionary(input: string): Map<string, DictionaryMember>;
51
+ export function parseByteSequenceDictionary(input: string): Map<string, Buffer>;
52
+ export function serializeByteSequence(bytes: Buffer | Uint8Array): string;
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // RFC 9421 signature base
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /** RFC 9421-conformant, host-bound profile. */
59
+ export const PROFILE_STRICT: 'ab2';
60
+ /** Byte-exact reproduction of the pre-0.6 base. Cannot bind the authority. */
61
+ export const PROFILE_LEGACY: 'ab1';
62
+ export type SignatureProfile = 'ab2' | 'ab1';
63
+
64
+ export class SignatureBaseError extends Error {
65
+ name: 'SignatureBaseError';
66
+ }
67
+
68
+ /** A runtime-neutral view of the request being verified. */
69
+ export interface SignatureRequestContext {
70
+ method: string;
71
+ /** 'http:' or 'https:' */
72
+ scheme: string;
73
+ /** Raw Host / :authority value. */
74
+ authority: string;
75
+ /** Absolute path, no query. */
76
+ path: string;
77
+ /** Query string WITHOUT the leading '?'. */
78
+ query: string;
79
+ header(name: string): string | string[] | undefined;
80
+ /** `originalUrl` equivalent; used by the `ab1` profile only. */
81
+ legacyTarget?: string;
82
+ }
83
+
84
+ export interface CoveredComponent {
85
+ name: string;
86
+ params: Parameters;
87
+ }
88
+
89
+ export function buildSignatureBase(options: {
90
+ components: CoveredComponent[];
91
+ /** The EXACT `Signature-Input` dictionary-member value as received. */
92
+ signatureParamsRaw: string;
93
+ request: SignatureRequestContext;
94
+ profile?: SignatureProfile;
95
+ }): string;
96
+
97
+ export function requestContextFromExpress(
98
+ req: unknown,
99
+ options?: { forwardedProto?: string },
100
+ ): SignatureRequestContext;
101
+
102
+ /** RFC 9421 §2.2.3: lowercase host, scheme's default port removed. */
103
+ export function normalizeAuthority(rawAuthority: string, scheme: string): string;
104
+
105
+ export function contentDigestHeader(
106
+ bodyBytes: Buffer,
107
+ algorithm?: 'sha-256' | 'sha-512',
108
+ ): string;
109
+
110
+ export function verifyContentDigest(
111
+ headerValue: string | string[] | undefined,
112
+ bodyBytes: Buffer,
113
+ ): { ok: true } | { ok: false; reason: string };
114
+
115
+ /**
116
+ * Resolves the exact body bytes a digest must cover. Throws when a signed
117
+ * request has a body but `req.rawBody` was not captured — hashing a
118
+ * re-serialization proves nothing about what the client sent.
119
+ */
120
+ export function resolveBodyBytes(
121
+ req: unknown,
122
+ options?: { allowReserializedBody?: boolean },
123
+ ): Buffer;
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // RFC 9421 verification (JWKS-backed)
127
+ // ---------------------------------------------------------------------------
128
+
129
+ export const SUPPORTED_SIGNATURE_ALGORITHMS: readonly string[];
130
+ export const STRICT_REQUIRED_COMPONENTS: readonly string[];
131
+ export const LEGACY_REQUIRED_COMPONENTS: readonly string[];
132
+
133
+ export interface JwksCache {
134
+ get(cacheKey: string): unknown;
135
+ put(cacheKey: string, entry: unknown): void;
136
+ clear(): void;
137
+ readonly size: number;
138
+ }
139
+
140
+ export function createJwksCache(options?: { maxEntries?: number }): JwksCache;
141
+
142
+ /** True for loopback, RFC 1918, link-local, CGNAT and reserved addresses. */
143
+ export function isBlockedSsrfHostname(hostname: string): boolean;
144
+
145
+ export type HttpSignatureVerification =
146
+ | { ok: true; keyid: string; identity: string; profile: SignatureProfile }
147
+ | { ok: false; status: number; code: string; publicMessage: string; detail: string };
148
+
149
+ export function verifyHttpMessageSignature(options: {
150
+ req: unknown;
151
+ requestContext: SignatureRequestContext;
152
+ signatureHeader: string;
153
+ signatureInputHeader: string;
154
+ jwks?: { keys: Array<Record<string, unknown>> } | null;
155
+ jwksUrl?: string | null;
156
+ jwksCache: JwksCache;
157
+ nonceCache: { claim(key: string, expiresAtMs: number): boolean | Promise<boolean> };
158
+ nonceNamespace: string;
159
+ maxAgeMs: number;
160
+ clockSkewMs: number;
161
+ requireAuthority?: boolean;
162
+ expectedAuthorities?: Set<string> | null;
163
+ acceptLegacyProfile?: boolean;
164
+ allowReserializedBody?: boolean;
165
+ fetchImpl?: typeof fetch;
166
+ nowMs?: number;
167
+ }): Promise<HttpSignatureVerification>;
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Exact-decimal money
171
+ // ---------------------------------------------------------------------------
172
+
173
+ export const COMPARISON_SCALE: 6;
174
+
175
+ /**
176
+ * Strict parse of a monetary amount. Returns null for everything `Number()`
177
+ * would silently coerce (`'abc'` -> NaN, `''` -> 0, booleans, arrays) — the
178
+ * exact inputs that let a malformed order total PASS a budget comparison.
179
+ */
180
+ export function parseAmount(
181
+ value: unknown,
182
+ options?: { allowZero?: boolean; allowNegative?: boolean },
183
+ ): { decimal: string; micros: bigint } | null;
184
+
185
+ /** Exact comparison. Returns null when either side is not a valid amount. */
186
+ export function compareAmounts(left: unknown, right: unknown): -1 | 0 | 1 | null;
187
+
188
+ export function toDecimalString(value: unknown): string | null;
189
+ export function toScaledBigInt(value: unknown, scale: number): bigint | null;
190
+ export function toMinorUnits(value: unknown, currency: unknown): bigint | null;
191
+ /** Hundredths of the major unit — the cart-hash wire format. */
192
+ export function toHundredths(value: unknown): number | null;
193
+ export function formatScaled(scaled: bigint, scale: number): string;
194
+ export function sumLineItems(
195
+ items: Array<{ quantity: number; unitHundredths: number }>,
196
+ ): number | null;
197
+ export function currencyExponent(currency: unknown): number;
198
+ export function normalizeCurrencyCode(currency: unknown): string | null;
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Replay cache
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export interface ReplayCache {
205
+ /**
206
+ * Atomic check-and-set. True when this caller won the key (first use), false
207
+ * on replay OR when a configured shared store is unreachable and
208
+ * `AGENTSBLOOM_REPLAY_FAIL_OPEN` is not set.
209
+ */
210
+ claim(key: string, expiresAtMs: number): boolean | Promise<boolean>;
211
+ /** Advisory only; never gate a decision on it. */
212
+ has(key: string): boolean | Promise<boolean>;
213
+ set(key: string, expiresAtMs: number): void;
214
+ clear(): void;
215
+ stats(): {
216
+ localSize: number;
217
+ maxLocalSize: number;
218
+ capacityEvictions: number;
219
+ sharedFailures: number;
220
+ shared: boolean;
221
+ failOpen: boolean;
222
+ };
223
+ }
224
+
225
+ export function createReplayCache(namespace: string, maxLocalSize?: number): ReplayCache;
226
+ export function isSharedStoreConfigured(): boolean;
227
+ export function isReplayFailOpenEnabled(): boolean;
228
+ export function sharedStoreOutageCount(): number;
229
+ export function resetSharedStoreDiagnostics(): void;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `@agentsbloom/sdk/protocol` — the wire-protocol primitives, published as a
3
+ * dependency-free subpath.
4
+ *
5
+ * Why this entry point exists
6
+ * --------------------------
7
+ *
8
+ * The RFC 9421 signature base, the RFC 8941 parser and the exact-decimal money
9
+ * helpers had been reimplemented per consumer: once in this SDK, once in
10
+ * `@agentsbloom/next`, once in `@agentsbloom/agent`, and once in the hosted
11
+ * gateway (`apps/backend-api/lib/rfc9421.js`). Every copy drifted, and the
12
+ * drift was not cosmetic — the gateway lowercased `@method`, folded the query
13
+ * into `@path`, and never covered `@authority`, so an agent signing the
14
+ * conformant base could not authenticate against it at all.
15
+ *
16
+ * Any Node consumer can now import the ONE implementation:
17
+ *
18
+ * import { buildSignatureBase, parseDictionary } from '@agentsbloom/sdk/protocol';
19
+ *
20
+ * The gateway deliberately avoided importing `@agentsbloom/sdk` because the
21
+ * main entry point pulls in OpenTelemetry and the MCP SDK. That concern does
22
+ * not apply here: every module re-exported below imports nothing but
23
+ * `node:crypto`, so this subpath adds no transitive dependencies and no
24
+ * startup cost.
25
+ *
26
+ * Runtimes without `node:crypto` (Edge, workers, browsers) should use the
27
+ * WebCrypto ports in `@agentsbloom/next`, which are kept byte-identical.
28
+ */
29
+
30
+ // --- RFC 8941 structured fields -------------------------------------------
31
+ export {
32
+ parseDictionary,
33
+ parseByteSequenceDictionary,
34
+ serializeByteSequence,
35
+ ByteSequence,
36
+ Token,
37
+ StructuredFieldError,
38
+ } from './structured-fields.js';
39
+
40
+ // --- RFC 9421 signature base ----------------------------------------------
41
+ export {
42
+ buildSignatureBase,
43
+ requestContextFromExpress,
44
+ normalizeAuthority,
45
+ contentDigestHeader,
46
+ verifyContentDigest,
47
+ resolveBodyBytes,
48
+ SignatureBaseError,
49
+ PROFILE_STRICT,
50
+ PROFILE_LEGACY,
51
+ } from './signature-base.js';
52
+
53
+ // --- RFC 9421 verification (JWKS-backed) ----------------------------------
54
+ export {
55
+ verifyHttpMessageSignature,
56
+ createJwksCache,
57
+ isBlockedSsrfHostname,
58
+ SUPPORTED_SIGNATURE_ALGORITHMS,
59
+ STRICT_REQUIRED_COMPONENTS,
60
+ LEGACY_REQUIRED_COMPONENTS,
61
+ } from './http-signatures.js';
62
+
63
+ // --- Exact-decimal money --------------------------------------------------
64
+ export {
65
+ parseAmount,
66
+ compareAmounts,
67
+ toDecimalString,
68
+ toScaledBigInt,
69
+ toMinorUnits,
70
+ toHundredths,
71
+ formatScaled,
72
+ sumLineItems,
73
+ currencyExponent,
74
+ normalizeCurrencyCode,
75
+ COMPARISON_SCALE,
76
+ } from './money.js';
77
+
78
+ // --- Bounded / cluster-wide replay cache ----------------------------------
79
+ export {
80
+ createReplayCache,
81
+ isSharedStoreConfigured,
82
+ isReplayFailOpenEnabled,
83
+ sharedStoreOutageCount,
84
+ resetSharedStoreDiagnostics,
85
+ } from './shared-store.js';