@arcblock/jwt 1.29.21 → 1.29.23

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/esm/index.d.mts CHANGED
@@ -35,6 +35,28 @@ type JwtVerifyOptions = Partial<{
35
35
  */
36
36
  declare function sign(signer: string, sk?: BytesType, payload?: {}, doSign?: boolean, version?: string): Promise<string>;
37
37
  declare function signV2(signer: string, sk?: BytesType, payload?: any): Promise<string>;
38
+ type PasskeyAssertion = {
39
+ authenticatorData: string;
40
+ clientDataJSON: string;
41
+ signature: string;
42
+ };
43
+ /**
44
+ * Compute the WebAuthn challenge for an unsigned JWT token.
45
+ * Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
46
+ *
47
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
48
+ * @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
49
+ */
50
+ declare function getChallenge(unsignedToken: string): string;
51
+ /**
52
+ * Assemble a complete JWT from an unsigned token and a passkey assertion.
53
+ * The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
54
+ *
55
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64)
56
+ * @param assertion - WebAuthn assertion with base64url-encoded fields
57
+ * @returns complete 3-part JWT string
58
+ */
59
+ declare function assemble(unsignedToken: string, assertion: PasskeyAssertion): string;
38
60
  declare function decode(token: string, bodyOnly?: true): JwtBody;
39
61
  declare function decode(token: string, bodyOnly?: false): JwtToken;
40
62
  /**
@@ -88,6 +110,23 @@ declare function signDelegationToken(signer: string, sk: BytesType, payload: {
88
110
  nbf?: string;
89
111
  exp?: string;
90
112
  }): Promise<string>;
113
+ /**
114
+ * Create an unsigned delegation token for passkey two-phase signing.
115
+ * Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
116
+ */
117
+ declare function signDelegationTokenUnsigned(signer: string, pk: BytesType, payload: {
118
+ sub: string;
119
+ ops: string[];
120
+ deny?: string[];
121
+ delegation?: string;
122
+ iat?: string;
123
+ nbf?: string;
124
+ exp?: string;
125
+ }): Promise<string>;
126
+ /**
127
+ * Assemble a complete delegation token from an unsigned token and a passkey assertion.
128
+ */
129
+ declare function assembleDelegationToken(unsignedToken: string, assertion: PasskeyAssertion): string;
91
130
  declare function decodeDelegationToken(token: string): DelegationToken;
92
131
  declare function verifyDelegationToken(token: string): Promise<boolean>;
93
132
  declare function checkDelegationTokenScope(scope: string, {
@@ -98,4 +137,4 @@ declare function checkDelegationTokenScope(scope: string, {
98
137
  deny?: string[];
99
138
  }): boolean;
100
139
  //#endregion
101
- export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, checkDelegationTokenScope, decode, decodeDelegationToken, sign, signDelegationToken, signV2, verify, verifyDelegationToken };
140
+ export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, PasskeyAssertion, assemble, assembleDelegationToken, checkDelegationTokenScope, decode, decodeDelegationToken, getChallenge, sign, signDelegationToken, signDelegationTokenUnsigned, signV2, verify, verifyDelegationToken };
package/esm/index.mjs CHANGED
@@ -3,12 +3,24 @@ import { Hasher, getSigner, types } from "@ocap/mcrypto";
3
3
  import { fromBase64, scopeMatchAny, toBase64, toHex } from "@ocap/util";
4
4
  import Debug from "debug";
5
5
  import stringify from "json-stable-stringify";
6
- import semver from "semver";
7
6
 
8
7
  //#region src/index.ts
9
8
  const debug = Debug("@arcblock/jwt");
10
9
  const JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN = "1.1.0";
11
10
  const hasher = Hasher.SHA3.hash256;
11
+ function coerceVersion(str) {
12
+ const m = str.match(/(\d+\.\d+\.\d+)/);
13
+ return m ? m[1] : null;
14
+ }
15
+ function semverGte(a, b) {
16
+ const pa = a.split(".").map(Number);
17
+ const pb = b.split(".").map(Number);
18
+ for (let i = 0; i < 3; i++) {
19
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
20
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
21
+ }
22
+ return true;
23
+ }
12
24
  /**
13
25
  *
14
26
  *
@@ -59,8 +71,8 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
59
71
  const bodyB64 = toBase64(stringify(body));
60
72
  debug("sign.body", body);
61
73
  const msgHex = toHex(`${headerB64}.${bodyB64}`);
62
- const coercedVersion = semver.coerce(version);
63
- const msgHash = coercedVersion && semver.gte(coercedVersion.version, JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) ? hasher(msgHex) : msgHex;
74
+ const coercedVersion = coerceVersion(version);
75
+ const msgHash = coercedVersion && semverGte(coercedVersion, JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) ? hasher(msgHex) : msgHex;
64
76
  // istanbul ignore if
65
77
  if (!doSign) return `${headerB64}.${bodyB64}`;
66
78
  return [
@@ -72,6 +84,32 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
72
84
  async function signV2(signer, sk, payload = {}) {
73
85
  return sign(signer, sk, payload, !!sk, "1.1.0");
74
86
  }
87
+ /**
88
+ * Compute the WebAuthn challenge for an unsigned JWT token.
89
+ * Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
90
+ *
91
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
92
+ * @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
93
+ */
94
+ function getChallenge(unsignedToken) {
95
+ if (!unsignedToken) throw new Error("Cannot compute challenge from empty token");
96
+ const parts = unsignedToken.split(".");
97
+ return hasher(toHex(`${parts[0]}.${parts[1]}`));
98
+ }
99
+ /**
100
+ * Assemble a complete JWT from an unsigned token and a passkey assertion.
101
+ * The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
102
+ *
103
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64)
104
+ * @param assertion - WebAuthn assertion with base64url-encoded fields
105
+ * @returns complete 3-part JWT string
106
+ */
107
+ function assemble(unsignedToken, assertion) {
108
+ if (!unsignedToken) throw new Error("Cannot assemble JWT from empty token");
109
+ if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble JWT: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
110
+ if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble JWT: assertion must contain authenticatorData, clientDataJSON, and signature");
111
+ return `${unsignedToken}.${toBase64(JSON.stringify(assertion))}`;
112
+ }
75
113
  function decode(token, bodyOnly = true) {
76
114
  const [headerB64, bodyB64, sigB64] = token.split(".");
77
115
  const header = JSON.parse(fromBase64(headerB64).toString());
@@ -154,18 +192,30 @@ async function verify(token, signerPk, options) {
154
192
  return false;
155
193
  }
156
194
  }
195
+ const alg = header.alg.toLowerCase();
196
+ if (alg === "passkey") {
197
+ const [, , sigB64] = token.split(".");
198
+ const assertionRaw = JSON.parse(fromBase64(sigB64).toString());
199
+ if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
200
+ debug("verify.error.incompletePasskeyAssertion");
201
+ return false;
202
+ }
203
+ const challenge = hasher(toHex(`${headerB64}.${bodyB64}`));
204
+ const extra = JSON.stringify({
205
+ authenticatorData: assertionRaw.authenticatorData,
206
+ clientDataJSON: assertionRaw.clientDataJSON
207
+ });
208
+ return await getSigner(types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, signerPk, extra);
209
+ }
157
210
  const signers = {
158
211
  secp256k1: getSigner(types.KeyType.SECP256K1),
159
212
  es256k: getSigner(types.KeyType.SECP256K1),
160
213
  ed25519: getSigner(types.KeyType.ED25519),
161
- ethereum: getSigner(types.KeyType.ETHEREUM),
162
- passkey: getSigner(types.KeyType.PASSKEY)
214
+ ethereum: getSigner(types.KeyType.ETHEREUM)
163
215
  };
