@enbox/agent 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/browser.mjs +12 -30
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/connect.js +22 -30
  4. package/dist/esm/connect.js.map +1 -1
  5. package/dist/esm/dwn-api.js +149 -22
  6. package/dist/esm/dwn-api.js.map +1 -1
  7. package/dist/esm/dwn-discovery-file.js +1 -1
  8. package/dist/esm/dwn-discovery-payload.js +20 -21
  9. package/dist/esm/dwn-discovery-payload.js.map +1 -1
  10. package/dist/esm/dwn-key-delivery.js.map +1 -1
  11. package/dist/esm/{oidc.js → enbox-connect-protocol.js} +236 -248
  12. package/dist/esm/enbox-connect-protocol.js.map +1 -0
  13. package/dist/esm/enbox-user-agent.js +18 -5
  14. package/dist/esm/enbox-user-agent.js.map +1 -1
  15. package/dist/esm/index.js +4 -2
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/local-dwn.js +21 -51
  18. package/dist/esm/local-dwn.js.map +1 -1
  19. package/dist/esm/permissions-api.js.map +1 -1
  20. package/dist/esm/store-data.js.map +1 -1
  21. package/dist/esm/sync-engine-level.js +1 -1
  22. package/dist/esm/sync-engine-level.js.map +1 -1
  23. package/dist/esm/sync-messages.js +1 -1
  24. package/dist/esm/sync-messages.js.map +1 -1
  25. package/dist/types/connect.d.ts +15 -19
  26. package/dist/types/connect.d.ts.map +1 -1
  27. package/dist/types/dwn-api.d.ts +46 -6
  28. package/dist/types/dwn-api.d.ts.map +1 -1
  29. package/dist/types/dwn-discovery-file.d.ts +1 -1
  30. package/dist/types/dwn-discovery-payload.d.ts +18 -19
  31. package/dist/types/dwn-discovery-payload.d.ts.map +1 -1
  32. package/dist/types/enbox-connect-protocol.d.ts +220 -0
  33. package/dist/types/enbox-connect-protocol.d.ts.map +1 -0
  34. package/dist/types/enbox-user-agent.d.ts +10 -1
  35. package/dist/types/enbox-user-agent.d.ts.map +1 -1
  36. package/dist/types/index.d.ts +1 -2
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/local-dwn.d.ts +16 -32
  39. package/dist/types/local-dwn.d.ts.map +1 -1
  40. package/package.json +9 -11
  41. package/src/connect.ts +40 -54
  42. package/src/dwn-api.ts +175 -29
  43. package/src/dwn-discovery-file.ts +1 -1
  44. package/src/dwn-discovery-payload.ts +23 -24
  45. package/src/dwn-key-delivery.ts +1 -1
  46. package/src/enbox-connect-protocol.ts +778 -0
  47. package/src/enbox-user-agent.ts +27 -4
  48. package/src/index.ts +4 -2
  49. package/src/local-dwn.ts +22 -53
  50. package/src/permissions-api.ts +3 -3
  51. package/src/store-data.ts +1 -1
  52. package/src/sync-engine-level.ts +1 -1
  53. package/src/sync-messages.ts +1 -1
  54. package/dist/esm/oidc.js.map +0 -1
  55. package/dist/types/oidc.d.ts +0 -250
  56. package/dist/types/oidc.d.ts.map +0 -1
  57. package/src/oidc.ts +0 -864
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Enbox Connect Protocol
3
+ *
4
+ * A capability delegation protocol for DWN access. Enables apps to request
5
+ * scoped permission grants from a wallet (provider), receiving a delegate DID
6
+ * with the granted permissions.
7
+ *
8
+ * Two transport modes:
9
+ * - Local (`dwn://connect`): same-device, direct HTTP against the local DWN
10
+ * - Remote (`enbox://connect`): cross-device, relay-mediated with QR/deep link
11
+ *
12
+ * The protocol uses JWTs for signing, JWE (XChaCha20-Poly1305) for encryption,
13
+ * and ECDH (Ed25519 → X25519 + HKDF) for key agreement.
14
+ */
1
15
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
16
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
17
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -20,110 +34,47 @@ var __rest = (this && this.__rest) || function (s, e) {
20
34
  };
21
35
  import { DidJwk } from '@enbox/dids';
22
36
  import { Convert, logger } from '@enbox/common';
23
- import { CryptoUtils, Ed25519, EdDsaAlgorithm, Hkdf, Sha256, X25519, XChaCha20Poly1305, } from '@enbox/crypto';
37
+ import { CryptoUtils, Ed25519, EdDsaAlgorithm, Hkdf, X25519, XChaCha20Poly1305, } from '@enbox/crypto';
24
38
  import { DwnInterfaceName, DwnMethodName } from '@enbox/dwn-sdk-js';
25
39
  import { AgentPermissionsApi } from './permissions-api.js';
26
40
  import { concatenateUrl } from './utils.js';
27
41
  import { DwnInterface } from './types/dwn.js';
28
42
  import { isRecordPermissionScope } from './dwn-api.js';
