@marcohefti/request-network-api-contracts 0.6.2 → 0.7.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 +9 -25
- package/docs/OPENAPI-0.31.0-AUDIT.md +91 -0
- package/docs/OVERVIEW.md +8 -11
- package/docs/PUBLISHING.md +7 -8
- package/docs/UPDATE-WORKFLOW.md +13 -16
- package/docs/UPDATES.md +8 -0
- package/fixtures/webhooks/client-id-linked.json +13 -0
- package/fixtures/webhooks/kyt-screening-completed.json +13 -0
- package/fixtures/webhooks/payment-confirmed.json +10 -5
- package/fixtures/webhooks/payment-failed.json +5 -11
- package/fixtures/webhooks/secure-payment-access-rejected.json +8 -0
- package/fixtures/webhooks/secure-payment-user-event.json +20 -0
- package/package.json +4 -1
- package/scripts/sync-openapi.mjs +266 -0
- package/scripts/sync-webhooks.mjs +128 -0
- package/scripts/verify.js +119 -0
- package/specs/README.md +11 -7
- package/specs/openapi/manifest.json +46 -0
- package/specs/openapi/request-network-auth-openapi.json +1 -0
- package/specs/openapi/request-network-auth-openapi.meta.json +13 -0
- package/specs/openapi/request-network-openapi.json +1 -1
- package/specs/openapi/request-network-openapi.meta.json +11 -2
- package/specs/webhooks/manifest.json +48 -0
- package/specs/webhooks/request-network-webhooks.json +486 -12
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const openApiDirectory = resolve(root, "specs/openapi");
|
|
10
|
+
const contractVersion = "0.7.0";
|
|
11
|
+
|
|
12
|
+
const sources = [
|
|
13
|
+
{
|
|
14
|
+
id: "request-api",
|
|
15
|
+
url: "https://api.request.network/open-api/openapi.json",
|
|
16
|
+
runtimeBaseUrl: "https://api.request.network",
|
|
17
|
+
filename: "request-network-openapi.json",
|
|
18
|
+
metaFilename: "request-network-openapi.meta.json",
|
|
19
|
+
patch: patchRequestApi,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "auth-api",
|
|
23
|
+
url: "https://auth.request.network/open-api/openapi.json",
|
|
24
|
+
runtimeBaseUrl: "https://auth.request.network",
|
|
25
|
+
filename: "request-network-auth-openapi.json",
|
|
26
|
+
metaFilename: "request-network-auth-openapi.meta.json",
|
|
27
|
+
patch: patchAuthApi,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const createSecurePaymentResponse = {
|
|
32
|
+
type: "object",
|
|
33
|
+
additionalProperties: true,
|
|
34
|
+
properties: {
|
|
35
|
+
requestIds: { type: "array", items: { type: "string" } },
|
|
36
|
+
securePaymentUrl: { type: "string", format: "uri" },
|
|
37
|
+
token: { type: "string" },
|
|
38
|
+
feePlan: { type: "object", nullable: true, additionalProperties: true },
|
|
39
|
+
},
|
|
40
|
+
required: ["requestIds", "securePaymentUrl", "token"],
|
|
41
|
+
description: "Compatibility schema from the public Secure Payments guide and verified production response keys.",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const findSecurePaymentResponse = {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: true,
|
|
47
|
+
properties: {
|
|
48
|
+
token: { type: "string" },
|
|
49
|
+
securePaymentUrl: { type: "string", format: "uri" },
|
|
50
|
+
status: { type: "string", enum: ["pending", "completed", "expired", "invalidated"] },
|
|
51
|
+
paymentType: { type: "string", enum: ["single", "batch"] },
|
|
52
|
+
createdAt: { type: "string", format: "date-time", nullable: true },
|
|
53
|
+
expiresAt: { type: "string", format: "date-time" },
|
|
54
|
+
feePlan: { type: "object", nullable: true, additionalProperties: true },
|
|
55
|
+
},
|
|
56
|
+
required: ["token", "securePaymentUrl", "status", "paymentType", "expiresAt"],
|
|
57
|
+
description: "Compatibility schema from the public Secure Payments guide.",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getSecurePaymentResponse = {
|
|
61
|
+
type: "object",
|
|
62
|
+
additionalProperties: true,
|
|
63
|
+
properties: {
|
|
64
|
+
paymentType: { type: "string", enum: ["single", "batch"] },
|
|
65
|
+
payee: { type: "string" },
|
|
66
|
+
payees: { type: "array", items: { type: "string" } },
|
|
67
|
+
network: { type: "string" },
|
|
68
|
+
amount: { type: "string" },
|
|
69
|
+
amounts: { type: "array", items: { type: "string" } },
|
|
70
|
+
paymentCurrency: { type: "string" },
|
|
71
|
+
paymentCurrencies: { type: "array", items: { type: "string" } },
|
|
72
|
+
isNativeCurrency: { oneOf: [{ type: "boolean" }, { type: "array", items: { type: "boolean" } }] },
|
|
73
|
+
status: { type: "string", enum: ["pending", "completed", "expired", "invalidated"] },
|
|
74
|
+
destination: { type: "object", additionalProperties: true },
|
|
75
|
+
destinations: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
76
|
+
reference: { type: "string", nullable: true },
|
|
77
|
+
paymentOptions: { type: "object", additionalProperties: true },
|
|
78
|
+
feePlan: { type: "object", nullable: true, additionalProperties: true },
|
|
79
|
+
},
|
|
80
|
+
required: ["paymentType", "network", "status"],
|
|
81
|
+
description: "Compatibility schema from the public Secure Payments guide.",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function sha256(value) {
|
|
85
|
+
return createHash("sha256").update(value).digest("hex");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function operationCount(spec) {
|
|
89
|
+
const methods = new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
90
|
+
return Object.values(spec.paths ?? {}).reduce(
|
|
91
|
+
(total, pathItem) => total + Object.keys(pathItem).filter((key) => methods.has(key)).length,
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeNullableUnions(node, stats) {
|
|
97
|
+
if (Array.isArray(node)) {
|
|
98
|
+
for (const value of node) normalizeNullableUnions(value, stats);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!node || typeof node !== "object") return;
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(node.type) && node.type.length === 2 && node.type.includes("null")) {
|
|
104
|
+
node.type = node.type.find((value) => value !== "null");
|
|
105
|
+
node.nullable = true;
|
|
106
|
+
stats.nullableUnions += 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const value of Object.values(node)) normalizeNullableUnions(value, stats);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function patchFeeDrift(node, stats) {
|
|
113
|
+
if (Array.isArray(node)) {
|
|
114
|
+
for (const value of node) patchFeeDrift(value, stats);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!node || typeof node !== "object") return;
|
|
118
|
+
|
|
119
|
+
const properties = node.properties;
|
|
120
|
+
const typeSchema = properties?.type;
|
|
121
|
+
if (
|
|
122
|
+
typeSchema &&
|
|
123
|
+
Array.isArray(typeSchema.enum) &&
|
|
124
|
+
["gas", "platform", "crosschain", "crypto-to-fiat", "offramp"].every((value) => typeSchema.enum.includes(value))
|
|
125
|
+
) {
|
|
126
|
+
if (!typeSchema.enum.includes("protocol")) {
|
|
127
|
+
typeSchema.enum.push("protocol");
|
|
128
|
+
stats.feeEnums += 1;
|
|
129
|
+
}
|
|
130
|
+
for (const key of ["amount", "amountInUSD", "amountInUsd"]) {
|
|
131
|
+
if (properties[key]?.type === "string" && properties[key].nullable !== true) {
|
|
132
|
+
properties[key].nullable = true;
|
|
133
|
+
stats.feeAmounts += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const value of Object.values(node)) patchFeeDrift(value, stats);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function setResponseSchema(spec, method, endpoint, status, schema) {
|
|
142
|
+
spec.paths[endpoint][method].responses[status].content["application/json"].schema = structuredClone(schema);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function withFeePlan(schema, feePlan) {
|
|
146
|
+
const result = structuredClone(schema);
|
|
147
|
+
result.properties.feePlan = structuredClone(feePlan);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function patchRequestApi(spec) {
|
|
152
|
+
const stats = { nullableUnions: 0, feeEnums: 0, feeAmounts: 0, securePaymentResponses: 0 };
|
|
153
|
+
normalizeNullableUnions(spec, stats);
|
|
154
|
+
patchFeeDrift(spec, stats);
|
|
155
|
+
|
|
156
|
+
for (const endpoint of ["/v2/secure-payments", "/v2/secure-payments/payouts", "/v2/secure-payments/batch-payouts"]) {
|
|
157
|
+
const feePlan = spec.paths[endpoint].post.responses["201"].content["application/json"].schema;
|
|
158
|
+
setResponseSchema(spec, "post", endpoint, "201", withFeePlan(createSecurePaymentResponse, feePlan));
|
|
159
|
+
stats.securePaymentResponses += 1;
|
|
160
|
+
}
|
|
161
|
+
const findFeePlan = spec.paths["/v2/secure-payments"].get.responses["200"].content["application/json"].schema;
|
|
162
|
+
setResponseSchema(spec, "get", "/v2/secure-payments", "200", withFeePlan(findSecurePaymentResponse, findFeePlan));
|
|
163
|
+
const tokenFeePlan = spec.paths["/v2/secure-payments/{token}"].get.responses["200"].content["application/json"].schema;
|
|
164
|
+
setResponseSchema(spec, "get", "/v2/secure-payments/{token}", "200", withFeePlan(getSecurePaymentResponse, tokenFeePlan));
|
|
165
|
+
stats.securePaymentResponses += 2;
|
|
166
|
+
|
|
167
|
+
spec["x-contract-patches"] = [
|
|
168
|
+
"oas30-nullable-unions",
|
|
169
|
+
"runtime-fee-drift",
|
|
170
|
+
"secure-payment-response-shapes",
|
|
171
|
+
];
|
|
172
|
+
return stats;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function patchAuthApi(spec) {
|
|
176
|
+
const stats = { nullableUnions: 0, webhookSecurityOperations: 0 };
|
|
177
|
+
normalizeNullableUnions(spec, stats);
|
|
178
|
+
|
|
179
|
+
spec.components ??= {};
|
|
180
|
+
spec.components.securitySchemes ??= {};
|
|
181
|
+
spec.components.securitySchemes.session_token = {
|
|
182
|
+
type: "apiKey",
|
|
183
|
+
in: "cookie",
|
|
184
|
+
name: "session_token",
|
|
185
|
+
};
|
|
186
|
+
spec.components.securitySchemes.client_id = {
|
|
187
|
+
type: "apiKey",
|
|
188
|
+
in: "header",
|
|
189
|
+
name: "x-client-id",
|
|
190
|
+
description: "Platform Client ID authentication verified against production webhook operations.",
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
for (const [endpoint, methods] of [
|
|
194
|
+
["/v1/webhook", ["get", "post"]],
|
|
195
|
+
["/v1/webhook/test", ["post"]],
|
|
196
|
+
["/v1/webhook/{webhookId}", ["put", "delete"]],
|
|
197
|
+
]) {
|
|
198
|
+
for (const method of methods) {
|
|
199
|
+
spec.paths[endpoint][method].security = [{ client_id: [] }, { session_token: [] }];
|
|
200
|
+
stats.webhookSecurityOperations += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
spec["x-contract-patches"] = ["oas30-nullable-unions", "platform-webhook-client-id-auth"];
|
|
205
|
+
return stats;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function main() {
|
|
209
|
+
await mkdir(openApiDirectory, { recursive: true });
|
|
210
|
+
const fetchedAt = new Date().toISOString();
|
|
211
|
+
const manifestSources = [];
|
|
212
|
+
|
|
213
|
+
for (const source of sources) {
|
|
214
|
+
const response = await fetch(source.url, { headers: { accept: "application/json" } });
|
|
215
|
+
if (!response.ok) throw new Error(`${source.id} fetch failed: ${response.status} ${response.statusText}`);
|
|
216
|
+
|
|
217
|
+
const rawBody = await response.text();
|
|
218
|
+
const spec = JSON.parse(rawBody);
|
|
219
|
+
const patchStats = source.patch(spec);
|
|
220
|
+
const normalizedBody = `${JSON.stringify(spec)}\n`;
|
|
221
|
+
|
|
222
|
+
await writeFile(resolve(openApiDirectory, source.filename), normalizedBody);
|
|
223
|
+
await writeFile(
|
|
224
|
+
resolve(openApiDirectory, source.metaFilename),
|
|
225
|
+
`${JSON.stringify({
|
|
226
|
+
url: source.url,
|
|
227
|
+
runtimeBaseUrl: source.runtimeBaseUrl,
|
|
228
|
+
fetchedAt,
|
|
229
|
+
etag: response.headers.get("etag"),
|
|
230
|
+
lastModified: response.headers.get("last-modified"),
|
|
231
|
+
rawSha256: sha256(rawBody),
|
|
232
|
+
normalizedSha256: sha256(normalizedBody),
|
|
233
|
+
patchStats,
|
|
234
|
+
}, null, 2)}\n`,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
manifestSources.push({
|
|
238
|
+
id: source.id,
|
|
239
|
+
filename: source.filename,
|
|
240
|
+
sourceUrl: source.url,
|
|
241
|
+
runtimeBaseUrl: source.runtimeBaseUrl,
|
|
242
|
+
openapi: spec.openapi,
|
|
243
|
+
apiVersion: spec.info?.version,
|
|
244
|
+
operationCount: operationCount(spec),
|
|
245
|
+
rawSha256: sha256(rawBody),
|
|
246
|
+
normalizedSha256: sha256(normalizedBody),
|
|
247
|
+
patches: spec["x-contract-patches"],
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const manifest = {
|
|
252
|
+
contractsVersion: contractVersion,
|
|
253
|
+
fetchedAt,
|
|
254
|
+
defaultEnvironment: "production",
|
|
255
|
+
supportedRuntimeHosts: ["https://api.request.network", "https://auth.request.network"],
|
|
256
|
+
minimumCompatibleClients: { typescript: "0.7.0", php: "0.7.0" },
|
|
257
|
+
sources: manifestSources,
|
|
258
|
+
};
|
|
259
|
+
await writeFile(resolve(openApiDirectory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
260
|
+
process.stdout.write(`Synced ${manifestSources.map(({ id, apiVersion, operationCount: count }) => `${id} ${apiVersion} (${count})`).join(", ")}\n`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main().catch((error) => {
|
|
264
|
+
console.error(error);
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const schemaPath = resolve(root, "specs/webhooks/request-network-webhooks.json");
|
|
9
|
+
|
|
10
|
+
function webhookOperation(summary, schemaName, exampleName) {
|
|
11
|
+
return {
|
|
12
|
+
post: {
|
|
13
|
+
summary,
|
|
14
|
+
parameters: [{ $ref: "#/components/parameters/XRequestNetworkSignature" }],
|
|
15
|
+
requestBody: {
|
|
16
|
+
required: true,
|
|
17
|
+
content: {
|
|
18
|
+
"application/json": {
|
|
19
|
+
schema: { $ref: `#/components/schemas/${schemaName}` },
|
|
20
|
+
examples: { [exampleName]: { $ref: `#/components/examples/${exampleName}` } },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
responses: {
|
|
25
|
+
200: { description: "Acknowledged" },
|
|
26
|
+
401: { description: "Signature verification failed" },
|
|
27
|
+
},
|
|
28
|
+
security: [{ RequestSignatureHMAC: [] }],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function eventSchema(event, properties, required, description) {
|
|
34
|
+
return {
|
|
35
|
+
allOf: [
|
|
36
|
+
{ $ref: "#/components/schemas/WebhookBase" },
|
|
37
|
+
{
|
|
38
|
+
type: "object",
|
|
39
|
+
additionalProperties: true,
|
|
40
|
+
properties: { event: { const: event }, ...properties },
|
|
41
|
+
required: ["event", ...required],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
description,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const schema = JSON.parse(await readFile(schemaPath, "utf8"));
|
|
50
|
+
schema.info.version = "0.2.0";
|
|
51
|
+
schema.info.description = "Community-maintained schemas for current Request Network platform/orchestrator webhooks and explicitly classified legacy events. Payload examples follow the official webhook guide fetched 2026-09-03.";
|
|
52
|
+
|
|
53
|
+
Object.assign(schema.webhooks, {
|
|
54
|
+
"client_id.linked": webhookOperation("Client ID linked through hosted onboarding", "ClientIdLinkedEvent", "client_id_linked"),
|
|
55
|
+
"kyt.screening.completed": webhookOperation("KYT screening reached a definitive result", "KytScreeningCompletedEvent", "kyt_screening_completed"),
|
|
56
|
+
"secure_payment.user_event": webhookOperation("Secure Payment Page user activity", "SecurePaymentUserEvent", "secure_payment_user_event"),
|
|
57
|
+
"secure_payment.access_rejected": webhookOperation("Payer-wallet access rejected", "SecurePaymentAccessRejectedEvent", "secure_payment_access_rejected"),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
Object.assign(schema.components.schemas.WebhookBase.properties, {
|
|
61
|
+
clientId: { type: "string", description: "Platform Client ID associated with the event." },
|
|
62
|
+
orchestratorId: { type: "string", description: "Orchestrator associated with the event when applicable." },
|
|
63
|
+
payerAddress: { type: "string", nullable: true, description: "Resolved payer wallet when available." },
|
|
64
|
+
payerEoaAddress: { type: "string", nullable: true, description: "Connected payer EOA when available." },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
Object.assign(schema.components.schemas, {
|
|
68
|
+
ClientIdLinkedEvent: eventSchema("client_id.linked", {
|
|
69
|
+
clientId: { type: "string" },
|
|
70
|
+
orchestratorId: { type: "string" },
|
|
71
|
+
linkId: { type: "string" },
|
|
72
|
+
intentId: { type: "string" },
|
|
73
|
+
externalId: { type: "string" },
|
|
74
|
+
destinationId: { type: "string" },
|
|
75
|
+
destinationWalletAddress: { type: "string" },
|
|
76
|
+
chain: { type: "string" },
|
|
77
|
+
currency: { type: "string" },
|
|
78
|
+
timestamp: { type: "string", format: "date-time" },
|
|
79
|
+
}, ["clientId", "orchestratorId", "linkId", "intentId", "externalId", "destinationId", "destinationWalletAddress", "chain", "currency", "timestamp"], "Orchestrator-only hosted onboarding completion event."),
|
|
80
|
+
KytScreeningCompletedEvent: eventSchema("kyt.screening.completed", {
|
|
81
|
+
paymentToken: { type: "string" },
|
|
82
|
+
clientId: { type: "string" },
|
|
83
|
+
orchestratorId: { type: "string" },
|
|
84
|
+
walletAddress: { type: "string" },
|
|
85
|
+
eoaAddress: { type: "string" },
|
|
86
|
+
smartAccountAddress: { type: "string", nullable: true },
|
|
87
|
+
status: { type: "string", enum: ["approved", "rejected"] },
|
|
88
|
+
provider: { type: "string" },
|
|
89
|
+
policyId: { type: "string", nullable: true },
|
|
90
|
+
timestamp: { type: "string", format: "date-time" },
|
|
91
|
+
}, ["paymentToken", "clientId", "walletAddress", "eoaAddress", "status", "provider", "timestamp"], "Definitive Secure Payment KYT result."),
|
|
92
|
+
SecurePaymentUserEvent: eventSchema("secure_payment.user_event", {
|
|
93
|
+
userEvent: { type: "string", enum: ["wallet_connected", "payment_sent_to_wallet", "payment_approved_in_wallet"] },
|
|
94
|
+
securePaymentToken: { type: "string" },
|
|
95
|
+
requestId: { type: "string" },
|
|
96
|
+
requestIds: { type: "array", items: { type: "string" } },
|
|
97
|
+
clientId: { type: "string" },
|
|
98
|
+
orchestratorId: { type: "string" },
|
|
99
|
+
occurredAt: { type: "string", format: "date-time" },
|
|
100
|
+
timestamp: { type: "string", format: "date-time" },
|
|
101
|
+
properties: { type: "object", additionalProperties: true },
|
|
102
|
+
}, ["userEvent", "securePaymentToken", "requestIds", "clientId", "occurredAt", "timestamp", "properties"], "Best-effort browser activity; never settlement proof."),
|
|
103
|
+
SecurePaymentAccessRejectedEvent: eventSchema("secure_payment.access_rejected", {
|
|
104
|
+
requestId: { type: "string" },
|
|
105
|
+
clientId: { type: "string" },
|
|
106
|
+
orchestratorId: { type: "string" },
|
|
107
|
+
attemptedPayerWalletAddress: { type: "string" },
|
|
108
|
+
timestamp: { type: "string", format: "date-time" },
|
|
109
|
+
}, ["requestId", "clientId", "attemptedPayerWalletAddress", "timestamp"], "Platform-only payer allowlist rejection event."),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
Object.assign(schema.components.examples, {
|
|
113
|
+
payment_confirmed: { summary: "Official current settlement example", value: JSON.parse(await readFile(resolve(root, "fixtures/webhooks/payment-confirmed.json"), "utf8")) },
|
|
114
|
+
payment_failed: { summary: "Official current failure example", value: JSON.parse(await readFile(resolve(root, "fixtures/webhooks/payment-failed.json"), "utf8")) },
|
|
115
|
+
client_id_linked: { summary: "Official hosted onboarding example", value: JSON.parse(await readFile(resolve(root, "fixtures/webhooks/client-id-linked.json"), "utf8")) },
|
|
116
|
+
kyt_screening_completed: { summary: "Official definitive KYT example", value: JSON.parse(await readFile(resolve(root, "fixtures/webhooks/kyt-screening-completed.json"), "utf8")) },
|
|
117
|
+
secure_payment_user_event: { summary: "Official Secure Payment Page activity example", value: JSON.parse(await readFile(resolve(root, "fixtures/webhooks/secure-payment-user-event.json"), "utf8")) },
|
|
118
|
+
secure_payment_access_rejected: { summary: "Official payer allowlist rejection example", value: JSON.parse(await readFile(resolve(root, "fixtures/webhooks/secure-payment-access-rejected.json"), "utf8")) },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await writeFile(schemaPath, `${JSON.stringify(schema, null, 2)}\n`);
|
|
122
|
+
process.stdout.write("Synchronized current webhook schemas and examples\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().catch((error) => {
|
|
126
|
+
console.error(error);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const root = path.join(__dirname, '..');
|
|
8
|
+
const files = [
|
|
9
|
+
path.join(root, 'specs', 'openapi', 'request-network-openapi.json'),
|
|
10
|
+
path.join(root, 'specs', 'openapi', 'request-network-openapi.meta.json'),
|
|
11
|
+
path.join(root, 'specs', 'openapi', 'request-network-auth-openapi.json'),
|
|
12
|
+
path.join(root, 'specs', 'openapi', 'request-network-auth-openapi.meta.json'),
|
|
13
|
+
path.join(root, 'specs', 'openapi', 'manifest.json'),
|
|
14
|
+
path.join(root, 'specs', 'webhooks', 'request-network-webhooks.json'),
|
|
15
|
+
path.join(root, 'specs', 'webhooks', 'manifest.json'),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
try {
|
|
20
|
+
const stats = fs.statSync(file);
|
|
21
|
+
if (!stats.size) {
|
|
22
|
+
throw new Error('file is empty');
|
|
23
|
+
}
|
|
24
|
+
process.stdout.write(`✔ ${path.relative(root, file)} (${stats.size} bytes)\n`);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`Failed to verify ${file}:`, error.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJson(relativePath) {
|
|
32
|
+
return JSON.parse(fs.readFileSync(path.join(root, relativePath), 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assert(condition, message) {
|
|
36
|
+
if (!condition) {
|
|
37
|
+
throw new Error(message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function operationCount(spec) {
|
|
42
|
+
const methods = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace']);
|
|
43
|
+
return Object.values(spec.paths || {}).reduce(
|
|
44
|
+
(total, item) => total + Object.keys(item).filter((key) => methods.has(key)).length,
|
|
45
|
+
0,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasArrayType(node) {
|
|
50
|
+
if (Array.isArray(node)) return node.some(hasArrayType);
|
|
51
|
+
if (!node || typeof node !== 'object') return false;
|
|
52
|
+
if (Array.isArray(node.type)) return true;
|
|
53
|
+
return Object.values(node).some(hasArrayType);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const packageJson = readJson('package.json');
|
|
58
|
+
const manifest = readJson('specs/openapi/manifest.json');
|
|
59
|
+
const requestSpec = readJson('specs/openapi/request-network-openapi.json');
|
|
60
|
+
const authSpec = readJson('specs/openapi/request-network-auth-openapi.json');
|
|
61
|
+
const requestMeta = readJson('specs/openapi/request-network-openapi.meta.json');
|
|
62
|
+
const authMeta = readJson('specs/openapi/request-network-auth-openapi.meta.json');
|
|
63
|
+
const webhookSpec = readJson('specs/webhooks/request-network-webhooks.json');
|
|
64
|
+
const webhookManifest = readJson('specs/webhooks/manifest.json');
|
|
65
|
+
|
|
66
|
+
assert(packageJson.version === manifest.contractsVersion, 'package and manifest versions differ');
|
|
67
|
+
assert(requestSpec.info.version === '0.31.0', 'unexpected Request API version');
|
|
68
|
+
assert(authSpec.info.version === '0.14.0', 'unexpected Auth API version');
|
|
69
|
+
assert(operationCount(requestSpec) === 82, 'Request API operation inventory must contain 82 operations');
|
|
70
|
+
assert(operationCount(authSpec) === 26, 'Auth API operation inventory must contain 26 operations');
|
|
71
|
+
assert(!requestSpec.paths['/v1/request/{paymentIntentId}/send'], 'obsolete v1 payment-intent send operation remains');
|
|
72
|
+
assert(!requestSpec.paths['/v2/request/payment-intents/{paymentIntentId}'], 'obsolete v2 payment-intent send operation remains');
|
|
73
|
+
assert(!hasArrayType(requestSpec) && !hasArrayType(authSpec), 'OpenAPI 3.0 documents contain unnormalized type arrays');
|
|
74
|
+
assert(requestSpec['x-contract-patches'].includes('runtime-fee-drift'), 'fee drift patch is not declared');
|
|
75
|
+
assert(requestSpec['x-contract-patches'].includes('secure-payment-response-shapes'), 'secure-payment response patch is not declared');
|
|
76
|
+
assert(authSpec['x-contract-patches'].includes('platform-webhook-client-id-auth'), 'Auth webhook security patch is not declared');
|
|
77
|
+
|
|
78
|
+
const createResponse = requestSpec.paths['/v2/secure-payments'].post.responses['201'].content['application/json'].schema;
|
|
79
|
+
for (const key of ['requestIds', 'securePaymentUrl', 'token']) {
|
|
80
|
+
assert(createResponse.required.includes(key), `secure-payment response does not require ${key}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const [endpoint, methods] of [
|
|
84
|
+
['/v1/webhook', ['get', 'post']],
|
|
85
|
+
['/v1/webhook/test', ['post']],
|
|
86
|
+
['/v1/webhook/{webhookId}', ['put', 'delete']],
|
|
87
|
+
]) {
|
|
88
|
+
for (const method of methods) {
|
|
89
|
+
const security = authSpec.paths[endpoint][method].security;
|
|
90
|
+
assert(security.some((item) => item.client_id), `${method.toUpperCase()} ${endpoint} lacks Client ID security`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const source of manifest.sources) {
|
|
95
|
+
const body = fs.readFileSync(path.join(root, 'specs/openapi', source.filename));
|
|
96
|
+
const digest = crypto.createHash('sha256').update(body).digest('hex');
|
|
97
|
+
const meta = source.id === 'request-api' ? requestMeta : authMeta;
|
|
98
|
+
assert(digest === source.normalizedSha256, `${source.id} normalized hash differs from manifest`);
|
|
99
|
+
assert(digest === meta.normalizedSha256, `${source.id} normalized hash differs from metadata`);
|
|
100
|
+
assert(source.runtimeBaseUrl === meta.runtimeBaseUrl, `${source.id} runtime host differs from metadata`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const [event, definition] of Object.entries(webhookManifest.currentEvents)) {
|
|
104
|
+
assert(webhookSpec.webhooks[event], `current webhook schema missing ${event}`);
|
|
105
|
+
const fixture = readJson(path.join('fixtures/webhooks', definition.fixture));
|
|
106
|
+
assert(fixture.event === event, `${definition.fixture} contains ${fixture.event}, expected ${event}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const fixtures of Object.values(webhookManifest.legacyEvents)) {
|
|
110
|
+
for (const fixture of Array.isArray(fixtures) ? fixtures : [fixtures]) {
|
|
111
|
+
assert(fs.existsSync(path.join(root, 'fixtures/webhooks', fixture)), `legacy fixture missing ${fixture}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
process.stdout.write('✔ contract inventories, patches, manifests, and webhook fixtures verified\n');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Contract verification failed:', error.message);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
package/specs/README.md
CHANGED
|
@@ -3,24 +3,28 @@
|
|
|
3
3
|
This folder holds the contracts consumed by the Request client SDKs.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
openapi/ #
|
|
6
|
+
openapi/ # normalized Request and Auth REST APIs plus provenance
|
|
7
7
|
webhooks/ # manually curated webhook schema
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## `openapi/`
|
|
11
11
|
- `request-network-openapi.json` – fetched via automation from the upstream
|
|
12
12
|
Request API.
|
|
13
|
-
- `request-network-openapi.
|
|
14
|
-
|
|
13
|
+
- `request-network-auth-openapi.json` – separately fetched Auth API contract.
|
|
14
|
+
- `*.meta.json` – source URL, runtime host, timestamp, raw/normalized hashes,
|
|
15
|
+
and patch statistics.
|
|
16
|
+
- `manifest.json` – release versions, operation counts, supported production
|
|
17
|
+
hosts, patches, and minimum compatible clients.
|
|
15
18
|
|
|
16
|
-
These files
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
when upstream changes land and commit the updated JSON + metadata.
|
|
19
|
+
These files change through `npm run sync:openapi` in this repository. Do not
|
|
20
|
+
merge the two APIs: their paths overlap while their hosts and credentials do
|
|
21
|
+
not.
|
|
20
22
|
|
|
21
23
|
## `webhooks/`
|
|
22
24
|
- `request-network-webhooks.json` – maintained manually. Update it when webhook
|
|
23
25
|
documentation or behaviour changes, and keep fixtures/tests in sync.
|
|
26
|
+
- `manifest.json` – classifies current versus legacy events and records
|
|
27
|
+
platform/orchestrator recipients.
|
|
24
28
|
|
|
25
29
|
This separation ensures automation never overwrites the manual webhook spec while
|
|
26
30
|
making it obvious which files are generated vs. curated.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contractsVersion": "0.7.0",
|
|
3
|
+
"fetchedAt": "2026-09-03T09:13:49.531Z",
|
|
4
|
+
"defaultEnvironment": "production",
|
|
5
|
+
"supportedRuntimeHosts": [
|
|
6
|
+
"https://api.request.network",
|
|
7
|
+
"https://auth.request.network"
|
|
8
|
+
],
|
|
9
|
+
"minimumCompatibleClients": {
|
|
10
|
+
"typescript": "0.7.0",
|
|
11
|
+
"php": "0.7.0"
|
|
12
|
+
},
|
|
13
|
+
"sources": [
|
|
14
|
+
{
|
|
15
|
+
"id": "request-api",
|
|
16
|
+
"filename": "request-network-openapi.json",
|
|
17
|
+
"sourceUrl": "https://api.request.network/open-api/openapi.json",
|
|
18
|
+
"runtimeBaseUrl": "https://api.request.network",
|
|
19
|
+
"openapi": "3.0.0",
|
|
20
|
+
"apiVersion": "0.31.0",
|
|
21
|
+
"operationCount": 82,
|
|
22
|
+
"rawSha256": "6b93f5b4a16cbc84e1bd1b65e4dbab804dbcdeb1ce623c125691f39e0e09bb3a",
|
|
23
|
+
"normalizedSha256": "284d42ef291c5ab83cb3328431112f82d847fb24228f9ca41a5b2f9272ca1ecd",
|
|
24
|
+
"patches": [
|
|
25
|
+
"oas30-nullable-unions",
|
|
26
|
+
"runtime-fee-drift",
|
|
27
|
+
"secure-payment-response-shapes"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "auth-api",
|
|
32
|
+
"filename": "request-network-auth-openapi.json",
|
|
33
|
+
"sourceUrl": "https://auth.request.network/open-api/openapi.json",
|
|
34
|
+
"runtimeBaseUrl": "https://auth.request.network",
|
|
35
|
+
"openapi": "3.0.0",
|
|
36
|
+
"apiVersion": "0.14.0",
|
|
37
|
+
"operationCount": 26,
|
|
38
|
+
"rawSha256": "9047ed549e768a44762c0d68785ebc495652d53127eaaded73452cebac1098f1",
|
|
39
|
+
"normalizedSha256": "b37039ced02dd198421280089642db409cdaeb2bbbf09a9753b6ad43043c39a1",
|
|
40
|
+
"patches": [
|
|
41
|
+
"oas30-nullable-unions",
|
|
42
|
+
"platform-webhook-client-id-auth"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|