@rerout/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/index.cjs +369 -0
- package/dist/index.d.cts +309 -0
- package/dist/index.d.ts +309 -0
- package/dist/index.js +334 -0
- package/package.json +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@rerout/sdk` are documented in this file. The format is
|
|
4
|
+
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-05-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial public release.
|
|
12
|
+
- `Rerout` client with `links`, `project`, and `qr` namespaces.
|
|
13
|
+
- Link operations: `create`, `list`, `get`, `update`, `delete`, `stats`.
|
|
14
|
+
- Project operations: `stats`, `me`.
|
|
15
|
+
- QR helpers: pure URL builder + signed SVG fetch.
|
|
16
|
+
- `verifyReroutSignature` — HMAC-SHA256 webhook signature verification with
|
|
17
|
+
configurable timestamp tolerance and constant-time comparison.
|
|
18
|
+
- `ReroutError` with stable `code`, `status`, `details`; `isRateLimited`,
|
|
19
|
+
`isServerError` convenience flags.
|
|
20
|
+
- ESM + CJS dual build with bundled `.d.ts` declarations.
|
|
21
|
+
|
|
22
|
+
[0.1.0]: https://github.com/ModestNerds-Co/rerout-sdks/releases/tag/typescript-v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codecraft Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @rerout/sdk
|
|
2
|
+
|
|
3
|
+
Official TypeScript / JavaScript SDK for the [Rerout](https://rerout.co) API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @rerout/sdk
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @rerout/sdk
|
|
11
|
+
# or
|
|
12
|
+
bun add @rerout/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Node 18+ (uses the global `fetch` and `AbortController`). Works in
|
|
16
|
+
modern bundlers, Bun, Deno, and Cloudflare Workers — pass a custom `fetch` in
|
|
17
|
+
edge runtimes if needed.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Rerout } from '@rerout/sdk'
|
|
23
|
+
|
|
24
|
+
const rerout = new Rerout({ apiKey: process.env.REROUT_API_KEY! })
|
|
25
|
+
|
|
26
|
+
const link = await rerout.links.create({
|
|
27
|
+
target_url: 'https://example.com/q4-sale',
|
|
28
|
+
domain_hostname: 'go.brand.com',
|
|
29
|
+
code: 'q4',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
console.log(link.short_url) // https://go.brand.com/q4
|
|
33
|
+
|
|
34
|
+
const stats = await rerout.project.stats(7)
|
|
35
|
+
console.log(`Last 7 days: ${stats.total_clicks} clicks, ${stats.qr_scans} QR scans`)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
### Construction
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const rerout = new Rerout({
|
|
44
|
+
apiKey: 'rrk_…', // required
|
|
45
|
+
baseUrl: 'https://api.rerout.co', // optional, defaults shown
|
|
46
|
+
timeoutMs: 30_000, // optional
|
|
47
|
+
fetch: customFetch, // optional — inject your own fetch
|
|
48
|
+
defaultHeaders: { // optional — added to every request
|
|
49
|
+
'user-agent': 'my-app/1.0',
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Links
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
rerout.links.create({ target_url, domain_hostname?, code?, expires_at?, ...seo })
|
|
58
|
+
rerout.links.list({ cursor?, limit? })
|
|
59
|
+
rerout.links.get(code)
|
|
60
|
+
rerout.links.update(code, { target_url?, expires_at?, is_active?, ...seo })
|
|
61
|
+
rerout.links.delete(code)
|
|
62
|
+
rerout.links.stats(code, days = 30)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Project
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
rerout.project.stats(days = 30)
|
|
69
|
+
rerout.project.me()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### QR codes
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
rerout.qr.url(code, { size?, margin?, ecc?, domain?, refresh? }) // returns string
|
|
76
|
+
await rerout.qr.svg(code, opts) // fetches the rendered SVG
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Webhook signature verification
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { verifyReroutSignature } from '@rerout/sdk'
|
|
83
|
+
|
|
84
|
+
const ok = verifyReroutSignature({
|
|
85
|
+
rawBody,
|
|
86
|
+
signatureHeader: req.headers['x-rerout-signature']!,
|
|
87
|
+
secret: process.env.REROUT_WEBHOOK_SECRET!,
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Defaults to a 5-minute timestamp tolerance; pass `toleranceSeconds: 0` to
|
|
92
|
+
disable that check.
|
|
93
|
+
|
|
94
|
+
## Error handling
|
|
95
|
+
|
|
96
|
+
Every method throws `ReroutError` on failure:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { ReroutError } from '@rerout/sdk'
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await rerout.links.create({ target_url: 'http://insecure.example' })
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof ReroutError) {
|
|
105
|
+
console.error(err.code) // 'bad_target_url'
|
|
106
|
+
console.error(err.status) // 400
|
|
107
|
+
console.error(err.message) // 'target_url must use https.'
|
|
108
|
+
if (err.isRateLimited) { /* back off */ }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Synthetic codes when the server didn't return a JSON body:
|
|
114
|
+
`network_error`, `timeout`, `unexpected_response`, `unauthorized`,
|
|
115
|
+
`forbidden`, `not_found`, `rate_limited`, `server_error`, `client_error`.
|
|
116
|
+
|
|
117
|
+
## Local development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install
|
|
121
|
+
npm run typecheck
|
|
122
|
+
npm run test
|
|
123
|
+
npm run build
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT — see [LICENSE](../LICENSE).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
24
|
+
DEFAULT_SIGNATURE_TOLERANCE_SECONDS: () => DEFAULT_SIGNATURE_TOLERANCE_SECONDS,
|
|
25
|
+
Links: () => Links,
|
|
26
|
+
Project: () => Project,
|
|
27
|
+
Qr: () => Qr,
|
|
28
|
+
Rerout: () => Rerout,
|
|
29
|
+
ReroutError: () => ReroutError,
|
|
30
|
+
buildQrUrl: () => buildQrUrl,
|
|
31
|
+
verifyReroutSignature: () => verifyReroutSignature
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/errors.ts
|
|
36
|
+
var ReroutError = class extends Error {
|
|
37
|
+
/** Stable error code, either from the API or a synthetic client-side one. */
|
|
38
|
+
code;
|
|
39
|
+
/** HTTP status code, or 0 when the request never reached the server. */
|
|
40
|
+
status;
|
|
41
|
+
/** The raw response body (parsed JSON or string), useful for debugging. */
|
|
42
|
+
details;
|
|
43
|
+
constructor(opts) {
|
|
44
|
+
super(opts.message);
|
|
45
|
+
this.name = "ReroutError";
|
|
46
|
+
this.code = opts.code;
|
|
47
|
+
this.status = opts.status;
|
|
48
|
+
this.details = opts.details;
|
|
49
|
+
}
|
|
50
|
+
/** True for HTTP 5xx responses (server-side issues). */
|
|
51
|
+
get isServerError() {
|
|
52
|
+
return this.status >= 500 && this.status < 600;
|
|
53
|
+
}
|
|
54
|
+
/** True for HTTP 429 — caller should back off and retry. */
|
|
55
|
+
get isRateLimited() {
|
|
56
|
+
return this.status === 429;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/qr.ts
|
|
61
|
+
function buildQrUrl(args) {
|
|
62
|
+
const base = args.baseUrl.replace(/\/+$/, "");
|
|
63
|
+
const url = new URL(`${base}/v1/links/${encodeURIComponent(args.code)}/qr`);
|
|
64
|
+
const opts = args.options ?? {};
|
|
65
|
+
if (opts.size !== void 0) url.searchParams.set("size", String(opts.size));
|
|
66
|
+
if (opts.margin !== void 0) url.searchParams.set("margin", String(opts.margin));
|
|
67
|
+
if (opts.ecc !== void 0) url.searchParams.set("ecc", opts.ecc);
|
|
68
|
+
if (opts.domain !== void 0) url.searchParams.set("domain", opts.domain);
|
|
69
|
+
if (opts.refresh !== void 0) {
|
|
70
|
+
url.searchParams.set("refresh", opts.refresh === true ? "1" : opts.refresh);
|
|
71
|
+
}
|
|
72
|
+
return url.toString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/client.ts
|
|
76
|
+
var DEFAULT_BASE_URL = "https://api.rerout.co";
|
|
77
|
+
var Rerout = class {
|
|
78
|
+
/** Link operations: create, list, get, update, delete, stats. */
|
|
79
|
+
links;
|
|
80
|
+
/** Project-level operations: aggregate stats. */
|
|
81
|
+
project;
|
|
82
|
+
/** QR helpers (pure URL builders + signed fetch). */
|
|
83
|
+
qr;
|
|
84
|
+
apiKey;
|
|
85
|
+
baseUrl;
|
|
86
|
+
fetchImpl;
|
|
87
|
+
timeoutMs;
|
|
88
|
+
defaultHeaders;
|
|
89
|
+
constructor(options) {
|
|
90
|
+
if (!options.apiKey || typeof options.apiKey !== "string") {
|
|
91
|
+
throw new ReroutError({
|
|
92
|
+
code: "missing_api_key",
|
|
93
|
+
message: "A project API key is required to construct Rerout.",
|
|
94
|
+
status: 0
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
this.apiKey = options.apiKey;
|
|
98
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
99
|
+
this.fetchImpl = options.fetch ?? (typeof fetch !== "undefined" ? fetch : (() => {
|
|
100
|
+
throw new ReroutError({
|
|
101
|
+
code: "missing_fetch",
|
|
102
|
+
message: "No global fetch available. Pass `fetch` in ReroutClientOptions or run on Node 18+.",
|
|
103
|
+
status: 0
|
|
104
|
+
});
|
|
105
|
+
})());
|
|
106
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
107
|
+
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
108
|
+
this.links = new Links(this);
|
|
109
|
+
this.project = new Project(this);
|
|
110
|
+
this.qr = new Qr(this);
|
|
111
|
+
}
|
|
112
|
+
/** @internal — invoked by Links / Project / Qr. */
|
|
113
|
+
async request(init) {
|
|
114
|
+
const url = new URL(this.baseUrl + init.path);
|
|
115
|
+
if (init.query) {
|
|
116
|
+
for (const [k, v] of Object.entries(init.query)) {
|
|
117
|
+
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const headers = {
|
|
121
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
122
|
+
accept: "application/json",
|
|
123
|
+
...this.defaultHeaders
|
|
124
|
+
};
|
|
125
|
+
const body = init.body === void 0 ? void 0 : JSON.stringify(init.body);
|
|
126
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
127
|
+
const abort = new AbortController();
|
|
128
|
+
const timer = setTimeout(() => abort.abort(), this.timeoutMs);
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await this.fetchImpl(url.toString(), {
|
|
132
|
+
method: init.method,
|
|
133
|
+
headers,
|
|
134
|
+
body,
|
|
135
|
+
signal: abort.signal
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new ReroutError({
|
|
139
|
+
code: abort.signal.aborted ? "timeout" : "network_error",
|
|
140
|
+
message: error instanceof Error ? error.message : "Request to Rerout failed before the server replied.",
|
|
141
|
+
status: 0,
|
|
142
|
+
details: error
|
|
143
|
+
});
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
const text = await response.text();
|
|
148
|
+
if (!response.ok) throw parseError(response.status, text);
|
|
149
|
+
if (text.length === 0) return void 0;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(text);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new ReroutError({
|
|
154
|
+
code: "unexpected_response",
|
|
155
|
+
message: "Rerout returned a non-JSON success body.",
|
|
156
|
+
status: response.status,
|
|
157
|
+
details: { body: text, error }
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Internal — used by Qr to expose the resolved base URL. */
|
|
162
|
+
get resolvedBaseUrl() {
|
|
163
|
+
return this.baseUrl;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
function parseError(status, body) {
|
|
167
|
+
if (body.length === 0) {
|
|
168
|
+
return new ReroutError({
|
|
169
|
+
code: synthCodeForStatus(status),
|
|
170
|
+
message: `Rerout returned HTTP ${status} with no body.`,
|
|
171
|
+
status
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(body);
|
|
176
|
+
return new ReroutError({
|
|
177
|
+
code: parsed.code ?? synthCodeForStatus(status),
|
|
178
|
+
message: parsed.message ?? `Rerout returned HTTP ${status}.`,
|
|
179
|
+
status,
|
|
180
|
+
details: parsed
|
|
181
|
+
});
|
|
182
|
+
} catch {
|
|
183
|
+
return new ReroutError({
|
|
184
|
+
code: synthCodeForStatus(status),
|
|
185
|
+
message: `Rerout returned HTTP ${status} (non-JSON body).`,
|
|
186
|
+
status,
|
|
187
|
+
details: { body }
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function synthCodeForStatus(status) {
|
|
192
|
+
if (status === 401) return "unauthorized";
|
|
193
|
+
if (status === 403) return "forbidden";
|
|
194
|
+
if (status === 404) return "not_found";
|
|
195
|
+
if (status === 429) return "rate_limited";
|
|
196
|
+
if (status >= 500) return "server_error";
|
|
197
|
+
return "client_error";
|
|
198
|
+
}
|
|
199
|
+
var Links = class {
|
|
200
|
+
/** @internal */
|
|
201
|
+
constructor(client) {
|
|
202
|
+
this.client = client;
|
|
203
|
+
}
|
|
204
|
+
client;
|
|
205
|
+
/** Create a new short link. */
|
|
206
|
+
create(input) {
|
|
207
|
+
return this.client.request({
|
|
208
|
+
method: "POST",
|
|
209
|
+
path: "/v1/links",
|
|
210
|
+
body: input
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Paginated list of links in the project. */
|
|
214
|
+
list(params) {
|
|
215
|
+
return this.client.request({
|
|
216
|
+
method: "GET",
|
|
217
|
+
path: "/v1/links",
|
|
218
|
+
query: params
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Get a single link by code. */
|
|
222
|
+
get(code) {
|
|
223
|
+
return this.client.request({
|
|
224
|
+
method: "GET",
|
|
225
|
+
path: `/v1/links/${encodeURIComponent(code)}`
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** Patch a link. Only fields present in `input` are changed. */
|
|
229
|
+
update(code, input) {
|
|
230
|
+
return this.client.request({
|
|
231
|
+
method: "PATCH",
|
|
232
|
+
path: `/v1/links/${encodeURIComponent(code)}`,
|
|
233
|
+
body: input
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** Soft-delete a link. The short URL stops redirecting and is gone from lists. */
|
|
237
|
+
delete(code) {
|
|
238
|
+
return this.client.request({
|
|
239
|
+
method: "DELETE",
|
|
240
|
+
path: `/v1/links/${encodeURIComponent(code)}`
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/** Per-link click stats. Defaults to 30 days. */
|
|
244
|
+
stats(code, days = 30) {
|
|
245
|
+
return this.client.request({
|
|
246
|
+
method: "GET",
|
|
247
|
+
path: `/v1/links/${encodeURIComponent(code)}/stats`,
|
|
248
|
+
query: { days }
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
var Project = class {
|
|
253
|
+
/** @internal */
|
|
254
|
+
constructor(client) {
|
|
255
|
+
this.client = client;
|
|
256
|
+
}
|
|
257
|
+
client;
|
|
258
|
+
/** Aggregate stats across every link in the project. */
|
|
259
|
+
stats(days = 30) {
|
|
260
|
+
return this.client.request({
|
|
261
|
+
method: "GET",
|
|
262
|
+
path: "/v1/projects/me/stats",
|
|
263
|
+
query: { days }
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** Info about the project that owns the current API key. */
|
|
267
|
+
me() {
|
|
268
|
+
return this.client.request({
|
|
269
|
+
method: "GET",
|
|
270
|
+
path: "/v1/projects/me"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var Qr = class {
|
|
275
|
+
/** @internal */
|
|
276
|
+
constructor(client) {
|
|
277
|
+
this.client = client;
|
|
278
|
+
}
|
|
279
|
+
client;
|
|
280
|
+
/**
|
|
281
|
+
* Build the URL the API serves the QR SVG from. Pure — does not call the API.
|
|
282
|
+
*/
|
|
283
|
+
url(code, options) {
|
|
284
|
+
return buildQrUrl({
|
|
285
|
+
baseUrl: this.client.resolvedBaseUrl,
|
|
286
|
+
code,
|
|
287
|
+
options
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Fetch the QR as an SVG string. Hits the same endpoint as `url()` but
|
|
292
|
+
* attaches the bearer token and returns the rendered body.
|
|
293
|
+
*/
|
|
294
|
+
async svg(code, options) {
|
|
295
|
+
return this.client.request({
|
|
296
|
+
method: "GET",
|
|
297
|
+
path: `/v1/links/${encodeURIComponent(code)}/qr` + qrQueryString(options)
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
function qrQueryString(options) {
|
|
302
|
+
if (!options) return "";
|
|
303
|
+
const params = new URLSearchParams();
|
|
304
|
+
if (options.size !== void 0) params.set("size", String(options.size));
|
|
305
|
+
if (options.margin !== void 0) params.set("margin", String(options.margin));
|
|
306
|
+
if (options.ecc !== void 0) params.set("ecc", options.ecc);
|
|
307
|
+
if (options.domain !== void 0) params.set("domain", options.domain);
|
|
308
|
+
if (options.refresh !== void 0) {
|
|
309
|
+
params.set("refresh", options.refresh === true ? "1" : options.refresh);
|
|
310
|
+
}
|
|
311
|
+
const qs = params.toString();
|
|
312
|
+
return qs.length === 0 ? "" : `?${qs}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/webhooks.ts
|
|
316
|
+
var import_node_crypto = require("crypto");
|
|
317
|
+
var DEFAULT_SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
318
|
+
function verifyReroutSignature(opts) {
|
|
319
|
+
if (!opts.signatureHeader || !opts.secret || opts.rawBody === void 0) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
const parts = parseSignatureHeader(opts.signatureHeader);
|
|
323
|
+
if (!parts) return false;
|
|
324
|
+
const tolerance = opts.toleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS;
|
|
325
|
+
if (tolerance > 0) {
|
|
326
|
+
const now = opts.now ? opts.now() : Math.floor(Date.now() / 1e3);
|
|
327
|
+
if (Math.abs(now - parts.timestamp) > tolerance) return false;
|
|
328
|
+
}
|
|
329
|
+
const expectedHex = (0, import_node_crypto.createHmac)("sha256", opts.secret).update(`${parts.timestamp}.${opts.rawBody}`).digest("hex");
|
|
330
|
+
const expected = safeFromHex(expectedHex);
|
|
331
|
+
const actual = safeFromHex(parts.v1);
|
|
332
|
+
if (!expected || !actual || expected.length !== actual.length) return false;
|
|
333
|
+
return (0, import_node_crypto.timingSafeEqual)(expected, actual);
|
|
334
|
+
}
|
|
335
|
+
function parseSignatureHeader(header) {
|
|
336
|
+
let timestamp;
|
|
337
|
+
let v1;
|
|
338
|
+
for (const segment of header.split(",")) {
|
|
339
|
+
const eq = segment.indexOf("=");
|
|
340
|
+
if (eq <= 0) continue;
|
|
341
|
+
const key = segment.slice(0, eq).trim().toLowerCase();
|
|
342
|
+
const value = segment.slice(eq + 1).trim();
|
|
343
|
+
if (key === "t") {
|
|
344
|
+
const parsed = Number.parseInt(value, 10);
|
|
345
|
+
if (Number.isFinite(parsed) && parsed > 0) timestamp = parsed;
|
|
346
|
+
} else if (key === "v1") {
|
|
347
|
+
v1 = value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (timestamp === void 0 || !v1) return null;
|
|
351
|
+
return { timestamp, v1 };
|
|
352
|
+
}
|
|
353
|
+
function safeFromHex(hex) {
|
|
354
|
+
if (hex.length === 0 || hex.length % 2 !== 0) return null;
|
|
355
|
+
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
356
|
+
return Buffer.from(hex, "hex");
|
|
357
|
+
}
|
|
358
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
359
|
+
0 && (module.exports = {
|
|
360
|
+
DEFAULT_BASE_URL,
|
|
361
|
+
DEFAULT_SIGNATURE_TOLERANCE_SECONDS,
|
|
362
|
+
Links,
|
|
363
|
+
Project,
|
|
364
|
+
Qr,
|
|
365
|
+
Rerout,
|
|
366
|
+
ReroutError,
|
|
367
|
+
buildQrUrl,
|
|
368
|
+
verifyReroutSignature
|
|
369
|
+
});
|