spaps-types 1.5.0 → 1.5.1

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/src/index.ts CHANGED
@@ -56,6 +56,26 @@ export interface UserWallet {
56
56
  updated_at: string;
57
57
  }
58
58
 
59
+ export interface ApiDiagnostic {
60
+ code: string;
61
+ message: string;
62
+ status?: number | null;
63
+ details?: any;
64
+ }
65
+
66
+ export interface ApiRemediation {
67
+ kind: string;
68
+ label?: string | null;
69
+ method?: string | null;
70
+ path?: string | null;
71
+ cli?: string | null;
72
+ requires?: string[];
73
+ details?: any;
74
+ command_template?: string | null;
75
+ safe_to_execute?: boolean | null;
76
+ operator_gate_required?: boolean | null;
77
+ }
78
+
59
79
  // API Response wrapper
60
80
  export interface ApiResponse<T = any> {
61
81
  success: boolean;
@@ -65,9 +85,14 @@ export interface ApiResponse<T = any> {
65
85
  message: string;
66
86
  details?: any;
67
87
  };
88
+ request_id?: string;
89
+ timestamp?: string;
90
+ diagnostics?: ApiDiagnostic[];
91
+ remediations?: ApiRemediation[];
68
92
  metadata?: {
69
93
  timestamp: string;
70
94
  request_id: string;
95
+ [key: string]: any;
71
96
  };
72
97
  }
73
98
 
