qredential 0.2.0 → 0.3.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/README.md +56 -2
- package/dist/crypto.d.ts +5 -1
- package/dist/crypto.js +21 -2
- package/dist/index.js +40 -5
- package/dist/sdjwt.d.ts +13 -2
- package/dist/sdjwt.js +25 -10
- package/dist/types.d.ts +10 -0
- package/package.json +16 -7
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
Verify a digital credential from a QR code with **no network connection**.
|
|
10
10
|
|
|
11
|
+
Someone shows you a code. You need to know one thing about them, and you need to know it is true.
|
|
12
|
+
This library reads the code, checks the signature, and answers, without asking anybody.
|
|
13
|
+
|
|
11
14
|
```ts
|
|
12
15
|
import { verify } from 'qredential'
|
|
13
16
|
|
|
@@ -19,6 +22,38 @@ No server call. No lookup. No account. The proof travels inside the QR code itse
|
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
|
25
|
+
## What this actually does
|
|
26
|
+
|
|
27
|
+
If you have not worked with digital credentials before, this section is for you. Nothing here
|
|
28
|
+
assumes you have.
|
|
29
|
+
|
|
30
|
+
A credential is a statement somebody signed. "This person is over 18." "This person may drive a
|
|
31
|
+
car." The signature is what makes it worth anything: it says an authority stands behind the
|
|
32
|
+
statement, and that nobody edited it afterwards.
|
|
33
|
+
|
|
34
|
+
On paper, and in a PDF, one signature covers the whole document. To prove a single line of it you
|
|
35
|
+
hand over all of it. Show a driving licence to prove your age and the other person also learns your
|
|
36
|
+
address, your licence number and your exact date of birth. They never asked for any of that. They
|
|
37
|
+
have it now anyway.
|
|
38
|
+
|
|
39
|
+
Selective disclosure breaks that trade. The issuer signs each fact separately, and the credential
|
|
40
|
+
carries only a fingerprint of each one. You choose which facts to reveal, and the rest stay
|
|
41
|
+
fingerprints that say nothing about their contents. The signature still checks out, because it was
|
|
42
|
+
never a signature over one indivisible blob.
|
|
43
|
+
|
|
44
|
+
All of it fits inside the code the verifier scans: the signature, the fingerprints, and the facts
|
|
45
|
+
you chose to share. Nothing is fetched while verifying, so a door, a bus, a rural clinic or an
|
|
46
|
+
aeroplane at cruising altitude can check a credential with no network at all.
|
|
47
|
+
|
|
48
|
+
There is one last piece, and it is the one people forget. Anything you can scan, you can photograph.
|
|
49
|
+
So the person presenting also has to sign a fresh challenge with a private key that never leaves
|
|
50
|
+
their device. Without that step, a screenshot of somebody else's credential would pass. This library
|
|
51
|
+
refuses any presentation that lacks it, and the section on [proving the
|
|
52
|
+
holder](#proving-the-holder-not-just-the-credential) explains how.
|
|
53
|
+
|
|
54
|
+
The format is SD-JWT, standardised as [RFC 9901](https://www.rfc-editor.org/rfc/rfc9901.html) in
|
|
55
|
+
November 2025. It is what the European digital identity wallets and the OpenID specifications use.
|
|
56
|
+
|
|
22
57
|
## Why this exists
|
|
23
58
|
|
|
24
59
|
I built the eCNH, Brazil's digital driver's licence, used by more than 40 million people. The part
|
|
@@ -92,9 +127,16 @@ ones to reveal, and the signature still checks out on what is left:
|
|
|
92
127
|
```ts
|
|
93
128
|
// The credential contains name, address, birth date, document number.
|
|
94
129
|
// The bar only gets to see one thing.
|
|
95
|
-
const presentation = await present(credential, {
|
|
130
|
+
const presentation = await present(credential, {
|
|
131
|
+
disclose: ['over_18'],
|
|
132
|
+
keyBinding: { key: holderPrivateJwk, audience: 'https://bar.example/door', nonce: challenge },
|
|
133
|
+
})
|
|
96
134
|
|
|
97
|
-
const result = await verify(presentation, {
|
|
135
|
+
const result = await verify(presentation, {
|
|
136
|
+
trust,
|
|
137
|
+
nonce: challenge,
|
|
138
|
+
audience: 'https://bar.example/door',
|
|
139
|
+
})
|
|
98
140
|
result.claims // { over_18: true }
|
|
99
141
|
result.claims.address // undefined, and it was never transmitted
|
|
100
142
|
```
|
|
@@ -172,6 +214,18 @@ If the cached list is too old, you get `result.ok === false` with
|
|
|
172
214
|
`result.reason === 'status_list_stale'` rather than a false yes. Deciding what to do when you cannot
|
|
173
215
|
be sure is your call, and the library refuses to make it quietly for you.
|
|
174
216
|
|
|
217
|
+
## Examples
|
|
218
|
+
|
|
219
|
+
Runnable scenarios, each a single file with no setup beyond `npm run build`:
|
|
220
|
+
[a door checking somebody is over 18](examples/age-check-at-the-door.mjs), [issuing a
|
|
221
|
+
credential](examples/issue-a-credential.mjs), [revoking one that is already in somebody's
|
|
222
|
+
pocket](examples/revoke-a-credential.mjs), and [what a credential costs in QR
|
|
223
|
+
characters](examples/sizes.mjs). CI runs all of them, so they cannot quietly stop working.
|
|
224
|
+
|
|
225
|
+
[examples/README.md](examples/README.md) has the index and two findings the prose does not make:
|
|
226
|
+
a presentation with key binding is larger than the credential it came from, and a revocation list
|
|
227
|
+
covering a million credentials is about 600 characters.
|
|
228
|
+
|
|
175
229
|
## API
|
|
176
230
|
|
|
177
231
|
Four functions. That is the whole surface.
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Alg, Jwk } from './types.js';
|
|
1
|
+
import type { Alg, Jwk, SdAlg } from './types.js';
|
|
2
2
|
export declare function importPrivateKey(jwk: Jwk, alg: Alg): Promise<CryptoKey>;
|
|
3
3
|
export declare function importPublicKey(jwk: Jwk, alg: Alg): Promise<CryptoKey>;
|
|
4
4
|
export declare function sign(data: string, key: CryptoKey, alg: Alg): Promise<Uint8Array>;
|
|
@@ -10,4 +10,8 @@ export declare function verifySignature(data: string, signature: string, key: Cr
|
|
|
10
10
|
* only be used one way here and guessing wrong fails loudly at import rather than quietly.
|
|
11
11
|
*/
|
|
12
12
|
export declare function algForJwk(jwk: Jwk): Alg;
|
|
13
|
+
export declare function isSupportedSdAlg(alg: string): alg is SdAlg;
|
|
14
|
+
/** Map an IANA Named Information Hash Algorithm name to its WebCrypto digest algorithm name. */
|
|
15
|
+
export declare function webCryptoDigestAlg(sdAlg: string): AlgorithmIdentifier;
|
|
16
|
+
export declare function digestHash(data: Uint8Array, sdAlg?: string): Promise<Uint8Array>;
|
|
13
17
|
export declare function sha256(data: Uint8Array): Promise<Uint8Array>;
|
package/dist/crypto.js
CHANGED
|
@@ -66,7 +66,26 @@ export function algForJwk(jwk) {
|
|
|
66
66
|
return 'EdDSA';
|
|
67
67
|
throw new QredentialError('unsupported_alg', `cannot tell which algorithm this key is for: kty ${String(jwk.kty)}, crv ${String(jwk.crv)}`);
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const SD_ALG_MAP = {
|
|
70
|
+
'sha-256': 'SHA-256',
|
|
71
|
+
'sha-384': 'SHA-384',
|
|
72
|
+
'sha-512': 'SHA-512',
|
|
73
|
+
};
|
|
74
|
+
export function isSupportedSdAlg(alg) {
|
|
75
|
+
return alg === 'sha-256' || alg === 'sha-384' || alg === 'sha-512';
|
|
76
|
+
}
|
|
77
|
+
/** Map an IANA Named Information Hash Algorithm name to its WebCrypto digest algorithm name. */
|
|
78
|
+
export function webCryptoDigestAlg(sdAlg) {
|
|
79
|
+
if (isSupportedSdAlg(sdAlg)) {
|
|
80
|
+
return SD_ALG_MAP[sdAlg];
|
|
81
|
+
}
|
|
82
|
+
throw new QredentialError('unsupported_alg', `unsupported _sd_alg: ${sdAlg}`);
|
|
83
|
+
}
|
|
84
|
+
export async function digestHash(data, sdAlg = 'sha-256') {
|
|
85
|
+
const algName = webCryptoDigestAlg(sdAlg);
|
|
86
|
+
const buf = await crypto.subtle.digest(algName, data);
|
|
71
87
|
return new Uint8Array(buf);
|
|
72
88
|
}
|
|
89
|
+
export async function sha256(data) {
|
|
90
|
+
return digestHash(data, 'sha-256');
|
|
91
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import { seconds, nowSeconds } from './duration.js';
|
|
|
4
4
|
import { pack, unpack, isEnvelope } from './envelope.js';
|
|
5
5
|
import { parseStatusList, readStatus, isStale } from './status.js';
|
|
6
6
|
import { REGISTERED_CLAIMS, digest, sdHash, makeDisclosure, splitCombined, joinCombined, disclosureLocations, reconstructClaims, } from './sdjwt.js';
|
|
7
|
-
import {
|
|
7
|
+
import { isSupportedSdAlg } from './crypto.js';
|
|
8
|
+
import { isQredentialError, QredentialError } from './errors.js';
|
|
8
9
|
export { QredentialError, isQredentialError } from './errors.js';
|
|
9
10
|
/**
|
|
10
11
|
* Turn a rejected result into a thrown {@link QredentialError}, for callers who would rather use
|
|
@@ -75,8 +76,12 @@ export async function issue(options) {
|
|
|
75
76
|
if (reserved.length > 0) {
|
|
76
77
|
throw new QredentialError('invalid_option', `these claim names describe the token, not the subject, and cannot be made disclosable: ${reserved.join(', ')}`);
|
|
77
78
|
}
|
|
79
|
+
const sdAlg = options.sdAlg ?? 'sha-256';
|
|
80
|
+
if (!isSupportedSdAlg(sdAlg)) {
|
|
81
|
+
throw new QredentialError('unsupported_alg', `unsupported _sd_alg: ${sdAlg}`);
|
|
82
|
+
}
|
|
78
83
|
const disclosures = disclosable.map((name) => makeDisclosure(name, options.claims[name]));
|
|
79
|
-
const digests = shuffle(await Promise.all(disclosures.map((d) => digest(d.raw))));
|
|
84
|
+
const digests = shuffle(await Promise.all(disclosures.map((d) => digest(d.raw, sdAlg))));
|
|
80
85
|
const payload = { iss: options.issuer, iat };
|
|
81
86
|
for (const [key, value] of Object.entries(options.claims)) {
|
|
82
87
|
if (!disclosable.includes(key))
|
|
@@ -105,7 +110,7 @@ export async function issue(options) {
|
|
|
105
110
|
}
|
|
106
111
|
if (digests.length > 0) {
|
|
107
112
|
payload['_sd'] = digests;
|
|
108
|
-
payload['_sd_alg'] =
|
|
113
|
+
payload['_sd_alg'] = sdAlg;
|
|
109
114
|
}
|
|
110
115
|
const header = { alg, typ: 'dc+sd-jwt', kid: options.kid };
|
|
111
116
|
const signingInput = `${b64urlJson(header)}.${b64urlJson(payload)}`;
|
|
@@ -162,13 +167,14 @@ export async function present(credential, options) {
|
|
|
162
167
|
if (bound === null) {
|
|
163
168
|
throw new QredentialError('invalid_option', 'this credential has no cnf claim, so the issuer never bound a holder key and a proof would mean nothing');
|
|
164
169
|
}
|
|
170
|
+
const sdAlg = readSdAlg(jwt);
|
|
165
171
|
const alg = options.keyBinding.alg ?? algForJwk(options.keyBinding.key);
|
|
166
172
|
const kbPayload = {
|
|
167
173
|
iat: nowSeconds(),
|
|
168
174
|
aud: options.keyBinding.audience,
|
|
169
175
|
nonce: options.keyBinding.nonce,
|
|
170
176
|
// Commits to exactly this set of disclosures, so a relay cannot add or strip one afterwards.
|
|
171
|
-
sd_hash: await sdHash(jwt, kept),
|
|
177
|
+
sd_hash: await sdHash(jwt, kept, sdAlg),
|
|
172
178
|
};
|
|
173
179
|
const kbInput = `${b64urlJson({ alg, typ: 'kb+jwt' })}.${b64urlJson(kbPayload)}`;
|
|
174
180
|
const holderKey = await importPrivateKey(options.keyBinding.key, alg);
|
|
@@ -189,6 +195,19 @@ function readHolderKey(jwt) {
|
|
|
189
195
|
return null;
|
|
190
196
|
}
|
|
191
197
|
}
|
|
198
|
+
/** Pull _sd_alg out of a credential's payload, defaulting to sha-256 per RFC 9901. */
|
|
199
|
+
function readSdAlg(jwt) {
|
|
200
|
+
const segments = jwt.split('.');
|
|
201
|
+
if (segments.length !== 3)
|
|
202
|
+
return 'sha-256';
|
|
203
|
+
try {
|
|
204
|
+
const payload = unb64urlJson(segments[1]);
|
|
205
|
+
return payload['_sd_alg'] ?? 'sha-256';
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return 'sha-256';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
192
211
|
export async function verify(input, options) {
|
|
193
212
|
const now = options.now ?? nowSeconds();
|
|
194
213
|
const skew = options.clockSkew ?? 60;
|
|
@@ -273,6 +292,9 @@ export async function verify(input, options) {
|
|
|
273
292
|
withheld = rebuilt.withheld;
|
|
274
293
|
}
|
|
275
294
|
catch (error) {
|
|
295
|
+
if (isQredentialError(error) && error.code === 'unsupported_alg') {
|
|
296
|
+
return reject('unsupported_alg', error.message);
|
|
297
|
+
}
|
|
276
298
|
return reject('digest_mismatch', error.message);
|
|
277
299
|
}
|
|
278
300
|
let revocationChecked = false;
|
|
@@ -436,7 +458,20 @@ async function checkHolderProof(input) {
|
|
|
436
458
|
holderVerified: false,
|
|
437
459
|
};
|
|
438
460
|
}
|
|
439
|
-
const
|
|
461
|
+
const sdAlg = payload['_sd_alg'] ?? 'sha-256';
|
|
462
|
+
let expected;
|
|
463
|
+
try {
|
|
464
|
+
expected = await sdHash(jwt, disclosures, sdAlg);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
if (isQredentialError(error) && error.code === 'unsupported_alg') {
|
|
468
|
+
return {
|
|
469
|
+
rejected: reject('unsupported_alg', error.message),
|
|
470
|
+
holderVerified: false,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
440
475
|
if (kbPayload['sd_hash'] !== expected) {
|
|
441
476
|
return {
|
|
442
477
|
rejected: reject('holder_proof_invalid', 'the holder proof commits to a different set of disclosures than the one presented'),
|
package/dist/sdjwt.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SdAlg } from './types.js';
|
|
1
2
|
export declare const SEPARATOR = "~";
|
|
2
3
|
/**
|
|
3
4
|
* Registered claims that describe the token rather than the subject.
|
|
@@ -27,9 +28,9 @@ export declare function parseDisclosure(raw: string): Disclosure;
|
|
|
27
28
|
* signature covers exactly this set of disclosures. Without it a relay could strip or add
|
|
28
29
|
* disclosures after the holder signed, and the proof would still check out.
|
|
29
30
|
*/
|
|
30
|
-
export declare function sdHash(jwt: string, disclosures: string[]): Promise<string>;
|
|
31
|
+
export declare function sdHash(jwt: string, disclosures: string[], alg?: string): Promise<string>;
|
|
31
32
|
/** Digest of a disclosure exactly as transmitted. Hashing a re-serialised copy would not match. */
|
|
32
|
-
export declare function digest(raw: string): Promise<string>;
|
|
33
|
+
export declare function digest(raw: string, alg?: string): Promise<string>;
|
|
33
34
|
export declare function splitCombined(combined: string): {
|
|
34
35
|
jwt: string;
|
|
35
36
|
disclosures: string[];
|
|
@@ -44,6 +45,16 @@ export declare function joinCombined(jwt: string, disclosures: string[], keyBind
|
|
|
44
45
|
* skipped: a verifier that silently ignores an injected claim is the bug that makes the format
|
|
45
46
|
* pointless.
|
|
46
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* The hash a credential says its digests were made with, validated.
|
|
50
|
+
*
|
|
51
|
+
* RFC 9901 section 4.1.1 requires sha-256 and allows any name from the IANA Named Information Hash
|
|
52
|
+
* Algorithm Registry. These three are the ones WebCrypto computes in every runtime this targets;
|
|
53
|
+
* anything else is refused by name rather than guessed at. Reading it in one place is what keeps
|
|
54
|
+
* the digest map, the claim reconstruction and the path resolver from disagreeing about which hash
|
|
55
|
+
* a credential uses, which is the kind of disagreement that makes a claim silently unreachable.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveSdAlg(payload: Record<string, unknown>): SdAlg;
|
|
47
58
|
/**
|
|
48
59
|
* Rebuild the claim set, following the processing model in RFC 9901 section 7.1.
|
|
49
60
|
*
|
package/dist/sdjwt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { b64url, b64urlJson, unb64urlJson, randomBytes, utf8 } from './bytes.js';
|
|
2
|
-
import {
|
|
2
|
+
import { digestHash, isSupportedSdAlg } from './crypto.js';
|
|
3
3
|
import { QredentialError } from './errors.js';
|
|
4
4
|
export const SEPARATOR = '~';
|
|
5
5
|
/**
|
|
@@ -55,12 +55,12 @@ export function parseDisclosure(raw) {
|
|
|
55
55
|
* signature covers exactly this set of disclosures. Without it a relay could strip or add
|
|
56
56
|
* disclosures after the holder signed, and the proof would still check out.
|
|
57
57
|
*/
|
|
58
|
-
export async function sdHash(jwt, disclosures) {
|
|
59
|
-
return b64url(await
|
|
58
|
+
export async function sdHash(jwt, disclosures, alg = 'sha-256') {
|
|
59
|
+
return b64url(await digestHash(utf8(joinCombined(jwt, disclosures)), alg));
|
|
60
60
|
}
|
|
61
61
|
/** Digest of a disclosure exactly as transmitted. Hashing a re-serialised copy would not match. */
|
|
62
|
-
export async function digest(raw) {
|
|
63
|
-
return b64url(await
|
|
62
|
+
export async function digest(raw, alg = 'sha-256') {
|
|
63
|
+
return b64url(await digestHash(utf8(raw), alg));
|
|
64
64
|
}
|
|
65
65
|
export function splitCombined(combined) {
|
|
66
66
|
const parts = combined.split(SEPARATOR);
|
|
@@ -102,6 +102,22 @@ export function joinCombined(jwt, disclosures, keyBinding) {
|
|
|
102
102
|
* skipped: a verifier that silently ignores an injected claim is the bug that makes the format
|
|
103
103
|
* pointless.
|
|
104
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* The hash a credential says its digests were made with, validated.
|
|
107
|
+
*
|
|
108
|
+
* RFC 9901 section 4.1.1 requires sha-256 and allows any name from the IANA Named Information Hash
|
|
109
|
+
* Algorithm Registry. These three are the ones WebCrypto computes in every runtime this targets;
|
|
110
|
+
* anything else is refused by name rather than guessed at. Reading it in one place is what keeps
|
|
111
|
+
* the digest map, the claim reconstruction and the path resolver from disagreeing about which hash
|
|
112
|
+
* a credential uses, which is the kind of disagreement that makes a claim silently unreachable.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveSdAlg(payload) {
|
|
115
|
+
const alg = payload['_sd_alg'] ?? 'sha-256';
|
|
116
|
+
if (!isSupportedSdAlg(alg)) {
|
|
117
|
+
throw new QredentialError('unsupported_alg', `unsupported _sd_alg: ${alg}`);
|
|
118
|
+
}
|
|
119
|
+
return alg;
|
|
120
|
+
}
|
|
105
121
|
/**
|
|
106
122
|
* Rebuild the claim set, following the processing model in RFC 9901 section 7.1.
|
|
107
123
|
*
|
|
@@ -116,12 +132,10 @@ export function joinCombined(jwt, disclosures, keyBinding) {
|
|
|
116
132
|
* verifier that silently ignores any of those is the bug that makes the format pointless.
|
|
117
133
|
*/
|
|
118
134
|
export async function reconstructClaims(payload, disclosures) {
|
|
119
|
-
const sdAlg = payload
|
|
120
|
-
if (sdAlg !== 'sha-256')
|
|
121
|
-
throw new QredentialError('unsupported_alg', `unsupported _sd_alg: ${sdAlg}`);
|
|
135
|
+
const sdAlg = resolveSdAlg(payload);
|
|
122
136
|
const byDigest = new Map();
|
|
123
137
|
for (const raw of disclosures) {
|
|
124
|
-
const dig = await digest(raw);
|
|
138
|
+
const dig = await digest(raw, sdAlg);
|
|
125
139
|
// Keying by digest would quietly swallow a repeat, and a presentation that sends the same
|
|
126
140
|
// disclosure twice is malformed however harmless it looks.
|
|
127
141
|
if (byDigest.has(dig)) {
|
|
@@ -261,9 +275,10 @@ function elementDigest(item) {
|
|
|
261
275
|
* meaning what it said.
|
|
262
276
|
*/
|
|
263
277
|
export async function disclosureLocations(payload, disclosures) {
|
|
278
|
+
const sdAlg = resolveSdAlg(payload);
|
|
264
279
|
const byDigest = new Map();
|
|
265
280
|
for (const raw of disclosures) {
|
|
266
|
-
byDigest.set(await digest(raw), { raw, parsed: parseDisclosure(raw) });
|
|
281
|
+
byDigest.set(await digest(raw, sdAlg), { raw, parsed: parseDisclosure(raw) });
|
|
267
282
|
}
|
|
268
283
|
const found = new Map();
|
|
269
284
|
const walk = (node, path, ancestors) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export type Alg = 'ES256' | 'EdDSA';
|
|
2
|
+
/**
|
|
3
|
+
* Hashing algorithms supported for SD-JWT selective disclosure (RFC 9901 section 4.1.1).
|
|
4
|
+
* From the IANA Named Information Hash Algorithm Registry.
|
|
5
|
+
*/
|
|
6
|
+
export type SdAlg = 'sha-256' | 'sha-384' | 'sha-512';
|
|
2
7
|
export interface Jwk {
|
|
3
8
|
kty: string;
|
|
4
9
|
crv?: string;
|
|
@@ -38,6 +43,11 @@ export interface IssueOptions {
|
|
|
38
43
|
key: Jwk;
|
|
39
44
|
kid: string;
|
|
40
45
|
alg?: Alg;
|
|
46
|
+
/**
|
|
47
|
+
* Hashing algorithm for selective disclosure and key binding sd_hash.
|
|
48
|
+
* Defaults to 'sha-256' (RFC 9901).
|
|
49
|
+
*/
|
|
50
|
+
sdAlg?: SdAlg;
|
|
41
51
|
claims: Record<string, unknown>;
|
|
42
52
|
/** Claim names the holder may withhold at presentation time. Everything else is always visible. */
|
|
43
53
|
disclose?: string[];
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qredential",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Verify a digital credential from a QR code with no network connection. SD-JWT selective disclosure, offline revocation, zero dependencies.",
|
|
5
5
|
"homepage": "https://qredential.js.org/",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/george-veras/qredential/issues"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
10
11
|
"main": "./dist/index.js",
|
|
11
12
|
"types": "./dist/index.d.ts",
|
|
12
13
|
"exports": {
|
|
@@ -20,11 +21,16 @@
|
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
23
27
|
"scripts": {
|
|
24
28
|
"build": "tsc -p tsconfig.build.json",
|
|
25
29
|
"test": "vitest run",
|
|
30
|
+
"coverage": "vitest run --coverage",
|
|
26
31
|
"test:watch": "vitest",
|
|
27
32
|
"typecheck": "tsc --noEmit",
|
|
33
|
+
"lint": "oxlint src test scripts",
|
|
28
34
|
"build:site": "node scripts/build-site.mjs",
|
|
29
35
|
"check:site": "node scripts/build-site.mjs && git diff --exit-code docs/",
|
|
30
36
|
"prepack": "npm run build",
|
|
@@ -34,6 +40,7 @@
|
|
|
34
40
|
"build:og": "node scripts/build-og.mjs",
|
|
35
41
|
"check:playground": "node scripts/check-playground.mjs",
|
|
36
42
|
"check:seo": "node scripts/check-seo.mjs",
|
|
43
|
+
"check:docs": "node scripts/check-docs.mjs",
|
|
37
44
|
"check:a11y": "node scripts/check-a11y.mjs"
|
|
38
45
|
},
|
|
39
46
|
"keywords": [
|
|
@@ -64,18 +71,20 @@
|
|
|
64
71
|
"node": ">=20"
|
|
65
72
|
},
|
|
66
73
|
"devDependencies": {
|
|
67
|
-
"@sd-jwt/core": "^0.
|
|
74
|
+
"@sd-jwt/core": "^0.21.0",
|
|
68
75
|
"@sd-jwt/crypto-nodejs": "^0.19.0",
|
|
69
|
-
"@types/node": "^
|
|
76
|
+
"@types/node": "^26.6.2",
|
|
70
77
|
"@vitest/browser": "^5.0.1",
|
|
71
78
|
"@vitest/browser-playwright": "^5.0.1",
|
|
79
|
+
"@vitest/coverage-v8": "^5.0.1",
|
|
72
80
|
"axe-core": "^4.13.0",
|
|
73
81
|
"esbuild": "^0.28.2",
|
|
74
|
-
"fast-check": "^
|
|
75
|
-
"marked": "^
|
|
82
|
+
"fast-check": "^4.10.1",
|
|
83
|
+
"marked": "^18.0.13",
|
|
84
|
+
"oxlint": "^1.85.0",
|
|
76
85
|
"playwright": "^1.63.0",
|
|
77
|
-
"qrcode-generator": "^
|
|
78
|
-
"typescript": "^
|
|
86
|
+
"qrcode-generator": "^2.0.4",
|
|
87
|
+
"typescript": "^7.0.2",
|
|
79
88
|
"vitest": "^5.0.1"
|
|
80
89
|
}
|
|
81
90
|
}
|