@riseworks/riseworks-sdk 1.0.4
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 +240 -0
- package/ai-skills/README.md +28 -0
- package/ai-skills/STANDARDS.md +66 -0
- package/ai-skills/references/API_WORKFLOWS.md +200 -0
- package/ai-skills/references/TYPES.md +190 -0
- package/ai-skills/rise-auth-and-setup/README.md +14 -0
- package/ai-skills/rise-auth-and-setup/SKILL.md +57 -0
- package/ai-skills/rise-auth-and-setup/metadata.json +12 -0
- package/ai-skills/rise-auth-and-setup/references/SETUP_PATTERNS.md +26 -0
- package/ai-skills/rise-debugging-and-errors/SKILL.md +51 -0
- package/ai-skills/rise-payments-workflows/README.md +19 -0
- package/ai-skills/rise-payments-workflows/SKILL.md +109 -0
- package/ai-skills/rise-payments-workflows/metadata.json +12 -0
- package/ai-skills/rise-payments-workflows/references/PAYMENT_PATTERNS.md +36 -0
- package/ai-skills/rise-sdk-integration/README.md +25 -0
- package/ai-skills/rise-sdk-integration/SKILL.md +130 -0
- package/ai-skills/rise-sdk-integration/metadata.json +12 -0
- package/ai-skills/rise-sdk-integration/references/PATTERNS.md +28 -0
- package/ai-skills/rise-security-and-approvals/SKILL.md +33 -0
- package/ai-skills/rise-teams-and-invites/README.md +13 -0
- package/ai-skills/rise-teams-and-invites/SKILL.md +66 -0
- package/ai-skills/rise-teams-and-invites/metadata.json +12 -0
- package/ai-skills/rise-teams-and-invites/references/TEAM_PATTERNS.md +25 -0
- package/ai-skills/rise-v1-migration/README.md +22 -0
- package/ai-skills/rise-v1-migration/SKILL.md +184 -0
- package/ai-skills/rise-v1-migration/metadata.json +13 -0
- package/ai-skills/rise-v1-migration/references/ENDPOINT_MAPPINGS.md +72 -0
- package/ai-skills/rise-v1-migration/references/MIGRATION_PATTERNS.md +66 -0
- package/ai-skills/rise-webhooks/README.md +41 -0
- package/ai-skills/rise-webhooks/SKILL.md +143 -0
- package/ai-skills/rise-webhooks/metadata.json +13 -0
- package/ai-skills/rise-webhooks/references/HANDLER.md +60 -0
- package/ai-skills/rise-webhooks/references/REGISTRATION.md +113 -0
- package/dist/index.cjs +1406 -0
- package/dist/index.d.cts +6865 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +6865 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1383 -0
- package/package.json +87 -0
- package/scripts/add-skills.mjs +158 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('node:crypto');
|
|
4
|
+
var ethers = require('ethers');
|
|
5
|
+
|
|
6
|
+
function _interopNamespaceDefault(e) {
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
|
|
24
|
+
|
|
25
|
+
const PRIVATE_KEY_LENGTH = 66;
|
|
26
|
+
const RISE_ID_LENGTH = 42;
|
|
27
|
+
const RISE_API_URLS = {
|
|
28
|
+
dev: "https://b2b-api.dev-riseworks.io",
|
|
29
|
+
stg: "https://integrations-api.staging-riseworks.io",
|
|
30
|
+
prod: "https://integrations-api.riseworks.io"
|
|
31
|
+
};
|
|
32
|
+
const EXPECTED_CHAIN_IDS = {
|
|
33
|
+
dev: [421614],
|
|
34
|
+
stg: [421614],
|
|
35
|
+
prod: [42161]
|
|
36
|
+
};
|
|
37
|
+
const CENTS_TO_TOKEN_UNITS = 10000n;
|
|
38
|
+
const generateIdempotencyKey = () => crypto.randomUUID();
|
|
39
|
+
const apiPath = (strings, ...values) => strings.reduce((path, part, i) => {
|
|
40
|
+
if (i >= values.length) return `${path}${part}`;
|
|
41
|
+
const value = String(values[i]);
|
|
42
|
+
if (value === "" || value === "." || value === "..") {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Invalid path parameter ${JSON.stringify(value)}: expected a non-empty id`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return `${path}${part}${encodeURIComponent(value)}`;
|
|
48
|
+
}, "");
|
|
49
|
+
class ApiGroup {
|
|
50
|
+
client;
|
|
51
|
+
constructor(client) {
|
|
52
|
+
this.client = client;
|
|
53
|
+
}
|
|
54
|
+
request(method, path, data, query, extraHeaders) {
|
|
55
|
+
return this.client.request(method, path, data, query, extraHeaders);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class WebhooksApi extends ApiGroup {
|
|
59
|
+
/**
|
|
60
|
+
* Register a new webhook endpoint to receive real-time notifications
|
|
61
|
+
*
|
|
62
|
+
* @param data - Webhook registration data including URL, events, and secret
|
|
63
|
+
* @returns Promise resolving to the registered webhook response
|
|
64
|
+
* @throws Error if registration fails or validation errors occur
|
|
65
|
+
*/
|
|
66
|
+
async register(data) {
|
|
67
|
+
return this.request(
|
|
68
|
+
"POST",
|
|
69
|
+
"/v2/webhooks/register",
|
|
70
|
+
data
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Update an existing webhook endpoint configuration
|
|
75
|
+
*
|
|
76
|
+
* @param webhookNanoid - Unique identifier of the webhook to update
|
|
77
|
+
* @param data - Updated webhook configuration data
|
|
78
|
+
* @returns Promise resolving to the updated webhook response
|
|
79
|
+
* @throws Error if update fails or webhook not found
|
|
80
|
+
*/
|
|
81
|
+
async update(webhookNanoid, data) {
|
|
82
|
+
return this.request(
|
|
83
|
+
"PUT",
|
|
84
|
+
apiPath`/v2/webhooks/${webhookNanoid}`,
|
|
85
|
+
data
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Retrieve details of a specific webhook endpoint
|
|
90
|
+
*
|
|
91
|
+
* @param webhookNanoid - Unique identifier of the webhook to retrieve
|
|
92
|
+
* @returns Promise resolving to the webhook details
|
|
93
|
+
* @throws Error if webhook not found or access denied
|
|
94
|
+
*/
|
|
95
|
+
async get(webhookNanoid) {
|
|
96
|
+
return this.request(
|
|
97
|
+
"GET",
|
|
98
|
+
apiPath`/v2/webhooks/${webhookNanoid}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* List all webhook endpoints for the authenticated user or organization
|
|
103
|
+
*
|
|
104
|
+
* @param params - Query parameters for filtering and pagination
|
|
105
|
+
* @returns Promise resolving to the list of webhook endpoints
|
|
106
|
+
* @throws Error if listing fails or access denied
|
|
107
|
+
*/
|
|
108
|
+
async list(params) {
|
|
109
|
+
return this.request(
|
|
110
|
+
"GET",
|
|
111
|
+
"/v2/webhooks",
|
|
112
|
+
void 0,
|
|
113
|
+
params
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Send a test event to a webhook endpoint to verify connectivity
|
|
118
|
+
*
|
|
119
|
+
* @param webhookNanoid - Unique identifier of the webhook to test
|
|
120
|
+
* @param data - Test event data to send
|
|
121
|
+
* @returns Promise resolving to the test delivery response
|
|
122
|
+
* @throws Error if test fails or webhook not found
|
|
123
|
+
*/
|
|
124
|
+
async test(webhookNanoid, data) {
|
|
125
|
+
return this.request(
|
|
126
|
+
"POST",
|
|
127
|
+
apiPath`/v2/webhooks/test/${webhookNanoid}`,
|
|
128
|
+
data
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Retry a failed webhook delivery attempt
|
|
133
|
+
*
|
|
134
|
+
* @param deliveryNanoid - Unique identifier of the failed delivery to retry
|
|
135
|
+
* @returns Promise resolving to the retry delivery response
|
|
136
|
+
* @throws Error if retry fails or delivery not found
|
|
137
|
+
*/
|
|
138
|
+
async retryDelivery(deliveryNanoid) {
|
|
139
|
+
return this.request(
|
|
140
|
+
"POST",
|
|
141
|
+
apiPath`/v2/webhooks/retry/${deliveryNanoid}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Retrieve delivery history for a specific webhook endpoint
|
|
146
|
+
*
|
|
147
|
+
* @param webhookNanoid - Unique identifier of the webhook
|
|
148
|
+
* @param params - Query parameters for filtering and pagination
|
|
149
|
+
* @returns Promise resolving to the delivery history
|
|
150
|
+
* @throws Error if history retrieval fails or webhook not found
|
|
151
|
+
*/
|
|
152
|
+
async getDeliveryHistory(webhookNanoid, params) {
|
|
153
|
+
return this.request(
|
|
154
|
+
"GET",
|
|
155
|
+
apiPath`/v2/webhooks/${webhookNanoid}/deliveries`,
|
|
156
|
+
void 0,
|
|
157
|
+
params
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Retrieve a single webhook event (the original event payload Rise emitted)
|
|
162
|
+
* for a specific endpoint. Pair with {@link getDeliveryHistory} to inspect the
|
|
163
|
+
* delivery attempts for the same event.
|
|
164
|
+
*
|
|
165
|
+
* @param params - Webhook endpoint nanoid and the event nanoid to fetch
|
|
166
|
+
* @returns Promise resolving to the webhook event detail
|
|
167
|
+
* @throws Error if the event or endpoint is not found or access is denied
|
|
168
|
+
*/
|
|
169
|
+
async getEvent(params) {
|
|
170
|
+
return this.request(
|
|
171
|
+
"GET",
|
|
172
|
+
apiPath`/v2/webhooks/${params.webhook_nanoid}/events/${params.event_nanoid}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
class AuthApi extends ApiGroup {
|
|
177
|
+
/**
|
|
178
|
+
* Get a Sign-In with Ethereum (SIWE) message for wallet authentication
|
|
179
|
+
*
|
|
180
|
+
* @param params - Authentication parameters including wallet address and Rise ID
|
|
181
|
+
* @returns Promise resolving to the SIWE message for signing
|
|
182
|
+
* @throws Error if message generation fails or invalid parameters
|
|
183
|
+
*/
|
|
184
|
+
async getSiwe(params) {
|
|
185
|
+
return this.request(
|
|
186
|
+
"GET",
|
|
187
|
+
"/v2/auth/siwe",
|
|
188
|
+
void 0,
|
|
189
|
+
params
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Verify a signed SIWE message to authenticate the user
|
|
194
|
+
*
|
|
195
|
+
* @param data - Verification data including the signed message and signature
|
|
196
|
+
* @returns Promise resolving to the authentication response with JWT token
|
|
197
|
+
* @throws Error if verification fails or signature is invalid
|
|
198
|
+
*/
|
|
199
|
+
async verifySiwe(data) {
|
|
200
|
+
return this.request("POST", "/v2/auth/verify", data);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
class CompanyApi extends ApiGroup {
|
|
204
|
+
/**
|
|
205
|
+
* Retrieve all users associated with a specific company
|
|
206
|
+
*
|
|
207
|
+
* @param params - Company parameters including company nanoid
|
|
208
|
+
* @returns Promise resolving to the list of company users
|
|
209
|
+
* @throws Error if retrieval fails or company not found
|
|
210
|
+
*/
|
|
211
|
+
async getUsers(params) {
|
|
212
|
+
return this.request(
|
|
213
|
+
"GET",
|
|
214
|
+
apiPath`/v2/company/${params.nanoid}/users`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Retrieve detailed information about a company/organization
|
|
219
|
+
*
|
|
220
|
+
* @param params - Company parameters including company nanoid
|
|
221
|
+
* @returns Promise resolving to the organization details
|
|
222
|
+
* @throws Error if retrieval fails or company not found
|
|
223
|
+
*/
|
|
224
|
+
async getDetails(params) {
|
|
225
|
+
return this.request(
|
|
226
|
+
"GET",
|
|
227
|
+
apiPath`/v2/company/${params.nanoid}/details`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Retrieve the current settings and configuration for a company
|
|
232
|
+
*
|
|
233
|
+
* @param params - Company parameters including company nanoid
|
|
234
|
+
* @returns Promise resolving to the company settings
|
|
235
|
+
* @throws Error if retrieval fails or company not found
|
|
236
|
+
*/
|
|
237
|
+
async getSettings(params) {
|
|
238
|
+
return this.request(
|
|
239
|
+
"GET",
|
|
240
|
+
apiPath`/v2/company/${params.nanoid}/settings`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Retrieve role-specific settings for a user within a company
|
|
245
|
+
*
|
|
246
|
+
* @param params - Company and user parameters
|
|
247
|
+
* @returns Promise resolving to the role settings
|
|
248
|
+
* @throws Error if retrieval fails or user/company not found
|
|
249
|
+
*/
|
|
250
|
+
async getRoleSettings(params) {
|
|
251
|
+
return this.request(
|
|
252
|
+
"GET",
|
|
253
|
+
apiPath`/v2/company/${params.nanoid}/members/${params.user_nanoid}/settings`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Retrieve contact information for a company/organization
|
|
258
|
+
*
|
|
259
|
+
* @param params - Company parameters including company nanoid
|
|
260
|
+
* @returns Promise resolving to the organization contacts
|
|
261
|
+
* @throws Error if retrieval fails or company not found
|
|
262
|
+
*/
|
|
263
|
+
async getContacts(params) {
|
|
264
|
+
return this.request(
|
|
265
|
+
"GET",
|
|
266
|
+
apiPath`/v2/company/${params.nanoid}/contacts`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Retrieve ownership information for a company/organization
|
|
271
|
+
*
|
|
272
|
+
* @param params - Company parameters including company nanoid
|
|
273
|
+
* @returns Promise resolving to the organization ownership details
|
|
274
|
+
* @throws Error if retrieval fails or company not found
|
|
275
|
+
*/
|
|
276
|
+
async getOwnership(params) {
|
|
277
|
+
return this.request(
|
|
278
|
+
"GET",
|
|
279
|
+
apiPath`/v2/company/${params.nanoid}/ownership`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
class EntityBalanceApi extends ApiGroup {
|
|
284
|
+
/**
|
|
285
|
+
* Retrieve the current balance for an entity (company, team, or user)
|
|
286
|
+
*
|
|
287
|
+
* @param params - Balance request parameters including entity identifier
|
|
288
|
+
* @returns Promise resolving to the entity balance information
|
|
289
|
+
* @throws Error if retrieval fails or entity not found
|
|
290
|
+
*/
|
|
291
|
+
async get(params) {
|
|
292
|
+
return this.request(
|
|
293
|
+
"GET",
|
|
294
|
+
"/v2/balance",
|
|
295
|
+
void 0,
|
|
296
|
+
params
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
class InvitesApi extends ApiGroup {
|
|
301
|
+
/**
|
|
302
|
+
* Send invitations to team members via email
|
|
303
|
+
*
|
|
304
|
+
* @param data - Invitation data including emails and team information
|
|
305
|
+
* @returns Promise resolving to the invitation response
|
|
306
|
+
* @throws Error if invitation fails or validation errors occur
|
|
307
|
+
*/
|
|
308
|
+
async send(data) {
|
|
309
|
+
return this.request("POST", "/v2/invites", data);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get typed data for manager invite signing (manual flow)
|
|
313
|
+
*
|
|
314
|
+
* @param data - Manager invite request data
|
|
315
|
+
* @returns Promise resolving to the typed data for manual signing
|
|
316
|
+
* @throws Error if typed data generation fails or validation errors occur
|
|
317
|
+
*/
|
|
318
|
+
async getManagerInviteTypedData(data) {
|
|
319
|
+
return this.request(
|
|
320
|
+
"POST",
|
|
321
|
+
"/v2/invites/manager",
|
|
322
|
+
data
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Send manager invite and sign
|
|
327
|
+
*/
|
|
328
|
+
async sendManagerInvite(data) {
|
|
329
|
+
const { privateKey, ...inviteData } = data;
|
|
330
|
+
const createResponse = await this.getManagerInviteTypedData(inviteData);
|
|
331
|
+
const { signer, signature } = await this.client.signTypedData(
|
|
332
|
+
createResponse.data.typed_data.domain,
|
|
333
|
+
createResponse.data.typed_data.types,
|
|
334
|
+
createResponse.data.typed_data.typed_data,
|
|
335
|
+
privateKey
|
|
336
|
+
);
|
|
337
|
+
const executeData = {
|
|
338
|
+
invites: createResponse.data.invites,
|
|
339
|
+
signer,
|
|
340
|
+
typed_data: createResponse.data.typed_data.typed_data,
|
|
341
|
+
signature
|
|
342
|
+
};
|
|
343
|
+
return this.executeManagerInviteWithSignedData(executeData);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Execute manager invite with pre-signed data (manual flow)
|
|
347
|
+
*
|
|
348
|
+
* @param data - Signed manager invite data including signature
|
|
349
|
+
* @returns Promise resolving to the executed manager invite response
|
|
350
|
+
* @throws Error if execution fails or signature is invalid
|
|
351
|
+
*/
|
|
352
|
+
async executeManagerInviteWithSignedData(data) {
|
|
353
|
+
return this.request(
|
|
354
|
+
"PUT",
|
|
355
|
+
"/v2/invites/manager",
|
|
356
|
+
data
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Retrieve all invitations for a company or team
|
|
361
|
+
*
|
|
362
|
+
* @param params - Query parameters for filtering and pagination
|
|
363
|
+
* @returns Promise resolving to the list of invitations
|
|
364
|
+
* @throws Error if retrieval fails or access denied
|
|
365
|
+
*/
|
|
366
|
+
async get(params) {
|
|
367
|
+
return this.request(
|
|
368
|
+
"GET",
|
|
369
|
+
"/v2/invites",
|
|
370
|
+
void 0,
|
|
371
|
+
params
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
class MeApi extends ApiGroup {
|
|
376
|
+
/**
|
|
377
|
+
* Retrieve information about the currently authenticated user
|
|
378
|
+
*
|
|
379
|
+
* @returns Promise resolving to the current user's information
|
|
380
|
+
* @throws Error if retrieval fails or authentication invalid
|
|
381
|
+
*/
|
|
382
|
+
async get() {
|
|
383
|
+
return this.request("GET", "/v2/me");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const assertPaymentTypedDataMatchesRequest = (toSign, requested, expectedChainIds) => {
|
|
387
|
+
const chainId = toSign.domain?.chainId;
|
|
388
|
+
if (expectedChainIds && typeof chainId === "number" && !expectedChainIds.includes(chainId)) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Refusing to sign: typed data chainId ${chainId} is not one of the expected chain ids [${expectedChainIds.join(", ")}] for this environment`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const signedFields = toSign.typed_data.data;
|
|
394
|
+
if (!signedFields) return;
|
|
395
|
+
if (typeof signedFields.paymentsCount === "string") {
|
|
396
|
+
const count = Number(signedFields.paymentsCount);
|
|
397
|
+
if (!Number.isInteger(count) || count > requested.to.length) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`Refusing to sign: typed data authorizes ${signedFields.paymentsCount} payments but only ${requested.to.length} were requested`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (typeof signedFields.maxAmountPerPayment === "string") {
|
|
404
|
+
const maxRequestedCents = requested.to.reduce(
|
|
405
|
+
(max, recipient) => recipient.amount_cents > max ? recipient.amount_cents : max,
|
|
406
|
+
0
|
|
407
|
+
);
|
|
408
|
+
if (BigInt(signedFields.maxAmountPerPayment) > BigInt(maxRequestedCents) * CENTS_TO_TOKEN_UNITS) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Refusing to sign: typed data authorizes up to ${signedFields.maxAmountPerPayment} token units per payment, above the requested maximum of ${maxRequestedCents} cents`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
class PaymentsApi extends ApiGroup {
|
|
416
|
+
/**
|
|
417
|
+
* Retrieve a list of payments with optional filtering and pagination
|
|
418
|
+
*
|
|
419
|
+
* @param params - Query parameters for filtering and pagination
|
|
420
|
+
* @returns Promise resolving to the list of payments
|
|
421
|
+
* @throws Error if retrieval fails or access denied
|
|
422
|
+
*/
|
|
423
|
+
async get(params) {
|
|
424
|
+
return this.request(
|
|
425
|
+
"GET",
|
|
426
|
+
"/v2/payments",
|
|
427
|
+
void 0,
|
|
428
|
+
params
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Get typed data for payment signing (manual flow)
|
|
433
|
+
*
|
|
434
|
+
* @param data - Payment request data
|
|
435
|
+
* @param idempotencyKey - Optional idempotency key sent as `x-idempotency-key`.
|
|
436
|
+
* When omitted, the transport generates a fresh one per call. Use a
|
|
437
|
+
* DIFFERENT key for the matching `executePaymentWithSignedData` call — the
|
|
438
|
+
* server takes the payment group id from the signed data, not the header, so
|
|
439
|
+
* the two calls must not share a key (`sendPayment` handles this for you).
|
|
440
|
+
* @returns Promise resolving to the typed data for manual signing
|
|
441
|
+
* @throws Error if typed data generation fails or validation errors occur
|
|
442
|
+
*/
|
|
443
|
+
async getPaymentTypedData(data, idempotencyKey) {
|
|
444
|
+
return this.request(
|
|
445
|
+
"POST",
|
|
446
|
+
"/v2/payments",
|
|
447
|
+
data,
|
|
448
|
+
void 0,
|
|
449
|
+
idempotencyKey ? { "x-idempotency-key": idempotencyKey } : void 0
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Create and send payment with signature.
|
|
454
|
+
*
|
|
455
|
+
* Before signing, the server-provided typed data is checked against what the
|
|
456
|
+
* caller asked for (chain id, payment count, max per-payment amount) — the
|
|
457
|
+
* SDK refuses to sign an authorization broader than the request. The result
|
|
458
|
+
* carries the prepare step's `failed_payments` and `duplicates` so silently
|
|
459
|
+
* skipped or repeated recipients are visible to the caller.
|
|
460
|
+
*/
|
|
461
|
+
async sendPayment(data) {
|
|
462
|
+
const { privateKey, idempotencyKey, ...paymentData } = data;
|
|
463
|
+
const createResponse = await this.getPaymentTypedData(
|
|
464
|
+
paymentData,
|
|
465
|
+
idempotencyKey
|
|
466
|
+
);
|
|
467
|
+
assertPaymentTypedDataMatchesRequest(
|
|
468
|
+
createResponse.data.to_sign,
|
|
469
|
+
paymentData,
|
|
470
|
+
this.client.getExpectedChainIds()
|
|
471
|
+
);
|
|
472
|
+
const { signer, signature } = await this.client.signTypedData(
|
|
473
|
+
createResponse.data.to_sign.domain,
|
|
474
|
+
createResponse.data.to_sign.types,
|
|
475
|
+
createResponse.data.to_sign.typed_data,
|
|
476
|
+
privateKey
|
|
477
|
+
);
|
|
478
|
+
const executeData = {
|
|
479
|
+
...paymentData,
|
|
480
|
+
signer,
|
|
481
|
+
typed_data: createResponse.data.to_sign.typed_data,
|
|
482
|
+
signature,
|
|
483
|
+
payment_data: createResponse.data.payment_data
|
|
484
|
+
};
|
|
485
|
+
const executeResponse = await this.executePaymentWithSignedData(executeData);
|
|
486
|
+
return {
|
|
487
|
+
...executeResponse,
|
|
488
|
+
failed_payments: createResponse.data.failed_payments,
|
|
489
|
+
duplicates: createResponse.data.duplicates
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Execute payment with pre-signed data (manual flow)
|
|
494
|
+
*
|
|
495
|
+
* @param data - Signed payment data including signature
|
|
496
|
+
* @param idempotencyKey - Optional idempotency key sent as `x-idempotency-key`.
|
|
497
|
+
* When omitted, the transport generates a fresh one per call. Use a fresh
|
|
498
|
+
* key here (not the one from `getPaymentTypedData`) — the server takes the
|
|
499
|
+
* payment group id from the signed data, so reusing the prepare key only
|
|
500
|
+
* collides with the request-dedup window.
|
|
501
|
+
* @returns Promise resolving to the executed payment response
|
|
502
|
+
* @throws Error if execution fails or signature is invalid
|
|
503
|
+
*/
|
|
504
|
+
async executePaymentWithSignedData(data, idempotencyKey) {
|
|
505
|
+
return this.request(
|
|
506
|
+
"PUT",
|
|
507
|
+
"/v2/payments",
|
|
508
|
+
data,
|
|
509
|
+
void 0,
|
|
510
|
+
idempotencyKey ? { "x-idempotency-key": idempotencyKey } : void 0
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
class PayrollApi extends ApiGroup {
|
|
515
|
+
/**
|
|
516
|
+
* Enable payroll functionality for a specific team
|
|
517
|
+
*
|
|
518
|
+
* @param data - Payroll enablement configuration data
|
|
519
|
+
* @returns Promise resolving to the payroll enablement response
|
|
520
|
+
* @throws Error if enablement fails or team not found
|
|
521
|
+
*/
|
|
522
|
+
async enable(params, data) {
|
|
523
|
+
return this.request(
|
|
524
|
+
"POST",
|
|
525
|
+
apiPath`/v2/payroll/team/${params.team_nanoid}`,
|
|
526
|
+
data
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Retrieve payroll information for a specific team
|
|
531
|
+
*
|
|
532
|
+
* @param params - Team payroll parameters including team nanoid
|
|
533
|
+
* @returns Promise resolving to the team payroll information
|
|
534
|
+
* @throws Error if retrieval fails or team not found
|
|
535
|
+
*/
|
|
536
|
+
async getTeamPayroll(params) {
|
|
537
|
+
return this.request(
|
|
538
|
+
"GET",
|
|
539
|
+
apiPath`/v2/payroll/team/${params.team_nanoid}`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
class TeamApi extends ApiGroup {
|
|
544
|
+
}
|
|
545
|
+
class UserApi extends ApiGroup {
|
|
546
|
+
/**
|
|
547
|
+
* Update the address information for the current user
|
|
548
|
+
*
|
|
549
|
+
* @param data - Updated user address information
|
|
550
|
+
* @returns Promise that resolves when update is complete
|
|
551
|
+
* @throws Error if update fails or validation errors occur
|
|
552
|
+
*/
|
|
553
|
+
async updateAddress(data) {
|
|
554
|
+
return this.request("PUT", "/v2/user/address", data);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Update the avatar/profile picture for the current user
|
|
558
|
+
*
|
|
559
|
+
* @param data - Avatar update data including image file
|
|
560
|
+
* @returns Promise resolving to the avatar update response
|
|
561
|
+
* @throws Error if update fails or file validation errors occur
|
|
562
|
+
*/
|
|
563
|
+
async updateAvatar(data) {
|
|
564
|
+
return this.request(
|
|
565
|
+
"POST",
|
|
566
|
+
"/v2/user/avatar/update",
|
|
567
|
+
data
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Retrieve all companies associated with the current user
|
|
572
|
+
*
|
|
573
|
+
* @returns Promise resolving to the list of user's companies
|
|
574
|
+
* @throws Error if retrieval fails or authentication invalid
|
|
575
|
+
*/
|
|
576
|
+
async getCompanies() {
|
|
577
|
+
return this.request("GET", "/v2/user/organizations");
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Retrieve all teams associated with the current user
|
|
581
|
+
*
|
|
582
|
+
* @returns Promise resolving to the list of user's teams
|
|
583
|
+
* @throws Error if retrieval fails or authentication invalid
|
|
584
|
+
*/
|
|
585
|
+
async getTeams() {
|
|
586
|
+
return this.request("GET", "/v2/user/teams");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
let RiseApiClient$1 = class RiseApiClient {
|
|
590
|
+
baseUrl;
|
|
591
|
+
jwtToken;
|
|
592
|
+
riseId;
|
|
593
|
+
privateKey;
|
|
594
|
+
timeout;
|
|
595
|
+
headers;
|
|
596
|
+
isGeneratingJwt = false;
|
|
597
|
+
// API Groups
|
|
598
|
+
webhooks;
|
|
599
|
+
auth;
|
|
600
|
+
company;
|
|
601
|
+
entityBalance;
|
|
602
|
+
invites;
|
|
603
|
+
me;
|
|
604
|
+
payments;
|
|
605
|
+
payroll;
|
|
606
|
+
team;
|
|
607
|
+
user;
|
|
608
|
+
constructor(options) {
|
|
609
|
+
if (!(options.jwtToken || options.riseIdAuth)) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
"Either JWT token or riseIdAuth configuration is required for authentication"
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (options.riseIdAuth) {
|
|
615
|
+
if (!(options.riseIdAuth.riseId && options.riseIdAuth.privateKey)) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
"riseIdAuth configuration must include both riseId and privateKey"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (!options.riseIdAuth.riseId.startsWith("0x") || options.riseIdAuth.riseId.length !== RISE_ID_LENGTH) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
"Rise ID should be a valid hex address starting with 0x and exactly 42 characters long"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (!options.riseIdAuth.privateKey.startsWith("0x") || options.riseIdAuth.privateKey.length !== PRIVATE_KEY_LENGTH) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
"Private key should be a valid hex string starting with 0x and 64 characters long"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (options.baseUrl) {
|
|
632
|
+
const isHttps = options.baseUrl.startsWith("https://");
|
|
633
|
+
const isLocalhost = /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(options.baseUrl);
|
|
634
|
+
if (!(isHttps || isLocalhost)) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`baseUrl must use https:// (got "${options.baseUrl}"). Plaintext http:// is only allowed for localhost.`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
this.baseUrl = options.baseUrl;
|
|
640
|
+
} else {
|
|
641
|
+
const environment = options.environment ?? "prod";
|
|
642
|
+
this.baseUrl = RISE_API_URLS[environment];
|
|
643
|
+
}
|
|
644
|
+
this.jwtToken = options.jwtToken ?? "";
|
|
645
|
+
this.riseId = options.riseIdAuth?.riseId ?? "";
|
|
646
|
+
this.privateKey = options.riseIdAuth?.privateKey ?? "";
|
|
647
|
+
this.timeout = options.timeout ?? 3e4;
|
|
648
|
+
const authHeader = this.jwtToken ? `Bearer ${this.jwtToken}` : "";
|
|
649
|
+
this.headers = {
|
|
650
|
+
"Content-Type": "application/json",
|
|
651
|
+
Authorization: authHeader,
|
|
652
|
+
...options.headers
|
|
653
|
+
};
|
|
654
|
+
this.webhooks = new WebhooksApi(this);
|
|
655
|
+
this.auth = new AuthApi(this);
|
|
656
|
+
this.company = new CompanyApi(this);
|
|
657
|
+
this.entityBalance = new EntityBalanceApi(this);
|
|
658
|
+
this.invites = new InvitesApi(this);
|
|
659
|
+
this.me = new MeApi(this);
|
|
660
|
+
this.payments = new PaymentsApi(this);
|
|
661
|
+
this.payroll = new PayrollApi(this);
|
|
662
|
+
this.team = new TeamApi(this);
|
|
663
|
+
this.user = new UserApi(this);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Update the JWT token for authentication
|
|
667
|
+
*
|
|
668
|
+
* @param newToken - New JWT token to use for authentication
|
|
669
|
+
* @throws Error if the token is empty or invalid
|
|
670
|
+
*/
|
|
671
|
+
updateToken(newToken) {
|
|
672
|
+
if (!newToken) {
|
|
673
|
+
throw new Error("JWT token cannot be empty");
|
|
674
|
+
}
|
|
675
|
+
this.jwtToken = newToken;
|
|
676
|
+
this.headers.Authorization = `Bearer ${this.jwtToken}`;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Get the current JWT token
|
|
680
|
+
*
|
|
681
|
+
* @returns The current JWT token string
|
|
682
|
+
*/
|
|
683
|
+
getToken() {
|
|
684
|
+
return this.jwtToken;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Check if the client is using JWT token authentication
|
|
688
|
+
*
|
|
689
|
+
* @returns True if using JWT token, false otherwise
|
|
690
|
+
*/
|
|
691
|
+
isUsingJwtToken() {
|
|
692
|
+
return !!this.jwtToken;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Check if the client is using Rise ID and private key authentication
|
|
696
|
+
*
|
|
697
|
+
* @returns True if using Rise ID auth, false otherwise
|
|
698
|
+
*/
|
|
699
|
+
isUsingRiseIdAuth() {
|
|
700
|
+
return !!(this.riseId && this.privateKey);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Get the Rise ID used for authentication
|
|
704
|
+
*
|
|
705
|
+
* @returns The Rise ID string
|
|
706
|
+
*/
|
|
707
|
+
getRiseId() {
|
|
708
|
+
return this.riseId;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Resolve the signing key (per-call override or the configured key) or throw.
|
|
712
|
+
* Private on purpose — the raw key is never handed out; callers sign via
|
|
713
|
+
* {@link signTypedData} / {@link signMessage} and receive only the signature.
|
|
714
|
+
*/
|
|
715
|
+
resolveSigningKey(overrideKey) {
|
|
716
|
+
const key = overrideKey ?? this.privateKey;
|
|
717
|
+
if (!key) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
"A private key is required to sign. Provide privateKey in the request or configure riseIdAuth when creating the client."
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
return key;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Sign EIP-712 typed data with the configured key (or a per-call override).
|
|
726
|
+
* The key is used in-process and never returned, so it cannot be exfiltrated.
|
|
727
|
+
*
|
|
728
|
+
* @returns The signer address and the signature
|
|
729
|
+
*/
|
|
730
|
+
async signTypedData(domain, types, value, overrideKey) {
|
|
731
|
+
const wallet = new ethers.ethers.Wallet(this.resolveSigningKey(overrideKey));
|
|
732
|
+
const signature = await wallet.signTypedData(domain, types, value);
|
|
733
|
+
return { signer: wallet.address, signature };
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Sign a plain message with the configured key (or a per-call override).
|
|
737
|
+
*
|
|
738
|
+
* @returns The signer address and the signature
|
|
739
|
+
*/
|
|
740
|
+
async signMessage(message, overrideKey) {
|
|
741
|
+
const wallet = new ethers.ethers.Wallet(this.resolveSigningKey(overrideKey));
|
|
742
|
+
const signature = await wallet.signMessage(message);
|
|
743
|
+
return { signer: wallet.address, signature };
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Build an error for a failed API response. The status line goes in the
|
|
747
|
+
* message; the raw response body is attached as a NON-enumerable `responseBody`
|
|
748
|
+
* so it can be inspected deliberately but does not leak into
|
|
749
|
+
* JSON.stringify(error) or structured loggers that serialize error fields.
|
|
750
|
+
*/
|
|
751
|
+
buildApiError(status, statusText, body) {
|
|
752
|
+
const error = new Error(`API request failed: ${status} ${statusText}`);
|
|
753
|
+
Object.defineProperty(error, "status", {
|
|
754
|
+
value: status,
|
|
755
|
+
enumerable: false
|
|
756
|
+
});
|
|
757
|
+
Object.defineProperty(error, "responseBody", {
|
|
758
|
+
value: body,
|
|
759
|
+
enumerable: false
|
|
760
|
+
});
|
|
761
|
+
return error;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Manually refresh the JWT token using Rise ID and private key
|
|
765
|
+
*
|
|
766
|
+
* @returns Promise resolving to the new JWT token
|
|
767
|
+
* @throws Error if Rise ID or private key not provided
|
|
768
|
+
*/
|
|
769
|
+
async refreshJwtToken() {
|
|
770
|
+
if (!(this.privateKey && this.riseId)) {
|
|
771
|
+
throw new Error("Rise ID and private key are required for JWT refresh");
|
|
772
|
+
}
|
|
773
|
+
return await this.generateJwtToken();
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Hostname of the configured base URL, or '' when it does not parse.
|
|
777
|
+
* Environment/chain checks compare hostnames, never URL substrings — a
|
|
778
|
+
* substring like 'riseworks.io' also matches attacker hosts such as
|
|
779
|
+
* evil.example/riseworks.io.
|
|
780
|
+
*/
|
|
781
|
+
getBaseHostname() {
|
|
782
|
+
try {
|
|
783
|
+
return new URL(this.baseUrl).hostname;
|
|
784
|
+
} catch {
|
|
785
|
+
return "";
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Get the current API environment
|
|
790
|
+
*
|
|
791
|
+
* @returns The current environment ('dev', 'stg', or 'prod')
|
|
792
|
+
*/
|
|
793
|
+
getEnvironment() {
|
|
794
|
+
if (this.baseUrl === RISE_API_URLS.dev) return "dev";
|
|
795
|
+
if (this.baseUrl === RISE_API_URLS.stg) return "stg";
|
|
796
|
+
if (this.baseUrl === RISE_API_URLS.prod) return "prod";
|
|
797
|
+
const hostname = this.getBaseHostname();
|
|
798
|
+
if (hostname === "dev-riseworks.io" || hostname.endsWith(".dev-riseworks.io")) {
|
|
799
|
+
return "dev";
|
|
800
|
+
}
|
|
801
|
+
if (hostname === "staging-riseworks.io" || hostname.endsWith(".staging-riseworks.io")) {
|
|
802
|
+
return "stg";
|
|
803
|
+
}
|
|
804
|
+
return "prod";
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Update the API environment and base URL
|
|
808
|
+
*
|
|
809
|
+
* @param environment - New environment to switch to
|
|
810
|
+
*/
|
|
811
|
+
updateEnvironment(environment) {
|
|
812
|
+
this.baseUrl = RISE_API_URLS[environment];
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Chain ids `sendPayment` will sign typed data for, resolved from the base
|
|
816
|
+
* URL. Null for custom base URLs (local nodes, proxies) — the pre-signing
|
|
817
|
+
* chain check is skipped there, since the expected chain is unknowable.
|
|
818
|
+
*/
|
|
819
|
+
getExpectedChainIds() {
|
|
820
|
+
const hostname = this.getBaseHostname();
|
|
821
|
+
const riseDomains = [
|
|
822
|
+
"riseworks.io",
|
|
823
|
+
"dev-riseworks.io",
|
|
824
|
+
"staging-riseworks.io"
|
|
825
|
+
];
|
|
826
|
+
const isKnownRiseUrl = Object.values(RISE_API_URLS).some((url) => url === this.baseUrl) || riseDomains.some(
|
|
827
|
+
(domain) => hostname === domain || hostname.endsWith(`.${domain}`)
|
|
828
|
+
);
|
|
829
|
+
return isKnownRiseUrl ? EXPECTED_CHAIN_IDS[this.getEnvironment()] : null;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Generate a JWT token using Sign-In with Ethereum (SIWE) authentication
|
|
833
|
+
*
|
|
834
|
+
* @returns Promise resolving to the generated JWT token
|
|
835
|
+
* @throws Error if Rise ID or private key not provided, or if generation fails
|
|
836
|
+
*/
|
|
837
|
+
async generateJwtToken() {
|
|
838
|
+
if (!(this.riseId && this.privateKey)) {
|
|
839
|
+
throw new Error("Rise ID and private key are required for JWT generation");
|
|
840
|
+
}
|
|
841
|
+
if (this.isGeneratingJwt) {
|
|
842
|
+
throw new Error("JWT generation already in progress");
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
await ethers.ethers.resolveAddress(this.riseId);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`Invalid Rise ID address: ${error instanceof Error ? error.message : "Address validation failed"}`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
this.isGeneratingJwt = true;
|
|
852
|
+
try {
|
|
853
|
+
const wallet = new ethers.ethers.Wallet(this.privateKey);
|
|
854
|
+
const walletAddress = wallet.address;
|
|
855
|
+
const siweUrl = `${this.baseUrl}/v2/auth/siwe?wallet=${walletAddress}&riseid=${this.riseId}`;
|
|
856
|
+
const siweResponse = await fetch(siweUrl);
|
|
857
|
+
if (!siweResponse.ok) {
|
|
858
|
+
const errorData = await siweResponse.json();
|
|
859
|
+
throw new Error(
|
|
860
|
+
`HTTP ${siweResponse.status}: ${errorData.data || "Unknown error"}`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const siweData = await siweResponse.json();
|
|
864
|
+
if (!siweData.success) {
|
|
865
|
+
throw new Error(siweData.data || "Failed to get SIWE message");
|
|
866
|
+
}
|
|
867
|
+
const siweMessage = siweData.data.siwe;
|
|
868
|
+
const signature = await wallet.signMessage(siweMessage);
|
|
869
|
+
const nonceMatch = siweMessage.match(/Nonce: (.+)/);
|
|
870
|
+
const nonce = nonceMatch?.[1]?.trim() ?? "";
|
|
871
|
+
if (!nonce) {
|
|
872
|
+
throw new Error("Could not extract nonce from SIWE message");
|
|
873
|
+
}
|
|
874
|
+
const verifyResponse = await fetch(`${this.baseUrl}/v2/auth/verify`, {
|
|
875
|
+
method: "POST",
|
|
876
|
+
headers: {
|
|
877
|
+
"Content-Type": "application/json"
|
|
878
|
+
},
|
|
879
|
+
body: JSON.stringify({
|
|
880
|
+
message: siweMessage,
|
|
881
|
+
sig: signature,
|
|
882
|
+
nonce
|
|
883
|
+
})
|
|
884
|
+
});
|
|
885
|
+
if (!verifyResponse.ok) {
|
|
886
|
+
const errorData = await verifyResponse.json();
|
|
887
|
+
throw new Error(
|
|
888
|
+
`HTTP ${verifyResponse.status}: ${errorData.data || "Verification failed"}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const verifyData = await verifyResponse.json();
|
|
892
|
+
if (!verifyData.success) {
|
|
893
|
+
throw new Error(verifyData.data || "Signature verification failed");
|
|
894
|
+
}
|
|
895
|
+
const jwtToken = verifyData.data.jwt;
|
|
896
|
+
this.jwtToken = jwtToken;
|
|
897
|
+
this.headers.Authorization = `Bearer ${this.jwtToken}`;
|
|
898
|
+
return jwtToken;
|
|
899
|
+
} finally {
|
|
900
|
+
this.isGeneratingJwt = false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Check if JWT token is valid by making a test request
|
|
905
|
+
*/
|
|
906
|
+
async isJwtValid() {
|
|
907
|
+
if (!this.jwtToken) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
const response = await fetch(`${this.baseUrl}/v2/me`, {
|
|
912
|
+
method: "GET",
|
|
913
|
+
headers: this.headers,
|
|
914
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
915
|
+
});
|
|
916
|
+
return response.ok;
|
|
917
|
+
} catch {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Helper function to parse response body from different formats
|
|
923
|
+
*/
|
|
924
|
+
async parseResponseBody(response) {
|
|
925
|
+
try {
|
|
926
|
+
return await response.json();
|
|
927
|
+
} catch {
|
|
928
|
+
try {
|
|
929
|
+
const text = await response.text();
|
|
930
|
+
return JSON.parse(text);
|
|
931
|
+
} catch {
|
|
932
|
+
return {};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Makes an HTTP request with proper error handling and type safety
|
|
938
|
+
*/
|
|
939
|
+
async request(method, path, data, query, extraHeaders) {
|
|
940
|
+
if (this.riseId && this.privateKey && !(this.jwtToken && await this.isJwtValid())) {
|
|
941
|
+
try {
|
|
942
|
+
await this.generateJwtToken();
|
|
943
|
+
} catch (error) {
|
|
944
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
945
|
+
throw new Error(`Failed to generate JWT token: ${errorMessage}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const methodUpper = method.toUpperCase();
|
|
949
|
+
const autoIdempotencyKey = (methodUpper === "POST" || methodUpper === "PUT") && !extraHeaders?.["x-idempotency-key"] ? { "x-idempotency-key": generateIdempotencyKey() } : void 0;
|
|
950
|
+
const headers = extraHeaders || autoIdempotencyKey ? { ...this.headers, ...extraHeaders, ...autoIdempotencyKey } : this.headers;
|
|
951
|
+
const url = new URL(path, this.baseUrl);
|
|
952
|
+
if (query) {
|
|
953
|
+
for (const [key, value] of Object.entries(query)) {
|
|
954
|
+
if (value === void 0 || value === null) continue;
|
|
955
|
+
if (Array.isArray(value)) {
|
|
956
|
+
for (const item of value) {
|
|
957
|
+
if (item !== void 0 && item !== null) {
|
|
958
|
+
url.searchParams.append(key, String(item));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
} else {
|
|
962
|
+
url.searchParams.append(key, String(value));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const controller = new AbortController();
|
|
967
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
968
|
+
try {
|
|
969
|
+
const response = await fetch(url.toString(), {
|
|
970
|
+
method,
|
|
971
|
+
headers,
|
|
972
|
+
body: data ? JSON.stringify(data) : void 0,
|
|
973
|
+
signal: controller.signal
|
|
974
|
+
});
|
|
975
|
+
clearTimeout(timeoutId);
|
|
976
|
+
if (!response.ok) {
|
|
977
|
+
const isIdempotent = ["GET", "HEAD", "OPTIONS"].includes(
|
|
978
|
+
method.toUpperCase()
|
|
979
|
+
);
|
|
980
|
+
if (response.status === 401 && this.riseId && this.privateKey && isIdempotent) {
|
|
981
|
+
clearTimeout(timeoutId);
|
|
982
|
+
try {
|
|
983
|
+
await this.generateJwtToken();
|
|
984
|
+
} catch (jwtError) {
|
|
985
|
+
const errorMessage = jwtError instanceof Error ? jwtError.message : "Unknown error";
|
|
986
|
+
throw new Error(`Failed to refresh JWT token: ${errorMessage}`);
|
|
987
|
+
}
|
|
988
|
+
const retryHeaders = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
|
|
989
|
+
const retryController = new AbortController();
|
|
990
|
+
const retryTimeoutId = setTimeout(
|
|
991
|
+
() => retryController.abort(),
|
|
992
|
+
this.timeout
|
|
993
|
+
);
|
|
994
|
+
try {
|
|
995
|
+
const retryResponse = await fetch(url.toString(), {
|
|
996
|
+
method,
|
|
997
|
+
headers: retryHeaders,
|
|
998
|
+
body: data ? JSON.stringify(data) : void 0,
|
|
999
|
+
signal: retryController.signal
|
|
1000
|
+
});
|
|
1001
|
+
if (!retryResponse.ok) {
|
|
1002
|
+
const errorData2 = await this.parseResponseBody(retryResponse);
|
|
1003
|
+
throw this.buildApiError(
|
|
1004
|
+
retryResponse.status,
|
|
1005
|
+
retryResponse.statusText,
|
|
1006
|
+
errorData2
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
return await this.parseResponseBody(retryResponse);
|
|
1010
|
+
} finally {
|
|
1011
|
+
clearTimeout(retryTimeoutId);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const errorData = await this.parseResponseBody(response);
|
|
1015
|
+
throw this.buildApiError(
|
|
1016
|
+
response.status,
|
|
1017
|
+
response.statusText,
|
|
1018
|
+
errorData
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
return await this.parseResponseBody(response);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
clearTimeout(timeoutId);
|
|
1024
|
+
if (error instanceof Error) {
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
throw new Error("Request failed");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
class BillPayApi {
|
|
1033
|
+
constructor(client) {
|
|
1034
|
+
this.client = client;
|
|
1035
|
+
}
|
|
1036
|
+
async listRecipients(params) {
|
|
1037
|
+
return this.client.rawRequest(
|
|
1038
|
+
"GET",
|
|
1039
|
+
apiPath`/v2/payments/teams/${params.team_nanoid}/external_entity`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
async createRecipient(params, data) {
|
|
1043
|
+
return this.client.rawRequest(
|
|
1044
|
+
"POST",
|
|
1045
|
+
apiPath`/v2/payments/teams/${params.team_nanoid}/external_entity/`,
|
|
1046
|
+
data
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
async prepareInstantPayment(data) {
|
|
1050
|
+
return this.client.rawRequest(
|
|
1051
|
+
"POST",
|
|
1052
|
+
"/v2/payments/external_entity/instant",
|
|
1053
|
+
data
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
async create(data) {
|
|
1057
|
+
return this.prepareInstantPayment(data);
|
|
1058
|
+
}
|
|
1059
|
+
async executeInstantPayment(data) {
|
|
1060
|
+
return this.client.rawRequest(
|
|
1061
|
+
"PUT",
|
|
1062
|
+
"/v2/payments/external_entity/instant",
|
|
1063
|
+
data
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
async execute(data) {
|
|
1067
|
+
return this.executeInstantPayment(data);
|
|
1068
|
+
}
|
|
1069
|
+
async sendInstantPayment(data) {
|
|
1070
|
+
const { privateKey, ...paymentData } = data;
|
|
1071
|
+
const createResponse = await this.prepareInstantPayment(paymentData);
|
|
1072
|
+
const { signer, signature } = await this.client.signTypedData(
|
|
1073
|
+
createResponse.data.domain,
|
|
1074
|
+
createResponse.data.types,
|
|
1075
|
+
createResponse.data.typed_data,
|
|
1076
|
+
privateKey
|
|
1077
|
+
);
|
|
1078
|
+
return this.executeInstantPayment({
|
|
1079
|
+
...paymentData,
|
|
1080
|
+
signer,
|
|
1081
|
+
typed_data: createResponse.data.typed_data,
|
|
1082
|
+
signature
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async send(data) {
|
|
1086
|
+
return this.sendInstantPayment(data);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
class RiseApiClient extends RiseApiClient$1 {
|
|
1090
|
+
organizations;
|
|
1091
|
+
billPay;
|
|
1092
|
+
teams;
|
|
1093
|
+
constructor(options) {
|
|
1094
|
+
super(options);
|
|
1095
|
+
const client = this;
|
|
1096
|
+
Object.assign(this.webhooks, {
|
|
1097
|
+
delete: async (webhookNanoid) => client.rawRequest(
|
|
1098
|
+
"DELETE",
|
|
1099
|
+
apiPath`/v2/webhooks/${webhookNanoid}`
|
|
1100
|
+
),
|
|
1101
|
+
listDeliveries: async (webhookNanoid, params) => this.webhooks.getDeliveryHistory(webhookNanoid, params)
|
|
1102
|
+
});
|
|
1103
|
+
Object.assign(this.payments, {
|
|
1104
|
+
prepare: async (data, idempotencyKey) => this.payments.getPaymentTypedData(data, idempotencyKey),
|
|
1105
|
+
create: async (data, idempotencyKey) => this.payments.getPaymentTypedData(data, idempotencyKey),
|
|
1106
|
+
execute: async (data, idempotencyKey) => this.payments.executePaymentWithSignedData(data, idempotencyKey)
|
|
1107
|
+
});
|
|
1108
|
+
Object.assign(this.invites, {
|
|
1109
|
+
prepareManagerInvite: async (data) => this.invites.getManagerInviteTypedData(data),
|
|
1110
|
+
executeManagerInvite: async (data) => this.invites.executeManagerInviteWithSignedData(data)
|
|
1111
|
+
});
|
|
1112
|
+
Object.assign(this.team, {
|
|
1113
|
+
create: async (data) => client.rawRequest("POST", "/v2/teams", data),
|
|
1114
|
+
get: async (params) => client.rawRequest(
|
|
1115
|
+
"GET",
|
|
1116
|
+
apiPath`/v2/teams/${params.team_nanoid}`
|
|
1117
|
+
),
|
|
1118
|
+
update: async (paramsOrData, data) => {
|
|
1119
|
+
const params = data ? paramsOrData : { team_nanoid: paramsOrData.team_nanoid };
|
|
1120
|
+
const updateData = data ?? paramsOrData;
|
|
1121
|
+
return client.rawRequest(
|
|
1122
|
+
"PUT",
|
|
1123
|
+
apiPath`/v2/teams/${params.team_nanoid}`,
|
|
1124
|
+
updateData
|
|
1125
|
+
);
|
|
1126
|
+
},
|
|
1127
|
+
delete: async (params) => client.rawRequest(
|
|
1128
|
+
"DELETE",
|
|
1129
|
+
apiPath`/v2/teams/${params.team_nanoid}`
|
|
1130
|
+
),
|
|
1131
|
+
getUsers: async (params) => client.rawRequest(
|
|
1132
|
+
"GET",
|
|
1133
|
+
apiPath`/v2/teams/${params.team_nanoid}/users`
|
|
1134
|
+
),
|
|
1135
|
+
getSettings: async (params) => client.rawRequest(
|
|
1136
|
+
"GET",
|
|
1137
|
+
apiPath`/v2/teams/${params.team_nanoid}/settings`
|
|
1138
|
+
),
|
|
1139
|
+
updateSettings: async (params, data) => client.rawRequest(
|
|
1140
|
+
"PUT",
|
|
1141
|
+
apiPath`/v2/teams/${params.team_nanoid}/settings`,
|
|
1142
|
+
data
|
|
1143
|
+
),
|
|
1144
|
+
getMemberSettings: async (params) => client.rawRequest(
|
|
1145
|
+
"GET",
|
|
1146
|
+
apiPath`/v2/teams/${params.team_nanoid}/member/${params.user_nanoid}/settings`
|
|
1147
|
+
),
|
|
1148
|
+
updateMemberSettings: async (params, data) => client.rawRequest(
|
|
1149
|
+
"PUT",
|
|
1150
|
+
apiPath`/v2/teams/${params.team_nanoid}/member/${params.user_nanoid}/settings`,
|
|
1151
|
+
data
|
|
1152
|
+
),
|
|
1153
|
+
getMemberSummary: async (params) => client.rawRequest(
|
|
1154
|
+
"GET",
|
|
1155
|
+
apiPath`/v2/teams/${params.team_nanoid}/member/${params.user_nanoid}/summary`
|
|
1156
|
+
),
|
|
1157
|
+
removeMember: async (params) => client.rawRequest(
|
|
1158
|
+
"DELETE",
|
|
1159
|
+
apiPath`/v2/teams/${params.team_nanoid}/member/${params.user_nanoid}`
|
|
1160
|
+
)
|
|
1161
|
+
});
|
|
1162
|
+
Object.assign(this.user, {
|
|
1163
|
+
// Alias for the generated getCompanies (same /v2/user/organizations route).
|
|
1164
|
+
getOrganizations: async () => client.rawRequest(
|
|
1165
|
+
"GET",
|
|
1166
|
+
"/v2/user/organizations"
|
|
1167
|
+
)
|
|
1168
|
+
});
|
|
1169
|
+
this.organizations = this.company;
|
|
1170
|
+
this.billPay = new BillPayApi(client);
|
|
1171
|
+
this.teams = this.team;
|
|
1172
|
+
}
|
|
1173
|
+
rawRequest(method, path, data, query) {
|
|
1174
|
+
return this.request(method, path, data, query);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const getUnixTimestamp = () => Math.floor(Date.now() / 1e3);
|
|
1179
|
+
class WebhookValidationError extends Error {
|
|
1180
|
+
constructor(message, code) {
|
|
1181
|
+
super(message);
|
|
1182
|
+
this.code = code;
|
|
1183
|
+
this.name = "WebhookValidationError";
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
class WebhookValidator {
|
|
1187
|
+
secret;
|
|
1188
|
+
tolerance;
|
|
1189
|
+
constructor(secret, options) {
|
|
1190
|
+
if (!secret) {
|
|
1191
|
+
throw new Error("Webhook secret is required");
|
|
1192
|
+
}
|
|
1193
|
+
this.secret = secret;
|
|
1194
|
+
this.tolerance = options?.tolerance ?? 600;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Validates a webhook event and returns the typed event object
|
|
1198
|
+
*
|
|
1199
|
+
* Throws an error if validation fails. Use validateEventSafe() for non-throwing validation.
|
|
1200
|
+
*
|
|
1201
|
+
* @param requestBody - The raw webhook request body (string or Buffer). Pass the raw bytes, NOT a parsed object — re-serializing changes the bytes and breaks signature verification.
|
|
1202
|
+
* @param signature - The X-Rise-Signature header value from the webhook request
|
|
1203
|
+
* @param options - Optional validation configuration
|
|
1204
|
+
* @returns The parsed and typed webhook event
|
|
1205
|
+
* @throws WebhookValidationError if validation fails
|
|
1206
|
+
*
|
|
1207
|
+
*/
|
|
1208
|
+
validateEvent(requestBody, signature, options) {
|
|
1209
|
+
const result = this.validateEventSafe(requestBody, signature, options);
|
|
1210
|
+
if (!(result.event && result.isValid)) {
|
|
1211
|
+
throw new WebhookValidationError(
|
|
1212
|
+
result.error ?? "Webhook validation failed",
|
|
1213
|
+
result.code ?? (result.timestampValid === false ? "TIMESTAMP_TOO_OLD" : "INVALID_SIGNATURE")
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
return result.event;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Safely validates a webhook event without throwing errors
|
|
1220
|
+
*
|
|
1221
|
+
* Returns a result object instead of throwing. Use validateEvent() for throwing validation.
|
|
1222
|
+
*
|
|
1223
|
+
* @param requestBody - The raw webhook request body (string or Buffer). Pass the raw bytes, NOT a parsed object — re-serializing changes the bytes and breaks signature verification.
|
|
1224
|
+
* @param signature - The X-Rise-Signature header value from the webhook request
|
|
1225
|
+
* @param options - Optional validation configuration
|
|
1226
|
+
* @returns WebhookValidationResult containing validation status and event data
|
|
1227
|
+
*
|
|
1228
|
+
*/
|
|
1229
|
+
validateEventSafe(requestBody, signature, options = {}) {
|
|
1230
|
+
const { tolerance = this.tolerance } = options;
|
|
1231
|
+
try {
|
|
1232
|
+
const body = requestBody;
|
|
1233
|
+
if (body === null || body === void 0) {
|
|
1234
|
+
throw new WebhookValidationError(
|
|
1235
|
+
"Request body cannot be null or undefined.",
|
|
1236
|
+
"MALFORMED_PAYLOAD"
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof body !== "string" && !Buffer.isBuffer(body)) {
|
|
1240
|
+
throw new WebhookValidationError(
|
|
1241
|
+
`Invalid request body type: ${typeof body}. Expected the raw string or Buffer body (a parsed object would break signature verification).`,
|
|
1242
|
+
"MALFORMED_PAYLOAD"
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
const payloadString = typeof body === "string" ? body : body.toString("utf8");
|
|
1246
|
+
if (!signature) {
|
|
1247
|
+
return {
|
|
1248
|
+
isValid: false,
|
|
1249
|
+
code: "INVALID_SIGNATURE",
|
|
1250
|
+
error: "Missing or invalid signature header"
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
const signatureParts = this.parseSignatureHeader(signature);
|
|
1254
|
+
if (!signatureParts) {
|
|
1255
|
+
return {
|
|
1256
|
+
isValid: false,
|
|
1257
|
+
code: "INVALID_SIGNATURE",
|
|
1258
|
+
error: "Invalid signature format - expected format: t=timestamp,v1=hash"
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const { timestamp, hash } = signatureParts;
|
|
1262
|
+
const currentTime = getUnixTimestamp();
|
|
1263
|
+
const timeDiff = currentTime - timestamp;
|
|
1264
|
+
if (timeDiff > tolerance) {
|
|
1265
|
+
return {
|
|
1266
|
+
isValid: false,
|
|
1267
|
+
code: "TIMESTAMP_TOO_OLD",
|
|
1268
|
+
error: `Timestamp too old (${timeDiff}s ago, tolerance: ${tolerance}s)`,
|
|
1269
|
+
timestampValid: false
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
if (timestamp > currentTime + 60) {
|
|
1273
|
+
return {
|
|
1274
|
+
isValid: false,
|
|
1275
|
+
code: "TIMESTAMP_TOO_OLD",
|
|
1276
|
+
error: `Timestamp is in the future (${timestamp - currentTime}s ahead)`,
|
|
1277
|
+
timestampValid: false
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const expectedHash = this.generateSignature(timestamp, payloadString);
|
|
1281
|
+
const signatureValid = this.compareSignatures(hash, expectedHash);
|
|
1282
|
+
if (!signatureValid) {
|
|
1283
|
+
return {
|
|
1284
|
+
isValid: false,
|
|
1285
|
+
code: "INVALID_SIGNATURE",
|
|
1286
|
+
error: "Invalid signature",
|
|
1287
|
+
timestampValid: true
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
const payloadObject = JSON.parse(payloadString);
|
|
1291
|
+
if (payloadObject.event_version?.includes("2.")) {
|
|
1292
|
+
switch (payloadObject.event_type) {
|
|
1293
|
+
case "payment.sent":
|
|
1294
|
+
return {
|
|
1295
|
+
isValid: true,
|
|
1296
|
+
event: payloadObject,
|
|
1297
|
+
timestampValid: true
|
|
1298
|
+
};
|
|
1299
|
+
case "withdraw_account.duplicated_detected":
|
|
1300
|
+
return {
|
|
1301
|
+
isValid: true,
|
|
1302
|
+
event: payloadObject,
|
|
1303
|
+
timestampValid: true
|
|
1304
|
+
};
|
|
1305
|
+
case "invite.accepted":
|
|
1306
|
+
return {
|
|
1307
|
+
isValid: true,
|
|
1308
|
+
event: payloadObject,
|
|
1309
|
+
timestampValid: true
|
|
1310
|
+
};
|
|
1311
|
+
case "deposit.received":
|
|
1312
|
+
return {
|
|
1313
|
+
isValid: true,
|
|
1314
|
+
event: payloadObject,
|
|
1315
|
+
timestampValid: true
|
|
1316
|
+
};
|
|
1317
|
+
case "payment.group.created":
|
|
1318
|
+
return {
|
|
1319
|
+
isValid: true,
|
|
1320
|
+
event: payloadObject,
|
|
1321
|
+
timestampValid: true
|
|
1322
|
+
};
|
|
1323
|
+
default:
|
|
1324
|
+
return {
|
|
1325
|
+
isValid: false,
|
|
1326
|
+
code: "UNKNOWN_EVENT_TYPE",
|
|
1327
|
+
error: `Unknown event type: ${payloadObject.event_type}`,
|
|
1328
|
+
timestampValid: true
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
isValid: false,
|
|
1334
|
+
code: "UNKNOWN_EVENT_TYPE",
|
|
1335
|
+
error: `Unknown event version: ${payloadObject.event_version}`,
|
|
1336
|
+
timestampValid: true
|
|
1337
|
+
};
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
return {
|
|
1340
|
+
isValid: false,
|
|
1341
|
+
code: error instanceof WebhookValidationError ? error.code : "MALFORMED_PAYLOAD",
|
|
1342
|
+
error: error instanceof Error ? error.message : "Unknown validation error"
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Generates a signature for a given timestamp and payload
|
|
1348
|
+
*
|
|
1349
|
+
* Useful for testing or custom validation logic.
|
|
1350
|
+
*
|
|
1351
|
+
* @param timestamp - Unix timestamp in seconds
|
|
1352
|
+
* @param payload - The webhook payload as a string
|
|
1353
|
+
* @returns The generated signature as a hexadecimal string
|
|
1354
|
+
*/
|
|
1355
|
+
generateSignature(timestamp, payload) {
|
|
1356
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
1357
|
+
return crypto__namespace.createHmac("sha256", this.secret).update(signedPayload).digest("hex");
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Parses the X-Rise-Signature header (format: t=timestamp,v1=hash)
|
|
1361
|
+
*
|
|
1362
|
+
* @param signature - The X-Rise-Signature header value
|
|
1363
|
+
* @returns Object containing timestamp and hash, or null if parsing fails
|
|
1364
|
+
*/
|
|
1365
|
+
parseSignatureHeader(signature) {
|
|
1366
|
+
try {
|
|
1367
|
+
const parts = signature.split(",");
|
|
1368
|
+
if (parts.length !== 2) return null;
|
|
1369
|
+
const timestampPart = parts[0]?.split("=");
|
|
1370
|
+
const hashPart = parts[1]?.split("=");
|
|
1371
|
+
if (timestampPart?.length !== 2 || hashPart?.length !== 2) return null;
|
|
1372
|
+
if (timestampPart[0] !== "t" || hashPart[0] !== "v1") return null;
|
|
1373
|
+
if (!timestampPart[1]) return null;
|
|
1374
|
+
const timestamp = Number.parseInt(timestampPart[1], 10);
|
|
1375
|
+
const hash = hashPart[1];
|
|
1376
|
+
if (Number.isNaN(timestamp) || !hash) return null;
|
|
1377
|
+
return { timestamp, hash };
|
|
1378
|
+
} catch {
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Compares two signatures using timing-safe comparison
|
|
1384
|
+
*
|
|
1385
|
+
* @param received - The signature received in the webhook request
|
|
1386
|
+
* @param expected - The signature calculated from the payload and secret
|
|
1387
|
+
* @returns True if signatures match, false otherwise
|
|
1388
|
+
*/
|
|
1389
|
+
compareSignatures(received, expected) {
|
|
1390
|
+
try {
|
|
1391
|
+
const receivedBuffer = Buffer.from(received, "hex");
|
|
1392
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
1393
|
+
if (receivedBuffer.length !== expectedBuffer.length) {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
return crypto__namespace.timingSafeEqual(receivedBuffer, expectedBuffer);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
exports.GeneratedRiseApiClient = RiseApiClient$1;
|
|
1404
|
+
exports.RiseApiClient = RiseApiClient;
|
|
1405
|
+
exports.WebhookValidationError = WebhookValidationError;
|
|
1406
|
+
exports.WebhookValidator = WebhookValidator;
|