settld 0.2.5 → 0.2.7
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/Dockerfile +2 -2
- package/package.json +2 -1
- package/packages/api-sdk/README.md +71 -0
- package/packages/api-sdk/src/client.js +1021 -0
- package/packages/api-sdk/src/express-middleware.js +163 -0
- package/packages/api-sdk/src/index.d.ts +1662 -0
- package/packages/api-sdk/src/index.js +10 -0
- package/packages/api-sdk/src/webhook-signature.js +182 -0
- package/packages/api-sdk/src/x402-autopay.js +210 -0
- package/scripts/ci/cli-pack-smoke.mjs +2 -0
- package/scripts/setup/login.mjs +6 -1
- package/scripts/setup/onboard.mjs +27 -1
- package/scripts/setup/onboarding-failure-taxonomy.mjs +11 -0
- package/services/magic-link/README.md +13 -4
- package/services/magic-link/src/buyer-auth.js +33 -2
- package/services/magic-link/src/decision-otp.js +33 -2
- package/services/magic-link/src/email-resend.js +89 -0
- package/services/magic-link/src/maintenance.js +26 -1
- package/services/magic-link/src/server.js +72 -11
- package/services/magic-link/src/smtp.js +19 -4
- package/src/api/app.js +6 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SettldWebhookNoMatchingSignatureError,
|
|
3
|
+
SettldWebhookSignatureHeaderError,
|
|
4
|
+
SettldWebhookTimestampToleranceError,
|
|
5
|
+
verifySettldWebhookSignature
|
|
6
|
+
} from "./webhook-signature.js";
|
|
7
|
+
|
|
8
|
+
function isBodyVerifiable(body) {
|
|
9
|
+
if (body === null || body === undefined) return false;
|
|
10
|
+
if (typeof body === "string") return true;
|
|
11
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) return true;
|
|
12
|
+
if (body instanceof Uint8Array) return true;
|
|
13
|
+
if (body instanceof ArrayBuffer) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readHeader(req, headerName) {
|
|
18
|
+
const key = String(headerName || "").toLowerCase();
|
|
19
|
+
if (typeof req?.get === "function") {
|
|
20
|
+
return req.get(key) ?? req.get(headerName) ?? null;
|
|
21
|
+
}
|
|
22
|
+
const headers = req?.headers;
|
|
23
|
+
if (!headers || typeof headers !== "object") return null;
|
|
24
|
+
const direct = headers[key];
|
|
25
|
+
if (Array.isArray(direct)) return direct[0] ?? null;
|
|
26
|
+
if (typeof direct === "string") return direct;
|
|
27
|
+
const alt = headers[headerName];
|
|
28
|
+
if (Array.isArray(alt)) return alt[0] ?? null;
|
|
29
|
+
return typeof alt === "string" ? alt : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveMiddlewareOptions(optionsOrTolerance) {
|
|
33
|
+
if (optionsOrTolerance === undefined || optionsOrTolerance === null) {
|
|
34
|
+
return {
|
|
35
|
+
toleranceSeconds: 300,
|
|
36
|
+
signatureHeaderName: "x-settld-signature",
|
|
37
|
+
timestampHeaderName: "x-settld-timestamp"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (typeof optionsOrTolerance === "number") {
|
|
41
|
+
return {
|
|
42
|
+
toleranceSeconds: optionsOrTolerance,
|
|
43
|
+
signatureHeaderName: "x-settld-signature",
|
|
44
|
+
timestampHeaderName: "x-settld-timestamp"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (typeof optionsOrTolerance !== "object" || Array.isArray(optionsOrTolerance)) {
|
|
48
|
+
throw new TypeError("options must be a number or plain object");
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
toleranceSeconds:
|
|
52
|
+
optionsOrTolerance.toleranceSeconds === undefined || optionsOrTolerance.toleranceSeconds === null
|
|
53
|
+
? 300
|
|
54
|
+
: Number(optionsOrTolerance.toleranceSeconds),
|
|
55
|
+
signatureHeaderName: optionsOrTolerance.signatureHeaderName || "x-settld-signature",
|
|
56
|
+
timestampHeaderName: optionsOrTolerance.timestampHeaderName || "x-settld-timestamp"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeErrorResponse(res, statusCode, code, message) {
|
|
61
|
+
if (typeof res?.status === "function") {
|
|
62
|
+
const withStatus = res.status(statusCode);
|
|
63
|
+
if (typeof withStatus?.json === "function") {
|
|
64
|
+
withStatus.json({
|
|
65
|
+
ok: false,
|
|
66
|
+
error: { code, message }
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof withStatus?.send === "function") {
|
|
71
|
+
withStatus.send(message);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (typeof res?.writeHead === "function") {
|
|
76
|
+
res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
77
|
+
if (typeof res?.end === "function") {
|
|
78
|
+
res.end(JSON.stringify({ ok: false, error: { code, message } }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (typeof res?.end === "function") {
|
|
83
|
+
res.end(JSON.stringify({ ok: false, error: { code, message } }));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function classifyVerificationError(err) {
|
|
88
|
+
if (err instanceof SettldWebhookSignatureHeaderError) {
|
|
89
|
+
return {
|
|
90
|
+
status: 400,
|
|
91
|
+
code: err.code || "SETTLD_WEBHOOK_SIGNATURE_HEADER_INVALID",
|
|
92
|
+
message: err.message
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (err instanceof SettldWebhookTimestampToleranceError) {
|
|
96
|
+
return {
|
|
97
|
+
status: 401,
|
|
98
|
+
code: err.code || "SETTLD_WEBHOOK_TIMESTAMP_OUTSIDE_TOLERANCE",
|
|
99
|
+
message: err.message
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (err instanceof SettldWebhookNoMatchingSignatureError) {
|
|
103
|
+
return {
|
|
104
|
+
status: 401,
|
|
105
|
+
code: err.code || "SETTLD_WEBHOOK_SIGNATURE_NO_MATCH",
|
|
106
|
+
message: err.message
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
status: 401,
|
|
111
|
+
code: "SETTLD_WEBHOOK_UNAUTHORIZED",
|
|
112
|
+
message: err?.message || "webhook signature verification failed"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Express-style middleware for Settld webhook verification.
|
|
118
|
+
*
|
|
119
|
+
* IMPORTANT:
|
|
120
|
+
* - This requires the raw request body bytes.
|
|
121
|
+
* - Use `req.rawBody` (preferred) or `req.body` as Buffer/string/typed array.
|
|
122
|
+
*/
|
|
123
|
+
export function verifySettldWebhook(secretOrResolver, optionsOrTolerance = 300) {
|
|
124
|
+
const options = resolveMiddlewareOptions(optionsOrTolerance);
|
|
125
|
+
const resolveSecret =
|
|
126
|
+
typeof secretOrResolver === "function" ? secretOrResolver : () => secretOrResolver;
|
|
127
|
+
|
|
128
|
+
return function settldWebhookMiddleware(req, res, next) {
|
|
129
|
+
Promise.resolve()
|
|
130
|
+
.then(() => resolveSecret(req))
|
|
131
|
+
.then((secret) => {
|
|
132
|
+
const rawBody = req?.rawBody ?? req?.body;
|
|
133
|
+
if (!isBodyVerifiable(rawBody)) {
|
|
134
|
+
writeErrorResponse(
|
|
135
|
+
res,
|
|
136
|
+
400,
|
|
137
|
+
"SETTLD_WEBHOOK_RAW_BODY_REQUIRED",
|
|
138
|
+
"Settld Webhook Error: request body is already parsed or missing raw bytes. Configure body-parser to preserve the raw buffer."
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const signatureHeader = readHeader(req, options.signatureHeaderName);
|
|
144
|
+
const timestampHeader = readHeader(req, options.timestampHeaderName);
|
|
145
|
+
verifySettldWebhookSignature(rawBody, signatureHeader ?? "", secret, {
|
|
146
|
+
toleranceSeconds: options.toleranceSeconds,
|
|
147
|
+
timestamp: timestampHeader
|
|
148
|
+
});
|
|
149
|
+
if (typeof next === "function") next();
|
|
150
|
+
})
|
|
151
|
+
.catch((err) => {
|
|
152
|
+
if (err instanceof TypeError && /secret is required/.test(String(err.message))) {
|
|
153
|
+
if (typeof next === "function") {
|
|
154
|
+
next(err);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const classified = classifyVerificationError(err);
|
|
159
|
+
writeErrorResponse(res, classified.status, classified.code, classified.message);
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|