164
- const alg = header.alg.toLowerCase();
165
216
  if (signers[alg]) {
166
217
  const msgHex = toHex(`${headerB64}.${bodyB64}`);
167
- const coercedBodyVersion = body.version ? semver.coerce(body.version) : null;
168
- const version = coercedBodyVersion ? coercedBodyVersion.version : "";
218
+ const version = (body.version ? coerceVersion(body.version) : null) || "";
169
219
  if (version && version === JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) return signers[alg].verify(hasher(msgHex), signature, signerPk);
170
220
  return signers[alg].verify(msgHex, signature, signerPk);
171
221
  }
@@ -190,6 +240,10 @@ const delegationTokenHeaders = {
190
240
  [types.KeyType.ETHEREUM]: {
191
241
  alg: "Ethereum",
192
242
  typ: "DelegationToken"
243
+ },
244
+ [types.KeyType.PASSKEY]: {
245
+ alg: "Passkey",
246
+ typ: "DelegationToken"
193
247
  }
194
248
  };
195
249
  async function signDelegationToken(signer, sk, payload) {
@@ -223,6 +277,45 @@ async function signDelegationToken(signer, sk, payload) {
223
277
  toBase64(getSigner(type.pk).sign(msgHash, sk))
224
278
  ].join(".");
225
279
  }
