@spreadspace/sdk 0.1.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/README.md +197 -0
- package/dist/chunk-2JW6MGIK.js +139 -0
- package/dist/chunk-2JW6MGIK.js.map +1 -0
- package/dist/index.cjs +1478 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1004 -0
- package/dist/index.d.ts +1004 -0
- package/dist/index.js +1284 -0
- package/dist/index.js.map +1 -0
- package/dist/webhooks.cjs +165 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +156 -0
- package/dist/webhooks.d.ts +156 -0
- package/dist/webhooks.js +11 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook signature verification + typed event envelopes.
|
|
3
|
+
*
|
|
4
|
+
* Byte-for-byte compatible with the server signer `api/src/Webhooks/WebhookSigner.cs`.
|
|
5
|
+
* Wire format mirrors Stripe's `Stripe-Signature`:
|
|
6
|
+
*
|
|
7
|
+
* SpreadSpace-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256(secret, "{t}.{body}")>
|
|
8
|
+
*
|
|
9
|
+
* The verifier:
|
|
10
|
+
* 1. Parses `t=` and `v1=` tokens (tolerant of extra/reordered/unknown pairs to
|
|
11
|
+
* stay forward-compatible with future schemes).
|
|
12
|
+
* 2. Recomputes HMAC-SHA256 over `"{timestamp}.{rawBody}"` with the secret.
|
|
13
|
+
* 3. Compares with `crypto.timingSafeEqual` on decoded bytes — never on hex
|
|
14
|
+
* strings (`===` short-circuits and leaks prefix info via timing).
|
|
15
|
+
* 4. Two-sided freshness check: rejects timestamps too far in the past (replay
|
|
16
|
+
* protection) AND too far in the future (attacker-controlled skew).
|
|
17
|
+
*
|
|
18
|
+
* Verification order matches the C# side: HMAC FIRST, then freshness. That way a
|
|
19
|
+
* forged-but-fresh request and a real-but-stale request fail indistinguishably.
|
|
20
|
+
*
|
|
21
|
+
* Tree-shakeable: importable as `@spreadspace/sdk/webhooks` so a webhook-receiver
|
|
22
|
+
* Lambda pulls in only the verifier, not the whole client + resource tree.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Canonical webhook event-type identifier strings. Stable wire constants stored
|
|
26
|
+
* verbatim in the envelope `type` field. `*` is a subscription wildcard, never
|
|
27
|
+
* an emitted `type`, so it is deliberately excluded from this union.
|
|
28
|
+
*/
|
|
29
|
+
type WebhookEventType = 'document.processed' | 'document.failed' | 'extraction.ready' | 'job.completed' | 'loan.classified';
|
|
30
|
+
/** `document.processed` payload. `extraction_id === document_id` if an extraction phase ran, `null` if classified-only. */
|
|
31
|
+
interface DocumentProcessedPayload {
|
|
32
|
+
document_id: string;
|
|
33
|
+
job_id: string;
|
|
34
|
+
borrower_id: string;
|
|
35
|
+
loan_id: string;
|
|
36
|
+
document_type: string;
|
|
37
|
+
processed_at: string;
|
|
38
|
+
extraction_id?: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** `document.failed` payload. `reason` mirrors `ExtractedDocument.ErrorMessage`. */
|
|
41
|
+
interface DocumentFailedPayload {
|
|
42
|
+
document_id: string;
|
|
43
|
+
job_id: string;
|
|
44
|
+
borrower_id: string;
|
|
45
|
+
loan_id: string;
|
|
46
|
+
reason: string;
|
|
47
|
+
failed_at: string;
|
|
48
|
+
}
|
|
49
|
+
/** `extraction.ready` payload. Fired once per doc when extracted line items become queryable. */
|
|
50
|
+
interface ExtractionReadyPayload {
|
|
51
|
+
extraction_id: string;
|
|
52
|
+
document_id: string;
|
|
53
|
+
job_id: string;
|
|
54
|
+
borrower_id: string;
|
|
55
|
+
loan_id: string;
|
|
56
|
+
document_type: string;
|
|
57
|
+
ready_at: string;
|
|
58
|
+
}
|
|
59
|
+
/** `job.completed` payload. Terminal package event, fired once every doc reaches a terminal state. */
|
|
60
|
+
interface JobCompletedPayload {
|
|
61
|
+
job_id: string;
|
|
62
|
+
borrower_id: string;
|
|
63
|
+
loan_id: string;
|
|
64
|
+
document_count_total: number;
|
|
65
|
+
document_count_succeeded: number;
|
|
66
|
+
document_count_failed: number;
|
|
67
|
+
completed_at: string;
|
|
68
|
+
}
|
|
69
|
+
/** `loan.classified` payload. Fired once per loan after the classification cascade finishes. */
|
|
70
|
+
interface LoanClassifiedPayload {
|
|
71
|
+
loan_id: string;
|
|
72
|
+
borrower_id: string;
|
|
73
|
+
job_id: string;
|
|
74
|
+
document_count: number;
|
|
75
|
+
classified_at: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Common envelope fields wrapping every event (the bytes that are signed).
|
|
79
|
+
* Property order is pinned server-side via `JsonPropertyOrder`; the signature is
|
|
80
|
+
* over the raw byte stream, so order is immaterial to the verifier but the
|
|
81
|
+
* fields are documented here for completeness. `livemode` is `false` for the
|
|
82
|
+
* sandbox simulator, `true` otherwise.
|
|
83
|
+
*/
|
|
84
|
+
interface WebhookEnvelopeBase {
|
|
85
|
+
id: string;
|
|
86
|
+
created: number;
|
|
87
|
+
tenant_id: string;
|
|
88
|
+
livemode: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Discriminated union over the five canonical webhook events. Switch on `type`
|
|
92
|
+
* to narrow `data` to the matching payload.
|
|
93
|
+
*/
|
|
94
|
+
type WebhookEvent = (WebhookEnvelopeBase & {
|
|
95
|
+
type: 'document.processed';
|
|
96
|
+
data: DocumentProcessedPayload;
|
|
97
|
+
}) | (WebhookEnvelopeBase & {
|
|
98
|
+
type: 'document.failed';
|
|
99
|
+
data: DocumentFailedPayload;
|
|
100
|
+
}) | (WebhookEnvelopeBase & {
|
|
101
|
+
type: 'extraction.ready';
|
|
102
|
+
data: ExtractionReadyPayload;
|
|
103
|
+
}) | (WebhookEnvelopeBase & {
|
|
104
|
+
type: 'job.completed';
|
|
105
|
+
data: JobCompletedPayload;
|
|
106
|
+
}) | (WebhookEnvelopeBase & {
|
|
107
|
+
type: 'loan.classified';
|
|
108
|
+
data: LoanClassifiedPayload;
|
|
109
|
+
});
|
|
110
|
+
/** Parsed envelope returned by {@link verifyAndParseWebhook}, narrowed on `type`. */
|
|
111
|
+
type VerifyAndParseResult = WebhookEvent;
|
|
112
|
+
/** Options for {@link verifyWebhook} / {@link verifyAndParseWebhook}. */
|
|
113
|
+
interface VerifyWebhookOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Maximum age (and maximum future skew) of the timestamp before rejection.
|
|
116
|
+
* Symmetric — `Math.abs(now - t)` must be ≤ this value. Default 300 seconds.
|
|
117
|
+
*/
|
|
118
|
+
freshnessTolerance?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Override the "current time" (unix seconds) used for the freshness check.
|
|
121
|
+
* Useful in tests; otherwise leave undefined and the verifier reads
|
|
122
|
+
* `Date.now() / 1000`.
|
|
123
|
+
*/
|
|
124
|
+
currentTimestamp?: number;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Thrown when a webhook signature fails verification or the envelope is
|
|
128
|
+
* structurally malformed. Self-contained — distinct from `SpreadSpaceError`
|
|
129
|
+
* because verifiers don't need the full request/response error machinery.
|
|
130
|
+
*/
|
|
131
|
+
declare class WebhookSignatureError extends Error {
|
|
132
|
+
readonly cause?: unknown;
|
|
133
|
+
constructor(message: string, cause?: unknown);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Verify a `SpreadSpace-Signature` header against a body and signing secret.
|
|
137
|
+
* Throws {@link WebhookSignatureError} on any failure (malformed header, bad
|
|
138
|
+
* HMAC, stale timestamp, wrong secret); returns `true` on success.
|
|
139
|
+
*
|
|
140
|
+
* @param rawBody The exact bytes received on the wire — NOT re-serialized JSON.
|
|
141
|
+
* Any byte-level transformation (re-stringify, gzip decode, encoding change)
|
|
142
|
+
* invalidates the signature.
|
|
143
|
+
* @param signature Contents of the `SpreadSpace-Signature` HTTP header.
|
|
144
|
+
* @param secret The plaintext signing secret (`whsec_...`). Never log this.
|
|
145
|
+
* @param options Optional freshness window + clock injection.
|
|
146
|
+
*/
|
|
147
|
+
declare function verifyWebhook(rawBody: string | Buffer | Uint8Array, signature: string, secret: string, options?: VerifyWebhookOptions): true;
|
|
148
|
+
/**
|
|
149
|
+
* Verify a webhook signature and JSON-parse the body into a typed
|
|
150
|
+
* {@link WebhookEvent} envelope. Throws {@link WebhookSignatureError} on
|
|
151
|
+
* signature failure or invalid JSON. Switch on `event.type` to narrow
|
|
152
|
+
* `event.data` to the matching payload.
|
|
153
|
+
*/
|
|
154
|
+
declare function verifyAndParseWebhook(rawBody: string | Buffer | Uint8Array, signature: string, secret: string, options?: VerifyWebhookOptions): VerifyAndParseResult;
|
|
155
|
+
|
|
156
|
+
export { type DocumentFailedPayload, type DocumentProcessedPayload, type ExtractionReadyPayload, type JobCompletedPayload, type LoanClassifiedPayload, type VerifyAndParseResult, type VerifyWebhookOptions, type WebhookEvent, type WebhookEventType, WebhookSignatureError, verifyAndParseWebhook, verifyWebhook };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook signature verification + typed event envelopes.
|
|
3
|
+
*
|
|
4
|
+
* Byte-for-byte compatible with the server signer `api/src/Webhooks/WebhookSigner.cs`.
|
|
5
|
+
* Wire format mirrors Stripe's `Stripe-Signature`:
|
|
6
|
+
*
|
|
7
|
+
* SpreadSpace-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256(secret, "{t}.{body}")>
|
|
8
|
+
*
|
|
9
|
+
* The verifier:
|
|
10
|
+
* 1. Parses `t=` and `v1=` tokens (tolerant of extra/reordered/unknown pairs to
|
|
11
|
+
* stay forward-compatible with future schemes).
|
|
12
|
+
* 2. Recomputes HMAC-SHA256 over `"{timestamp}.{rawBody}"` with the secret.
|
|
13
|
+
* 3. Compares with `crypto.timingSafeEqual` on decoded bytes — never on hex
|
|
14
|
+
* strings (`===` short-circuits and leaks prefix info via timing).
|
|
15
|
+
* 4. Two-sided freshness check: rejects timestamps too far in the past (replay
|
|
16
|
+
* protection) AND too far in the future (attacker-controlled skew).
|
|
17
|
+
*
|
|
18
|
+
* Verification order matches the C# side: HMAC FIRST, then freshness. That way a
|
|
19
|
+
* forged-but-fresh request and a real-but-stale request fail indistinguishably.
|
|
20
|
+
*
|
|
21
|
+
* Tree-shakeable: importable as `@spreadspace/sdk/webhooks` so a webhook-receiver
|
|
22
|
+
* Lambda pulls in only the verifier, not the whole client + resource tree.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Canonical webhook event-type identifier strings. Stable wire constants stored
|
|
26
|
+
* verbatim in the envelope `type` field. `*` is a subscription wildcard, never
|
|
27
|
+
* an emitted `type`, so it is deliberately excluded from this union.
|
|
28
|
+
*/
|
|
29
|
+
type WebhookEventType = 'document.processed' | 'document.failed' | 'extraction.ready' | 'job.completed' | 'loan.classified';
|
|
30
|
+
/** `document.processed` payload. `extraction_id === document_id` if an extraction phase ran, `null` if classified-only. */
|
|
31
|
+
interface DocumentProcessedPayload {
|
|
32
|
+
document_id: string;
|
|
33
|
+
job_id: string;
|
|
34
|
+
borrower_id: string;
|
|
35
|
+
loan_id: string;
|
|
36
|
+
document_type: string;
|
|
37
|
+
processed_at: string;
|
|
38
|
+
extraction_id?: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** `document.failed` payload. `reason` mirrors `ExtractedDocument.ErrorMessage`. */
|
|
41
|
+
interface DocumentFailedPayload {
|
|
42
|
+
document_id: string;
|
|
43
|
+
job_id: string;
|
|
44
|
+
borrower_id: string;
|
|
45
|
+
loan_id: string;
|
|
46
|
+
reason: string;
|
|
47
|
+
failed_at: string;
|
|
48
|
+
}
|
|
49
|
+
/** `extraction.ready` payload. Fired once per doc when extracted line items become queryable. */
|
|
50
|
+
interface ExtractionReadyPayload {
|
|
51
|
+
extraction_id: string;
|
|
52
|
+
document_id: string;
|
|
53
|
+
job_id: string;
|
|
54
|
+
borrower_id: string;
|
|
55
|
+
loan_id: string;
|
|
56
|
+
document_type: string;
|
|
57
|
+
ready_at: string;
|
|
58
|
+
}
|
|
59
|
+
/** `job.completed` payload. Terminal package event, fired once every doc reaches a terminal state. */
|
|
60
|
+
interface JobCompletedPayload {
|
|
61
|
+
job_id: string;
|
|
62
|
+
borrower_id: string;
|
|
63
|
+
loan_id: string;
|
|
64
|
+
document_count_total: number;
|
|
65
|
+
document_count_succeeded: number;
|
|
66
|
+
document_count_failed: number;
|
|
67
|
+
completed_at: string;
|
|
68
|
+
}
|
|
69
|
+
/** `loan.classified` payload. Fired once per loan after the classification cascade finishes. */
|
|
70
|
+
interface LoanClassifiedPayload {
|
|
71
|
+
loan_id: string;
|
|
72
|
+
borrower_id: string;
|
|
73
|
+
job_id: string;
|
|
74
|
+
document_count: number;
|
|
75
|
+
classified_at: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Common envelope fields wrapping every event (the bytes that are signed).
|
|
79
|
+
* Property order is pinned server-side via `JsonPropertyOrder`; the signature is
|
|
80
|
+
* over the raw byte stream, so order is immaterial to the verifier but the
|
|
81
|
+
* fields are documented here for completeness. `livemode` is `false` for the
|
|
82
|
+
* sandbox simulator, `true` otherwise.
|
|
83
|
+
*/
|
|
84
|
+
interface WebhookEnvelopeBase {
|
|
85
|
+
id: string;
|
|
86
|
+
created: number;
|
|
87
|
+
tenant_id: string;
|
|
88
|
+
livemode: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Discriminated union over the five canonical webhook events. Switch on `type`
|
|
92
|
+
* to narrow `data` to the matching payload.
|
|
93
|
+
*/
|
|
94
|
+
type WebhookEvent = (WebhookEnvelopeBase & {
|
|
95
|
+
type: 'document.processed';
|
|
96
|
+
data: DocumentProcessedPayload;
|
|
97
|
+
}) | (WebhookEnvelopeBase & {
|
|
98
|
+
type: 'document.failed';
|
|
99
|
+
data: DocumentFailedPayload;
|
|
100
|
+
}) | (WebhookEnvelopeBase & {
|
|
101
|
+
type: 'extraction.ready';
|
|
102
|
+
data: ExtractionReadyPayload;
|
|
103
|
+
}) | (WebhookEnvelopeBase & {
|
|
104
|
+
type: 'job.completed';
|
|
105
|
+
data: JobCompletedPayload;
|
|
106
|
+
}) | (WebhookEnvelopeBase & {
|
|
107
|
+
type: 'loan.classified';
|
|
108
|
+
data: LoanClassifiedPayload;
|
|
109
|
+
});
|
|
110
|
+
/** Parsed envelope returned by {@link verifyAndParseWebhook}, narrowed on `type`. */
|
|
111
|
+
type VerifyAndParseResult = WebhookEvent;
|
|
112
|
+
/** Options for {@link verifyWebhook} / {@link verifyAndParseWebhook}. */
|
|
113
|
+
interface VerifyWebhookOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Maximum age (and maximum future skew) of the timestamp before rejection.
|
|
116
|
+
* Symmetric — `Math.abs(now - t)` must be ≤ this value. Default 300 seconds.
|
|
117
|
+
*/
|
|
118
|
+
freshnessTolerance?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Override the "current time" (unix seconds) used for the freshness check.
|
|
121
|
+
* Useful in tests; otherwise leave undefined and the verifier reads
|
|
122
|
+
* `Date.now() / 1000`.
|
|
123
|
+
*/
|
|
124
|
+
currentTimestamp?: number;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Thrown when a webhook signature fails verification or the envelope is
|
|
128
|
+
* structurally malformed. Self-contained — distinct from `SpreadSpaceError`
|
|
129
|
+
* because verifiers don't need the full request/response error machinery.
|
|
130
|
+
*/
|
|
131
|
+
declare class WebhookSignatureError extends Error {
|
|
132
|
+
readonly cause?: unknown;
|
|
133
|
+
constructor(message: string, cause?: unknown);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Verify a `SpreadSpace-Signature` header against a body and signing secret.
|
|
137
|
+
* Throws {@link WebhookSignatureError} on any failure (malformed header, bad
|
|
138
|
+
* HMAC, stale timestamp, wrong secret); returns `true` on success.
|
|
139
|
+
*
|
|
140
|
+
* @param rawBody The exact bytes received on the wire — NOT re-serialized JSON.
|
|
141
|
+
* Any byte-level transformation (re-stringify, gzip decode, encoding change)
|
|
142
|
+
* invalidates the signature.
|
|
143
|
+
* @param signature Contents of the `SpreadSpace-Signature` HTTP header.
|
|
144
|
+
* @param secret The plaintext signing secret (`whsec_...`). Never log this.
|
|
145
|
+
* @param options Optional freshness window + clock injection.
|
|
146
|
+
*/
|
|
147
|
+
declare function verifyWebhook(rawBody: string | Buffer | Uint8Array, signature: string, secret: string, options?: VerifyWebhookOptions): true;
|
|
148
|
+
/**
|
|
149
|
+
* Verify a webhook signature and JSON-parse the body into a typed
|
|
150
|
+
* {@link WebhookEvent} envelope. Throws {@link WebhookSignatureError} on
|
|
151
|
+
* signature failure or invalid JSON. Switch on `event.type` to narrow
|
|
152
|
+
* `event.data` to the matching payload.
|
|
153
|
+
*/
|
|
154
|
+
declare function verifyAndParseWebhook(rawBody: string | Buffer | Uint8Array, signature: string, secret: string, options?: VerifyWebhookOptions): VerifyAndParseResult;
|
|
155
|
+
|
|
156
|
+
export { type DocumentFailedPayload, type DocumentProcessedPayload, type ExtractionReadyPayload, type JobCompletedPayload, type LoanClassifiedPayload, type VerifyAndParseResult, type VerifyWebhookOptions, type WebhookEvent, type WebhookEventType, WebhookSignatureError, verifyAndParseWebhook, verifyWebhook };
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spreadspace/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official TypeScript SDK for the SpreadSpace API.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./webhooks": {
|
|
18
|
+
"types": "./dist/webhooks.d.ts",
|
|
19
|
+
"import": "./dist/webhooks.js",
|
|
20
|
+
"require": "./dist/webhooks.cjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\""
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"decimal.js": "10.6.0",
|
|
37
|
+
"lossless-json": "4.3.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.10.5",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
|
42
|
+
"@typescript-eslint/parser": "^8.59.3",
|
|
43
|
+
"eslint": "^10.4.0",
|
|
44
|
+
"tsup": "^8.3.5",
|
|
45
|
+
"typescript": "^5.7.3",
|
|
46
|
+
"vitest": "^3.0.5"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"spreadspace",
|
|
56
|
+
"api",
|
|
57
|
+
"sdk",
|
|
58
|
+
"typescript",
|
|
59
|
+
"lending",
|
|
60
|
+
"documents"
|
|
61
|
+
]
|
|
62
|
+
}
|