@railhook/node 2.12.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 ADDED
@@ -0,0 +1,476 @@
1
+ # @railhook/node
2
+
3
+ Official Node.js SDK for [Railhook](https://github.com/vadymkykalo/railhook).
4
+
5
+ **Zero runtime dependencies.** This SDK talks to the API using Node's built-in
6
+ `node:https` module — no third-party HTTP client, no transitive dependency
7
+ tree to audit. `npm ls --prod` on this package prints nothing.
8
+
9
+ ## Scope
10
+
11
+ This SDK covers the **send + verify** surface of the API: Events, Endpoints,
12
+ Subscriptions, Deliveries, Incoming Sources, Incoming Events, and webhook
13
+ signature verification — 7 of the platform's 35 API controllers. It
14
+ does **not** wrap Transformations, Rules, Workflows, Schemas, DLQ, Analytics,
15
+ Usage, Alerts, Incidents, PII rules, Audit Log, Tunnels, API keys, Members,
16
+ or Projects. Those are dashboard/API-only today; use the [Generic
17
+ Requests](#generic-requests) escape hatch below to call them directly if you
18
+ need them before the SDK grows to cover them.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @railhook/node
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { Railhook } from '@railhook/node';
30
+
31
+ const client = new Railhook({
32
+ apiKey: process.env.RAILHOOK_API_KEY, // e.g. 'Kz1uAIM8VeJUQN7yGSYCst64WxNLabBHfOYbrPlJ1yk'
33
+ baseUrl: 'http://localhost:8080', // optional, defaults to localhost
34
+ });
35
+
36
+ // Send an event
37
+ const event = await client.events.send({
38
+ type: 'order.completed',
39
+ data: {
40
+ orderId: 'ord_12345',
41
+ amount: 99.99,
42
+ currency: 'USD',
43
+ },
44
+ });
45
+
46
+ console.log(`Event created: ${event.eventId}`);
47
+ console.log(`Deliveries created: ${event.deliveriesCreated}`);
48
+ ```
49
+
50
+ ## API Reference
51
+
52
+ ### Events
53
+
54
+ ```typescript
55
+ // Send event with idempotency key
56
+ const event = await client.events.send(
57
+ { type: 'order.completed', data: { orderId: '123' } },
58
+ 'unique-idempotency-key'
59
+ );
60
+ ```
61
+
62
+ ### Endpoints
63
+
64
+ ```typescript
65
+ // Create endpoint
66
+ const endpoint = await client.endpoints.create(projectId, {
67
+ url: 'https://api.example.com/webhooks',
68
+ description: 'Production webhooks',
69
+ enabled: true,
70
+ });
71
+
72
+ // List endpoints — the API paginates this one, so the endpoints are in .content
73
+ const page = await client.endpoints.list(projectId, { page: 0, size: 20 });
74
+ for (const endpoint of page.content) {
75
+ console.log(endpoint.url);
76
+ }
77
+
78
+ // Update endpoint
79
+ await client.endpoints.update(projectId, endpointId, {
80
+ enabled: false,
81
+ });
82
+
83
+ // Delete endpoint
84
+ await client.endpoints.delete(projectId, endpointId);
85
+
86
+ // Rotate secret
87
+ const updated = await client.endpoints.rotateSecret(projectId, endpointId);
88
+ console.log(`New secret: ${updated.secret}`);
89
+
90
+ // Test endpoint connectivity
91
+ const result = await client.endpoints.test(projectId, endpointId);
92
+ console.log(`Test ${result.success ? 'passed' : 'failed'}: ${result.latencyMs}ms`);
93
+ console.log(`${result.httpStatusCode} — ${result.message}`);
94
+ ```
95
+
96
+ ### Subscriptions
97
+
98
+ ```typescript
99
+ // Subscribe endpoint to an event type
100
+ const subscription = await client.subscriptions.create(projectId, {
101
+ endpointId: endpoint.id,
102
+ eventType: 'order.completed',
103
+ enabled: true,
104
+ });
105
+
106
+ // List subscriptions — a bare array; unlike endpoints, this one is not paginated
107
+ const subscriptions = await client.subscriptions.list(projectId);
108
+
109
+ // Update subscription
110
+ await client.subscriptions.update(projectId, subscriptionId, {
111
+ eventType: 'order.shipped',
112
+ enabled: true,
113
+ });
114
+
115
+ // Delete subscription
116
+ await client.subscriptions.delete(projectId, subscriptionId);
117
+ ```
118
+
119
+ ### Deliveries
120
+
121
+ ```typescript
122
+ // List deliveries with filters
123
+ const deliveries = await client.deliveries.list(projectId, {
124
+ status: 'FAILED',
125
+ page: 0,
126
+ size: 20,
127
+ });
128
+
129
+ console.log(`Total failed: ${deliveries.totalElements}`);
130
+
131
+ // Get delivery attempts
132
+ const attempts = await client.deliveries.getAttempts(deliveryId);
133
+ for (const attempt of attempts) {
134
+ console.log(`Attempt ${attempt.attemptNumber}: ${attempt.httpStatusCode} (${attempt.durationMs}ms)`);
135
+ }
136
+
137
+ // Replay failed delivery
138
+ await client.deliveries.replay(deliveryId);
139
+ ```
140
+
141
+ ## Incoming Webhooks
142
+
143
+ Receive, validate, and forward webhooks from third-party providers (Stripe, GitHub, Twilio, etc.).
144
+
145
+ ### Incoming Sources
146
+
147
+ ```typescript
148
+ // Create an incoming source with HMAC verification
149
+ const source = await client.incomingSources.create(projectId, {
150
+ name: 'Stripe Webhooks',
151
+ slug: 'stripe',
152
+ providerType: 'STRIPE',
153
+ verificationMode: 'HMAC_GENERIC',
154
+ hmacSecret: 'whsec_...',
155
+ hmacHeaderName: 'Stripe-Signature',
156
+ });
157
+
158
+ console.log(`Ingress URL: ${source.ingressUrl}`);
159
+
160
+ // List sources
161
+ const sources = await client.incomingSources.list(projectId);
162
+
163
+ // Update source
164
+ await client.incomingSources.update(projectId, sourceId, {
165
+ name: 'Stripe Production',
166
+ rateLimitPerSecond: 100,
167
+ });
168
+
169
+ // Delete source
170
+ await client.incomingSources.delete(projectId, sourceId);
171
+ ```
172
+
173
+ ### Incoming Destinations
174
+
175
+ ```typescript
176
+ // Add a forwarding destination
177
+ const dest = await client.incomingSources.createDestination(projectId, sourceId, {
178
+ url: 'https://your-api.com/webhooks/stripe',
179
+ enabled: true,
180
+ maxAttempts: 5,
181
+ timeoutSeconds: 30,
182
+ });
183
+
184
+ // List destinations
185
+ const dests = await client.incomingSources.listDestinations(projectId, sourceId);
186
+
187
+ // Update destination
188
+ await client.incomingSources.updateDestination(projectId, sourceId, destId, {
189
+ enabled: false,
190
+ });
191
+
192
+ // Delete destination
193
+ await client.incomingSources.deleteDestination(projectId, sourceId, destId);
194
+ ```
195
+
196
+ ### Incoming Events
197
+
198
+ ```typescript
199
+ // List incoming events (with optional source filter)
200
+ const events = await client.incomingEvents.list(projectId, {
201
+ sourceId: source.id,
202
+ page: 0,
203
+ size: 20,
204
+ });
205
+
206
+ // Get event details
207
+ const event = await client.incomingEvents.get(projectId, eventId);
208
+
209
+ // Get forward attempts for an event
210
+ const attempts = await client.incomingEvents.getAttempts(projectId, eventId);
211
+
212
+ // Replay event to all destinations
213
+ const result = await client.incomingEvents.replay(projectId, eventId);
214
+ console.log(`Replayed to ${result.destinationsCount} destinations`);
215
+ ```
216
+
217
+ ## Webhook Signature Verification
218
+
219
+ Verify incoming webhooks in your endpoint:
220
+
221
+ ```typescript
222
+ import { verifySignature, constructEvent } from '@railhook/node';
223
+
224
+ app.post('/webhooks', (req, res) => {
225
+ const payload = req.body; // raw body string
226
+ const signature = req.headers['x-signature'];
227
+ const secret = process.env.WEBHOOK_SECRET;
228
+
229
+ try {
230
+ // Option 1: Just verify
231
+ verifySignature(payload, signature, secret);
232
+
233
+ // Option 2: Verify and parse
234
+ const event = constructEvent(payload, req.headers, secret);
235
+
236
+ // event.data is the parsed body; event.eventId / event.deliveryId /
237
+ // event.timestamp come from the X-Event-Id / X-Delivery-Id / X-Timestamp
238
+ // headers. See "What lands on your endpoint" below for event.type.
239
+ console.log(`Delivery ${event.deliveryId} of event ${event.eventId}:`, event.data);
240
+ handleOrderCompleted(event.data);
241
+
242
+ res.status(200).send('OK');
243
+ } catch (err) {
244
+ console.error('Webhook verification failed:', err.message);
245
+ res.status(400).send('Invalid signature');
246
+ }
247
+ });
248
+ ```
249
+
250
+ ### What lands on your endpoint
251
+
252
+ Railhook PUTs the event's **payload** on the wire, not an envelope. This:
253
+
254
+ ```typescript
255
+ await client.events.send({ type: 'order.completed', data: { orderId: 'ord_1' } });
256
+ ```
257
+
258
+ arrives at your endpoint as the `data` object alone —
259
+
260
+ ```http
261
+ POST /webhooks HTTP/1.1
262
+ Content-Type: application/json
263
+ X-Signature: t=1738000000000,v1=<hex hmac-sha256>
264
+ X-Timestamp: 1738000000000
265
+ X-Event-Id: 6f0e…
266
+ X-Delivery-Id: 91ab…
267
+ X-Sequence-Number: 0
268
+ Idempotency-Key: 6f0e…-<endpoint-id>
269
+
270
+ {"orderId":"ord_1"}
271
+ ```
272
+
273
+ So `constructEvent` fills `eventId`, `deliveryId` and `timestamp` from the
274
+ headers and `data` from the body, but **`type` is empty**: the event type is
275
+ not on the wire for a default subscription. Route on the payload, on the
276
+ endpoint you registered, or set the subscription's `payloadTemplate` to wrap
277
+ the event so `type` becomes part of the body.
278
+
279
+ The signature is computed over `` `${timestamp}.${rawBody}` `` with HMAC-SHA256
280
+ and the endpoint secret, and the server rejects timestamps more than **300
281
+ seconds** old — verify against the *raw* body bytes, before any JSON parse and
282
+ re-serialize.
283
+
284
+ ### Express.js Example
285
+
286
+ ```typescript
287
+ import express from 'express';
288
+ import { constructEvent } from '@railhook/node';
289
+
290
+ const app = express();
291
+
292
+ // Important: Use raw body for signature verification
293
+ app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
294
+ const event = constructEvent(
295
+ req.body.toString(),
296
+ req.headers,
297
+ process.env.WEBHOOK_SECRET
298
+ );
299
+
300
+ // Process event...
301
+ res.sendStatus(200);
302
+ });
303
+ ```
304
+
305
+ ## Error Handling
306
+
307
+ ```typescript
308
+ import {
309
+ RailhookError,
310
+ RateLimitError,
311
+ AuthenticationError,
312
+ ValidationError
313
+ } from '@railhook/node';
314
+
315
+ try {
316
+ await client.events.send({ type: 'test', data: {} });
317
+ } catch (err) {
318
+ if (err instanceof RateLimitError) {
319
+ // retryAfter is milliseconds. err.rateLimitInfo.reset is the raw
320
+ // X-RateLimit-Reset header, which the API sends in Unix *seconds*.
321
+ console.log(`Rate limited. Retry after ${err.retryAfter}ms`);
322
+ await sleep(err.retryAfter);
323
+ } else if (err instanceof AuthenticationError) {
324
+ console.error('Invalid API key');
325
+ } else if (err instanceof ValidationError) {
326
+ console.error('Validation failed:', err.fieldErrors);
327
+ } else if (err instanceof RailhookError) {
328
+ console.error(`Error ${err.status}: ${err.message}`);
329
+ }
330
+ }
331
+ ```
332
+
333
+ ### Error Response Format
334
+
335
+ All API errors return a consistent JSON body:
336
+
337
+ ```json
338
+ {
339
+ "error": "error_code",
340
+ "message": "Human-readable description",
341
+ "status": 400,
342
+ "fieldErrors": { "field": "reason" }
343
+ }
344
+ ```
345
+
346
+ - **`error`** — machine-readable error code (`snake_case`), always present
347
+ - **`message`** — human-readable description, always present
348
+ - **`status`** — HTTP status code (integer), always present
349
+ - **`fieldErrors`** — field-level validation details (only present for `validation_error`)
350
+
351
+ ### Error Codes Reference
352
+
353
+ | HTTP Status | `error` Code | SDK Exception | Description |
354
+ |---|---|---|---|
355
+ | 400 | `validation_error` | `ValidationError` | Invalid request parameters; see `fieldErrors` |
356
+ | 400 | `invalid_request` | `RailhookError` | Malformed or semantically invalid request |
357
+ | 401 | `unauthorized` | `AuthenticationError` | Missing or invalid API key / expired token |
358
+ | 403 | `forbidden` | `RailhookError` | Insufficient permissions for the action |
359
+ | 404 | `not_found` | `NotFoundError` | Requested resource does not exist |
360
+ | 413 | `payload_too_large` | `RailhookError` | Request body exceeds maximum allowed size |
361
+ | 422 | `unprocessable_entity` | `RailhookError` | Valid syntax but violates business rules |
362
+ | 429 | `rate_limit_exceeded` | `RateLimitError` | Too many requests; check `X-RateLimit-*` headers |
363
+ | 500 | `internal_error` | `RailhookError` | Unexpected server error |
364
+
365
+ ## Generic Requests
366
+
367
+ As the API expands, you can call any endpoint directly without waiting for SDK updates:
368
+
369
+ ```typescript
370
+ // GET
371
+ const schemas = await client.get<any[]>('/api/v1/projects/proj_123/schemas');
372
+
373
+ // POST with body and idempotency key
374
+ const result = await client.post('/api/v1/some/new/endpoint', { key: 'value' }, 'idempotency-key');
375
+
376
+ // PUT
377
+ await client.put('/api/v1/projects/proj_123/settings', { timezone: 'UTC' });
378
+
379
+ // PATCH
380
+ await client.patch('/api/v1/projects/proj_123/settings', { timezone: 'UTC' });
381
+
382
+ // DELETE
383
+ await client.delete('/api/v1/projects/proj_123/tags/old-tag');
384
+
385
+ // Fully custom request (any HTTP method)
386
+ const data = await client.request<any>('OPTIONS', '/api/v1/some/path');
387
+ ```
388
+
389
+ All generic methods use the same authentication, error handling, and rate-limit logic as the built-in methods.
390
+
391
+ ## Configuration
392
+
393
+ ```typescript
394
+ const client = new Railhook({
395
+ apiKey: process.env.RAILHOOK_API_KEY, // Required: Your project API key
396
+ baseUrl: 'https://api.example.com', // Optional: API base URL (default: http://localhost:8080)
397
+ timeout: 30000, // Optional: Request timeout in ms (default: 30000)
398
+ });
399
+ ```
400
+
401
+ ### Timeouts and retries
402
+
403
+ `timeout` is a per-request socket timeout; hitting it rejects with
404
+ `RailhookError` (`code: 'timeout'`, `status: 0`). A connection-level failure
405
+ rejects the same way with `code: 'network_error'`.
406
+
407
+ **The client does not retry.** One SDK call is exactly one HTTP request — no
408
+ backoff, no idempotent replay. That is deliberate: `events.send` accepts an
409
+ `Idempotency-Key`, so a retry policy belongs to the caller who knows whether
410
+ reissuing the request is safe. What *is* retried is the delivery itself, by the
411
+ platform, on the subscription's `retryDelays` ladder.
412
+
413
+ ## Authentication
414
+
415
+ Every request the client makes carries `X-API-Key: <your key>` — the project
416
+ API key, created in the dashboard or via
417
+ `POST /api/v1/projects/{projectId}/api-keys`. The SDK never sends a bearer
418
+ token and has no login surface: JWT-authenticated endpoints (auth, projects,
419
+ organizations, members, API keys) are not part of it. Bootstrapping a project
420
+ and a key is a one-time step you do with the dashboard, the CLI, or plain
421
+ HTTP.
422
+
423
+ ## TypeScript Support
424
+
425
+ This SDK is written in TypeScript and includes full type definitions:
426
+
427
+ ```typescript
428
+ import type {
429
+ Event,
430
+ EventResponse,
431
+ Endpoint,
432
+ Delivery,
433
+ DeliveryStatus
434
+ } from '@railhook/node';
435
+ ```
436
+
437
+ ## Development
438
+
439
+ ### Running Tests
440
+
441
+ **Local (requires Node.js 16+):**
442
+ ```bash
443
+ npm install
444
+ npm test
445
+ ```
446
+
447
+ **Docker:**
448
+ ```bash
449
+ docker run --rm -v $(pwd):/app -w /app node:20-alpine sh -c "npm install && npm test"
450
+ ```
451
+
452
+ ### Live-API smoke check
453
+
454
+ `npm test` stubs the transport, so it cannot see a renamed field. To drive the
455
+ SDK against a real instance:
456
+
457
+ ```bash
458
+ make up # from the repo root
459
+ npm run smoke:live # SMOKE_API_BASE_URL overrides the target
460
+ ```
461
+
462
+ It registers a throwaway org, walks endpoint → subscription → event →
463
+ deliveries → attempts → incoming, checks each error envelope, and verifies a
464
+ signature the running server itself produced. It is a script, not a test —
465
+ `jest.config.js` roots at `src/`, so `npm test` never collects it and still
466
+ passes with no backend.
467
+
468
+ ### Building
469
+
470
+ ```bash
471
+ npm run build
472
+ ```
473
+
474
+ ## License
475
+
476
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const client_1 = require("../client");
4
+ const errors_1 = require("../errors");
5
+ describe('Railhook Client', () => {
6
+ describe('constructor', () => {
7
+ it('should create client with API key', () => {
8
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
9
+ expect(client).toBeInstanceOf(client_1.Railhook);
10
+ });
11
+ it('should throw error without API key', () => {
12
+ expect(() => new client_1.Railhook({ apiKey: '' })).toThrow('API key is required');
13
+ });
14
+ it('should use default base URL', () => {
15
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
16
+ expect(client).toBeDefined();
17
+ });
18
+ it('should accept custom base URL', () => {
19
+ const client = new client_1.Railhook({
20
+ apiKey: 'test_api_key',
21
+ baseUrl: 'https://api.example.com',
22
+ });
23
+ expect(client).toBeDefined();
24
+ });
25
+ it('should accept custom timeout', () => {
26
+ const client = new client_1.Railhook({
27
+ apiKey: 'test_api_key',
28
+ timeout: 60000,
29
+ });
30
+ expect(client).toBeDefined();
31
+ });
32
+ it('should initialize all API modules', () => {
33
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
34
+ expect(client.events).toBeDefined();
35
+ expect(client.endpoints).toBeDefined();
36
+ expect(client.subscriptions).toBeDefined();
37
+ expect(client.deliveries).toBeDefined();
38
+ });
39
+ });
40
+ });
41
+ describe('Generic Request Methods', () => {
42
+ it('should expose get method', () => {
43
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
44
+ expect(typeof client.get).toBe('function');
45
+ });
46
+ it('should expose post method', () => {
47
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
48
+ expect(typeof client.post).toBe('function');
49
+ });
50
+ it('should expose put method', () => {
51
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
52
+ expect(typeof client.put).toBe('function');
53
+ });
54
+ it('should expose patch method', () => {
55
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
56
+ expect(typeof client.patch).toBe('function');
57
+ });
58
+ it('should expose delete method', () => {
59
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
60
+ expect(typeof client.delete).toBe('function');
61
+ });
62
+ it('should expose request method for fully custom calls', () => {
63
+ const client = new client_1.Railhook({ apiKey: 'test_api_key' });
64
+ expect(typeof client.request).toBe('function');
65
+ });
66
+ });
67
+ describe('Error Classes', () => {
68
+ describe('RailhookError', () => {
69
+ it('should have correct properties', () => {
70
+ const error = new errors_1.RailhookError('Test error', 500, 'test_code');
71
+ expect(error.message).toBe('Test error');
72
+ expect(error.status).toBe(500);
73
+ expect(error.code).toBe('test_code');
74
+ expect(error.name).toBe('RailhookError');
75
+ });
76
+ });
77
+ describe('AuthenticationError', () => {
78
+ it('should have correct defaults', () => {
79
+ const error = new errors_1.AuthenticationError();
80
+ expect(error.message).toBe('Invalid API key');
81
+ expect(error.status).toBe(401);
82
+ expect(error.code).toBe('authentication_error');
83
+ expect(error.name).toBe('AuthenticationError');
84
+ });
85
+ it('should accept custom message', () => {
86
+ const error = new errors_1.AuthenticationError('Custom auth error');
87
+ expect(error.message).toBe('Custom auth error');
88
+ });
89
+ });
90
+ describe('ValidationError', () => {
91
+ it('should have field errors', () => {
92
+ const fieldErrors = { email: 'Invalid email', url: 'Invalid URL' };
93
+ const error = new errors_1.ValidationError('Validation failed', fieldErrors);
94
+ expect(error.message).toBe('Validation failed');
95
+ expect(error.status).toBe(400);
96
+ expect(error.code).toBe('validation_error');
97
+ expect(error.fieldErrors).toEqual(fieldErrors);
98
+ });
99
+ it('should default to empty field errors', () => {
100
+ const error = new errors_1.ValidationError('Validation failed');
101
+ expect(error.fieldErrors).toEqual({});
102
+ });
103
+ });
104
+ });
105
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpZW50LnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvX190ZXN0c19fL2NsaWVudC50ZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsc0NBQXFDO0FBQ3JDLHNDQUFnRjtBQUVoRixRQUFRLENBQUMsaUJBQWlCLEVBQUUsR0FBRyxFQUFFO0lBQy9CLFFBQVEsQ0FBQyxhQUFhLEVBQUUsR0FBRyxFQUFFO1FBQzNCLEVBQUUsQ0FBQyxtQ0FBbUMsRUFBRSxHQUFHLEVBQUU7WUFDM0MsTUFBTSxNQUFNLEdBQUcsSUFBSSxpQkFBUSxDQUFDLEVBQUUsTUFBTSxFQUFFLGNBQWMsRUFBRSxDQUFDLENBQUM7WUFDeEQsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLGNBQWMsQ0FBQyxpQkFBUSxDQUFDLENBQUM7UUFDMUMsQ0FBQyxDQUFDLENBQUM7UUFFSCxFQUFFLENBQUMsb0NBQW9DLEVBQUUsR0FBRyxFQUFFO1lBQzVDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLGlCQUFRLENBQUMsRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO1FBQzVFLENBQUMsQ0FBQyxDQUFDO1FBRUgsRUFBRSxDQUFDLDZCQUE2QixFQUFFLEdBQUcsRUFBRTtZQUNyQyxNQUFNLE1BQU0sR0FBRyxJQUFJLGlCQUFRLENBQUMsRUFBRSxNQUFNLEVBQUUsY0FBYyxFQUFFLENBQUMsQ0FBQztZQUN4RCxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDL0IsQ0FBQyxDQUFDLENBQUM7UUFFSCxFQUFFLENBQUMsK0JBQStCLEVBQUUsR0FBRyxFQUFFO1lBQ3ZDLE1BQU0sTUFBTSxHQUFHLElBQUksaUJBQVEsQ0FBQztnQkFDMUIsTUFBTSxFQUFFLGNBQWM7Z0JBQ3RCLE9BQU8sRUFBRSx5QkFBeUI7YUFDbkMsQ0FBQyxDQUFDO1lB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QyxDQUFDO1lBQ3ZELE1BQU0sQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQ3hDLENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQyxDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IFJhaWxob29rIH0gZnJvbSAnLi4vY2xpZW50JztcbmltcG9ydCB7IFJhaWxob29rRXJyb3IsIEF1dGhlbnRpY2F0aW9uRXJyb3IsIFZhbGlkYXRpb25FcnJvciB9IGZyb20gJy4uL2Vycm9ycyc7XG5cbmRlc2NyaWJlKCdSYWlsaG9vayBDbGllbnQnLCAoKSA9PiB7XG4gIGRlc2NyaWJlKCdjb25zdHJ1Y3RvcicsICgpID0+IHtcbiAgICBpdCgnc2hvdWxkIGNyZWF0ZSBjbGllbnQgd2l0aCBBUEkga2V5JywgKCkgPT4ge1xuICAgICAgY29uc3QgY2xpZW50ID0gbmV3IFJhaWxob29rKHsgYXBpS2V5OiAndGVzdF9hcGlfa2V5JyB9KTtcbiAgICAgIGV4cGVjdChjbGllbnQpLnRvQmVJbnN0YW5jZU9mKFJhaWxob29rKTtcbiAgICB9KTtcblxuICAgIGl0KCdzaG91bGQgdGhyb3cgZXJyb3Igd2l0aG91dCBBUEkga2V5JywgKCkgPT4ge1xuICAgICAgZXhwZWN0KCgpID0+IG5ldyBSYWlsaG9vayh7IGFwaUtleTogJycgfSkpLnRvVGhyb3coJ0FQSSBrZXkgaXMgcmVxdWlyZWQnKTtcbiAgICB9KTtcblxuICAgIGl0KCdzaG91bGQgdXNlIGRlZmF1bHQgYmFzZSBVUkwnLCAoKSA9PiB7XG4gICAgICBjb25zdCBjbGllbnQgPSBuZXcgUmFpbGhvb2soeyBhcGlLZXk6ICd0ZXN0X2FwaV9rZXknIH0pO1xuICAgICAgZXhwZWN0KGNsaWVudCkudG9CZURlZmluZWQoKTtcbiAgICB9KTtcblxuICAgIGl0KCdzaG91bGQgYWNjZXB0IGN1c3RvbSBiYXNlIFVSTCcsICgpID0+IHtcbiAgICAgIGNvbnN0IGNsaWVudCA9IG5ldyBSYWlsaG9vayh7XG4gICAgICAgIGFwaUtleTogJ3Rlc3RfYXBpX2tleScsXG4gICAgICAgIGJhc2VVcmw6ICdodHRwczovL2FwaS5leGFtcGxlLmNvbScsXG4gICAgICB9KTtcbiAgICAgIGV4cGVjdChjbGllbnQpLnRvQmVEZWZpbmVkKCk7XG4gICAgfSk7XG5cbiAgICBpdCgnc2hvdWxkIGFjY2VwdCBjdXN0b20gdGltZW91dCcsICgpID0+IHtcbiAgICAgIGNvbnN0IGNsaWVudCA9IG5ldyBSYWlsaG9vayh7XG4gICAgICAgIGFwaUtleTogJ3Rlc3RfYXBpX2tleScsXG4gICAgICAgIHRpbWVvdXQ6IDYwMDAwLFxuICAgICAgfSk7XG4gICAgICBleHBlY3QoY2xpZW50KS50b0JlRGVmaW5lZCgpO1xuICAgIH0pO1xuXG4gICAgaXQoJ3Nob3VsZCBpbml0aWFsaXplIGFsbCBBUEkgbW9kdWxlcycsICgpID0+IHtcbiAgICAgIGNvbnN0IGNsaWVudCA9IG5ldyBSYWlsaG9vayh7IGFwaUtleTogJ3Rlc3RfYXBpX2tleScgfSk7XG4gICAgICBleHBlY3QoY2xpZW50LmV2ZW50cykudG9CZURlZmluZWQoKTtcbiAgICAgIGV4cGVjdChjbGllbnQuZW5kcG9pbnRzKS50b0JlRGVmaW5lZCgpO1xuICAgICAgZXhwZWN0KGNsaWVudC5zdWJzY3JpcHRpb25zKS50b0JlRGVmaW5lZCgpO1xuICAgICAgZXhwZWN0KGNsaWVudC5kZWxpdmVyaWVzKS50b0JlRGVmaW5lZCgpO1xuICAgIH0pO1xuICB9KTtcbn0pO1xuXG5kZXNjcmliZSgnR2VuZXJpYyBSZXF1ZXN0IE1ldGhvZHMnLCAoKSA9PiB7XG4gIGl0KCdzaG91bGQgZXhwb3NlIGdldCBtZXRob2QnLCAoKSA9PiB7XG4gICAgY29uc3QgY2xpZW50ID0gbmV3IFJhaWxob29rKHsgYXBpS2V5OiAndGVzdF9hcGlfa2V5JyB9KTtcbiAgICBleHBlY3QodHlwZW9mIGNsaWVudC5nZXQpLnRvQmUoJ2Z1bmN0aW9uJyk7XG4gIH0pO1xuXG4gIGl0KCdzaG91bGQgZXhwb3NlIHBvc3QgbWV0aG9kJywgKCkgPT4ge1xuICAgIGNvbnN0IGNsaWVudCA9IG5ldyBSYWlsaG9vayh7IGFwaUtleTogJ3Rlc3RfYXBpX2tleScgfSk7XG4gICAgZXhwZWN0KHR5cGVvZiBjbGllbnQucG9zdCkudG9CZSgnZnVuY3Rpb24nKTtcbiAgfSk7XG5cbiAgaXQoJ3Nob3VsZCBleHBvc2UgcHV0IG1ldGhvZCcsICgpID0+IHtcbiAgICBjb25zdCBjbGllbnQgPSBuZXcgUmFpbGhvb2soeyBhcGlLZXk6ICd0ZXN0X2FwaV9rZXknIH0pO1xuICAgIGV4cGVjdCh0eXBlb2YgY2xpZW50LnB1dCkudG9CZSgnZnVuY3Rpb24nKTtcbiAgfSk7XG5cbiAgaXQoJ3Nob3VsZCBleHBvc2UgcGF0Y2ggbWV0aG9kJywgKCkgPT4ge1xuICAgIGNvbnN0IGNsaWVudCA9IG5ldyBSYWlsaG9vayh7IGFwaUtleTogJ3Rlc3RfYXBpX2tleScgfSk7XG4gICAgZXhwZWN0KHR5cGVvZiBjbGllbnQucGF0Y2gpLnRvQmUoJ2Z1bmN0aW9uJyk7XG4gIH0pO1xuXG4gIGl0KCdzaG91bGQgZXhwb3NlIGRlbGV0ZSBtZXRob2QnLCAoKSA9PiB7XG4gICAgY29uc3QgY2xpZW50ID0gbmV3IFJhaWxob29rKHsgYXBpS2V5OiAndGVzdF9hcGlfa2V5JyB9KTtcbiAgICBleHBlY3QodHlwZW9mIGNsaWVudC5kZWxldGUpLnRvQmUoJ2Z1bmN0aW9uJyk7XG4gIH0pO1xuXG4gIGl0KCdzaG91bGQgZXhwb3NlIHJlcXVlc3QgbWV0aG9kIGZvciBmdWxseSBjdXN0b20gY2FsbHMnLCAoKSA9PiB7XG4gICAgY29uc3QgY2xpZW50ID0gbmV3IFJhaWxob29rKHsgYXBpS2V5OiAndGVzdF9hcGlfa2V5JyB9KTtcbiAgICBleHBlY3QodHlwZW9mIGNsaWVudC5yZXF1ZXN0KS50b0JlKCdmdW5jdGlvbicpO1xuICB9KTtcbn0pO1xuXG5kZXNjcmliZSgnRXJyb3IgQ2xhc3NlcycsICgpID0+IHtcbiAgZGVzY3JpYmUoJ1JhaWxob29rRXJyb3InLCAoKSA9PiB7XG4gICAgaXQoJ3Nob3VsZCBoYXZlIGNvcnJlY3QgcHJvcGVydGllcycsICgpID0+IHtcbiAgICAgIGNvbnN0IGVycm9yID0gbmV3IFJhaWxob29rRXJyb3IoJ1Rlc3QgZXJyb3InLCA1MDAsICd0ZXN0X2NvZGUnKTtcbiAgICAgIGV4cGVjdChlcnJvci5tZXNzYWdlKS50b0JlKCdUZXN0IGVycm9yJyk7XG4gICAgICBleHBlY3QoZXJyb3Iuc3RhdHVzKS50b0JlKDUwMCk7XG4gICAgICBleHBlY3QoZXJyb3IuY29kZSkudG9CZSgndGVzdF9jb2RlJyk7XG4gICAgICBleHBlY3QoZXJyb3IubmFtZSkudG9CZSgnUmFpbGhvb2tFcnJvcicpO1xuICAgIH0pO1xuICB9KTtcblxuICBkZXNjcmliZSgnQXV0aGVudGljYXRpb25FcnJvcicsICgpID0+IHtcbiAgICBpdCgnc2hvdWxkIGhhdmUgY29ycmVjdCBkZWZhdWx0cycsICgpID0+IHtcbiAgICAgIGNvbnN0IGVycm9yID0gbmV3IEF1dGhlbnRpY2F0aW9uRXJyb3IoKTtcbiAgICAgIGV4cGVjdChlcnJvci5tZXNzYWdlKS50b0JlKCdJbnZhbGlkIEFQSSBrZXknKTtcbiAgICAgIGV4cGVjdChlcnJvci5zdGF0dXMpLnRvQmUoNDAxKTtcbiAgICAgIGV4cGVjdChlcnJvci5jb2RlKS50b0JlKCdhdXRoZW50aWNhdGlvbl9lcnJvcicpO1xuICAgICAgZXhwZWN0KGVycm9yLm5hbWUpLnRvQmUoJ0F1dGhlbnRpY2F0aW9uRXJyb3InKTtcbiAgICB9KTtcblxuICAgIGl0KCdzaG91bGQgYWNjZXB0IGN1c3RvbSBtZXNzYWdlJywgKCkgPT4ge1xuICAgICAgY29uc3QgZXJyb3IgPSBuZXcgQXV0aGVudGljYXRpb25FcnJvcignQ3VzdG9tIGF1dGggZXJyb3InKTtcbiAgICAgIGV4cGVjdChlcnJvci5tZXNzYWdlKS50b0JlKCdDdXN0b20gYXV0aCBlcnJvcicpO1xuICAgIH0pO1xuICB9KTtcblxuICBkZXNjcmliZSgnVmFsaWRhdGlvbkVycm9yJywgKCkgPT4ge1xuICAgIGl0KCdzaG91bGQgaGF2ZSBmaWVsZCBlcnJvcnMnLCAoKSA9PiB7XG4gICAgICBjb25zdCBmaWVsZEVycm9ycyA9IHsgZW1haWw6ICdJbnZhbGlkIGVtYWlsJywgdXJsOiAnSW52YWxpZCBVUkwnIH07XG4gICAgICBjb25zdCBlcnJvciA9IG5ldyBWYWxpZGF0aW9uRXJyb3IoJ1ZhbGlkYXRpb24gZmFpbGVkJywgZmllbGRFcnJvcnMpO1xuICAgICAgZXhwZWN0KGVycm9yLm1lc3NhZ2UpLnRvQmUoJ1ZhbGlkYXRpb24gZmFpbGVkJyk7XG4gICAgICBleHBlY3QoZXJyb3Iuc3RhdHVzKS50b0JlKDQwMCk7XG4gICAgICBleHBlY3QoZXJyb3IuY29kZSkudG9CZSgndmFsaWRhdGlvbl9lcnJvcicpO1xuICAgICAgZXhwZWN0KGVycm9yLmZpZWxkRXJyb3JzKS50b0VxdWFsKGZpZWxkRXJyb3JzKTtcbiAgICB9KTtcblxuICAgIGl0KCdzaG91bGQgZGVmYXVsdCB0byBlbXB0eSBmaWVsZCBlcnJvcnMnLCAoKSA9PiB7XG4gICAgICBjb25zdCBlcnJvciA9IG5ldyBWYWxpZGF0aW9uRXJyb3IoJ1ZhbGlkYXRpb24gZmFpbGVkJyk7XG4gICAgICBleHBlY3QoZXJyb3IuZmllbGRFcnJvcnMpLnRvRXF1YWwoe30pO1xuICAgIH0pO1xuICB9KTtcbn0pO1xuIl19
@@ -0,0 +1 @@
1
+ export {};