@relume_io/partner-node 2.0.0 → 2.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/LICENSE +21 -0
- package/README.md +19 -4
- package/dist/bin.cjs +78 -5
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +73 -6
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-HPVVBUR7.js → chunk-6WSDAFUV.js} +189 -1
- package/dist/chunk-6WSDAFUV.js.map +1 -0
- package/dist/index.cjs +35 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +26 -176
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-HPVVBUR7.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
|
@@ -95,6 +95,7 @@ trust where the builder is embedded.
|
|
|
95
95
|
| Bad or missing signature, or stale `t` | `401 invalid_signature`; your callback isn't called |
|
|
96
96
|
| Unknown `type`, or no callback registered | `400 unsupported_call_type` |
|
|
97
97
|
| Body that isn't valid for its `type` | `400 invalid_request` |
|
|
98
|
+
| `environment` isn't your secret key's | `400 environment_mismatch`; your callback isn't called |
|
|
98
99
|
| `onGetUser` returns `null` | `404 user_not_found` |
|
|
99
100
|
| `onChargeCredits` returns `{ ok: false }` | `402 insufficient_credits`, with `balance` |
|
|
100
101
|
| A callback throws, or returns a bad shape | `500 internal_error`, and `onHandlerError` gets the error |
|
|
@@ -107,8 +108,12 @@ Every response is JSON with `Cache-Control: no-store`.
|
|
|
107
108
|
|
|
108
109
|
## The callbacks
|
|
109
110
|
|
|
110
|
-
Each callback gets a second argument, `{ type, requestId }`.
|
|
111
|
-
`requestId` to Relume support.
|
|
111
|
+
Each callback gets a second argument, `{ type, environment, requestId }`.
|
|
112
|
+
Quote the `requestId` to Relume support. `environment` is always your secret
|
|
113
|
+
key's (`'test'` for `sk_test_…`, `'live'` for `sk_live_…`): every call Relume
|
|
114
|
+
signs says which environment it is for, and one for the other environment is
|
|
115
|
+
refused before a callback runs, so a test call can never move a live wallet.
|
|
116
|
+
Still give test and live their own endpoint URLs, each with its own client.
|
|
112
117
|
|
|
113
118
|
### `onChargeCredits(charge) → { ok, balance? }`
|
|
114
119
|
|
|
@@ -192,7 +197,7 @@ body, the call is answered `500` and `onHandlerError` explains why.
|
|
|
192
197
|
|
|
193
198
|
```ts
|
|
194
199
|
Relume.init({
|
|
195
|
-
secretKey: 'sk_test_…', // required;
|
|
200
|
+
secretKey: 'sk_test_…', // required; sk_test_… or sk_live_…
|
|
196
201
|
apiBaseUrl: 'https://…', // defaults to production; http only for localhost
|
|
197
202
|
apiTimeoutMs: 15_000, // per partner API request attempt
|
|
198
203
|
maxRetries: 2, // retries for 429s, 5xx answers and failed connections
|
|
@@ -303,6 +308,8 @@ It calls your endpoint the way Relume does, and checks that it:
|
|
|
303
308
|
- rejects another key's signature, a body changed after signing, a timestamp
|
|
304
309
|
more than 5 minutes off either way, and a call with no signature, with
|
|
305
310
|
`401`;
|
|
311
|
+
- refuses a correctly signed call whose `environment` isn't the key's, with
|
|
312
|
+
`400`;
|
|
306
313
|
- answers `user.get` and `credits.get` with bodies Relume accepts, and
|
|
307
314
|
`user.get` with `404` for a user id it has never seen;
|
|
308
315
|
- debits a charge, and only once when the same `chargeId` comes again, with
|
|
@@ -320,7 +327,11 @@ It calls your endpoint the way Relume does, and checks that it:
|
|
|
320
327
|
signed-in user.
|
|
321
328
|
|
|
322
329
|
It finds the session route next to a `--url` ending in `/calls`; pass
|
|
323
|
-
`--session-url` if yours is elsewhere.
|
|
330
|
+
`--session-url` if yours is elsewhere. If a login answers that route before
|
|
331
|
+
the SDK does, give the check a test user's cookie so it gets past it: in
|
|
332
|
+
`RELUME_SESSION_HEADERS` (one `Name: value` a line), or with
|
|
333
|
+
`--session-header "Cookie: …"` (repeatable). Prefer the variable: a cookie on
|
|
334
|
+
the command line leaks like a key does.
|
|
324
335
|
|
|
325
336
|
Give it the key in `RELUME_SECRET_KEY`, not `--secret-key`: a key on the
|
|
326
337
|
command line lands in your shell history, CI logs and the process list.
|
|
@@ -360,3 +371,7 @@ npm run test:dist # builds, then loads the ESM and CJS output
|
|
|
360
371
|
|
|
361
372
|
Releases go out from CI on a `partner-sdk/v*` tag, together with
|
|
362
373
|
`@relume_io/partner-embed`: see [Releasing](../README.md#releasing).
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
MIT. See [LICENSE](./LICENSE).
|
package/dist/bin.cjs
CHANGED
|
@@ -30,6 +30,13 @@ function isHttpsUrl(value) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/calls.ts
|
|
34
|
+
function keyEnvironment(secretKey) {
|
|
35
|
+
if (secretKey.startsWith("sk_test_")) return "test";
|
|
36
|
+
if (secretKey.startsWith("sk_live_")) return "live";
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
// src/session.ts
|
|
34
41
|
var EMBED_HEADER = "Relume-Embed";
|
|
35
42
|
var EMBED_HEADER_VALUE = "1";
|
|
@@ -116,6 +123,7 @@ var TAMPERED = "rejects a body changed after signing";
|
|
|
116
123
|
var STALE = "rejects a timestamp more than 5 minutes old";
|
|
117
124
|
var FUTURE = "rejects a timestamp more than 5 minutes ahead";
|
|
118
125
|
var UNSIGNED = "rejects a call with no signature";
|
|
126
|
+
var OTHER_ENVIRONMENT = "refuses a call for the other environment";
|
|
119
127
|
var USER_GET = "answers user.get with a valid profile";
|
|
120
128
|
var UNKNOWN_USER = "answers user.get 404 for a user it doesn't know";
|
|
121
129
|
var CREDITS_GET = "answers credits.get with a valid balance";
|
|
@@ -192,6 +200,11 @@ function networkReason(error) {
|
|
|
192
200
|
}
|
|
193
201
|
async function checkEndpoint(options) {
|
|
194
202
|
const { url, secretKey, userId } = options;
|
|
203
|
+
const environment = keyEnvironment(secretKey);
|
|
204
|
+
if (environment === null) {
|
|
205
|
+
throw new TypeError("The secret key must start with sk_test_ or sk_live_.");
|
|
206
|
+
}
|
|
207
|
+
const otherEnvironment = environment === "test" ? "live" : "test";
|
|
195
208
|
const fetchImpl = options.fetch ?? ((...args) => fetch(...args));
|
|
196
209
|
const now = options.now ?? Date.now;
|
|
197
210
|
const nowSeconds = () => Math.floor(now() / 1e3);
|
|
@@ -260,7 +273,7 @@ async function checkEndpoint(options) {
|
|
|
260
273
|
}
|
|
261
274
|
async function readBalance() {
|
|
262
275
|
const answer = await signed(
|
|
263
|
-
{ type: "credits.get", userId },
|
|
276
|
+
{ type: "credits.get", environment, userId },
|
|
264
277
|
READ_TIMEOUT_MS
|
|
265
278
|
);
|
|
266
279
|
if (!isOk(answer)) {
|
|
@@ -282,6 +295,7 @@ async function checkEndpoint(options) {
|
|
|
282
295
|
try {
|
|
283
296
|
const response = await fetchImpl(sessionUrl2, {
|
|
284
297
|
...init,
|
|
298
|
+
headers: { ...options.sessionHeaders, ...init.headers },
|
|
285
299
|
redirect: "manual",
|
|
286
300
|
signal: AbortSignal.timeout(READ_TIMEOUT_MS)
|
|
287
301
|
});
|
|
@@ -354,7 +368,7 @@ async function checkEndpoint(options) {
|
|
|
354
368
|
});
|
|
355
369
|
if (answer.answered && answer.status === status) return null;
|
|
356
370
|
if (answer.answered && answer.status === 401) {
|
|
357
|
-
return `${said(answer)}:
|
|
371
|
+
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`;
|
|
358
372
|
}
|
|
359
373
|
return `${said(answer)}; expected ${status} ${code}`;
|
|
360
374
|
}
|
|
@@ -389,9 +403,10 @@ async function checkEndpoint(options) {
|
|
|
389
403
|
}
|
|
390
404
|
}
|
|
391
405
|
const found = { wrongKey: false, balance: null, charged: null, reversed: false };
|
|
392
|
-
const read = JSON.stringify({ type: "credits.get", userId });
|
|
406
|
+
const read = JSON.stringify({ type: "credits.get", environment, userId });
|
|
393
407
|
const tampered = JSON.stringify({
|
|
394
408
|
type: "credits.get",
|
|
409
|
+
environment,
|
|
395
410
|
userId,
|
|
396
411
|
note: "changed after signing"
|
|
397
412
|
});
|
|
@@ -419,6 +434,7 @@ async function checkEndpoint(options) {
|
|
|
419
434
|
STALE,
|
|
420
435
|
FUTURE,
|
|
421
436
|
UNSIGNED,
|
|
437
|
+
OTHER_ENVIRONMENT,
|
|
422
438
|
USER_GET,
|
|
423
439
|
UNKNOWN_USER,
|
|
424
440
|
CREDITS_GET,
|
|
@@ -453,8 +469,22 @@ async function checkEndpoint(options) {
|
|
|
453
469
|
return `${said(answer)}; expected 401 invalid_signature`;
|
|
454
470
|
});
|
|
455
471
|
}
|
|
472
|
+
await run(OTHER_ENVIRONMENT, async () => {
|
|
473
|
+
const answer = await signed(
|
|
474
|
+
{ type: "credits.get", environment: otherEnvironment, userId },
|
|
475
|
+
READ_TIMEOUT_MS
|
|
476
|
+
);
|
|
477
|
+
if (answer.answered && answer.status === 400) return null;
|
|
478
|
+
if (isOk(answer)) {
|
|
479
|
+
return `${said(answer)} to a ${otherEnvironment} call signed with your ${environment} key: check the body's environment is your key's, and answer 400 environment_mismatch`;
|
|
480
|
+
}
|
|
481
|
+
return `${said(answer)}; expected 400 environment_mismatch`;
|
|
482
|
+
});
|
|
456
483
|
await run(USER_GET, async () => {
|
|
457
|
-
const answer = await signed(
|
|
484
|
+
const answer = await signed(
|
|
485
|
+
{ type: "user.get", environment, userId },
|
|
486
|
+
READ_TIMEOUT_MS
|
|
487
|
+
);
|
|
458
488
|
if (answer.answered && answer.status === 404) {
|
|
459
489
|
return `answered 404: it doesn't know user "${userId}". Pass a --user your wallet knows`;
|
|
460
490
|
}
|
|
@@ -465,7 +495,7 @@ async function checkEndpoint(options) {
|
|
|
465
495
|
});
|
|
466
496
|
await run(UNKNOWN_USER, async () => {
|
|
467
497
|
const answer = await signed(
|
|
468
|
-
{ type: "user.get", userId: randomId("u_check_unknown") },
|
|
498
|
+
{ type: "user.get", environment, userId: randomId("u_check_unknown") },
|
|
469
499
|
READ_TIMEOUT_MS
|
|
470
500
|
);
|
|
471
501
|
return answer.answered && answer.status === 404 ? null : `${said(answer)} for a user id your wallet has never seen; expected 404`;
|
|
@@ -505,6 +535,7 @@ async function checkEndpoint(options) {
|
|
|
505
535
|
const answer = await signed(
|
|
506
536
|
{
|
|
507
537
|
type: "credits.reverse",
|
|
538
|
+
environment,
|
|
508
539
|
userId,
|
|
509
540
|
reversalId: randomId("rv_check"),
|
|
510
541
|
...left,
|
|
@@ -524,6 +555,7 @@ async function checkEndpoint(options) {
|
|
|
524
555
|
const chargeId = randomId("ch_check");
|
|
525
556
|
const charge = JSON.stringify({
|
|
526
557
|
type: "credits.charge",
|
|
558
|
+
environment,
|
|
527
559
|
userId,
|
|
528
560
|
chargeId,
|
|
529
561
|
partnerCredits: CHECK_CHARGE_CREDITS,
|
|
@@ -580,6 +612,7 @@ async function checkEndpoint(options) {
|
|
|
580
612
|
const answer = await signed(
|
|
581
613
|
{
|
|
582
614
|
type: "credits.charge",
|
|
615
|
+
environment,
|
|
583
616
|
userId,
|
|
584
617
|
chargeId: declinedId,
|
|
585
618
|
partnerCredits: amount,
|
|
@@ -610,6 +643,7 @@ async function checkEndpoint(options) {
|
|
|
610
643
|
const answer = await signed(
|
|
611
644
|
{
|
|
612
645
|
type: "credits.reverse",
|
|
646
|
+
environment,
|
|
613
647
|
userId,
|
|
614
648
|
reversalId: randomId("rv_check"),
|
|
615
649
|
partnerCredits: CHECK_CHARGE_CREDITS,
|
|
@@ -639,6 +673,7 @@ async function checkEndpoint(options) {
|
|
|
639
673
|
const answer = await signed(
|
|
640
674
|
{
|
|
641
675
|
type: "credits.charge",
|
|
676
|
+
environment,
|
|
642
677
|
userId,
|
|
643
678
|
chargeId: lateId,
|
|
644
679
|
partnerCredits: CHECK_CHARGE_CREDITS,
|
|
@@ -658,6 +693,7 @@ async function checkEndpoint(options) {
|
|
|
658
693
|
}
|
|
659
694
|
const reversal = JSON.stringify({
|
|
660
695
|
type: "credits.reverse",
|
|
696
|
+
environment,
|
|
661
697
|
userId,
|
|
662
698
|
reversalId: randomId("rv_check"),
|
|
663
699
|
partnerCredits: 1,
|
|
@@ -724,6 +760,12 @@ reversals. It also checks that another site can't read your session route.
|
|
|
724
760
|
charges that user and gives every credit back.
|
|
725
761
|
--session-url The session route your page's embed calls. Defaults to
|
|
726
762
|
/session next to a --url ending in /calls.
|
|
763
|
+
--session-header
|
|
764
|
+
A header for every request to the session route, as
|
|
765
|
+
"Name: value". Repeatable. Pass a test user's cookie when a
|
|
766
|
+
login answers that route before the SDK does. Prefer
|
|
767
|
+
$RELUME_SESSION_HEADERS, one header a line: a cookie is a
|
|
768
|
+
credential too.
|
|
727
769
|
--secret-key Your test secret key. Prefer $RELUME_SECRET_KEY, the default:
|
|
728
770
|
a key on the command line lands in your shell history, CI
|
|
729
771
|
logs and the process list.
|
|
@@ -732,6 +774,9 @@ reversals. It also checks that another site can't read your session route.
|
|
|
732
774
|
|
|
733
775
|
Exits 0 when every check passes, 1 when any fails or can't run, and 2 for
|
|
734
776
|
bad arguments.`;
|
|
777
|
+
var HEADER_NAME = /^[!#$%&'*+.^`|~\w-]+$/;
|
|
778
|
+
var HEADER_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
779
|
+
var PROBE_HEADERS = /^(relume-embed|origin|sec-fetch-.*|access-control-request-.*)$/i;
|
|
735
780
|
var LABEL = {
|
|
736
781
|
pass: "PASS",
|
|
737
782
|
fail: "FAIL",
|
|
@@ -753,6 +798,7 @@ ${USAGE}`);
|
|
|
753
798
|
url: { type: "string" },
|
|
754
799
|
user: { type: "string" },
|
|
755
800
|
"session-url": { type: "string" },
|
|
801
|
+
"session-header": { type: "string", multiple: true },
|
|
756
802
|
"secret-key": { type: "string" },
|
|
757
803
|
live: { type: "boolean" },
|
|
758
804
|
help: { type: "boolean", short: "h" }
|
|
@@ -796,6 +842,27 @@ ${USAGE}`);
|
|
|
796
842
|
}
|
|
797
843
|
sessionUrl = parsed2.href;
|
|
798
844
|
}
|
|
845
|
+
const headerLines = values["session-header"] ?? (io.env.RELUME_SESSION_HEADERS ?? "").split("\n").filter((line) => line.trim() !== "");
|
|
846
|
+
let sessionHeaders;
|
|
847
|
+
for (const header of headerLines) {
|
|
848
|
+
const colon = header.indexOf(":");
|
|
849
|
+
const name = header.slice(0, colon).trim();
|
|
850
|
+
const value = header.slice(colon + 1).trim();
|
|
851
|
+
if (colon < 0 || !HEADER_NAME.test(name)) {
|
|
852
|
+
return usageError('Each session header must be "Name: value".');
|
|
853
|
+
}
|
|
854
|
+
if (!HEADER_VALUE.test(value)) {
|
|
855
|
+
return usageError(
|
|
856
|
+
`The ${name} session header's value has characters a header can't carry.`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
if (PROBE_HEADERS.test(name)) {
|
|
860
|
+
return usageError(
|
|
861
|
+
`A session header can't set ${name}: the check sets it to test the route.`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
sessionHeaders = { ...sessionHeaders, [name]: value };
|
|
865
|
+
}
|
|
799
866
|
const userId = values.user ?? "";
|
|
800
867
|
if (userId.length < 1 || userId.length > 128) {
|
|
801
868
|
return usageError("--user is required: a user id of 1 to 128 characters.");
|
|
@@ -809,6 +876,11 @@ ${USAGE}`);
|
|
|
809
876
|
"That is a publishable key. Pass the secret key (sk_test_\u2026)."
|
|
810
877
|
);
|
|
811
878
|
}
|
|
879
|
+
if (keyEnvironment(secretKey) === null) {
|
|
880
|
+
return usageError(
|
|
881
|
+
"The secret key must start with sk_test_ (or sk_live_ with --live)."
|
|
882
|
+
);
|
|
883
|
+
}
|
|
812
884
|
if (secretKey.startsWith("sk_live_") && !values.live) {
|
|
813
885
|
return usageError(
|
|
814
886
|
"That is a live key, and the check charges the user. Use your test key, or pass --live to run it against a live wallet."
|
|
@@ -821,6 +893,7 @@ ${USAGE}`);
|
|
|
821
893
|
secretKey,
|
|
822
894
|
userId,
|
|
823
895
|
sessionUrl,
|
|
896
|
+
sessionHeaders,
|
|
824
897
|
fetch: io.fetch,
|
|
825
898
|
onResult: (result) => {
|
|
826
899
|
io.log(`${LABEL[result.status]} ${result.name}`);
|