@openverifiable/connector-bluesky 1.0.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 (64) hide show
  1. package/lib/__fixtures__/did-documents.d.ts +54 -0
  2. package/lib/__fixtures__/did-documents.d.ts.map +1 -0
  3. package/lib/__fixtures__/did-documents.js +56 -0
  4. package/lib/__fixtures__/metadata.d.ts +70 -0
  5. package/lib/__fixtures__/metadata.d.ts.map +1 -0
  6. package/lib/__fixtures__/metadata.js +56 -0
  7. package/lib/__fixtures__/oauth-errors.d.ts +42 -0
  8. package/lib/__fixtures__/oauth-errors.d.ts.map +1 -0
  9. package/lib/__fixtures__/oauth-errors.js +41 -0
  10. package/lib/__fixtures__/profile-responses.d.ts +34 -0
  11. package/lib/__fixtures__/profile-responses.d.ts.map +1 -0
  12. package/lib/__fixtures__/profile-responses.js +28 -0
  13. package/lib/__fixtures__/token-responses.d.ts +38 -0
  14. package/lib/__fixtures__/token-responses.d.ts.map +1 -0
  15. package/lib/__fixtures__/token-responses.js +33 -0
  16. package/lib/__tests__/integration/real-account-helpers.d.ts +43 -0
  17. package/lib/__tests__/integration/real-account-helpers.d.ts.map +1 -0
  18. package/lib/__tests__/integration/real-account-helpers.js +84 -0
  19. package/lib/__tests__/integration/real-account.test.d.ts +12 -0
  20. package/lib/__tests__/integration/real-account.test.d.ts.map +1 -0
  21. package/lib/__tests__/integration/real-account.test.js +118 -0
  22. package/lib/client-assertion.d.ts +19 -0
  23. package/lib/client-assertion.d.ts.map +1 -0
  24. package/lib/client-assertion.js +50 -0
  25. package/lib/client-assertion.test.d.ts +6 -0
  26. package/lib/client-assertion.test.d.ts.map +1 -0
  27. package/lib/client-assertion.test.js +234 -0
  28. package/lib/constant.d.ts +24 -0
  29. package/lib/constant.d.ts.map +1 -0
  30. package/lib/constant.js +77 -0
  31. package/lib/dpop.d.ts +24 -0
  32. package/lib/dpop.d.ts.map +1 -0
  33. package/lib/dpop.js +138 -0
  34. package/lib/dpop.test.d.ts +6 -0
  35. package/lib/dpop.test.d.ts.map +1 -0
  36. package/lib/dpop.test.js +266 -0
  37. package/lib/index.d.ts +12 -0
  38. package/lib/index.d.ts.map +1 -0
  39. package/lib/index.js +1129 -0
  40. package/lib/index.test.d.ts +7 -0
  41. package/lib/index.test.d.ts.map +1 -0
  42. package/lib/index.test.js +329 -0
  43. package/lib/mock.d.ts +5 -0
  44. package/lib/mock.d.ts.map +1 -0
  45. package/lib/mock.js +24 -0
  46. package/lib/pds-discovery.d.ts +18 -0
  47. package/lib/pds-discovery.d.ts.map +1 -0
  48. package/lib/pds-discovery.js +248 -0
  49. package/lib/pds-discovery.test.d.ts +6 -0
  50. package/lib/pds-discovery.test.d.ts.map +1 -0
  51. package/lib/pds-discovery.test.js +281 -0
  52. package/lib/pkce.d.ts +16 -0
  53. package/lib/pkce.d.ts.map +1 -0
  54. package/lib/pkce.js +46 -0
  55. package/lib/pkce.test.d.ts +6 -0
  56. package/lib/pkce.test.d.ts.map +1 -0
  57. package/lib/pkce.test.js +117 -0
  58. package/lib/test-utils.d.ts +65 -0
  59. package/lib/test-utils.d.ts.map +1 -0
  60. package/lib/test-utils.js +132 -0
  61. package/lib/types.d.ts +221 -0
  62. package/lib/types.d.ts.map +1 -0
  63. package/lib/types.js +95 -0
  64. package/package.json +96 -0