@@ -640,11 +665,13 @@ export interface CryptoInvoiceResponse {
640
665
  export interface CryptoInvoiceStatusSnapshot {
641
666
  id: string;
642
667
  status: CryptoInvoiceStatus;
643
- asset: CryptoAsset;
644
- network: CryptoNetwork;
645
- expires_at: string;
646
- underpaid: boolean;
647
- overpaid: boolean;
668
+ amount?: string;
669
+ asset?: CryptoAsset;
670
+ network?: CryptoNetwork;
671
+ expires_at?: string | null;
672
+ settled_at?: string | null;
673
+ underpaid?: boolean | string | null;
674
+ overpaid?: boolean | string | null;
648
675
  }
649
676
 
650
677
  export interface CryptoReconcileRequest {
@@ -652,6 +679,14 @@ export interface CryptoReconcileRequest {
652
679
  reconToken?: string;
653
680
  }
654
681
 
682
+ export interface CryptoReconcileResult {
683
+ reconciled: boolean;
684
+ message?: string;
685
+ job_id?: string;
686
+ scheduled_at?: string;
687
+ cursor?: Record<string, any>;
688
+ }
689
+
655
690
  export interface VerifyCryptoWebhookSignatureOptions {
656
691
  body: string | Uint8Array | Record<string, any>;
657
692
  signature: string | null | undefined;
@@ -896,6 +931,21 @@ export {
896
931
  type SecureMessageOutput
897
932
  } from './schemas/secureMessages';
898
933
 
934
+ export {
935
+ SPAPS_WEBHOOK_EVENT_CATALOG,
936
+ SPAPS_WEBHOOK_EVENT_TYPES,
937
+ isSpapsWebhookEventType,
938
+ parseSpapsWebhookEnvelope,
939
+ spapsWebhookEnvelopeSchema,
940
+ verifyAndParseSpapsWebhook,
941
+ verifySpapsWebhookSignature,
942
+ type SpapsWebhookEnvelope,
943
+ type SpapsWebhookEventPayload,
944
+ type SpapsWebhookEventType,
945
+ type SpapsWebhookHmacSha256,
946
+ type VerifySpapsWebhookSignatureOptions,
947
+ } from './webhook-events';
948
+
899
949
  // Re-export compatibility (helps with migration)
900
950
  export type JWTPayload = TokenPayload;
901
951
  export type DecodedToken = {
@@ -0,0 +1,202 @@
1
+ import { z } from 'zod';
2
+
3
+ import {
4
+ SPAPS_WEBHOOK_EVENT_CATALOG,
5
+ SPAPS_WEBHOOK_EVENT_TYPES,
6
+ type GeneratedSpapsWebhookEventType,
7
+ } from './generated/webhookEventCatalog';
8
+
9
+ declare const require: ((id: string) => unknown) | undefined;
10
+
11
+ interface HmacLike {
12
+ update(data: string): HmacLike;
13
+ digest(encoding: 'hex'): string;
14
+ }
15
+
16
+ interface NodeCryptoLike {
17
+ createHmac?: (algorithm: 'sha256', key: string) => HmacLike;
18
+ timingSafeEqual?: (left: Uint8Array, right: Uint8Array) => boolean;
19
+ }
20
+
21
+ export { SPAPS_WEBHOOK_EVENT_CATALOG, SPAPS_WEBHOOK_EVENT_TYPES };
22
+
23
+ export type SpapsWebhookEventType = GeneratedSpapsWebhookEventType;
24
+
25
+ export type SpapsWebhookEventPayload<TEventType extends SpapsWebhookEventType = SpapsWebhookEventType> =
26
+ Record<string, unknown> & { readonly __eventType?: TEventType };
27
+
28
+ export interface SpapsWebhookEnvelope<
29
+ TEventType extends SpapsWebhookEventType = SpapsWebhookEventType,
30
+ TData extends Record<string, unknown> = SpapsWebhookEventPayload<TEventType>
31
+ > {
32
+ id: string;
33
+ type: TEventType;
34
+ schema_version: number;
35
+ application_id: string;
36
+ created_at: string;
37
+ data: TData;
38
+ }
39
+
40
+ export type SpapsWebhookHmacSha256 = (secret: string, payload: string) => string;
41
+
42
+ export interface VerifySpapsWebhookSignatureOptions {
43
+ body: string | Uint8Array | object;
44
+ signature: string | null | undefined;
45
+ secret: string;
46
+ toleranceSeconds?: number;
47
+ nowSeconds?: number;
48
+ hmacSha256Hex?: SpapsWebhookHmacSha256;
49
+ }
50
+
51
+ const eventTypeSet = new Set<string>(SPAPS_WEBHOOK_EVENT_TYPES);
52
+
53
+ export function isSpapsWebhookEventType(value: string): value is SpapsWebhookEventType {
54
+ return eventTypeSet.has(value);
55
+ }
56
+
57
+ export const spapsWebhookEnvelopeSchema = z.object({
58
+ id: z.string().min(1),
59
+ type: z.string().refine(isSpapsWebhookEventType, {
60
+ message: 'unknown SPAPS webhook event type',
61
+ }),
62
+ schema_version: z.number().int().positive(),
63
+ application_id: z.string().min(1),
64
+ created_at: z.string().min(1),
65
+ data: z.record(z.string(), z.unknown()),
66
+ });
67
+
68
+ export function parseSpapsWebhookEnvelope(body: string | Uint8Array | object): SpapsWebhookEnvelope {
69
+ const parsed = typeof body === 'string' || body instanceof Uint8Array
70
+ ? JSON.parse(rawBodyToString(body))
71
+ : body;
72
+ return spapsWebhookEnvelopeSchema.parse(parsed) as SpapsWebhookEnvelope;
73
+ }
74
+
75
+ export function verifySpapsWebhookSignature(options: VerifySpapsWebhookSignatureOptions): boolean {
76
+ const {
77
+ body,
78
+ signature,
79
+ secret,
80
+ toleranceSeconds = 300,
81
+ nowSeconds,
82
+ hmacSha256Hex,
83
+ } = options;
84
+
85
+ if (!signature) {
86
+ throw new Error('Missing webhook signature');
87
+ }
88
+
89
+ const parts = parseSignatureHeader(signature);
90
+ const timestamp = parts.t;
91
+ const expected = parts.v1;
92
+ if (!timestamp || !expected) {
93
+ throw new Error('Invalid webhook signature format');
94
+ }
95
+
96
+ const rawBody = rawBodyToString(body);
97
+ const payload = `${timestamp}.${rawBody}`;
98
+ const computed = (hmacSha256Hex ?? defaultHmacSha256Hex)(secret, payload);
99
+
100
+ if (!timingSafeHexEqual(expected, computed)) {
101
+ throw new Error('Invalid webhook signature');
102
+ }
103
+
104
+ const timestampSeconds = Number(timestamp);
105
+ if (!Number.isFinite(timestampSeconds)) {
106
+ throw new Error('Invalid webhook signature timestamp');
107
+ }
108
+ if (toleranceSeconds > 0) {
109
+ const currentSeconds = nowSeconds ?? Date.now() / 1000;
110
+ if (Math.abs(currentSeconds - timestampSeconds) > toleranceSeconds) {
111
+ throw new Error('Webhook signature timestamp outside tolerance window');
112
+ }
113
+ }
114
+
115
+ return true;
116
+ }
117
+
118
+ export function verifyAndParseSpapsWebhook(options: VerifySpapsWebhookSignatureOptions): SpapsWebhookEnvelope {
119
+ verifySpapsWebhookSignature(options);
120
+ return parseSpapsWebhookEnvelope(options.body);
121
+ }
122
+
123
+ function parseSignatureHeader(signature: string): Record<string, string> {
124
+ return signature.split(',').reduce<Record<string, string>>((accumulator, part) => {
125
+ const [key, value] = part.split('=');
126
+ if (key && value) {
127
+ accumulator[key.trim()] = value.trim();
128
+ }
129
+ return accumulator;
130
+ }, {});
131
+ }
132
+
133
+ function rawBodyToString(body: string | Uint8Array | object): string {
134
+ if (typeof body === 'string') {
135
+ return body;
136
+ }
137
+ if (body instanceof Uint8Array) {
138
+ return utf8BytesToString(body);
139
+ }
140
+ return JSON.stringify(body ?? {});
141
+ }
142
+
143
+ function utf8BytesToString(bytes: Uint8Array): string {
144
+ let encoded = '';
145
+ for (const byte of bytes) {
146
+ encoded += `%${byte.toString(16).padStart(2, '0')}`;
147
+ }
148
+ try {
149
+ return decodeURIComponent(encoded);
150
+ } catch {
151
+ throw new Error('Webhook body bytes must be valid UTF-8');
152
+ }
153
+ }
154
+
155
+ function defaultHmacSha256Hex(secret: string, payload: string): string {
156
+ const cryptoModule = loadNodeCrypto();
157
+ if (!cryptoModule?.createHmac) {
158
+ throw new Error('No HMAC-SHA256 provider available; pass hmacSha256Hex');
159
+ }
160
+ const hmac = cryptoModule.createHmac('sha256', secret);
161
+ hmac.update(payload);
162
+ return hmac.digest('hex');
163
+ }
164
+
165
+ function loadNodeCrypto(): NodeCryptoLike | undefined {
166
+ if (typeof require !== 'function') {
167
+ return undefined;
168
+ }
169
+ try {
170
+ return require('crypto') as NodeCryptoLike;
171
+ } catch {
172
+ return undefined;
173
+ }
174
+ }
175
+
176
+ function timingSafeHexEqual(expectedHex: string, computedHex: string): boolean {
177
+ const expected = hexToBytes(expectedHex);
178
+ const computed = hexToBytes(computedHex);
179
+ if (expected.length !== computed.length) {
180
+ return false;
181
+ }
182
+ const cryptoModule = loadNodeCrypto();
183
+ if (cryptoModule?.timingSafeEqual) {
184
+ return cryptoModule.timingSafeEqual(expected, computed);
185
+ }
186
+ let diff = 0;
187
+ for (let index = 0; index < expected.length; index += 1) {
188
+ diff |= expected[index] ^ computed[index];
189
+ }
190
+ return diff === 0;
191
+ }
192
+
193
+ function hexToBytes(hex: string): Uint8Array {
194
+ if (hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) {
195
+ throw new Error('Invalid webhook signature digest');
196
+ }
197
+ const bytes = new Uint8Array(hex.length / 2);
198
+ for (let index = 0; index < hex.length; index += 2) {
199
+ bytes[index / 2] = Number.parseInt(hex.slice(index, index + 2), 16);
200
+ }
201
+ return bytes;
202
+ }