@relume_io/partner-node 1.0.0 → 2.0.1
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/LICENSE +21 -0
- package/README.md +60 -23
- package/dist/bin.cjs +186 -9
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +185 -10
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-LOMLIKT2.js → chunk-HPVVBUR7.js} +137 -27
- package/dist/chunk-HPVVBUR7.js.map +1 -0
- package/dist/index.cjs +91 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +45 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-LOMLIKT2.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Relume Tech PTY LTD
|
|
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
CHANGED
|
@@ -60,10 +60,10 @@ Then, in Relume's partner settings, set your endpoint URL to
|
|
|
60
60
|
|
|
61
61
|
## What it mounts
|
|
62
62
|
|
|
63
|
-
| Route | Called by | Does
|
|
64
|
-
| --------------------- | -------------------------- |
|
|
65
|
-
| `GET /relume/session` | `@relume_io/partner-embed` |
|
|
66
|
-
| `POST /relume/calls` | Relume | Verifies `Relume-Signature` and the 5-minute window, routes by `type`, runs your callback, validates what it returns.
|
|
63
|
+
| Route | Called by | Does |
|
|
64
|
+
| --------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `GET /relume/session` | `@relume_io/partner-embed` | Refuses another site, resolves the user with `getUserIdFromRequest`, calls `POST /sessions`, returns `{ clientSecret, expiresAt }`. |
|
|
66
|
+
| `POST /relume/calls` | Relume | Verifies `Relume-Signature` and the 5-minute window, routes by `type`, runs your callback, validates what it returns. |
|
|
67
67
|
|
|
68
68
|
`relume.express()` answers exactly `/session` and `/calls` under its mount
|
|
69
69
|
path (`app.use('/relume', …)`), or `/relume/session` and `/relume/calls` when
|
|
@@ -75,20 +75,33 @@ CORS preflights (`OPTIONS`) are left to your app: `express()` passes them on
|
|
|
75
75
|
to the next middleware, and `fetchHandler` answers `204` with no CORS headers.
|
|
76
76
|
|
|
77
77
|
If your backend is on another origin from your page, pass `sessionUrl` to
|
|
78
|
-
`Relume.init` in the embed and allow your page's origin, with credentials
|
|
79
|
-
that route's CORS response. Use an exact
|
|
80
|
-
`Origin` and never use `*`.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
`Relume.init` in the embed and allow your page's origin, with credentials and
|
|
79
|
+
the `Relume-Embed` header, in that route's CORS response. Use an exact
|
|
80
|
+
allowlist: never reflect the request `Origin` and never use `*`.
|
|
81
|
+
|
|
82
|
+
Any site can make a signed-in user's browser call the session route, so it
|
|
83
|
+
refuses two things before it looks at the user: a request with
|
|
84
|
+
`Sec-Fetch-Site: cross-site`, which only another site's page sends, and one
|
|
85
|
+
without the `Relume-Embed: 1` header `@relume_io/partner-embed` sends. The
|
|
86
|
+
header makes a cross-origin request need a CORS preflight, so a link or form
|
|
87
|
+
on another site can't get one. Neither helps against a script on your own
|
|
88
|
+
page: any script there can ask the route for a session for the signed-in
|
|
89
|
+
user, or answer the builder's session request with a client secret for
|
|
90
|
+
another of your users, which opens the builder as them. Load only scripts you
|
|
91
|
+
trust where the builder is embedded.
|
|
92
|
+
|
|
93
|
+
| Situation | Answer |
|
|
94
|
+
| -------------------------------------------- | ------------------------------------------------------------ |
|
|
95
|
+
| Bad or missing signature, or stale `t` | `401 invalid_signature`; your callback isn't called |
|
|
96
|
+
| Unknown `type`, or no callback registered | `400 unsupported_call_type` |
|
|
97
|
+
| Body that isn't valid for its `type` | `400 invalid_request` |
|
|
98
|
+
| `onGetUser` returns `null` | `404 user_not_found` |
|
|
99
|
+
| `onChargeCredits` returns `{ ok: false }` | `402 insufficient_credits`, with `balance` |
|
|
100
|
+
| A callback throws, or returns a bad shape | `500 internal_error`, and `onHandlerError` gets the error |
|
|
101
|
+
| No `Relume-Embed: 1` header (session route) | `400 embed_header_missing`, and `onHandlerError` says why |
|
|
102
|
+
| `Sec-Fetch-Site: cross-site` (session route) | `403 cross_site_request` |
|
|
103
|
+
| Nobody signed in (session route) | `401 not_signed_in` |
|
|
104
|
+
| `POST /sessions` refused (session route) | Relume's status and `{ error }`, e.g. `403 user_deactivated` |
|
|
92
105
|
|
|
93
106
|
Every response is JSON with `Cache-Control: no-store`.
|
|
94
107
|
|
|
@@ -135,7 +148,8 @@ A whole number of your own credits, zero or more.
|
|
|
135
148
|
Read the user from your own session. Never take a user id from the query
|
|
136
149
|
string or body. Gets the Express/Node request for `express()`, or the
|
|
137
150
|
`Request` for `fetchHandler`. Pass that type to `Relume.init<…>` to type
|
|
138
|
-
`req`; it
|
|
151
|
+
`req`; it must have `headers` (`SessionRequest`), which every framework's
|
|
152
|
+
request does, and is `{ headers: unknown }` otherwise.
|
|
139
153
|
|
|
140
154
|
## Other frameworks
|
|
141
155
|
|
|
@@ -164,6 +178,8 @@ export default { fetch: (request: Request) => relume.fetchHandler(request) };
|
|
|
164
178
|
|
|
165
179
|
**Anything else:** call `relume.handleCall({ rawBody, signatureHeader, requestId })`
|
|
166
180
|
and `relume.handleSession(request)`. Each returns `{ status, headers, body }`.
|
|
181
|
+
`handleSession` reads the request's headers, so pass it the fetch `Request`,
|
|
182
|
+
the Node request, or an object with `headers`.
|
|
167
183
|
|
|
168
184
|
### Raw bodies
|
|
169
185
|
|
|
@@ -177,7 +193,7 @@ body, the call is answered `500` and `onHandlerError` explains why.
|
|
|
177
193
|
```ts
|
|
178
194
|
Relume.init({
|
|
179
195
|
secretKey: 'sk_test_…', // required; test or live
|
|
180
|
-
apiBaseUrl: 'https://…', // defaults to production
|
|
196
|
+
apiBaseUrl: 'https://…', // defaults to production; http only for localhost
|
|
181
197
|
apiTimeoutMs: 15_000, // per partner API request attempt
|
|
182
198
|
maxRetries: 2, // retries for 429s, 5xx answers and failed connections
|
|
183
199
|
fetch, // custom fetch for partner API requests
|
|
@@ -186,7 +202,9 @@ Relume.init({
|
|
|
186
202
|
```
|
|
187
203
|
|
|
188
204
|
`Relume.init` returns a new client every time, with no global state, so test
|
|
189
|
-
and live clients can run side by side.
|
|
205
|
+
and live clients can run side by side. It throws for an `apiBaseUrl` that
|
|
206
|
+
isn't `https`, except `http://localhost`, `127.0.0.1` and `[::1]`: every request
|
|
207
|
+
carries your secret key.
|
|
190
208
|
|
|
191
209
|
## Calling the partner API
|
|
192
210
|
|
|
@@ -293,8 +311,23 @@ It calls your endpoint the way Relume does, and checks that it:
|
|
|
293
311
|
- changes nothing when a charge it never debited is reversed, and debits
|
|
294
312
|
nothing if that charge arrives afterwards (record a reversed `chargeId` as
|
|
295
313
|
declined, see `onReverseCharge` above);
|
|
296
|
-
- credits a reversal, and only once per `reversalId
|
|
297
|
-
|
|
314
|
+
- credits a reversal, and only once per `reversalId`;
|
|
315
|
+
- doesn't let another site read the session route: it asks for a session from
|
|
316
|
+
`https://evil.example`, preflight and `GET`, and fails if the CORS response
|
|
317
|
+
grants that origin;
|
|
318
|
+
- refuses a session request without the `Relume-Embed` header with `400`, and
|
|
319
|
+
one with `Sec-Fetch-Site: cross-site` with `403`, before looking for a
|
|
320
|
+
signed-in user.
|
|
321
|
+
|
|
322
|
+
It finds the session route next to a `--url` ending in `/calls`; pass
|
|
323
|
+
`--session-url` if yours is elsewhere. If a login answers that route before
|
|
324
|
+
the SDK does, give the check a test user's cookie so it gets past it: in
|
|
325
|
+
`RELUME_SESSION_HEADERS` (one `Name: value` a line), or with
|
|
326
|
+
`--session-header "Cookie: …"` (repeatable). Prefer the variable: a cookie on
|
|
327
|
+
the command line leaks like a key does.
|
|
328
|
+
|
|
329
|
+
Give it the key in `RELUME_SECRET_KEY`, not `--secret-key`: a key on the
|
|
330
|
+
command line lands in your shell history, CI logs and the process list.
|
|
298
331
|
`--user` must be a user your wallet knows, holding at least 2 credits. The
|
|
299
332
|
check charges them 2 and reverses everything it charged, and it prints a
|
|
300
333
|
`NOTE` if the balance doesn't end where it started. Each check prints `PASS`,
|
|
@@ -331,3 +364,7 @@ npm run test:dist # builds, then loads the ESM and CJS output
|
|
|
331
364
|
|
|
332
365
|
Releases go out from CI on a `partner-sdk/v*` tag, together with
|
|
333
366
|
`@relume_io/partner-embed`: see [Releasing](../README.md#releasing).
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT. See [LICENSE](./LICENSE).
|
package/dist/bin.cjs
CHANGED
|
@@ -30,6 +30,10 @@ function isHttpsUrl(value) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/session.ts
|
|
34
|
+
var EMBED_HEADER = "Relume-Embed";
|
|
35
|
+
var EMBED_HEADER_VALUE = "1";
|
|
36
|
+
|
|
33
37
|
// src/signing.ts
|
|
34
38
|
var SIGNATURE_HEADER = "Relume-Signature";
|
|
35
39
|
var REQUEST_ID_HEADER = "Relume-Request-Id";
|
|
@@ -100,6 +104,11 @@ var READ_TIMEOUT_MS = 4e3;
|
|
|
100
104
|
var CHARGE_TIMEOUT_MS = 4e3;
|
|
101
105
|
var REVERSE_TIMEOUT_MS = 1e4;
|
|
102
106
|
var SKEW_SECONDS = 600;
|
|
107
|
+
var FOREIGN_ORIGIN = "https://evil.example";
|
|
108
|
+
var SESSION_CORS = "doesn't let another site read the session route";
|
|
109
|
+
var SESSION_HEADER = "refuses a session request without the Relume-Embed header";
|
|
110
|
+
var SESSION_CROSS_SITE = "refuses a session request from another site";
|
|
111
|
+
var SESSION_CHECKS = [SESSION_CORS, SESSION_HEADER, SESSION_CROSS_SITE];
|
|
103
112
|
var ACCEPT = "accepts a correctly signed call";
|
|
104
113
|
var ROTATION = "accepts a key rotation header, with your key second";
|
|
105
114
|
var OTHER_KEY = "rejects a signature made with another key";
|
|
@@ -166,6 +175,13 @@ function optionalBalanceProblem(body) {
|
|
|
166
175
|
function bodyBalance(body) {
|
|
167
176
|
return body !== MALFORMED && isWhole(body.balance, 0) ? body.balance : void 0;
|
|
168
177
|
}
|
|
178
|
+
function sessionUrlFor(callsUrl) {
|
|
179
|
+
const url = new URL(callsUrl);
|
|
180
|
+
if (!/\/calls\/?$/.test(url.pathname)) return null;
|
|
181
|
+
url.pathname = url.pathname.replace(/calls\/?$/, "session");
|
|
182
|
+
url.search = "";
|
|
183
|
+
return url.href;
|
|
184
|
+
}
|
|
169
185
|
function randomId(prefix) {
|
|
170
186
|
return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
171
187
|
}
|
|
@@ -262,6 +278,117 @@ async function checkEndpoint(options) {
|
|
|
262
278
|
}
|
|
263
279
|
return { balance: answer.body.balance };
|
|
264
280
|
}
|
|
281
|
+
async function askSession(sessionUrl2, init) {
|
|
282
|
+
try {
|
|
283
|
+
const response = await fetchImpl(sessionUrl2, {
|
|
284
|
+
...init,
|
|
285
|
+
headers: { ...options.sessionHeaders, ...init.headers },
|
|
286
|
+
redirect: "manual",
|
|
287
|
+
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
288
|
+
});
|
|
289
|
+
const text = await response.text();
|
|
290
|
+
return {
|
|
291
|
+
answer: {
|
|
292
|
+
answered: true,
|
|
293
|
+
status: response.status,
|
|
294
|
+
ok: response.status >= 200 && response.status < 300,
|
|
295
|
+
body: parseBody(text)
|
|
296
|
+
},
|
|
297
|
+
allowOrigin: response.headers.get("access-control-allow-origin")?.trim() ?? null
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${READ_TIMEOUT_MS / 1e3}s` : networkReason(error);
|
|
301
|
+
return { answer: { answered: false, reason }, allowOrigin: null };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const session = { answered: false };
|
|
305
|
+
async function sessionCorsProblem(sessionUrl2) {
|
|
306
|
+
const probes = [
|
|
307
|
+
[
|
|
308
|
+
"the preflight",
|
|
309
|
+
{
|
|
310
|
+
method: "OPTIONS",
|
|
311
|
+
headers: {
|
|
312
|
+
Origin: FOREIGN_ORIGIN,
|
|
313
|
+
"Access-Control-Request-Method": "GET",
|
|
314
|
+
"Access-Control-Request-Headers": EMBED_HEADER.toLowerCase()
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
[
|
|
319
|
+
"GET",
|
|
320
|
+
{
|
|
321
|
+
method: "GET",
|
|
322
|
+
headers: {
|
|
323
|
+
Origin: FOREIGN_ORIGIN,
|
|
324
|
+
[EMBED_HEADER]: EMBED_HEADER_VALUE,
|
|
325
|
+
"Sec-Fetch-Site": "cross-site",
|
|
326
|
+
"Sec-Fetch-Mode": "cors"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
];
|
|
331
|
+
let problem = null;
|
|
332
|
+
for (const [what, init] of probes) {
|
|
333
|
+
const { answer, allowOrigin } = await askSession(sessionUrl2, init);
|
|
334
|
+
if (!answer.answered) return `${what} to ${sessionUrl2} ${answer.reason}`;
|
|
335
|
+
if (allowOrigin === FOREIGN_ORIGIN) {
|
|
336
|
+
problem ??= `${what} to ${sessionUrl2} from ${FOREIGN_ORIGIN} answered Access-Control-Allow-Origin: ${FOREIGN_ORIGIN}, so a page on any site can read what this route answers for a signed-in user. Allow only your own page origins, never the request's Origin`;
|
|
337
|
+
}
|
|
338
|
+
if (init.method === "GET") {
|
|
339
|
+
session.answered = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return problem;
|
|
343
|
+
}
|
|
344
|
+
async function sessionRouteMissing(sessionUrl2) {
|
|
345
|
+
const { answer } = await askSession(sessionUrl2, {
|
|
346
|
+
method: "GET",
|
|
347
|
+
headers: { [EMBED_HEADER]: EMBED_HEADER_VALUE }
|
|
348
|
+
});
|
|
349
|
+
return answer.answered && answer.status === 404;
|
|
350
|
+
}
|
|
351
|
+
async function sessionRefusalProblem(sessionUrl2, headers, status, code) {
|
|
352
|
+
const { answer } = await askSession(sessionUrl2, {
|
|
353
|
+
method: "GET",
|
|
354
|
+
headers
|
|
355
|
+
});
|
|
356
|
+
if (answer.answered && answer.status === status) return null;
|
|
357
|
+
if (answer.answered && answer.status === 401) {
|
|
358
|
+
return options.sessionHeaders ? `${said(answer)} with the session headers: they didn't get past the login (an expired cookie?), or the route looked for a signed-in user first. Answer ${status} ${code} before reading the user` : `${said(answer)}: something asked for a signed-in user first. If that's a login in front of the route, pass --session-header with a test user's cookie so the check gets past it; otherwise answer ${status} ${code} before reading the user`;
|
|
359
|
+
}
|
|
360
|
+
return `${said(answer)}; expected ${status} ${code}`;
|
|
361
|
+
}
|
|
362
|
+
const sessionUrl = options.sessionUrl ?? sessionUrlFor(url);
|
|
363
|
+
const notFound = sessionUrl && await sessionRouteMissing(sessionUrl);
|
|
364
|
+
if (sessionUrl === null || notFound) {
|
|
365
|
+
skip(
|
|
366
|
+
SESSION_CHECKS,
|
|
367
|
+
sessionUrl === null ? "--url doesn't end in /calls; pass --session-url with the session route your page's embed calls" : `GET ${sessionUrl} answered 404; pass --session-url with the session route your page's embed calls`
|
|
368
|
+
);
|
|
369
|
+
} else {
|
|
370
|
+
await run(SESSION_CORS, () => sessionCorsProblem(sessionUrl));
|
|
371
|
+
if (!session.answered) {
|
|
372
|
+
skip(SESSION_CHECKS.slice(1), "needs the session route to answer");
|
|
373
|
+
} else {
|
|
374
|
+
await run(
|
|
375
|
+
SESSION_HEADER,
|
|
376
|
+
() => sessionRefusalProblem(sessionUrl, {}, 400, "embed_header_missing")
|
|
377
|
+
);
|
|
378
|
+
await run(
|
|
379
|
+
SESSION_CROSS_SITE,
|
|
380
|
+
() => sessionRefusalProblem(
|
|
381
|
+
sessionUrl,
|
|
382
|
+
{
|
|
383
|
+
[EMBED_HEADER]: EMBED_HEADER_VALUE,
|
|
384
|
+
"Sec-Fetch-Site": "cross-site"
|
|
385
|
+
},
|
|
386
|
+
403,
|
|
387
|
+
"cross_site_request"
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
265
392
|
const found = { wrongKey: false, balance: null, charged: null, reversed: false };
|
|
266
393
|
const read = JSON.stringify({ type: "credits.get", userId });
|
|
267
394
|
const tampered = JSON.stringify({
|
|
@@ -587,22 +714,34 @@ async function checkEndpoint(options) {
|
|
|
587
714
|
}
|
|
588
715
|
|
|
589
716
|
// src/cli.ts
|
|
590
|
-
var USAGE = `Usage: npx @relume_io/partner-node check --url <endpoint> --user <userId>
|
|
717
|
+
var USAGE = `Usage: RELUME_SECRET_KEY=sk_test_\u2026 npx @relume_io/partner-node check --url <endpoint> --user <userId>
|
|
591
718
|
|
|
592
719
|
Checks your partner endpoint against the Relume partner protocol: signatures,
|
|
593
720
|
user.get, credits.get, and the idempotency and decline rules for charges and
|
|
594
|
-
reversals.
|
|
721
|
+
reversals. It also checks that another site can't read your session route.
|
|
595
722
|
|
|
596
|
-
--url
|
|
597
|
-
--user
|
|
598
|
-
|
|
599
|
-
--
|
|
600
|
-
|
|
601
|
-
--
|
|
602
|
-
|
|
723
|
+
--url The endpoint Relume calls, e.g. https://partner.example/relume/calls
|
|
724
|
+
--user A user your wallet knows, with at least ${CHECK_CHARGE_CREDITS} credits. The check
|
|
725
|
+
charges that user and gives every credit back.
|
|
726
|
+
--session-url The session route your page's embed calls. Defaults to
|
|
727
|
+
/session next to a --url ending in /calls.
|
|
728
|
+
--session-header
|
|
729
|
+
A header for every request to the session route, as
|
|
730
|
+
"Name: value". Repeatable. Pass a test user's cookie when a
|
|
731
|
+
login answers that route before the SDK does. Prefer
|
|
732
|
+
$RELUME_SESSION_HEADERS, one header a line: a cookie is a
|
|
733
|
+
credential too.
|
|
734
|
+
--secret-key Your test secret key. Prefer $RELUME_SECRET_KEY, the default:
|
|
735
|
+
a key on the command line lands in your shell history, CI
|
|
736
|
+
logs and the process list.
|
|
737
|
+
--live Allow an sk_live_ key. The check then charges a real wallet.
|
|
738
|
+
-h, --help Show this help.
|
|
603
739
|
|
|
604
740
|
Exits 0 when every check passes, 1 when any fails or can't run, and 2 for
|
|
605
741
|
bad arguments.`;
|
|
742
|
+
var HEADER_NAME = /^[!#$%&'*+.^`|~\w-]+$/;
|
|
743
|
+
var HEADER_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
744
|
+
var PROBE_HEADERS = /^(relume-embed|origin|sec-fetch-.*|access-control-request-.*)$/i;
|
|
606
745
|
var LABEL = {
|
|
607
746
|
pass: "PASS",
|
|
608
747
|
fail: "FAIL",
|
|
@@ -623,6 +762,8 @@ ${USAGE}`);
|
|
|
623
762
|
options: {
|
|
624
763
|
url: { type: "string" },
|
|
625
764
|
user: { type: "string" },
|
|
765
|
+
"session-url": { type: "string" },
|
|
766
|
+
"session-header": { type: "string", multiple: true },
|
|
626
767
|
"secret-key": { type: "string" },
|
|
627
768
|
live: { type: "boolean" },
|
|
628
769
|
help: { type: "boolean", short: "h" }
|
|
@@ -653,6 +794,40 @@ ${USAGE}`);
|
|
|
653
794
|
if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:") {
|
|
654
795
|
return usageError("--url must be an http or https URL.");
|
|
655
796
|
}
|
|
797
|
+
let sessionUrl;
|
|
798
|
+
if (values["session-url"] !== void 0) {
|
|
799
|
+
const value = values["session-url"].trim();
|
|
800
|
+
let parsed2 = null;
|
|
801
|
+
try {
|
|
802
|
+
parsed2 = new URL(value);
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
if (parsed2?.protocol !== "https:" && parsed2?.protocol !== "http:") {
|
|
806
|
+
return usageError(`--session-url must be an http or https URL: ${value}`);
|
|
807
|
+
}
|
|
808
|
+
sessionUrl = parsed2.href;
|
|
809
|
+
}
|
|
810
|
+
const headerLines = values["session-header"] ?? (io.env.RELUME_SESSION_HEADERS ?? "").split("\n").filter((line) => line.trim() !== "");
|
|
811
|
+
let sessionHeaders;
|
|
812
|
+
for (const header of headerLines) {
|
|
813
|
+
const colon = header.indexOf(":");
|
|
814
|
+
const name = header.slice(0, colon).trim();
|
|
815
|
+
const value = header.slice(colon + 1).trim();
|
|
816
|
+
if (colon < 0 || !HEADER_NAME.test(name)) {
|
|
817
|
+
return usageError('Each session header must be "Name: value".');
|
|
818
|
+
}
|
|
819
|
+
if (!HEADER_VALUE.test(value)) {
|
|
820
|
+
return usageError(
|
|
821
|
+
`The ${name} session header's value has characters a header can't carry.`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (PROBE_HEADERS.test(name)) {
|
|
825
|
+
return usageError(
|
|
826
|
+
`A session header can't set ${name}: the check sets it to test the route.`
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
sessionHeaders = { ...sessionHeaders, [name]: value };
|
|
830
|
+
}
|
|
656
831
|
const userId = values.user ?? "";
|
|
657
832
|
if (userId.length < 1 || userId.length > 128) {
|
|
658
833
|
return usageError("--user is required: a user id of 1 to 128 characters.");
|
|
@@ -677,6 +852,8 @@ ${USAGE}`);
|
|
|
677
852
|
url: endpoint.href,
|
|
678
853
|
secretKey,
|
|
679
854
|
userId,
|
|
855
|
+
sessionUrl,
|
|
856
|
+
sessionHeaders,
|
|
680
857
|
fetch: io.fetch,
|
|
681
858
|
onResult: (result) => {
|
|
682
859
|
io.log(`${LABEL[result.status]} ${result.name}`);
|