package/lib/dpop.js ADDED
@@ -0,0 +1,138 @@
1
+ import { SignJWT } from 'jose';
2
+ import { randomBytes } from 'crypto';
3
+ import { createHash } from 'crypto';
4
+ import { DPOP_CONFIG } from './constant.js';
5
+ /**
6
+ * Generate ES256 DPoP key pair
7
+ * Uses Web Crypto API (Node.js crypto.webcrypto)
8
+ */
9
+ export async function generateDPoPKeyPair() {
10
+ const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
11
+ const keyPair = await crypto.subtle.generateKey({
12
+ name: 'ECDSA',
13
+ namedCurve: 'P-256', // ES256 uses P-256
14
+ }, true, // extractable (needed for server-side storage)
15
+ DPOP_CONFIG.keyUsages);
16
+ return {
17
+ publicKey: keyPair.publicKey,
18
+ privateKey: keyPair.privateKey,
19
+ };
20
+ }
21
+ /**
22
+ * Export public key as JWK for inclusion in DPoP proof header
23
+ */
24
+ export async function exportPublicKeyAsJWK(publicKey) {
25
+ const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
26
+ const jwk = await crypto.subtle.exportKey('jwk', publicKey);
27
+ // Remove private key fields if present
28
+ const { d, ...publicJwk } = jwk;
29
+ return publicJwk;
30
+ }
31
+ /**
32
+ * Create DPoP proof JWT for token endpoint requests
33
+ */
34
+ export async function createDPoPProofForToken(keyPair, httpMethod, tokenEndpointUrl, nonce) {
35
+ const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
36
+ const publicKeyJwk = await exportPublicKeyAsJWK(keyPair.publicKey);
37
+ // Generate unique jti
38
+ const jti = base64UrlEncode(randomBytes(DPOP_CONFIG.jtiLength));
39
+ const now = Math.floor(Date.now() / 1000);
40
+ const jwt = new SignJWT({
41
+ jti,
42
+ htm: httpMethod,
43
+ htu: tokenEndpointUrl,
44
+ iat: now,
45
+ exp: now + 60,
46
+ ...(nonce && { nonce }),
47
+ })
48
+ .setProtectedHeader({
49
+ typ: 'dpop+jwt',
50
+ alg: DPOP_CONFIG.algorithm,
51
+ jwk: publicKeyJwk,
52
+ });
53
+ // Sign with private key
54
+ // Note: jose library handles the signing, but we need to convert CryptoKey to JWK format
55
+ // For now, we'll use a workaround - in production, you'd use a proper JWT library
56
+ // that supports CryptoKey directly
57
+ // This is a simplified version - in production, use a library that supports CryptoKey
58
+ // For now, we'll need to export the private key and use it with jose
59
+ const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
60
+ // Ensure kty is defined and cast to JWK
61
+ if (!privateKeyJwk.kty || typeof privateKeyJwk.kty !== 'string') {
62
+ throw new Error('Private key JWK missing kty field');
63
+ }
64
+ // Import as JWK for jose - create a properly typed JWK object
65
+ const { importJWK } = await import('jose');
66
+ const jwkWithKty = {
67
+ ...privateKeyJwk,
68
+ kty: privateKeyJwk.kty,
69
+ };
70
+ const signingKey = await importJWK(jwkWithKty, DPOP_CONFIG.algorithm);
71
+ return await jwt.sign(signingKey);
72
+ }
73
+ /**
74
+ * Create DPoP proof JWT for resource server (PDS) requests
75
+ * Includes ath (access token hash) field
76
+ */
77
+ export async function createDPoPProofForResource(keyPair, httpMethod, resourceUrl, accessToken, nonce) {
78
+ const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
79
+ const publicKeyJwk = await exportPublicKeyAsJWK(keyPair.publicKey);
80
+ // Hash access token for ath field (same as S256 PKCE challenge)
81
+ const tokenHash = createHash('sha256').update(accessToken).digest();
82
+ const ath = base64UrlEncode(tokenHash);
83
+ const jti = base64UrlEncode(randomBytes(DPOP_CONFIG.jtiLength));
84
+ const now = Math.floor(Date.now() / 1000);
85
+ const jwt = new SignJWT({
86
+ jti,
87
+ htm: httpMethod,
88
+ htu: resourceUrl,
89
+ iat: now,
90
+ exp: now + 60,
91
+ ath,
92
+ ...(nonce && { nonce }),
93
+ })
94
+ .setProtectedHeader({
95
+ typ: 'dpop+jwt',
96
+ alg: DPOP_CONFIG.algorithm,
97
+ jwk: publicKeyJwk,
98
+ });
99
+ const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
100
+ // Ensure kty is defined and cast to JWK
101
+ if (!privateKeyJwk.kty || typeof privateKeyJwk.kty !== 'string') {
102
+ throw new Error('Private key JWK missing kty field');
103
+ }
104
+ // Import as JWK for jose - create a properly typed JWK object
105
+ const { importJWK } = await import('jose');
106
+ const jwkWithKty = {
107
+ ...privateKeyJwk,
108
+ kty: privateKeyJwk.kty,
109
+ };
110
+ const signingKey = await importJWK(jwkWithKty, DPOP_CONFIG.algorithm);
111
+ return await jwt.sign(signingKey);
112
+ }
113
+ /**
114
+ * Extract DPoP nonce from response headers
115
+ */
116
+ export function extractDPoPNonce(headers) {
117
+ if (headers instanceof Headers) {
118
+ return headers.get('dpop-nonce') || null;
119
+ }
120
+ const dpopNonce = headers['dpop-nonce'] || headers['DPoP-Nonce'];
121
+ if (typeof dpopNonce === 'string') {
122
+ return dpopNonce;
123
+ }
124
+ if (Array.isArray(dpopNonce) && dpopNonce.length > 0 && typeof dpopNonce[0] === 'string') {
125
+ return dpopNonce[0];
126
+ }
127
+ return null;
128
+ }
129
+ /**
130
+ * Base64url encode
131
+ */
132
+ function base64UrlEncode(buffer) {
133
+ return buffer
134
+ .toString('base64')
135
+ .replace(/\+/g, '-')
136
+ .replace(/\//g, '_')
137
+ .replace(/=/g, '');
138
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * DPoP Tests
3
+ * Tests for Demonstrating Proof of Possession (RFC 9449) implementation
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=dpop.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dpop.test.d.ts","sourceRoot":"","sources":["../src/dpop.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * DPoP Tests
3
+ * Tests for Demonstrating Proof of Possession (RFC 9449) implementation
4
+ */
5
+ import { describe, it, expect } from '@jest/globals';
6
+ import { generateDPoPKeyPair, exportPublicKeyAsJWK, createDPoPProofForToken, createDPoPProofForResource, extractDPoPNonce, } from './dpop.js';
7
+ import { sha256Base64Url, createMockDPoPNonceError, } from './test-utils.js';
8
+ describe('DPoP Implementation', () => {
9
+ describe('generateDPoPKeyPair', () => {
10
+ it('should generate valid DPoP key pair', async () => {
11
+ const keyPair = await generateDPoPKeyPair();
12
+ expect(keyPair.publicKey).toBeDefined();
13
+ expect(keyPair.privateKey).toBeDefined();
14
+ expect(keyPair.publicKey).toBeInstanceOf(CryptoKey);
15
+ expect(keyPair.privateKey).toBeInstanceOf(CryptoKey);
16
+ });
17
+ it('should generate ES256 key pairs with P-256 curve', async () => {
18
+ const keyPair = await generateDPoPKeyPair();
19
+ const crypto = globalThis.crypto;
20
+ // Verify key algorithm
21
+ const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
22
+ expect(publicKeyJwk.kty).toBe('EC');
23
+ expect(publicKeyJwk.crv).toBe('P-256');
24
+ // Note: alg is not part of JWK standard, it's inferred from the key type
25
+ });
26
+ it('should generate different key pairs each time', async () => {
27
+ const keyPair1 = await generateDPoPKeyPair();
28
+ const keyPair2 = await generateDPoPKeyPair();
29
+ // Export and compare
30
+ const crypto = globalThis.crypto;
31
+ const jwk1 = await crypto.subtle.exportKey('jwk', keyPair1.privateKey);
32
+ const jwk2 = await crypto.subtle.exportKey('jwk', keyPair2.privateKey);
33
+ expect(jwk1.d).not.toBe(jwk2.d);
34
+ });
35
+ it('should generate extractable keys', async () => {
36
+ const keyPair = await generateDPoPKeyPair();
37
+ const crypto = globalThis.crypto;
38
+ // Should be able to export keys
39
+ const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
40
+ const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
41
+ expect(publicJwk).toBeDefined();
42
+ expect(privateJwk).toBeDefined();
43
+ });
44
+ });
45
+ describe('exportPublicKeyAsJWK', () => {
46
+ it('should export public key as JWK without private fields', async () => {
47
+ const keyPair = await generateDPoPKeyPair();
48
+ const publicJwk = await exportPublicKeyAsJWK(keyPair.publicKey);
49
+ expect(publicJwk.kty).toBe('EC');
50
+ expect(publicJwk.crv).toBe('P-256');
51
+ expect(publicJwk.x).toBeDefined();
52
+ expect(publicJwk.y).toBeDefined();
53
+ expect(publicJwk.d).toBeUndefined(); // Private key field should not be present
54
+ });
55
+ });
56
+ describe('createDPoPProofForToken', () => {
57
+ it('should create valid DPoP proof JWT for token endpoint', async () => {
58
+ const keyPair = await generateDPoPKeyPair();
59
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
60
+ const proof = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint);
61
+ expect(proof).toBeDefined();
62
+ expect(typeof proof).toBe('string');
63
+ // Decode and verify structure
64
+ const parts = proof.split('.');
65
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
66
+ expect(decoded).toBeDefined();
67
+ });
68
+ it('should include required JWT header fields', async () => {
69
+ const keyPair = await generateDPoPKeyPair();
70
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
71
+ const proof = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint);
72
+ // Decode header
73
+ const parts = proof.split('.');
74
+ const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
75
+ expect(header.typ).toBe('dpop+jwt');
76
+ expect(header.alg).toBe('ES256');
77
+ expect(header.jwk).toBeDefined();
78
+ expect(header.jwk.kty).toBe('EC');
79
+ expect(header.jwk.crv).toBe('P-256');
80
+ });
81
+ it('should include required JWT payload fields', async () => {
82
+ const keyPair = await generateDPoPKeyPair();
83
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
84
+ const proof = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint);
85
+ const parts = proof.split('.');
86
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
87
+ expect(decoded.jti).toBeDefined();
88
+ expect(decoded.htm).toBe('POST');
89
+ expect(decoded.htu).toBe(tokenEndpoint);
90
+ expect(decoded.iat).toBeDefined();
91
+ expect(decoded.exp).toBeDefined();
92
+ });
93
+ it('should include nonce when provided', async () => {
94
+ const keyPair = await generateDPoPKeyPair();
95
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
96
+ const nonce = 'test-nonce-12345';
97
+ const proof = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint, nonce);
98
+ const parts = proof.split('.');
99
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
100
+ expect(decoded.nonce).toBe(nonce);
101
+ });
102
+ it('should include unique jti in each proof', async () => {
103
+ const keyPair = await generateDPoPKeyPair();
104
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
105
+ const proof1 = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint);
106
+ const proof2 = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint);
107
+ const parts1 = proof1.split('.');
108
+ const parts2 = proof2.split('.');
109
+ const decoded1 = JSON.parse(Buffer.from(parts1[1], 'base64url').toString());
110
+ const decoded2 = JSON.parse(Buffer.from(parts2[1], 'base64url').toString());
111
+ expect(decoded1.jti).not.toBe(decoded2.jti);
112
+ });
113
+ it('should sign proof with correct private key', async () => {
114
+ const keyPair = await generateDPoPKeyPair();
115
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
116
+ const proof = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint);
117
+ // Verify signature can be validated by checking JWT structure
118
+ // Note: We can't verify with the same key pair because the public key
119
+ // from generateDPoPKeyPair only has 'sign' usage, not 'verify'
120
+ // In production, the server would verify using the public key from the JWT header
121
+ const parts = proof.split('.');
122
+ expect(parts.length).toBe(3); // header.payload.signature
123
+ expect(proof).toBeDefined();
124
+ });
125
+ it('should include correct HTTP method and URL', async () => {
126
+ const keyPair = await generateDPoPKeyPair();
127
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
128
+ const proof = await createDPoPProofForToken(keyPair, 'GET', tokenEndpoint);
129
+ const parts = proof.split('.');
130
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
131
+ expect(decoded.htm).toBe('GET');
132
+ expect(decoded.htu).toBe(tokenEndpoint);
133
+ });
134
+ });
135
+ describe('createDPoPProofForResource', () => {
136
+ it('should create valid DPoP proof JWT for resource server', async () => {
137
+ const keyPair = await generateDPoPKeyPair();
138
+ const resourceUrl = 'https://bsky.social/xrpc/app.bsky.actor.getProfile';
139
+ const accessToken = 'test_access_token_12345';
140
+ const proof = await createDPoPProofForResource(keyPair, 'GET', resourceUrl, accessToken);
141
+ expect(proof).toBeDefined();
142
+ expect(typeof proof).toBe('string');
143
+ });
144
+ it('should include ath (access token hash) field', async () => {
145
+ const keyPair = await generateDPoPKeyPair();
146
+ const resourceUrl = 'https://bsky.social/xrpc/app.bsky.actor.getProfile';
147
+ const accessToken = 'test_access_token_12345';
148
+ const proof = await createDPoPProofForResource(keyPair, 'GET', resourceUrl, accessToken);
149
+ const parts = proof.split('.');
150
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
151
+ expect(decoded.ath).toBeDefined();
152
+ // Verify ath is correct hash of access token
153
+ const expectedAth = sha256Base64Url(accessToken);
154
+ expect(decoded.ath).toBe(expectedAth);
155
+ });
156
+ it('should include all required fields plus ath', async () => {
157
+ const keyPair = await generateDPoPKeyPair();
158
+ const resourceUrl = 'https://bsky.social/xrpc/app.bsky.actor.getProfile';
159
+ const accessToken = 'test_access_token_12345';
160
+ const proof = await createDPoPProofForResource(keyPair, 'GET', resourceUrl, accessToken);
161
+ const parts = proof.split('.');
162
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
163
+ expect(decoded.jti).toBeDefined();
164
+ expect(decoded.htm).toBe('GET');
165
+ expect(decoded.htu).toBe(resourceUrl);
166
+ expect(decoded.iat).toBeDefined();
167
+ expect(decoded.exp).toBeDefined();
168
+ expect(decoded.ath).toBeDefined();
169
+ });
170
+ it('should include nonce when provided', async () => {
171
+ const keyPair = await generateDPoPKeyPair();
172
+ const resourceUrl = 'https://bsky.social/xrpc/app.bsky.actor.getProfile';
173
+ const accessToken = 'test_access_token_12345';
174
+ const nonce = 'test-nonce-67890';
175
+ const proof = await createDPoPProofForResource(keyPair, 'GET', resourceUrl, accessToken, nonce);
176
+ const parts = proof.split('.');
177
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
178
+ expect(decoded.nonce).toBe(nonce);
179
+ });
180
+ it('should sign proof with correct private key', async () => {
181
+ const keyPair = await generateDPoPKeyPair();
182
+ const resourceUrl = 'https://bsky.social/xrpc/app.bsky.actor.getProfile';
183
+ const accessToken = 'test_access_token_12345';
184
+ const proof = await createDPoPProofForResource(keyPair, 'GET', resourceUrl, accessToken);
185
+ // Verify signature can be validated by checking JWT structure
186
+ // Note: We can't verify with the same key pair because the public key
187
+ // from generateDPoPKeyPair only has 'sign' usage, not 'verify'
188
+ // In production, the server would verify using the public key from the JWT header
189
+ const parts = proof.split('.');
190
+ expect(parts.length).toBe(3); // header.payload.signature
191
+ expect(proof).toBeDefined();
192
+ });
193
+ });
194
+ describe('extractDPoPNonce', () => {
195
+ it('should extract nonce from Headers object', () => {
196
+ const headers = new Headers();
197
+ headers.set('dpop-nonce', 'test-nonce-12345');
198
+ const nonce = extractDPoPNonce(headers);
199
+ expect(nonce).toBe('test-nonce-12345');
200
+ });
201
+ it('should extract nonce from record with lowercase key', () => {
202
+ const headers = { 'dpop-nonce': 'test-nonce-12345' };
203
+ const nonce = extractDPoPNonce(headers);
204
+ expect(nonce).toBe('test-nonce-12345');
205
+ });
206
+ it('should extract nonce from record with uppercase key', () => {
207
+ const headers = { 'DPoP-Nonce': 'test-nonce-12345' };
208
+ const nonce = extractDPoPNonce(headers);
209
+ expect(nonce).toBe('test-nonce-12345');
210
+ });
211
+ it('should return null when nonce is not present', () => {
212
+ const headers = { 'content-type': 'application/json' };
213
+ const nonce = extractDPoPNonce(headers);
214
+ expect(nonce).toBeNull();
215
+ });
216
+ it('should handle array values in headers', () => {
217
+ const headers = { 'dpop-nonce': ['test-nonce-12345'] };
218
+ const nonce = extractDPoPNonce(headers);
219
+ expect(nonce).toBe('test-nonce-12345');
220
+ });
221
+ it('should handle undefined headers', () => {
222
+ const headers = {};
223
+ const nonce = extractDPoPNonce(headers);
224
+ expect(nonce).toBeNull();
225
+ });
226
+ });
227
+ describe('Nonce Retry Flow', () => {
228
+ it('should handle use_dpop_nonce error response', () => {
229
+ const errorResponse = createMockDPoPNonceError('new-nonce-12345', 401);
230
+ expect(errorResponse.statusCode).toBe(401);
231
+ expect(errorResponse.headers['dpop-nonce']).toBe('new-nonce-12345');
232
+ expect(errorResponse.body).toContain('use_dpop_nonce');
233
+ const nonce = extractDPoPNonce(errorResponse.headers);
234
+ expect(nonce).toBe('new-nonce-12345');
235
+ });
236
+ it('should create proof with nonce after receiving error', async () => {
237
+ const keyPair = await generateDPoPKeyPair();
238
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
239
+ const nonce = 'new-nonce-from-error';
240
+ // First attempt without nonce (would fail)
241
+ // Second attempt with nonce
242
+ const proof = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint, nonce);
243
+ const parts = proof.split('.');
244
+ const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
245
+ expect(decoded.nonce).toBe(nonce);
246
+ });
247
+ });
248
+ describe('Nonce Rotation and Persistence', () => {
249
+ it('should handle nonce changes during session', async () => {
250
+ const keyPair = await generateDPoPKeyPair();
251
+ const tokenEndpoint = 'https://bsky.social/oauth/token';
252
+ // Initial nonce
253
+ const nonce1 = 'nonce-1';
254
+ const proof1 = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint, nonce1);
255
+ // Updated nonce
256
+ const nonce2 = 'nonce-2';
257
+ const proof2 = await createDPoPProofForToken(keyPair, 'POST', tokenEndpoint, nonce2);
258
+ const parts1 = proof1.split('.');
259
+ const parts2 = proof2.split('.');
260
+ const decoded1 = JSON.parse(Buffer.from(parts1[1], 'base64url').toString());
261
+ const decoded2 = JSON.parse(Buffer.from(parts2[1], 'base64url').toString());
262
+ expect(decoded1.nonce).toBe(nonce1);
263
+ expect(decoded2.nonce).toBe(nonce2);
264
+ });
265
+ });
266
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Bluesky/AT Protocol OAuth 2.0 connector implementation
3
+ * Implements PAR (Pushed Authorization Requests), PKCE, and DPoP
4
+ * https://atproto.com/specs/oauth
5
+ */
6
+ import type { CreateConnector, SocialConnector } from '@logto/connector-kit';
7
+ /**
8
+ * Create the Bluesky connector instance
9
+ */
10
+ declare const createBlueskyConnector: CreateConnector<SocialConnector>;
11
+ export default createBlueskyConnector;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAIV,eAAe,EACf,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAkmB9B;;GAEG;AACH,QAAA,MAAM,sBAAsB,EAAE,eAAe,CAAC,eAAe,CAW5D,CAAC;AAEF,eAAe,sBAAsB,CAAC"}