@sdxc/jwt 2026.10.25 → 2026.11.10
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 +20 -0
- package/dist/cli-contract.d.ts +41 -0
- package/dist/cli-contract.js +112 -0
- package/dist/cli-decode.d.ts +13 -0
- package/dist/cli-decode.js +21 -0
- package/dist/cli-input.d.ts +83 -0
- package/dist/cli-input.js +168 -0
- package/dist/cli-jwks.d.ts +12 -0
- package/dist/cli-jwks.js +19 -0
- package/dist/cli-keygen.d.ts +12 -0
- package/dist/cli-keygen.js +20 -0
- package/dist/cli-sign.d.ts +15 -0
- package/dist/cli-sign.js +43 -0
- package/dist/cli-verify.d.ts +19 -0
- package/dist/cli-verify.js +117 -0
- package/dist/cli.d.ts +42 -0
- package/dist/cli.js +21 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -506,6 +506,26 @@ test("a signed token verifies against the published set", async () => {
|
|
|
506
506
|
});
|
|
507
507
|
```
|
|
508
508
|
|
|
509
|
+
## Command line
|
|
510
|
+
|
|
511
|
+
```sh
|
|
512
|
+
bunx @sdxc/jwt --help
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
With [`@sdxc/kit`](https://www.npmjs.com/package/@sdxc/kit) installed, `sdxc pkg jwt …` runs the same command line, using the version your project installs.
|
|
516
|
+
|
|
517
|
+
```sh
|
|
518
|
+
sdxc-jwt keygen --alg ES256 > key.json # a key pair as JSON (--format pem for PEM)
|
|
519
|
+
sdxc-jwt jwks --key-file key.json > jwks.json # its public key set
|
|
520
|
+
echo '{"sub":"user-123","exp":"15m"}' | sdxc-jwt sign --key-env JWT_KEY
|
|
521
|
+
sdxc-jwt decode "$TOKEN" # header and claims, unverified
|
|
522
|
+
sdxc-jwt verify "$TOKEN" --jwks https://auth.example.com/.well-known/jwks.json --iss https://auth.example.com
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Tokens and claims come from an argument, a path or stdin, and keys from `--key-file` or `--key-env`.
|
|
526
|
+
`decode` and `verify` take `--json`. `verify` exits 1 on an invalid token and 2 when its input or
|
|
527
|
+
keys cannot be read.
|
|
528
|
+
|
|
509
529
|
## Versioning
|
|
510
530
|
|
|
511
531
|
Releases are dated rather than semantic. A version is the UTC date it was published, written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one release goes out per day.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt` command line as a contract: `decode`, `verify`, `sign`, `keygen` and `jwks`,
|
|
3
|
+
* with their arguments and flags. Help, usage errors and every handler's typed `ctx.flags`
|
|
4
|
+
* come from this one declaration.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/** Every command `sdxc-jwt` takes. */
|
|
10
|
+
export declare const JWT_COMMANDS: import("@sdxc/cli").Group<Omit<{
|
|
11
|
+
readonly description: "sdxc-jwt — decode, verify and sign JSON Web Tokens, and generate their keys";
|
|
12
|
+
readonly decode: import("@sdxc/cli").Command<{
|
|
13
|
+
readonly token: import("@sdxc/cli").Argument<string>;
|
|
14
|
+
}, {
|
|
15
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
16
|
+
}>;
|
|
17
|
+
readonly verify: import("@sdxc/cli").Command<{
|
|
18
|
+
readonly token: import("@sdxc/cli").Argument<string>;
|
|
19
|
+
}, {
|
|
20
|
+
readonly jwks: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
21
|
+
readonly keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
22
|
+
readonly iss: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
23
|
+
readonly aud: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
24
|
+
readonly alg: import("@sdxc/cli").ValueFlag<"ES256" | "RS256" | "Ed25519", false, true, false>;
|
|
25
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
26
|
+
}>;
|
|
27
|
+
readonly sign: import("@sdxc/cli").Command<{
|
|
28
|
+
readonly claims: import("@sdxc/cli").Argument<string>;
|
|
29
|
+
}, {
|
|
30
|
+
keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
31
|
+
keyEnv: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
32
|
+
}>;
|
|
33
|
+
readonly keygen: import("@sdxc/cli").Command<Readonly<Record<string, import("@sdxc/cli").Argument<unknown>>>, {
|
|
34
|
+
readonly alg: import("@sdxc/cli").ValueFlag<"ES256" | "RS256" | "Ed25519", true, false, false>;
|
|
35
|
+
readonly format: import("@sdxc/cli").ValueFlag<"pair" | "pem", true, false, false>;
|
|
36
|
+
}>;
|
|
37
|
+
readonly jwks: import("@sdxc/cli").Command<Readonly<Record<string, import("@sdxc/cli").Argument<unknown>>>, {
|
|
38
|
+
keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
39
|
+
keyEnv: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
40
|
+
}>;
|
|
41
|
+
}, "description" | "hidden" | "arguments" | "flags" | "tasks">, Record<never, never>, Record<never, never>>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt` command line as a contract: `decode`, `verify`, `sign`, `keygen` and `jwks`,
|
|
3
|
+
* with their arguments and flags. Help, usage errors and every handler's typed `ctx.flags`
|
|
4
|
+
* come from this one declaration.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { argument, command, commands, flag } from "@sdxc/cli";
|
|
10
|
+
import * as s from "remix/data-schema";
|
|
11
|
+
import * as Algorithm from "./algorithm.js";
|
|
12
|
+
/** Every algorithm a key can be generated for or a token verified under. */
|
|
13
|
+
const ALGORITHMS = [Algorithm.ES256, Algorithm.RS256, Algorithm.Ed25519];
|
|
14
|
+
/** A token given on the command line, or `-` (the default) to read it from stdin. */
|
|
15
|
+
const TOKEN = argument(s.string(), {
|
|
16
|
+
default: "-",
|
|
17
|
+
valueName: "token",
|
|
18
|
+
description: "The compact token, or - to read it from stdin",
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Where a key pair from `sdxc-jwt keygen` comes from: a file or a named environment
|
|
22
|
+
* variable, so the private key stays out of shell history and process listings.
|
|
23
|
+
*/
|
|
24
|
+
const KEY_SOURCE = {
|
|
25
|
+
keyFile: flag.string("key-file", {
|
|
26
|
+
valueName: "path",
|
|
27
|
+
description: "Read the key pair from this file (- for stdin)",
|
|
28
|
+
heading: "Key",
|
|
29
|
+
}),
|
|
30
|
+
keyEnv: flag.string("key-env", {
|
|
31
|
+
valueName: "name",
|
|
32
|
+
description: "Read the key pair from this environment variable",
|
|
33
|
+
heading: "Key",
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
/** `--json`, for every command that prints a report. */
|
|
37
|
+
const JSON_FLAG = flag.boolean("json", { description: "Print the report as JSON" });
|
|
38
|
+
/** Every command `sdxc-jwt` takes. */
|
|
39
|
+
export const JWT_COMMANDS = commands({
|
|
40
|
+
description: "sdxc-jwt — decode, verify and sign JSON Web Tokens, and generate their keys",
|
|
41
|
+
decode: command({
|
|
42
|
+
description: "Print a token's header and claims without verifying it",
|
|
43
|
+
arguments: { token: TOKEN },
|
|
44
|
+
flags: { json: JSON_FLAG },
|
|
45
|
+
examples: ["decode eyJhbGciOi…", "decode < token.txt --json"],
|
|
46
|
+
}),
|
|
47
|
+
verify: command({
|
|
48
|
+
description: "Check a token's signature, expiry and any expected issuer or audience; exits 1 when it is invalid",
|
|
49
|
+
arguments: { token: TOKEN },
|
|
50
|
+
flags: {
|
|
51
|
+
jwks: flag.string("jwks", {
|
|
52
|
+
valueName: "url",
|
|
53
|
+
description: "Verify against the key set served at this URL",
|
|
54
|
+
heading: "Keys",
|
|
55
|
+
}),
|
|
56
|
+
keyFile: flag.string("key-file", {
|
|
57
|
+
valueName: "path",
|
|
58
|
+
description: "Verify against a JWKS, a public JWK or a key pair from keygen",
|
|
59
|
+
heading: "Keys",
|
|
60
|
+
}),
|
|
61
|
+
iss: flag.string("iss", { valueName: "issuer", description: "Require this issuer" }),
|
|
62
|
+
aud: flag.string("aud", { valueName: "audience", description: "Require this audience" }),
|
|
63
|
+
alg: flag
|
|
64
|
+
.string("alg", {
|
|
65
|
+
schema: s.enum_(ALGORITHMS),
|
|
66
|
+
description: "Accept only this algorithm (repeatable; default: every supported one)",
|
|
67
|
+
})
|
|
68
|
+
.multiple(),
|
|
69
|
+
json: JSON_FLAG,
|
|
70
|
+
},
|
|
71
|
+
examples: [
|
|
72
|
+
"verify $TOKEN --jwks https://auth.example.com/.well-known/jwks.json --iss https://auth.example.com",
|
|
73
|
+
"verify --key-file jwks.json --aud api < token.txt --json",
|
|
74
|
+
],
|
|
75
|
+
}),
|
|
76
|
+
sign: command({
|
|
77
|
+
description: "Sign a JSON object of claims; exp, iat and nbf may be durations such as 1h",
|
|
78
|
+
arguments: {
|
|
79
|
+
claims: argument(s.string(), {
|
|
80
|
+
default: "-",
|
|
81
|
+
valueName: "claims.json",
|
|
82
|
+
description: "A JSON file of claims, or - to read them from stdin",
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
flags: KEY_SOURCE,
|
|
86
|
+
examples: [
|
|
87
|
+
"sign claims.json --key-file key.json",
|
|
88
|
+
`echo '{"sub":"user-123","exp":"15m"}' | sign --key-env JWT_KEY`,
|
|
89
|
+
],
|
|
90
|
+
}),
|
|
91
|
+
keygen: command({
|
|
92
|
+
description: "Generate a signing key pair, as the JSON sign and verify read or as PEM for other tools",
|
|
93
|
+
flags: {
|
|
94
|
+
alg: flag.string("alg", {
|
|
95
|
+
default: Algorithm.ES256,
|
|
96
|
+
schema: s.enum_(ALGORITHMS),
|
|
97
|
+
description: `Algorithm to generate for: ${ALGORITHMS.join(", ")}`,
|
|
98
|
+
}),
|
|
99
|
+
format: flag.string("format", {
|
|
100
|
+
default: "pair",
|
|
101
|
+
schema: s.enum_(["pair", "pem"]),
|
|
102
|
+
description: "pair (JSON with id, alg and both PEMs) or pem (private then public key)",
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
examples: ["keygen > key.json", "keygen --alg Ed25519 --format pem"],
|
|
106
|
+
}),
|
|
107
|
+
jwks: command({
|
|
108
|
+
description: "Print the public key set for a key pair, ready to serve as /.well-known/jwks.json",
|
|
109
|
+
flags: KEY_SOURCE,
|
|
110
|
+
examples: ["jwks --key-file key.json > jwks.json"],
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt decode` handler: prints a token's header and claims as the token presents them,
|
|
3
|
+
* with no key and no checks, for reading a token before deciding how to verify it.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<{
|
|
9
|
+
readonly token: import("@sdxc/cli").Argument<string>;
|
|
10
|
+
}, {
|
|
11
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
12
|
+
}>, []>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt decode` handler: prints a token's header and claims as the token presents them,
|
|
3
|
+
* with no key and no checks, for reading a token before deciding how to verify it.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import { createCommand, success } from "@sdxc/cli";
|
|
9
|
+
import { isFailure } from "@sdxc/result";
|
|
10
|
+
import { JWT_COMMANDS } from "./cli-contract.js";
|
|
11
|
+
import { decodeToken, formatToken, readToken } from "./cli-input.js";
|
|
12
|
+
export default createCommand(JWT_COMMANDS.decode, async (ctx) => {
|
|
13
|
+
let token = await readToken(ctx.arguments.token, ctx);
|
|
14
|
+
if (isFailure(token))
|
|
15
|
+
return token;
|
|
16
|
+
let decoded = decodeToken(token.data);
|
|
17
|
+
if (isFailure(decoded))
|
|
18
|
+
return decoded;
|
|
19
|
+
ctx.stdout(ctx.flags.json ? `${JSON.stringify(decoded.data)}\n` : formatToken(decoded.data));
|
|
20
|
+
return success();
|
|
21
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input every `sdxc-jwt` command shares: a token from an argument or stdin, a file or stdin
|
|
3
|
+
* read whole, and a key pair from a file or an environment variable. Input that cannot be read
|
|
4
|
+
* or parsed answers an exit-2 failure naming its source, so a handler returns it as it is.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Env, ExitError, Stdin } from "@sdxc/cli";
|
|
10
|
+
import type { Result } from "@sdxc/result";
|
|
11
|
+
import type { ProtectedHeaderParameters } from "jose";
|
|
12
|
+
import { JWK } from "./jwk.js";
|
|
13
|
+
import { JWT } from "./jwt.js";
|
|
14
|
+
/** What a command reads its input through. */
|
|
15
|
+
export interface InputContext {
|
|
16
|
+
cwd: string;
|
|
17
|
+
env: Env;
|
|
18
|
+
stdin: Stdin;
|
|
19
|
+
}
|
|
20
|
+
/** A decoded token: its protected header and its claims, both as the token presents them. */
|
|
21
|
+
export interface DecodedToken {
|
|
22
|
+
header: ProtectedHeaderParameters;
|
|
23
|
+
payload: JWT.Payload;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The message of anything thrown, for a diagnostic line.
|
|
27
|
+
*
|
|
28
|
+
* @param error - What was caught.
|
|
29
|
+
*/
|
|
30
|
+
export declare function messageOf(error: unknown): string;
|
|
31
|
+
/**
|
|
32
|
+
* Reads a file whole, or stdin when `source` is `-`. A terminal on stdin is refused, so a
|
|
33
|
+
* command run with nothing piped exits at once instead of waiting.
|
|
34
|
+
*
|
|
35
|
+
* @param source - A path relative to the run's directory, or `-`.
|
|
36
|
+
* @param ctx - The run's directory and stdin.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readSource(source: string, ctx: InputContext): Promise<Result<string, ExitError>>;
|
|
39
|
+
/**
|
|
40
|
+
* The token a command was given, trimmed, so a file with a trailing newline reads as the
|
|
41
|
+
* token it holds.
|
|
42
|
+
*
|
|
43
|
+
* @param token - The argument as typed, `-` for stdin.
|
|
44
|
+
* @param ctx - The run's stdin.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readToken(token: string, ctx: InputContext): Promise<Result<string, ExitError>>;
|
|
47
|
+
/**
|
|
48
|
+
* Parses JSON read from `source`.
|
|
49
|
+
*
|
|
50
|
+
* @param text - The text read.
|
|
51
|
+
* @param source - Where it came from, for the diagnostic.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseJSON(text: string, source: string): Result<unknown, ExitError>;
|
|
54
|
+
/**
|
|
55
|
+
* A token's header and claims, read without verifying the signature.
|
|
56
|
+
*
|
|
57
|
+
* @param token - The compact token.
|
|
58
|
+
*/
|
|
59
|
+
export declare function decodeToken(token: string): Result<DecodedToken, ExitError>;
|
|
60
|
+
/**
|
|
61
|
+
* A decoded token for people: the header and the claims as indented JSON, and when the
|
|
62
|
+
* token expires.
|
|
63
|
+
*
|
|
64
|
+
* @param decoded - The decoded token.
|
|
65
|
+
*/
|
|
66
|
+
export declare function formatToken(decoded: DecodedToken): string;
|
|
67
|
+
/**
|
|
68
|
+
* Imports a key pair in the shape `sdxc-jwt keygen` writes.
|
|
69
|
+
*
|
|
70
|
+
* @param value - The parsed JSON.
|
|
71
|
+
* @param source - Where it came from, for the diagnostic.
|
|
72
|
+
*/
|
|
73
|
+
export declare function importKeyPair(value: unknown, source: string): Promise<Result<JWK.KeyPair, ExitError>>;
|
|
74
|
+
/**
|
|
75
|
+
* The key pair named by exactly one of `--key-file` and `--key-env`.
|
|
76
|
+
*
|
|
77
|
+
* @param flags - The two flags as given.
|
|
78
|
+
* @param ctx - The run's directory, environment and stdin.
|
|
79
|
+
*/
|
|
80
|
+
export declare function readKeyPair(flags: {
|
|
81
|
+
keyFile?: string;
|
|
82
|
+
keyEnv?: string;
|
|
83
|
+
}, ctx: InputContext): Promise<Result<JWK.KeyPair, ExitError>>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input every `sdxc-jwt` command shares: a token from an argument or stdin, a file or stdin
|
|
3
|
+
* read whole, and a key pair from a file or an environment variable. Input that cannot be read
|
|
4
|
+
* or parsed answers an exit-2 failure naming its source, so a handler returns it as it is.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { EXIT_CODES, exit, success } from "@sdxc/cli";
|
|
12
|
+
import { isFailure } from "@sdxc/result";
|
|
13
|
+
import { decodeProtectedHeader } from "jose";
|
|
14
|
+
import * as s from "remix/data-schema";
|
|
15
|
+
import * as Algorithm from "./algorithm.js";
|
|
16
|
+
import { JWK, LEGACY_EDDSA } from "./jwk.js";
|
|
17
|
+
import { JWT } from "./jwt.js";
|
|
18
|
+
/** The JSON `sdxc-jwt keygen` writes and `JWK.generateKeyPair` answers. */
|
|
19
|
+
const SERIALIZED_KEY_PAIR = s.object({
|
|
20
|
+
id: s.string(),
|
|
21
|
+
alg: s.enum_([Algorithm.ES256, Algorithm.RS256, Algorithm.Ed25519, LEGACY_EDDSA]),
|
|
22
|
+
publicKey: s.string(),
|
|
23
|
+
privateKey: s.string(),
|
|
24
|
+
created: s.number(),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* The message of anything thrown, for a diagnostic line.
|
|
28
|
+
*
|
|
29
|
+
* @param error - What was caught.
|
|
30
|
+
*/
|
|
31
|
+
export function messageOf(error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reads a file whole, or stdin when `source` is `-`. A terminal on stdin is refused, so a
|
|
36
|
+
* command run with nothing piped exits at once instead of waiting.
|
|
37
|
+
*
|
|
38
|
+
* @param source - A path relative to the run's directory, or `-`.
|
|
39
|
+
* @param ctx - The run's directory and stdin.
|
|
40
|
+
*/
|
|
41
|
+
export async function readSource(source, ctx) {
|
|
42
|
+
if (source === "-") {
|
|
43
|
+
if (ctx.stdin.isTTY)
|
|
44
|
+
return exit(EXIT_CODES.usage, "Nothing piped on stdin");
|
|
45
|
+
return success(await ctx.stdin.text());
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return success(await readFile(resolve(ctx.cwd, source), "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return exit(EXIT_CODES.usage, `Cannot read ${source}: ${messageOf(error)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The token a command was given, trimmed, so a file with a trailing newline reads as the
|
|
56
|
+
* token it holds.
|
|
57
|
+
*
|
|
58
|
+
* @param token - The argument as typed, `-` for stdin.
|
|
59
|
+
* @param ctx - The run's stdin.
|
|
60
|
+
*/
|
|
61
|
+
export async function readToken(token, ctx) {
|
|
62
|
+
let text = token === "-" ? await readSource(token, ctx) : success(token);
|
|
63
|
+
if (isFailure(text))
|
|
64
|
+
return text;
|
|
65
|
+
let trimmed = text.data.trim();
|
|
66
|
+
if (trimmed === "")
|
|
67
|
+
return exit(EXIT_CODES.usage, "No token given");
|
|
68
|
+
return success(trimmed);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parses JSON read from `source`.
|
|
72
|
+
*
|
|
73
|
+
* @param text - The text read.
|
|
74
|
+
* @param source - Where it came from, for the diagnostic.
|
|
75
|
+
*/
|
|
76
|
+
export function parseJSON(text, source) {
|
|
77
|
+
try {
|
|
78
|
+
return success(JSON.parse(text));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return exit(EXIT_CODES.usage, `${source} is not valid JSON: ${messageOf(error)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A token's header and claims, read without verifying the signature.
|
|
86
|
+
*
|
|
87
|
+
* @param token - The compact token.
|
|
88
|
+
*/
|
|
89
|
+
export function decodeToken(token) {
|
|
90
|
+
try {
|
|
91
|
+
return success({ header: decodeProtectedHeader(token), payload: JWT.decode(token).payload });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return exit(EXIT_CODES.usage, `Not a JWT: ${messageOf(error)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A decoded token for people: the header and the claims as indented JSON, and when the
|
|
99
|
+
* token expires.
|
|
100
|
+
*
|
|
101
|
+
* @param decoded - The decoded token.
|
|
102
|
+
*/
|
|
103
|
+
export function formatToken(decoded) {
|
|
104
|
+
let lines = [
|
|
105
|
+
"Header",
|
|
106
|
+
JSON.stringify(decoded.header, null, 2),
|
|
107
|
+
"",
|
|
108
|
+
"Payload",
|
|
109
|
+
JSON.stringify(decoded.payload, null, 2),
|
|
110
|
+
];
|
|
111
|
+
let token = new JWT(decoded.payload);
|
|
112
|
+
if (token.expiresAt) {
|
|
113
|
+
let state = token.expired ? "expired" : "expires";
|
|
114
|
+
lines.push("", `Token ${state} at ${token.expiresAt.toISOString()}`);
|
|
115
|
+
}
|
|
116
|
+
return `${lines.join("\n")}\n`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Imports a key pair in the shape `sdxc-jwt keygen` writes.
|
|
120
|
+
*
|
|
121
|
+
* @param value - The parsed JSON.
|
|
122
|
+
* @param source - Where it came from, for the diagnostic.
|
|
123
|
+
*/
|
|
124
|
+
export async function importKeyPair(value, source) {
|
|
125
|
+
let parsed = s.parseSafe(SERIALIZED_KEY_PAIR, value);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
return exit(EXIT_CODES.usage, `${source} is not a key pair from sdxc-jwt keygen`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return success(await JWK.importKeyPair(parsed.value));
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return exit(EXIT_CODES.usage, `${source} holds an unusable key pair: ${messageOf(error)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The text of the key pair named by `--key-file` or `--key-env`.
|
|
138
|
+
*
|
|
139
|
+
* @param flags - The two flags as given; `keyFile` wins when both are.
|
|
140
|
+
* @param ctx - The run's directory, environment and stdin.
|
|
141
|
+
*/
|
|
142
|
+
async function readKeyText(flags, ctx) {
|
|
143
|
+
if (flags.keyFile !== undefined)
|
|
144
|
+
return readSource(flags.keyFile, ctx);
|
|
145
|
+
let value = ctx.env[flags.keyEnv ?? ""];
|
|
146
|
+
if (!value)
|
|
147
|
+
return exit(EXIT_CODES.usage, `$${flags.keyEnv} is not set`);
|
|
148
|
+
return success(value);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The key pair named by exactly one of `--key-file` and `--key-env`.
|
|
152
|
+
*
|
|
153
|
+
* @param flags - The two flags as given.
|
|
154
|
+
* @param ctx - The run's directory, environment and stdin.
|
|
155
|
+
*/
|
|
156
|
+
export async function readKeyPair(flags, ctx) {
|
|
157
|
+
if ((flags.keyFile === undefined) === (flags.keyEnv === undefined)) {
|
|
158
|
+
return exit(EXIT_CODES.usage, "Pass exactly one of --key-file <path> or --key-env <name>");
|
|
159
|
+
}
|
|
160
|
+
let source = flags.keyFile ?? `$${flags.keyEnv}`;
|
|
161
|
+
let text = await readKeyText(flags, ctx);
|
|
162
|
+
if (isFailure(text))
|
|
163
|
+
return text;
|
|
164
|
+
let json = parseJSON(text.data, source);
|
|
165
|
+
if (isFailure(json))
|
|
166
|
+
return json;
|
|
167
|
+
return importKeyPair(json.data, source);
|
|
168
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt jwks` handler: prints the public key set for a key pair, carrying only the
|
|
3
|
+
* public members, so a static issuer can serve it as `/.well-known/jwks.json`.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<Readonly<Record<string, import("@sdxc/cli").Argument<unknown>>>, {
|
|
9
|
+
keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
10
|
+
keyEnv: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
11
|
+
}>, []>;
|
|
12
|
+
export default _default;
|
package/dist/cli-jwks.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt jwks` handler: prints the public key set for a key pair, carrying only the
|
|
3
|
+
* public members, so a static issuer can serve it as `/.well-known/jwks.json`.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import { createCommand, success } from "@sdxc/cli";
|
|
9
|
+
import { isFailure } from "@sdxc/result";
|
|
10
|
+
import { JWT_COMMANDS } from "./cli-contract.js";
|
|
11
|
+
import { readKeyPair } from "./cli-input.js";
|
|
12
|
+
import { JWK } from "./jwk.js";
|
|
13
|
+
export default createCommand(JWT_COMMANDS.jwks, async (ctx) => {
|
|
14
|
+
let pair = await readKeyPair(ctx.flags, ctx);
|
|
15
|
+
if (isFailure(pair))
|
|
16
|
+
return pair;
|
|
17
|
+
ctx.stdout(`${JSON.stringify(JWK.toJSON([pair.data]), null, 2)}\n`);
|
|
18
|
+
return success();
|
|
19
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt keygen` handler: generates a key pair and prints it as the JSON `sign`, `jwks`
|
|
3
|
+
* and `verify --key-file` read, or as PKCS#8 and SPKI PEM for tools outside this package.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<Readonly<Record<string, import("@sdxc/cli").Argument<unknown>>>, {
|
|
9
|
+
readonly alg: import("@sdxc/cli").ValueFlag<"ES256" | "RS256" | "Ed25519", true, false, false>;
|
|
10
|
+
readonly format: import("@sdxc/cli").ValueFlag<"pair" | "pem", true, false, false>;
|
|
11
|
+
}>, []>;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt keygen` handler: generates a key pair and prints it as the JSON `sign`, `jwks`
|
|
3
|
+
* and `verify --key-file` read, or as PKCS#8 and SPKI PEM for tools outside this package.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
import { createCommand, success } from "@sdxc/cli";
|
|
9
|
+
import { JWT_COMMANDS } from "./cli-contract.js";
|
|
10
|
+
import { JWK } from "./jwk.js";
|
|
11
|
+
export default createCommand(JWT_COMMANDS.keygen, async (ctx) => {
|
|
12
|
+
let pair = await JWK.generateKeyPair(ctx.flags.alg);
|
|
13
|
+
if (ctx.flags.format === "pem") {
|
|
14
|
+
ctx.stdout(`${pair.privateKey.trim()}\n${pair.publicKey.trim()}\n`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
ctx.stdout(`${JSON.stringify(pair, null, 2)}\n`);
|
|
18
|
+
}
|
|
19
|
+
return success();
|
|
20
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt sign` handler: signs a JSON object of claims with a key pair from a file or an
|
|
3
|
+
* environment variable and prints the compact token. `exp`, `iat` and `nbf` may be durations
|
|
4
|
+
* measured from now, as `new JWT(claims)` reads them.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<{
|
|
10
|
+
readonly claims: import("@sdxc/cli").Argument<string>;
|
|
11
|
+
}, {
|
|
12
|
+
keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
13
|
+
keyEnv: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
14
|
+
}>, []>;
|
|
15
|
+
export default _default;
|
package/dist/cli-sign.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt sign` handler: signs a JSON object of claims with a key pair from a file or an
|
|
3
|
+
* environment variable and prints the compact token. `exp`, `iat` and `nbf` may be durations
|
|
4
|
+
* measured from now, as `new JWT(claims)` reads them.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { createCommand, EXIT_CODES, exit, success, usage } from "@sdxc/cli";
|
|
10
|
+
import { isFailure } from "@sdxc/result";
|
|
11
|
+
import * as s from "remix/data-schema";
|
|
12
|
+
import { JWT_COMMANDS } from "./cli-contract.js";
|
|
13
|
+
import { messageOf, parseJSON, readKeyPair, readSource } from "./cli-input.js";
|
|
14
|
+
import { JWT } from "./jwt.js";
|
|
15
|
+
/** A claim set: a JSON object, any members. */
|
|
16
|
+
const CLAIMS = s.record(s.string(), s.any());
|
|
17
|
+
export default createCommand(JWT_COMMANDS.sign, async (ctx) => {
|
|
18
|
+
let source = ctx.arguments.claims;
|
|
19
|
+
if (source === "-" && ctx.flags.keyFile === "-") {
|
|
20
|
+
return usage("Only one of the claims and the key can come from stdin");
|
|
21
|
+
}
|
|
22
|
+
let pair = await readKeyPair(ctx.flags, ctx);
|
|
23
|
+
if (isFailure(pair))
|
|
24
|
+
return pair;
|
|
25
|
+
let text = await readSource(source, ctx);
|
|
26
|
+
if (isFailure(text))
|
|
27
|
+
return text;
|
|
28
|
+
let json = parseJSON(text.data, source === "-" ? "stdin" : source);
|
|
29
|
+
if (isFailure(json))
|
|
30
|
+
return json;
|
|
31
|
+
let claims = s.parseSafe(CLAIMS, json.data);
|
|
32
|
+
if (!claims.success || Array.isArray(json.data)) {
|
|
33
|
+
return exit(EXIT_CODES.usage, "The claims must be a JSON object");
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
let token = new JWT(claims.value);
|
|
37
|
+
ctx.stdout(`${await token.sign(pair.data.alg, [pair.data])}\n`);
|
|
38
|
+
return success();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return exit(EXIT_CODES.usage, `Cannot sign these claims: ${messageOf(error)}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt verify` handler: checks a token's signature, `exp` and `nbf`, plus any issuer,
|
|
3
|
+
* audience and algorithms asked for, against a key set from a URL or a file. A token that fails
|
|
4
|
+
* exits 1; keys that cannot be read exit 2, so a script tells a bad token from a bad setup.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: import("@sdxc/cli").CommandAction<import("@sdxc/cli").Command<{
|
|
10
|
+
readonly token: import("@sdxc/cli").Argument<string>;
|
|
11
|
+
}, {
|
|
12
|
+
readonly jwks: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
13
|
+
readonly keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
14
|
+
readonly iss: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
15
|
+
readonly aud: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
16
|
+
readonly alg: import("@sdxc/cli").ValueFlag<"ES256" | "RS256" | "Ed25519", false, true, false>;
|
|
17
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
18
|
+
}>, []>;
|
|
19
|
+
export default _default;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sdxc-jwt verify` handler: checks a token's signature, `exp` and `nbf`, plus any issuer,
|
|
3
|
+
* audience and algorithms asked for, against a key set from a URL or a file. A token that fails
|
|
4
|
+
* exits 1; keys that cannot be read exit 2, so a script tells a bad token from a bad setup.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { createCommand, EXIT_CODES, exit, failure, success, usage } from "@sdxc/cli";
|
|
10
|
+
import { isFailure } from "@sdxc/result";
|
|
11
|
+
import * as s from "remix/data-schema";
|
|
12
|
+
import { JWT_COMMANDS } from "./cli-contract.js";
|
|
13
|
+
import { decodeToken, formatToken, importKeyPair, messageOf, parseJSON, readSource, readToken, } from "./cli-input.js";
|
|
14
|
+
import { JWK } from "./jwk.js";
|
|
15
|
+
import { JWT } from "./jwt.js";
|
|
16
|
+
/** How long a key set URL has to answer before the run gives up on it. */
|
|
17
|
+
const JWKS_TIMEOUT_MS = 10_000;
|
|
18
|
+
/** One JWK, keeping every member: RFC 7517 requires only `kty`. */
|
|
19
|
+
const KEY = s.object({ kty: s.string() }, { unknownKeys: "passthrough" });
|
|
20
|
+
/** A JWKS document. */
|
|
21
|
+
const KEY_SET = s.object({ keys: s.array(KEY) });
|
|
22
|
+
/**
|
|
23
|
+
* Fetches the key set served at `url`.
|
|
24
|
+
*
|
|
25
|
+
* @param url - What `--jwks` was given.
|
|
26
|
+
* @param signal - Aborted when the run is cancelled.
|
|
27
|
+
*/
|
|
28
|
+
async function fetchKeys(url, signal) {
|
|
29
|
+
let parsed = URL.parse(url);
|
|
30
|
+
if (!parsed || (parsed.protocol !== "https:" && parsed.protocol !== "http:")) {
|
|
31
|
+
return exit(EXIT_CODES.usage, `--jwks expects an http(s) URL; got ${url}`);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
let response = await fetch(parsed, {
|
|
35
|
+
headers: { accept: "application/json" },
|
|
36
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(JWKS_TIMEOUT_MS)]),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
return exit(EXIT_CODES.usage, `${url} answered ${response.status}`);
|
|
40
|
+
return parseJSON(await response.text(), url);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return exit(EXIT_CODES.usage, `Cannot fetch ${url}: ${messageOf(error)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The key set in `value`, which may be a JWKS document, one public JWK, or a key pair from
|
|
48
|
+
* `sdxc-jwt keygen` (its public half).
|
|
49
|
+
*
|
|
50
|
+
* @param value - The parsed JSON.
|
|
51
|
+
* @param source - Where it came from, for the diagnostic.
|
|
52
|
+
*/
|
|
53
|
+
async function toKeySet(value, source) {
|
|
54
|
+
let set = s.parseSafe(KEY_SET, value);
|
|
55
|
+
if (set.success)
|
|
56
|
+
return success(set.value);
|
|
57
|
+
let key = s.parseSafe(KEY, value);
|
|
58
|
+
if (key.success)
|
|
59
|
+
return success({ keys: [key.value] });
|
|
60
|
+
let pair = await importKeyPair(value, source);
|
|
61
|
+
if (isFailure(pair)) {
|
|
62
|
+
return exit(EXIT_CODES.usage, `${source} is not a JWKS, a JWK or a key pair from keygen`);
|
|
63
|
+
}
|
|
64
|
+
return success(JWK.toJSON([pair.data]));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The key set named by exactly one of `--jwks` and `--key-file`.
|
|
68
|
+
*
|
|
69
|
+
* @param flags - The two flags as given.
|
|
70
|
+
* @param ctx - The run's directory, stdin and abort signal.
|
|
71
|
+
*/
|
|
72
|
+
async function readKeys(flags, ctx) {
|
|
73
|
+
let source = flags.jwks ?? flags.keyFile ?? "";
|
|
74
|
+
let text = flags.jwks
|
|
75
|
+
? await fetchKeys(flags.jwks, ctx.signal)
|
|
76
|
+
: await readSource(source, ctx).then((read) => isFailure(read) ? read : parseJSON(read.data, source));
|
|
77
|
+
if (isFailure(text))
|
|
78
|
+
return text;
|
|
79
|
+
return toKeySet(text.data, source);
|
|
80
|
+
}
|
|
81
|
+
export default createCommand(JWT_COMMANDS.verify, async (ctx) => {
|
|
82
|
+
let { jwks, keyFile, iss, aud, alg, json } = ctx.flags;
|
|
83
|
+
if ((jwks === undefined) === (keyFile === undefined)) {
|
|
84
|
+
return usage("Pass exactly one of --jwks <url> or --key-file <path>");
|
|
85
|
+
}
|
|
86
|
+
if (keyFile === "-" && ctx.arguments.token === "-") {
|
|
87
|
+
return usage("Only one of the token and the key set can come from stdin");
|
|
88
|
+
}
|
|
89
|
+
let token = await readToken(ctx.arguments.token, ctx);
|
|
90
|
+
if (isFailure(token))
|
|
91
|
+
return token;
|
|
92
|
+
let keys = await readKeys({ jwks, keyFile }, ctx);
|
|
93
|
+
if (isFailure(keys))
|
|
94
|
+
return keys;
|
|
95
|
+
try {
|
|
96
|
+
await JWT.verify(token.data, await JWK.importLocal(keys.data), {
|
|
97
|
+
issuer: iss,
|
|
98
|
+
audience: aud,
|
|
99
|
+
algorithms: alg.length > 0 ? alg : undefined,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
let reason = messageOf(error);
|
|
104
|
+
if (!json)
|
|
105
|
+
return failure(new Error(`Invalid token: ${reason}`));
|
|
106
|
+
ctx.stdout(`${JSON.stringify({ valid: false, error: reason })}\n`);
|
|
107
|
+
return exit(EXIT_CODES.failure);
|
|
108
|
+
}
|
|
109
|
+
let decoded = decodeToken(token.data);
|
|
110
|
+
if (isFailure(decoded))
|
|
111
|
+
return decoded;
|
|
112
|
+
if (json)
|
|
113
|
+
ctx.stdout(`${JSON.stringify({ valid: true, ...decoded.data })}\n`);
|
|
114
|
+
else
|
|
115
|
+
ctx.stdout(`Valid token\n\n${formatToken(decoded.data)}`);
|
|
116
|
+
return success();
|
|
117
|
+
});
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `sdxc-jwt` command-line program: the contract in `cli-contract.ts` with each command mapped
|
|
4
|
+
* onto its handler, which loads only when that command runs. Exit codes: 0 done, 1 an invalid
|
|
5
|
+
* token, 2 usage or unreadable input.
|
|
6
|
+
*
|
|
7
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
8
|
+
* @copyright Sergio Xalambrí 2026
|
|
9
|
+
*/
|
|
10
|
+
/** The `sdxc-jwt` program, run by the bin entry below and by tests through `invoke()`. */
|
|
11
|
+
export declare const cli: import("@sdxc/cli").CLI<import("@sdxc/cli").Group<Omit<{
|
|
12
|
+
readonly description: "sdxc-jwt — decode, verify and sign JSON Web Tokens, and generate their keys";
|
|
13
|
+
readonly decode: import("@sdxc/cli").Command<{
|
|
14
|
+
readonly token: import("@sdxc/cli").Argument<string>;
|
|
15
|
+
}, {
|
|
16
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
17
|
+
}>;
|
|
18
|
+
readonly verify: import("@sdxc/cli").Command<{
|
|
19
|
+
readonly token: import("@sdxc/cli").Argument<string>;
|
|
20
|
+
}, {
|
|
21
|
+
readonly jwks: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
22
|
+
readonly keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
23
|
+
readonly iss: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
24
|
+
readonly aud: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
25
|
+
readonly alg: import("@sdxc/cli").ValueFlag<"ES256" | "RS256" | "Ed25519", false, true, false>;
|
|
26
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
27
|
+
}>;
|
|
28
|
+
readonly sign: import("@sdxc/cli").Command<{
|
|
29
|
+
readonly claims: import("@sdxc/cli").Argument<string>;
|
|
30
|
+
}, {
|
|
31
|
+
keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
32
|
+
keyEnv: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
33
|
+
}>;
|
|
34
|
+
readonly keygen: import("@sdxc/cli").Command<Readonly<Record<string, import("@sdxc/cli").Argument<unknown>>>, {
|
|
35
|
+
readonly alg: import("@sdxc/cli").ValueFlag<"ES256" | "RS256" | "Ed25519", true, false, false>;
|
|
36
|
+
readonly format: import("@sdxc/cli").ValueFlag<"pair" | "pem", true, false, false>;
|
|
37
|
+
}>;
|
|
38
|
+
readonly jwks: import("@sdxc/cli").Command<Readonly<Record<string, import("@sdxc/cli").Argument<unknown>>>, {
|
|
39
|
+
keyFile: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
40
|
+
keyEnv: import("@sdxc/cli").ValueFlag<string, false, false, false>;
|
|
41
|
+
}>;
|
|
42
|
+
}, "description" | "hidden" | "arguments" | "flags" | "tasks">, Record<never, never>, Record<never, never>>, []>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `sdxc-jwt` command-line program: the contract in `cli-contract.ts` with each command mapped
|
|
4
|
+
* onto its handler, which loads only when that command runs. Exit codes: 0 done, 1 an invalid
|
|
5
|
+
* token, 2 usage or unreadable input.
|
|
6
|
+
*
|
|
7
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
8
|
+
* @copyright Sergio Xalambrí 2026
|
|
9
|
+
*/
|
|
10
|
+
import { createCLI, lazy } from "@sdxc/cli";
|
|
11
|
+
import { JWT_COMMANDS } from "./cli-contract.js";
|
|
12
|
+
/** The `sdxc-jwt` program, run by the bin entry below and by tests through `invoke()`. */
|
|
13
|
+
export const cli = createCLI({ name: "sdxc-jwt", commands: JWT_COMMANDS });
|
|
14
|
+
cli.map(JWT_COMMANDS.decode, lazy(() => import("./cli-decode.js")));
|
|
15
|
+
cli.map(JWT_COMMANDS.verify, lazy(() => import("./cli-verify.js")));
|
|
16
|
+
cli.map(JWT_COMMANDS.sign, lazy(() => import("./cli-sign.js")));
|
|
17
|
+
cli.map(JWT_COMMANDS.keygen, lazy(() => import("./cli-keygen.js")));
|
|
18
|
+
cli.map(JWT_COMMANDS.jwks, lazy(() => import("./cli-jwks.js")));
|
|
19
|
+
if (import.meta.main) {
|
|
20
|
+
process.exit(await cli.run(process.argv.slice(2)));
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/jwt",
|
|
3
|
-
"version": "2026.10
|
|
3
|
+
"version": "2026.11.10",
|
|
4
4
|
"description": "JWT payload classes and the keys that sign them",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sdxc-jwt": "dist/cli.js"
|
|
8
|
+
},
|
|
6
9
|
"type": "module",
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./dist/index.js",
|
|
@@ -10,12 +13,14 @@
|
|
|
10
13
|
"./r2": "./dist/r2-key-storage.js"
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
16
|
+
"@sdxc/cli": "2026.11.10",
|
|
13
17
|
"@sdxc/duration": "2026.10.13",
|
|
14
18
|
"@sdxc/result": "2026.10.13",
|
|
15
|
-
"@sdxc/uuid": "2026.10
|
|
16
|
-
"jose": "^6.2.13"
|
|
19
|
+
"@sdxc/uuid": "2026.11.10",
|
|
20
|
+
"jose": "^6.2.13",
|
|
21
|
+
"remix": "3.2.0"
|
|
17
22
|
},
|
|
18
|
-
"gitHead": "
|
|
23
|
+
"gitHead": "6191b60a13885b1d826884d574c4a393f9b8eeee",
|
|
19
24
|
"publishConfig": {
|
|
20
25
|
"access": "public"
|
|
21
26
|
},
|