43
+ // ---------------------------------------------------------------------------
44
+ // URL building
45
+ // ---------------------------------------------------------------------------
29
46
  /**
30
- * Gets the correct OIDC endpoint out of the {@link OidcEndpoint} options provided.
31
- * Handles a trailing slash on baseURL
47
+ * Builds the URL for a connect server endpoint.
32
48
  *
33
- * @param {Object} options the options object
34
- * @param {string} options.baseURL for example `http://foo.com/connect/
35
- * @param {OidcEndpoint} options.endpoint the OIDC endpoint desired
36
- * @param {string} options.authParam this is the unique id which must be provided when getting the `authorize` endpoint
37
- * @param {string} options.tokenParam this is the random state as b64url which must be provided with the `token` endpoint
49
+ * @param options.baseURL - The connect server base URL (e.g. `http://localhost:3000/connect/`)
50
+ * @param options.endpoint - The endpoint type
51
+ * @param options.authParam - Required for `authorize` endpoint (the request ID)
52
+ * @param options.tokenParam - Required for `token` endpoint (the state value)
38
53
  */
39
- function buildOidcUrl({ baseURL, endpoint, authParam, tokenParam, }) {
54
+ function buildConnectUrl({ baseURL, endpoint, authParam, tokenParam, }) {
40
55
  switch (endpoint) {
41
- /** 1. client sends {@link PushedAuthRequest} & client receives {@link PushedAuthResponse} */
42
56
  case 'pushedAuthorizationRequest':
43
57
  return concatenateUrl(baseURL, 'par');
44
- /** 2. provider gets {@link EnboxConnectAuthRequest} */
45
58
  case 'authorize':
46
59
  if (!authParam) {
47
- throw new Error(`authParam must be providied when building a token URL`);
60
+ throw new Error('authParam must be provided when building an authorize URL');
48
61
  }
49
62
  return concatenateUrl(baseURL, `authorize/${authParam}.jwt`);
50
- /** 3. provider sends {@link EnboxConnectAuthResponse} */
51
63
  case 'callback':
52
- return concatenateUrl(baseURL, `callback`);
53
- /** 4. client gets {@link EnboxConnectAuthResponse */
64
+ return concatenateUrl(baseURL, 'callback');
54
65
  case 'token':
55
66
  if (!tokenParam) {
56
- throw new Error(`tokenParam must be providied when building a token URL`);
67
+ throw new Error('tokenParam must be provided when building a token URL');
57
68
  }
58
69
  return concatenateUrl(baseURL, `token/${tokenParam}.jwt`);
59
- // TODO: metadata endpoints?
60
70
  default:
61
- throw new Error(`No matches for endpoint specified: ${endpoint}`);
71
+ throw new Error(`Unknown connect endpoint: ${endpoint}`);
62
72
  }
63
73
  }
64
- /**
65
- * Generates a cryptographically random "code challenge" in
66
- * accordance with the RFC 7636 PKCE specification.
67
- *
68
- * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 | RFC 7636 }
69
- */
70
- function generateCodeChallenge() {
71
- return __awaiter(this, void 0, void 0, function* () {
72
- const codeVerifierBytes = CryptoUtils.randomBytes(32);
73
- const codeChallengeBytes = yield Sha256.digest({ data: codeVerifierBytes });
74
- const codeChallengeBase64Url = Convert.uint8Array(codeChallengeBytes).toBase64Url();
75
- return { codeChallengeBytes, codeChallengeBase64Url };
76
- });
77
- }
78
- /** Client creates the {@link EnboxConnectAuthRequest} */
79
- function createAuthRequest(options) {
80
- return __awaiter(this, void 0, void 0, function* () {
81
- // Generate a random state value to associate the authorization request with the response.
82
- const stateBytes = CryptoUtils.randomBytes(16);
83
- // Generate a random nonce value to associate the ID Token with the authorization request.
84
- const nonceBytes = CryptoUtils.randomBytes(16);
85
- const requestObject = Object.assign(Object.assign({}, options), { nonce: Convert.uint8Array(nonceBytes).toBase64Url(), response_type: 'id_token', response_mode: 'direct_post', state: Convert.uint8Array(stateBytes).toBase64Url(), client_metadata: {
86
- subject_syntax_types_supported: ['did:dht', 'did:jwk'],
87
- } });
88
- return requestObject;
89
- });
90
- }
91
- /** Encrypts the auth request with the key which will be passed through QR code */
92
- function encryptAuthRequest(_a) {
93
- return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, }) {
94
- const protectedHeader = {
95
- alg: 'dir',
96
- cty: 'JWT',
97
- enc: 'XC20P',
98
- typ: 'JWT',
99
- };
100
- const nonce = CryptoUtils.randomBytes(24);
101
- const additionalData = Convert.object(protectedHeader).toUint8Array();
102
- const jwtBytes = Convert.string(jwt).toUint8Array();
103
- const ciphertextAndTag = yield XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
104
- /** The cipher output concatenates the encrypted data and tag
105
- * so we need to extract the values for use in the JWE. */
106
- const ciphertext = ciphertextAndTag.subarray(0, -16);
107
- const authenticationTag = ciphertextAndTag.subarray(-16);
108
- const compactJwe = [
109
- Convert.object(protectedHeader).toBase64Url(),
110
- '', // Empty string since there is no wrapped key.
111
- Convert.uint8Array(nonce).toBase64Url(),
112
- Convert.uint8Array(ciphertext).toBase64Url(),
113
- Convert.uint8Array(authenticationTag).toBase64Url(),
114
- ].join('.');
115
- return compactJwe;
116
- });
117
- }
118
- /** Create a response object compatible with Web5 Connect and OIDC SIOPv2 */
119
- function createResponseObject(options) {
120
- return __awaiter(this, void 0, void 0, function* () {
121
- const currentTimeInSeconds = Math.floor(Date.now() / 1000);
122
- const responseObject = Object.assign(Object.assign({}, options), { iat: currentTimeInSeconds, exp: currentTimeInSeconds + 600 });
123
- return responseObject;
124
- });
125
- }
126
- /** sign an object and transform it into a jwt using a did */
74
+ // ---------------------------------------------------------------------------
75
+ // JWT signing and verification
76
+ // ---------------------------------------------------------------------------
77
+ /** Signs an object as a JWT using an Ed25519 DID key. */
127
78
  function signJwt(_a) {
128
79
  return __awaiter(this, arguments, void 0, function* ({ did, data, }) {
129
80
  const header = Convert.object({
@@ -132,37 +83,32 @@ function signJwt(_a) {
132
83
  typ: 'JWT',
133
84
  }).toBase64Url();
134
85
  const payload = Convert.object(data).toBase64Url();
135
- // signs using ed25519 EdDSA
136
86
  const signer = yield did.getSigner();
137
87
  const signature = yield signer.sign({
138
88
  data: Convert.string(`${header}.${payload}`).toUint8Array(),
139
89
  });
140
90
  const signatureBase64Url = Convert.uint8Array(signature).toBase64Url();
141
- const jwt = `${header}.${payload}.${signatureBase64Url}`;
142
- return jwt;
91
+ return `${header}.${payload}.${signatureBase64Url}`;
143
92
  });
144
93
  }
145
- /** Take the decrypted JWT and verify it was signed by its public DID. Return parsed object. */
94
+ /** Verifies a JWT signature using the DID in the `kid` header. Returns the parsed payload. */
146
95
  function verifyJwt(_a) {
147
96
  return __awaiter(this, arguments, void 0, function* ({ jwt }) {
148
97
  var _b, _c;
149
98
  const [headerB64U, payloadB64U, signatureB64U] = jwt.split('.');
150
- // Convert the header back to a JOSE object and verify that the 'kid' header value is present.
151
99
  const header = Convert.base64Url(headerB64U).toObject();
152
100
  if (!header.kid) {
153
- throw new Error(`OIDC: Object could not be verified due to missing 'kid' header value.`);
101
+ throw new Error('Connect: JWT missing required "kid" header value.');
154
102
  }
155
- // Resolve the Client DID document.
156
103
  const { didDocument } = yield DidJwk.resolve(header.kid.split('#')[0]);
157
104
  if (!didDocument) {
158
- throw new Error('OIDC: Object could not be verified due to Client DID resolution issue.');
105
+ throw new Error('Connect: JWT verification failed could not resolve DID.');
159
106
  }
160
- // Get the public key used to sign the Object from the DID document.
161
107
  const { publicKeyJwk } = (_c = (_b = didDocument.verificationMethod) === null || _b === void 0 ? void 0 : _b.find((method) => {
162
108
  return method.id === header.kid;
163
109
  })) !== null && _c !== void 0 ? _c : {};
164
110
  if (!publicKeyJwk) {
165
- throw new Error('OIDC: Object could not be verified due to missing public key in DID document.');
111
+ throw new Error('Connect: JWT verification failed public key not found in DID document.');
166
112
  }
167
113
  const EdDsa = new EdDsaAlgorithm();
168
114
  const isValid = yield EdDsa.verify({
@@ -171,86 +117,56 @@ function verifyJwt(_a) {
171
117
  data: Convert.string(`${headerB64U}.${payloadB64U}`).toUint8Array(),
172
118
  });
173
119
  if (!isValid) {
174
- throw new Error('OIDC: Object failed verification due to invalid signature.');
120
+ throw new Error('Connect: JWT verification failed invalid signature.');
175
121
  }
176
- const object = Convert.base64Url(payloadB64U).toObject();
177
- return object;
122
+ return Convert.base64Url(payloadB64U).toObject();
178
123
  });
179
124
  }
180
- /**
181
- * Fetches the {@EnboxConnectAuthRequest} from the authorize endpoint and decrypts it
182
- * using the encryption key passed via QR code.
183
- */
184
- const getAuthRequest = (request_uri, encryption_key) => __awaiter(void 0, void 0, void 0, function* () {
185
- const authRequest = yield fetch(request_uri, { signal: AbortSignal.timeout(30000) });
186
- const jwe = yield authRequest.text();
187
- const jwt = yield decryptAuthRequest({
188
- jwe,
189
- encryption_key,
190
- });
191
- const web5ConnectAuthRequest = (yield verifyJwt({
192
- jwt,
193
- }));
194
- return web5ConnectAuthRequest;
195
- });
196
- /** Take the encrypted JWE, decrypt using the code challenge and return a JWT string which will need to be verified */
197
- function decryptAuthRequest(_a) {
198
- return __awaiter(this, arguments, void 0, function* ({ jwe, encryption_key, }) {
199
- const [protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U,] = jwe.split('.');
200
- const encryptionKeyBytes = Convert.base64Url(encryption_key).toUint8Array();
201
- const protectedHeader = Convert.base64Url(protectedHeaderB64U).toUint8Array();
202
- const additionalData = protectedHeader;
203
- const nonce = Convert.base64Url(nonceB64U).toUint8Array();
204
- const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
205
- const authenticationTag = Convert.base64Url(authenticationTagB64U).toUint8Array();
206
- // The cipher expects the encrypted data and tag to be concatenated.
207
- const ciphertextAndTag = new Uint8Array([
208
- ...ciphertext,
209
- ...authenticationTag,
210
- ]);
211
- const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: encryptionKeyBytes, nonce, additionalData });
212
- const jwt = Convert.uint8Array(decryptedJwtBytes).toString();
213
- return jwt;
125
+ // ---------------------------------------------------------------------------
126
+ // Encryption: request (symmetric key via QR/deep link)
127
+ // ---------------------------------------------------------------------------
128
+ /** Encrypts the connect request JWT with a symmetric key (shared via QR code or deep link). */
129
+ function encryptRequest(_a) {
130
+ return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, }) {
131
+ const protectedHeader = {
132
+ alg: 'dir',
133
+ cty: 'JWT',
134
+ enc: 'XC20P',
135
+ typ: 'JWT',
136
+ };
137
+ const nonce = CryptoUtils.randomBytes(24);
138
+ const additionalData = Convert.object(protectedHeader).toUint8Array();
139
+ const jwtBytes = Convert.string(jwt).toUint8Array();
140
+ const ciphertextAndTag = yield XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
141
+ const ciphertext = ciphertextAndTag.subarray(0, -16);
142
+ const authenticationTag = ciphertextAndTag.subarray(-16);
143
+ return [
144
+ Convert.object(protectedHeader).toBase64Url(),
145
+ '', // No wrapped key (direct encryption).
146
+ Convert.uint8Array(nonce).toBase64Url(),
147
+ Convert.uint8Array(ciphertext).toBase64Url(),
148
+ Convert.uint8Array(authenticationTag).toBase64Url(),
149
+ ].join('.');
214
150
  });
215
151
  }
