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/README.md +25 -0
- package/dist/docs.d.ts +22 -0
- package/dist/docs.d.ts.map +1 -1
- package/dist/generated/webhookEventCatalog.d.ts +884 -0
- package/dist/generated/webhookEventCatalog.d.ts.map +1 -0
- package/dist/generated/webhookEventCatalog.js +1073 -0
- package/dist/index.d.ts +38 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/webhook-events.d.ts +37 -0
- package/dist/webhook-events.d.ts.map +1 -0
- package/dist/webhook-events.js +140 -0
- package/package.json +1 -1
- package/src/docs.ts +23 -0
- package/src/generated/webhookEventCatalog.ts +1077 -0
- package/src/index.ts +55 -5
- package/src/webhook-events.ts +202 -0
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
+
}
|