280
+ /**
281
+ * Create an unsigned delegation token for passkey two-phase signing.
282
+ * Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
283
+ */
284
+ async function signDelegationTokenUnsigned(signer, pk, payload) {
285
+ if (isValid(signer) === false) throw new Error("Cannot sign DelegationToken with invalid signer");
286
+ const type = toTypeInfo(signer);
287
+ if (type.pk === void 0) throw new Error("Cannot determine key type from signer");
288
+ const header = delegationTokenHeaders[type.pk];
289
+ const headerB64 = toBase64(stringify(header));
290
+ const delegationSignerMethod = extractMethod(signer) || "abt";
291
+ const now = Math.floor(Date.now() / 1e3);
292
+ const pkHex = typeof pk === "string" ? pk : toHex(pk);
293
+ const body = {
294
+ iss: toDid(signer, delegationSignerMethod),
295
+ sub: payload.sub,
296
+ iat: payload.iat || String(now),
297
+ nbf: payload.nbf || String(now),
298
+ exp: payload.exp || String(now + 300),
299
+ version: DELEGATION_TOKEN_VERSION,
300
+ ops: payload.ops,
301
+ pk: pkHex
302
+ };
303
+ if (payload.deny && payload.deny.length > 0) {
304
+ for (const d of payload.deny) if (!d.startsWith("fg:")) throw new Error(`Invalid deny pattern "${d}": must start with "fg:"`);
305
+ body.deny = payload.deny;
306
+ }
307
+ if (payload.delegation) body.delegation = payload.delegation;
308
+ return `${headerB64}.${toBase64(stringify(body))}`;
309
+ }
310
+ /**
311
+ * Assemble a complete delegation token from an unsigned token and a passkey assertion.
312
+ */
313
+ function assembleDelegationToken(unsignedToken, assertion) {
314
+ if (!unsignedToken) throw new Error("Cannot assemble DelegationToken from empty token");
315
+ if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble DelegationToken: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
316
+ if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble DelegationToken: assertion must contain authenticatorData, clientDataJSON, and signature");
317
+ return `${unsignedToken}.${toBase64(JSON.stringify(assertion))}`;
318
+ }
226
319
  function decodeDelegationToken(token) {
227
320
  const parts = token.split(".");
228
321
  if (parts.length !== 3) throw new Error("Invalid delegation token format: expected 3 parts");
@@ -265,13 +358,27 @@ async function verifyDelegationToken(token) {
265
358
  debug("delegationToken.verify.error.futureIat");
266
359
  return false;
267
360
  }
361
+ const alg = header.alg.toLowerCase();
362
+ if (alg === "passkey") {
363
+ const [, , sigB64] = token.split(".");
364
+ const assertionRaw = JSON.parse(fromBase64(sigB64).toString());
365
+ if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
366
+ debug("delegationToken.verify.error.incompletePasskeyAssertion");
367
+ return false;
368
+ }
369
+ const challenge = hasher(toHex(`${headerB64}.${bodyB64}`));
370
+ const extra = JSON.stringify({
371
+ authenticatorData: assertionRaw.authenticatorData,
372
+ clientDataJSON: assertionRaw.clientDataJSON
373
+ });
374
+ return await getSigner(types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, body.pk, extra);
375
+ }
268
376
  const signers = {
269
377
  ed25519: getSigner(types.KeyType.ED25519),
270
378
  es256k: getSigner(types.KeyType.SECP256K1),
271
379
  secp256k1: getSigner(types.KeyType.SECP256K1),
272
380
  ethereum: getSigner(types.KeyType.ETHEREUM)
273
381
  };