216
- /**
217
- * The client uses to decrypt the jwe obtained from the auth server which contains
218
- * the {@link EnboxConnectAuthResponse} that was sent by the provider to the auth server.
219
- *
220
- * @async
221
- * @param {BearerDid} clientDid - The did that was initially used by the client for ECDH at connect init.
222
- * @param {string} jwe - The encrypted data as a jwe.
223
- * @param {string} pin - The pin that was obtained from the user.
224
- */
225
- function decryptAuthResponse(clientDid, jwe, pin) {
226
- return __awaiter(this, void 0, void 0, function* () {
152
+ /** Decrypts an encrypted connect request JWE using the symmetric key from the QR/deep link. */
153
+ function decryptRequest(_a) {
154
+ return __awaiter(this, arguments, void 0, function* ({ jwe, encryptionKey, }) {
227
155
  const [protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U,] = jwe.split('.');
228
- // get the delegatedid public key from the header
229
- const header = Convert.base64Url(protectedHeaderB64U).toObject();
230
- if (!header.kid) {
231
- throw new Error('JWE protected header is missing required "kid" property');
232
- }
233
- const delegateResolvedDid = yield DidJwk.resolve(header.kid.split('#')[0]);
234
- // derive ECDH shared key using the provider's public key and our clientDid private key
235
- const sharedKey = yield Oidc.deriveSharedKey(clientDid, delegateResolvedDid.didDocument);
236
- // add the pin to the AAD
237
- const additionalData = Object.assign(Object.assign({}, header), { pin: pin });
238
- const AAD = Convert.object(additionalData).toUint8Array();
156
+ const encryptionKeyBytes = Convert.base64Url(encryptionKey).toUint8Array();
157
+ const additionalData = Convert.base64Url(protectedHeaderB64U).toUint8Array();
239
158
  const nonce = Convert.base64Url(nonceB64U).toUint8Array();
240
159
  const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
241
160
  const authenticationTag = Convert.base64Url(authenticationTagB64U).toUint8Array();
242
- // The cipher expects the encrypted data and tag to be concatenated.
243
- const ciphertextAndTag = new Uint8Array([
244
- ...ciphertext,
245
- ...authenticationTag,
246
- ]);
247
- // decrypt using the sharedKey
248
- const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: sharedKey, nonce, additionalData: AAD });
249
- const jwt = Convert.uint8Array(decryptedJwtBytes).toString();
250
- return jwt;
161
+ const ciphertextAndTag = new Uint8Array([...ciphertext, ...authenticationTag]);
162
+ const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: encryptionKeyBytes, nonce, additionalData });
163
+ return Convert.uint8Array(decryptedJwtBytes).toString();
251
164
  });
252
165
  }
253
- /** Derives a shared ECDH private key in order to encrypt the {@link EnboxConnectAuthResponse} */
166
+ // ---------------------------------------------------------------------------
167
+ // Encryption: response (ECDH shared key + optional PIN)
168
+ // ---------------------------------------------------------------------------
169
+ /** Derives a shared ECDH key for encrypting/decrypting the connect response. */
254
170
  function deriveSharedKey(privateKeyDid, publicKeyDid) {
255
171
  return __awaiter(this, void 0, void 0, function* () {
256
172
  var _a, _b;
@@ -258,34 +174,32 @@ function deriveSharedKey(privateKeyDid, publicKeyDid) {
258
174
  const publicJwk = (_a = publicKeyDid.verificationMethod) === null || _a === void 0 ? void 0 : _a[0].publicKeyJwk;
259
175
  const privateJwk = (_b = privatePortableDid.privateKeys) === null || _b === void 0 ? void 0 : _b[0];
260
176
  publicJwk.alg = 'EdDSA';
261
- const publicX25519 = yield Ed25519.convertPublicKeyToX25519({
262
- publicKey: publicJwk,
263
- });
264
- const privateX25519 = yield Ed25519.convertPrivateKeyToX25519({
265
- privateKey: privateJwk,
266
- });
177
+ const publicX25519 = yield Ed25519.convertPublicKeyToX25519({ publicKey: publicJwk });
178
+ const privateX25519 = yield Ed25519.convertPrivateKeyToX25519({ privateKey: privateJwk });
267
179
  const sharedKey = yield X25519.sharedSecret({
268
180
  privateKeyA: privateX25519,
269
181
  publicKeyB: publicX25519,
270
182
  });
271
- const sharedEncryptionKey = yield Hkdf.deriveKeyBytes({
183
+ return Hkdf.deriveKeyBytes({
272
184
  baseKeyBytes: new Uint8Array(sharedKey),
273
185
  hash: 'SHA-256',
274
186
  salt: new Uint8Array(),
275
187
  info: new Uint8Array(),
276
188
  length: 256,
277
189
  });
278
- return sharedEncryptionKey;
279
190
  });
280
191
  }
281
192
  /**
282
- * Encrypts the auth response jwt. Requires a randomPin is added to the AAD of the
283
- * encryption algorithm in order to prevent man in the middle and eavesdropping attacks.
284
- * The keyid of the delegate did is used to pass the public key to the client in order
285
- * for the client to derive the shared ECDH private key.
193
+ * Encrypts the connect response JWT.
194
+ *
195
+ * For remote (relay-mediated) flows, `pin` is required it is added to the
196
+ * AAD to prevent MITM attacks via the untrusted relay.
197
+ *
198
+ * For local (same-device) flows, `pin` may be omitted — the ECDH encryption
199
+ * alone is sufficient when there is no untrusted intermediary.
286
200
  */
287
- function encryptAuthResponse(_a) {
288
- return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, delegateDidKeyId, randomPin, }) {
201
+ function encryptResponse(_a) {
202
+ return __awaiter(this, arguments, void 0, function* ({ jwt, encryptionKey, delegateDidKeyId, pin, }) {
289
203
  const protectedHeader = {
290
204
  alg: 'dir',
291
205
  cty: 'JWT',
@@ -294,60 +208,117 @@ function encryptAuthResponse(_a) {
294
208
  kid: delegateDidKeyId,
295
209
  };
296
210
  const nonce = CryptoUtils.randomBytes(24);
297
- const additionalData = Convert.object(Object.assign(Object.assign({}, protectedHeader), { pin: randomPin })).toUint8Array();
211
+ // Build AAD include PIN if provided (remote flows).
212
+ const aadObject = pin
213
+ ? Object.assign(Object.assign({}, protectedHeader), { pin }) : Object.assign({}, protectedHeader);
214
+ const additionalData = Convert.object(aadObject).toUint8Array();
298
215
  const jwtBytes = Convert.string(jwt).toUint8Array();
299
216
  const ciphertextAndTag = yield XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
300
- /** The cipher output concatenates the encrypted data and tag
301
- * so we need to extract the values for use in the JWE. */
302
217
  const ciphertext = ciphertextAndTag.subarray(0, -16);
303
218
  const authenticationTag = ciphertextAndTag.subarray(-16);
304
- const compactJwe = [
219
+ return [
305
220
  Convert.object(protectedHeader).toBase64Url(),
306
- '', // Empty string since there is no wrapped key.
221
+ '', // No wrapped key (direct encryption).
307
222
  Convert.uint8Array(nonce).toBase64Url(),
308
223
  Convert.uint8Array(ciphertext).toBase64Url(),
309
224
  Convert.uint8Array(authenticationTag).toBase64Url(),
310
225
  ].join('.');
311
- return compactJwe;
312
226
  });
313
227
  }
228
+ /**
229
+ * Decrypts the connect response JWE using ECDH + optional PIN.
230
+ *
231
+ * @param clientDid - The ephemeral DID used at connect initiation (for ECDH).
232
+ * @param jwe - The encrypted response JWE.
233
+ * @param pin - The PIN entered by the user (required for remote flows, omit for local).
234
+ */
235
+ function decryptResponse(clientDid, jwe, pin) {
236
+ return __awaiter(this, void 0, void 0, function* () {
237
+ const [protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U,] = jwe.split('.');
238
+ const header = Convert.base64Url(protectedHeaderB64U).toObject();
239
+ if (!header.kid) {
240
+ throw new Error('Connect: JWE protected header is missing required "kid" property.');
241
+ }
242
+ const delegateResolvedDid = yield DidJwk.resolve(header.kid.split('#')[0]);
243
+ const sharedKey = yield EnboxConnectProtocol.deriveSharedKey(clientDid, delegateResolvedDid.didDocument);
244
+ // Build AAD — include PIN if provided (must match what was used during encryption).
245
+ const aadObject = pin
246
+ ? Object.assign(Object.assign({}, header), { pin }) : Object.assign({}, header);
247
+ const AAD = Convert.object(aadObject).toUint8Array();
248
+ const nonce = Convert.base64Url(nonceB64U).toUint8Array();
249
+ const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
250
+ const authenticationTag = Convert.base64Url(authenticationTagB64U).toUint8Array();
251
+ const ciphertextAndTag = new Uint8Array([...ciphertext, ...authenticationTag]);
252
+ const decryptedJwtBytes = yield XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: sharedKey, nonce, additionalData: AAD });
253
+ return Convert.uint8Array(decryptedJwtBytes).toString();
254
+ });
255
+ }
256
+ // ---------------------------------------------------------------------------
257
+ // Request creation and retrieval
258
+ // ---------------------------------------------------------------------------
259
+ /** Creates an {@link EnboxConnectRequest}. */
260
+ function createConnectRequest(options) {
261
+ return __awaiter(this, void 0, void 0, function* () {
262
+ var _a;
263
+ const stateBytes = CryptoUtils.randomBytes(16);
264
+ const nonceBytes = CryptoUtils.randomBytes(16);
265
+ return Object.assign(Object.assign({}, options), { nonce: Convert.uint8Array(nonceBytes).toBase64Url(), responseMode: 'direct_post', state: Convert.uint8Array(stateBytes).toBase64Url(), supportedDidMethods: (_a = options.supportedDidMethods) !== null && _a !== void 0 ? _a : ['did:dht', 'did:jwk'] });
266
+ });
267
+ }
268
+ /**
269
+ * Fetches an encrypted connect request from the authorize endpoint
270
+ * and decrypts it using the encryption key from the QR/deep link.
271
+ */
272
+ function getConnectRequest(requestUri, encryptionKey) {
273
+ return __awaiter(this, void 0, void 0, function* () {
274
+ const response = yield fetch(requestUri, { signal: AbortSignal.timeout(30000) });
275
+ const jwe = yield response.text();
276
+ const jwt = yield decryptRequest({ jwe, encryptionKey });
277
+ return (yield verifyJwt({ jwt }));
278
+ });
279
+ }
280
+ // ---------------------------------------------------------------------------
281
+ // Response creation
282
+ // ---------------------------------------------------------------------------
283
+ /** Creates an {@link EnboxConnectResponse} with timestamps. */
284
+ function createConnectResponse(options) {
285
+ return __awaiter(this, void 0, void 0, function* () {
286
+ const currentTimeInSeconds = Math.floor(Date.now() / 1000);
287
+ return Object.assign(Object.assign({}, options), { iat: currentTimeInSeconds, exp: currentTimeInSeconds + 600 });
288
+ });
289
+ }
290
+ // ---------------------------------------------------------------------------
291
+ // Permission grants
292
+ // ---------------------------------------------------------------------------
314
293
  function shouldUseDelegatePermission(scope) {
315
- // Currently all record permissions are treated as delegated permissions
316
- // In the future only methods that modify state will be delegated and the rest will be normal permissions
317
294
  if (isRecordPermissionScope(scope)) {
318
295
  return true;
319
296
  }
320
297
  else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) {
321
- // ProtocolConfigure messages are also delegated, as they modify state
322
298
  return true;
323
299
  }
324
- // All other permissions are not treated as delegated
325
300
  return false;
326
301
  }
327
302
  /**
328
- * Creates the permission grants that assign to the selectedDid the level of
329
- * permissions that the web app requested in the {@link EnboxConnectAuthRequest}
303
+ * Creates permission grants that assign the requested scopes to a delegate DID.
330
304
  */
331
305
  function createPermissionGrants(selectedDid, delegateBearerDid, agent, scopes) {
332
306
  return __awaiter(this, void 0, void 0, function* () {
333
307
  const permissionsApi = new AgentPermissionsApi({ agent });
334
- // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/enboxorg/enbox/issues/849
335
- logger.log(`Creating permission grants for ${scopes.length} scopes given...`);
308
+ logger.log(`Creating permission grants for ${scopes.length} scopes...`);
336
309
  const permissionGrants = yield Promise.all(scopes.map((scope) => {
337
- // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission.
338
310
  const delegated = shouldUseDelegatePermission(scope);
339
311
  return permissionsApi.createGrant({
340
312
  delegated,
341
313
  store: true,
342
314
  grantedTo: delegateBearerDid.uri,
343
315
  scope,
344
- dateExpires: '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional
316
+ dateExpires: '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires configurable
345
317
  author: selectedDid,
346
318
  });
347
319
  }));
348
320
  logger.log(`Sending ${permissionGrants.length} permission grants to remote DWN...`);
349
321
  const messagePromises = permissionGrants.map((grant) => __awaiter(this, void 0, void 0, function* () {
350
- // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there
351
322
  const _a = grant.message, { encodedData } = _a, rawMessage = __rest(_a, ["encodedData"]);
352
323
  const data = Convert.base64Url(encodedData).toUint8Array();
353
324
  const { reply } = yield agent.sendDwnRequest({
@@ -357,17 +328,14 @@ function createPermissionGrants(selectedDid, delegateBearerDid, agent, scopes) {
357
328
  dataStream: new Blob([data]),
358
329
  rawMessage,
359
330
  });
360
- // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
361
331
  if (reply.status.code !== 202 && reply.status.code !== 409) {
362
332
  logger.error(`Error sending RecordsWrite: ${reply.status.detail}`);
363
- logger.error(`RecordsWrite message: ${rawMessage}`);
364
- throw new Error(`Could not send the message. Error details: ${reply.status.detail}`);
333
+ throw new Error(`Could not send permission grant. Error: ${reply.status.detail}`);
365
334
  }
366
335
  return grant.message;
367
336
  }));
368
337
  try {
369
- const messages = yield Promise.all(messagePromises);
370
- return messages;
338
+ return yield Promise.all(messagePromises);
371
339
  }
372
340
  catch (error) {
373
341
  logger.error(`Error during batch-send of permission grants: ${error}`);
@@ -375,8 +343,12 @@ function createPermissionGrants(selectedDid, delegateBearerDid, agent, scopes) {
375
343
  }
376
344
  });
377
345
  }
346
+ // ---------------------------------------------------------------------------
347
+ // Protocol installation
348
+ // ---------------------------------------------------------------------------
378
349
  /**
379
- * Installs the protocol required by the Client on the Provider if it doesn't already exist.
350
+ * Installs a DWN protocol on the provider's DWN if it doesn't already exist.
351
+ * Ensures the protocol is available on both the local and remote DWN.
380
352
  */
381
353
  function prepareProtocol(selectedDid, agent, protocolDefinition) {
382
354
  return __awaiter(this, void 0, void 0, function* () {
@@ -387,23 +359,19 @@ function prepareProtocol(selectedDid, agent, protocolDefinition) {
387
359
  messageParams: { filter: { protocol: protocolDefinition.protocol } },
388
360
  });
389
361
  if (queryMessage.reply.status.code !== 200) {
390
- // if the query failed, throw an error
391
362
  throw new Error(`Could not fetch protocol: ${queryMessage.reply.status.detail}`);
392
363
  }
393
364
  else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) {
394
365
  logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`);
395
- // send the protocol definition to the remote DWN first, if it passes we can process it locally
396
366
  const { reply: sendReply, message: configureMessage } = yield agent.sendDwnRequest({
397
367
  author: selectedDid,
398
368
  target: selectedDid,
399
369
  messageType: DwnInterface.ProtocolsConfigure,
400
370
  messageParams: { definition: protocolDefinition },
401
371
  });
402
- // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
403
372
  if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
404
373
  throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
405
374
  }
406
- // process the protocol locally, we don't have to check if it exists as this is just a convenience over waiting for sync.
407
375
  yield agent.processDwnRequest({
408
376
  author: selectedDid,
409
377
  target: selectedDid,
@@ -413,7 +381,6 @@ function prepareProtocol(selectedDid, agent, protocolDefinition) {
413
381
  }
414
382
  else {
415
383
  logger.log(`Protocol already exists: ${protocolDefinition.protocol}`);
416
- // the protocol already exists, let's make sure it exists on the remote DWN as the requesting app will need it
417
384
  const configureMessage = queryMessage.reply.entries[0];
418
385
  const { reply: sendReply } = yield agent.sendDwnRequest({
419
386
  author: selectedDid,
@@ -427,66 +394,65 @@ function prepareProtocol(selectedDid, agent, protocolDefinition) {
427
394
  }
428
395
  });
429
396
  }
397
+ // ---------------------------------------------------------------------------
398
+ // Full wallet-side flow (provider submits response)
399
+ // ---------------------------------------------------------------------------
430
400
  /**
431
- * Creates a delegate did which the web app will use as its future indentity.
432
- * Assigns to that DID the level of permissions that the web app requested in
433
- * the {@link EnboxConnectAuthRequest}. Encrypts via ECDH key that the web app
434
- * will have access to because the web app has the public key which it provided
435
- * in the {@link EnboxConnectAuthRequest}. Then sends the ciphertext of this
436
- * {@link EnboxConnectAuthResponse} to the callback endpoint. Which the
437
- * web app will need to retrieve from the token endpoint and decrypt with the pin to access.
401
+ * Executes the full wallet-side (provider) flow:
402
+ * 1. Creates a delegate DID
403
+ * 2. Installs requested protocols
404
+ * 3. Creates permission grants
405
+ * 4. Builds, signs, and encrypts the response
406
+ * 5. POSTs the encrypted response to the callback URL
407
+ *
408
+ * @param selectedDid - The provider's DID that is granting access.
409
+ * @param connectRequest - The decoded connect request from the app.
410
+ * @param pin - The PIN for response encryption AAD (required for remote flows).
411
+ * @param agent - The agent instance for DWN operations.
438
412
  */
439
- function submitAuthResponse(selectedDid, authRequest, randomPin, agent) {
413
+ function submitConnectResponse(selectedDid, connectRequest, pin, agent) {
440
414
  return __awaiter(this, void 0, void 0, function* () {
441
415
  const delegateBearerDid = yield DidJwk.create();
442
416
  const delegatePortableDid = yield delegateBearerDid.export();
443
- // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this.
444
- const delegateGrantPromises = authRequest.permissionRequests.map((permissionRequest) => __awaiter(this, void 0, void 0, function* () {
417
+ const delegateGrantPromises = connectRequest.permissionRequests.map((permissionRequest) => __awaiter(this, void 0, void 0, function* () {
445
418
  const { protocolDefinition, permissionScopes } = permissionRequest;
446
- // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with.
447
419
  const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol);
448
420
  if (!grantsMatchProtocolUri) {
449
- throw new Error('All permission scopes must match the protocol uri they are provided with.');
421
+ throw new Error('All permission scopes must match the protocol URI they are provided with.');
450
422
  }
451
423
  yield prepareProtocol(selectedDid, agent, protocolDefinition);
452
- const permissionGrants = yield Oidc.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes);
453
- return permissionGrants;
424
+ return EnboxConnectProtocol.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes);
454
425
  }));
455
426
  const delegateGrants = (yield Promise.all(delegateGrantPromises)).flat();
456
- logger.log('Generating auth response object...');
457
- const responseObject = yield Oidc.createResponseObject({
458
- //* the IDP's did that was selected to be connected
459
- iss: selectedDid,
460
- //* the client's new identity
461
- sub: delegateBearerDid.uri,
462
- //* the client's temporary ephemeral did used for connect
463
- aud: authRequest.client_id,
464
- //* the nonce of the original auth request
465
- nonce: authRequest.nonce,
427
+ logger.log('Building connect response...');
428
+ const responseObject = yield EnboxConnectProtocol.createConnectResponse({
429
+ providerDid: selectedDid,
430
+ delegateDid: delegateBearerDid.uri,
431
+ aud: connectRequest.clientDid,
432
+ nonce: connectRequest.nonce,
466
433
  delegateGrants,
467
434
  delegatePortableDid,
468
435
  });
469
- // Sign the Response Object using the ephemeral DID's signing key.
470
- logger.log('Signing auth response object...');
471
- const responseObjectJwt = yield Oidc.signJwt({
436
+ logger.log('Signing connect response...');
437
+ const responseObjectJwt = yield EnboxConnectProtocol.signJwt({
472
438
  did: delegateBearerDid,
473
439
  data: responseObject,
474
440
  });
475
- const clientDid = yield DidJwk.resolve(authRequest.client_id);
476
- const sharedKey = yield Oidc.deriveSharedKey(delegateBearerDid, clientDid === null || clientDid === void 0 ? void 0 : clientDid.didDocument);
477
- logger.log('Encrypting auth response object...');
478
- const encryptedResponse = yield Oidc.encryptAuthResponse({
441
+ const clientDid = yield DidJwk.resolve(connectRequest.clientDid);
442
+ const sharedKey = yield EnboxConnectProtocol.deriveSharedKey(delegateBearerDid, clientDid === null || clientDid === void 0 ? void 0 : clientDid.didDocument);
443
+ logger.log('Encrypting connect response...');
444
+ const encryptedResponse = yield EnboxConnectProtocol.encryptResponse({
479
445
  jwt: responseObjectJwt,
480
446
  encryptionKey: sharedKey,
481
447
  delegateDidKeyId: delegateBearerDid.document.verificationMethod[0].id,
482
- randomPin,
448
+ pin,
483
449
  });
484
450
  const formEncodedRequest = new URLSearchParams({
485
451
  id_token: encryptedResponse,
486
- state: authRequest.state,
452
+ state: connectRequest.state,
487
453
  }).toString();
488
- logger.log(`Sending auth response object to Web5 Connect server: ${authRequest.redirect_uri}`);
489
- yield fetch(authRequest.redirect_uri, {
454
+ logger.log(`Sending connect response to: ${connectRequest.callbackUrl}`);
455
+ yield fetch(connectRequest.callbackUrl, {
490
456
  body: formEncodedRequest,
491
457
  method: 'POST',
492
458
  headers: {
@@ -496,20 +462,42 @@ function submitAuthResponse(selectedDid, authRequest, randomPin, agent) {
496
462
  });
497
463
  });
498
464
  }
465
+ // ---------------------------------------------------------------------------
466
+ // Namespace export
467
+ // ---------------------------------------------------------------------------
468
+ export const EnboxConnectProtocol = {
469
+ buildConnectUrl,
470
+ signJwt,
471
+ verifyJwt,
472
+ encryptRequest,
473
+ decryptRequest,
474
+ encryptResponse,
475
+ decryptResponse,
476
+ deriveSharedKey,
477
+ createConnectRequest,
478
+ getConnectRequest,
479
+ createConnectResponse,
480
+ createPermissionGrants,
481
+ submitConnectResponse,
482
+ };
483
+ // ---------------------------------------------------------------------------
484
+ // Deprecated aliases — migration aid from the old `Oidc` namespace
485
+ // ---------------------------------------------------------------------------
486
+ /** @deprecated Use {@link EnboxConnectProtocol} instead. */
499
487
  export const Oidc = {
500
- createAuthRequest,
501
- encryptAuthRequest,
502
- getAuthRequest,
503
- decryptAuthRequest,
488
+ createAuthRequest: createConnectRequest,
489
+ encryptAuthRequest: encryptRequest,
490
+ getAuthRequest: getConnectRequest,
491
+ decryptAuthRequest: decryptRequest,
504
492
  createPermissionGrants,
505
- createResponseObject,
506
- encryptAuthResponse,
507
- decryptAuthResponse,
493
+ createResponseObject: createConnectResponse,
494
+ encryptAuthResponse: encryptResponse,
495
+ decryptAuthResponse: decryptResponse,
508
496
  deriveSharedKey,
509
497
  signJwt,
510
498
  verifyJwt,
511
- buildOidcUrl,
512
- generateCodeChallenge,
513
- submitAuthResponse,
499
+ buildOidcUrl: buildConnectUrl,
500
+ generateCodeChallenge: undefined, // Removed — PKCE was never functional.
501
+ submitAuthResponse: submitConnectResponse,
514
502
  };
515
- //# sourceMappingURL=oidc.js.map
503
+ //# sourceMappingURL=enbox-connect-protocol.js.map