274
- const alg = header.alg.toLowerCase();
275
382
  if (!signers[alg]) {
276
383
  debug("delegationToken.verify.error.unknownAlg");
277
384
  return false;
@@ -289,4 +396,4 @@ function checkDelegationTokenScope(scope, { ops, deny }) {
289
396
  }
290
397
 
291
398
  //#endregion
292
- export { checkDelegationTokenScope, decode, decodeDelegationToken, sign, signDelegationToken, signV2, verify, verifyDelegationToken };
399
+ export { assemble, assembleDelegationToken, checkDelegationTokenScope, decode, decodeDelegationToken, getChallenge, sign, signDelegationToken, signDelegationTokenUnsigned, signV2, verify, verifyDelegationToken };
package/lib/index.cjs CHANGED
@@ -6,13 +6,24 @@ let debug = require("debug");
6
6
  debug = require_rolldown_runtime.__toESM(debug);
7
7
  let json_stable_stringify = require("json-stable-stringify");
8
8
  json_stable_stringify = require_rolldown_runtime.__toESM(json_stable_stringify);
9
- let semver = require("semver");
10
- semver = require_rolldown_runtime.__toESM(semver);
11
9
 
12
10
  //#region src/index.ts
13
11
  const debug$1 = (0, debug.default)("@arcblock/jwt");
14
12
  const JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN = "1.1.0";
15
13
  const hasher = _ocap_mcrypto.Hasher.SHA3.hash256;
14
+ function coerceVersion(str) {
15
+ const m = str.match(/(\d+\.\d+\.\d+)/);
16
+ return m ? m[1] : null;
17
+ }
18
+ function semverGte(a, b) {
19
+ const pa = a.split(".").map(Number);
20
+ const pb = b.split(".").map(Number);
21
+ for (let i = 0; i < 3; i++) {
22
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
23
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
24
+ }
25
+ return true;
26
+ }
16
27
  /**
17
28
  *
18
29
  *
@@ -63,8 +74,8 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
63
74
  const bodyB64 = (0, _ocap_util.toBase64)((0, json_stable_stringify.default)(body));
64
75
  debug$1("sign.body", body);
65
76
  const msgHex = (0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`);
66
- const coercedVersion = semver.default.coerce(version);
67
- const msgHash = coercedVersion && semver.default.gte(coercedVersion.version, JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) ? hasher(msgHex) : msgHex;
77
+ const coercedVersion = coerceVersion(version);
78
+ const msgHash = coercedVersion && semverGte(coercedVersion, JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) ? hasher(msgHex) : msgHex;
68
79
  // istanbul ignore if
69
80
  if (!doSign) return `${headerB64}.${bodyB64}`;
70
81
  return [
@@ -76,6 +87,32 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
76
87
  async function signV2(signer, sk, payload = {}) {
77
88
  return sign(signer, sk, payload, !!sk, "1.1.0");
78
89
  }
90
+ /**
91
+ * Compute the WebAuthn challenge for an unsigned JWT token.
92
+ * Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
93
+ *
94
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
95
+ * @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
96
+ */
97
+ function getChallenge(unsignedToken) {
98
+ if (!unsignedToken) throw new Error("Cannot compute challenge from empty token");
99
+ const parts = unsignedToken.split(".");
100
+ return hasher((0, _ocap_util.toHex)(`${parts[0]}.${parts[1]}`));
101
+ }
102
+ /**
103
+ * Assemble a complete JWT from an unsigned token and a passkey assertion.
104
+ * The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
105
+ *
106
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64)
107
+ * @param assertion - WebAuthn assertion with base64url-encoded fields
108
+ * @returns complete 3-part JWT string
109
+ */
110
+ function assemble(unsignedToken, assertion) {
111
+ if (!unsignedToken) throw new Error("Cannot assemble JWT from empty token");
112
+ if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble JWT: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
113
+ if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble JWT: assertion must contain authenticatorData, clientDataJSON, and signature");
114
+ return `${unsignedToken}.${(0, _ocap_util.toBase64)(JSON.stringify(assertion))}`;
115
+ }
79
116
  function decode(token, bodyOnly = true) {
80
117
  const [headerB64, bodyB64, sigB64] = token.split(".");
81
118
  const header = JSON.parse((0, _ocap_util.fromBase64)(headerB64).toString());
@@ -158,18 +195,30 @@ async function verify(token, signerPk, options) {
158
195
  return false;
159
196
  }
160
197
  }
198
+ const alg = header.alg.toLowerCase();
199
+ if (alg === "passkey") {
200
+ const [, , sigB64] = token.split(".");
201
+ const assertionRaw = JSON.parse((0, _ocap_util.fromBase64)(sigB64).toString());
202
+ if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
203
+ debug$1("verify.error.incompletePasskeyAssertion");
204
+ return false;
205
+ }
206
+ const challenge = hasher((0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`));
207
+ const extra = JSON.stringify({
208
+ authenticatorData: assertionRaw.authenticatorData,
209
+ clientDataJSON: assertionRaw.clientDataJSON
210
+ });
211
+ return await (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, signerPk, extra);
212
+ }
161
213
  const signers = {
162
214
  secp256k1: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
163
215
  es256k: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
164
216
  ed25519: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ED25519),
165
- ethereum: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ETHEREUM),
166
- passkey: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.PASSKEY)
217
+ ethereum: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ETHEREUM)
167
218
  };
168
- const alg = header.alg.toLowerCase();
169
219
  if (signers[alg]) {
170
220
  const msgHex = (0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`);
171
- const coercedBodyVersion = body.version ? semver.default.coerce(body.version) : null;
172
- const version = coercedBodyVersion ? coercedBodyVersion.version : "";
221
+ const version = (body.version ? coerceVersion(body.version) : null) || "";
173
222
  if (version && version === JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) return signers[alg].verify(hasher(msgHex), signature, signerPk);
174
223
  return signers[alg].verify(msgHex, signature, signerPk);
175
224
  }
@@ -194,6 +243,10 @@ const delegationTokenHeaders = {
194
243
  [_ocap_mcrypto.types.KeyType.ETHEREUM]: {
195
244
  alg: "Ethereum",
196
245
  typ: "DelegationToken"
246
+ },
247
+ [_ocap_mcrypto.types.KeyType.PASSKEY]: {
248
+ alg: "Passkey",
249
+ typ: "DelegationToken"
197
250
  }
198
251
  };
199
252
  async function signDelegationToken(signer, sk, payload) {
@@ -227,6 +280,45 @@ async function signDelegationToken(signer, sk, payload) {
227
280
  (0, _ocap_util.toBase64)((0, _ocap_mcrypto.getSigner)(type.pk).sign(msgHash, sk))
228
281
  ].join(".");
229
282
  }
283
+ /**
284
+ * Create an unsigned delegation token for passkey two-phase signing.
285
+ * Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
286
+ */
287
+ async function signDelegationTokenUnsigned(signer, pk, payload) {
288
+ if ((0, _arcblock_did.isValid)(signer) === false) throw new Error("Cannot sign DelegationToken with invalid signer");
289
+ const type = (0, _arcblock_did.toTypeInfo)(signer);
290
+ if (type.pk === void 0) throw new Error("Cannot determine key type from signer");
291
+ const header = delegationTokenHeaders[type.pk];
292
+ const headerB64 = (0, _ocap_util.toBase64)((0, json_stable_stringify.default)(header));
293
+ const delegationSignerMethod = (0, _arcblock_did.extractMethod)(signer) || "abt";
294
+ const now = Math.floor(Date.now() / 1e3);
295
+ const pkHex = typeof pk === "string" ? pk : (0, _ocap_util.toHex)(pk);
296
+ const body = {
297
+ iss: (0, _arcblock_did.toDid)(signer, delegationSignerMethod),
298
+ sub: payload.sub,
299
+ iat: payload.iat || String(now),
300
+ nbf: payload.nbf || String(now),
301
+ exp: payload.exp || String(now + 300),
302
+ version: DELEGATION_TOKEN_VERSION,
303
+ ops: payload.ops,
304
+ pk: pkHex
305
+ };
306
+ if (payload.deny && payload.deny.length > 0) {
307
+ for (const d of payload.deny) if (!d.startsWith("fg:")) throw new Error(`Invalid deny pattern "${d}": must start with "fg:"`);
308
+ body.deny = payload.deny;
309
+ }
310
+ if (payload.delegation) body.delegation = payload.delegation;
311
+ return `${headerB64}.${(0, _ocap_util.toBase64)((0, json_stable_stringify.default)(body))}`;
312
+ }
313
+ /**
314
+ * Assemble a complete delegation token from an unsigned token and a passkey assertion.
315
+ */
316
+ function assembleDelegationToken(unsignedToken, assertion) {
317
+ if (!unsignedToken) throw new Error("Cannot assemble DelegationToken from empty token");
318
+ if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble DelegationToken: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
319
+ if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble DelegationToken: assertion must contain authenticatorData, clientDataJSON, and signature");
320
+ return `${unsignedToken}.${(0, _ocap_util.toBase64)(JSON.stringify(assertion))}`;
321
+ }
230
322
  function decodeDelegationToken(token) {
231
323
  const parts = token.split(".");
232
324
  if (parts.length !== 3) throw new Error("Invalid delegation token format: expected 3 parts");
@@ -269,13 +361,27 @@ async function verifyDelegationToken(token) {
269
361
  debug$1("delegationToken.verify.error.futureIat");
270
362
  return false;
271
363
  }
364
+ const alg = header.alg.toLowerCase();
365
+ if (alg === "passkey") {
366
+ const [, , sigB64] = token.split(".");
367
+ const assertionRaw = JSON.parse((0, _ocap_util.fromBase64)(sigB64).toString());
368
+ if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
369
+ debug$1("delegationToken.verify.error.incompletePasskeyAssertion");
370
+ return false;
371
+ }
372
+ const challenge = hasher((0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`));
373
+ const extra = JSON.stringify({
374
+ authenticatorData: assertionRaw.authenticatorData,
375
+ clientDataJSON: assertionRaw.clientDataJSON
376
+ });
377
+ return await (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, body.pk, extra);
378
+ }
272
379
  const signers = {
273
380
  ed25519: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ED25519),
274
381
  es256k: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
275
382
  secp256k1: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
276
383
  ethereum: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ETHEREUM)
277
384
  };
278
- const alg = header.alg.toLowerCase();
279
385
  if (!signers[alg]) {
280
386
  debug$1("delegationToken.verify.error.unknownAlg");
281
387
  return false;
@@ -293,11 +399,15 @@ function checkDelegationTokenScope(scope, { ops, deny }) {
293
399
  }
294
400
 
295
401
  //#endregion
402
+ exports.assemble = assemble;
403
+ exports.assembleDelegationToken = assembleDelegationToken;
296
404
  exports.checkDelegationTokenScope = checkDelegationTokenScope;
297
405
  exports.decode = decode;
298
406
  exports.decodeDelegationToken = decodeDelegationToken;
407
+ exports.getChallenge = getChallenge;
299
408
  exports.sign = sign;
300
409
  exports.signDelegationToken = signDelegationToken;
410
+ exports.signDelegationTokenUnsigned = signDelegationTokenUnsigned;
301
411
  exports.signV2 = signV2;
302
412
  exports.verify = verify;
303
413
  exports.verifyDelegationToken = verifyDelegationToken;
package/lib/index.d.cts CHANGED
@@ -35,6 +35,28 @@ type JwtVerifyOptions = Partial<{
35
35
  */
36
36
  declare function sign(signer: string, sk?: BytesType, payload?: {}, doSign?: boolean, version?: string): Promise<string>;
37
37
  declare function signV2(signer: string, sk?: BytesType, payload?: any): Promise<string>;
38
+ type PasskeyAssertion = {
39
+ authenticatorData: string;
40
+ clientDataJSON: string;
41
+ signature: string;
42
+ };
43
+ /**
44
+ * Compute the WebAuthn challenge for an unsigned JWT token.
45
+ * Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
46
+ *
47
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
48
+ * @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
49
+ */
50
+ declare function getChallenge(unsignedToken: string): string;
51
+ /**
52
+ * Assemble a complete JWT from an unsigned token and a passkey assertion.
53
+ * The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
54
+ *
55
+ * @param unsignedToken - the unsigned token (headerB64.bodyB64)
56
+ * @param assertion - WebAuthn assertion with base64url-encoded fields
57
+ * @returns complete 3-part JWT string
58
+ */
59
+ declare function assemble(unsignedToken: string, assertion: PasskeyAssertion): string;
38
60
  declare function decode(token: string, bodyOnly?: true): JwtBody;
39
61
  declare function decode(token: string, bodyOnly?: false): JwtToken;
40
62
  /**
@@ -88,6 +110,23 @@ declare function signDelegationToken(signer: string, sk: BytesType, payload: {
88
110
  nbf?: string;
89
111
  exp?: string;
90
112
  }): Promise<string>;
113
+ /**
114
+ * Create an unsigned delegation token for passkey two-phase signing.
115
+ * Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
116
+ */
117
+ declare function signDelegationTokenUnsigned(signer: string, pk: BytesType, payload: {
118
+ sub: string;
119
+ ops: string[];
120
+ deny?: string[];
121
+ delegation?: string;
122
+ iat?: string;
123
+ nbf?: string;
124
+ exp?: string;
125
+ }): Promise<string>;
126
+ /**
127
+ * Assemble a complete delegation token from an unsigned token and a passkey assertion.
128
+ */
129
+ declare function assembleDelegationToken(unsignedToken: string, assertion: PasskeyAssertion): string;
91
130
  declare function decodeDelegationToken(token: string): DelegationToken;
92
131
  declare function verifyDelegationToken(token: string): Promise<boolean>;
93
132
  declare function checkDelegationTokenScope(scope: string, {
@@ -98,4 +137,4 @@ declare function checkDelegationTokenScope(scope: string, {
98
137
  deny?: string[];
99
138
  }): boolean;
100
139
  //#endregion
101
- export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, checkDelegationTokenScope, decode, decodeDelegationToken, sign, signDelegationToken, signV2, verify, verifyDelegationToken };
140
+ export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, PasskeyAssertion, assemble, assembleDelegationToken, checkDelegationTokenScope, decode, decodeDelegationToken, getChallenge, sign, signDelegationToken, signDelegationTokenUnsigned, signV2, verify, verifyDelegationToken };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@arcblock/jwt",
3
3
  "description": "JSON Web Token variant for arcblock DID solutions",
4
4
  "type": "module",
5
- "version": "1.29.21",
5
+ "version": "1.29.23",
6
6
  "author": {
7
7
  "name": "wangshijun",
8
8
  "email": "shijun@arcblock.io",
@@ -19,18 +19,16 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "@arcblock/did": "1.29.21",
23
- "@ocap/mcrypto": "1.29.21",
24
- "@ocap/util": "1.29.21",
22
+ "@arcblock/did": "1.29.23",
23
+ "@ocap/mcrypto": "1.29.23",
24
+ "@ocap/util": "1.29.23",
25
25
  "debug": "^4.4.3",
26
- "json-stable-stringify": "^1.0.1",
27
- "semver": "^7.6.3"
26
+ "json-stable-stringify": "^1.0.1"
28
27
  },
29
28
  "devDependencies": {
30
- "@ocap/wallet": "1.29.21",
29
+ "@ocap/wallet": "1.29.23",
31
30
  "@types/json-stable-stringify": "^1.0.36",
32
31
  "@types/node": "^22.7.5",
33
- "@types/semver": "^7.5.8",
34
32
  "tsdown": "^0.18.4",
35
33
  "tslib": "^2.4.0"
36
34
  },