@openverifiable/connector-bluesky 1.0.0 → 1.0.2
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/lib/client-assertion.d.ts +8 -2
- package/lib/client-assertion.d.ts.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +74 -10
- package/lib/types.d.ts +3 -3
- package/package.json +7 -6
- package/lib/__fixtures__/did-documents.js +0 -56
- package/lib/__fixtures__/metadata.js +0 -56
- package/lib/__fixtures__/oauth-errors.js +0 -41
- package/lib/__fixtures__/profile-responses.js +0 -28
- package/lib/__fixtures__/token-responses.js +0 -33
- package/lib/__tests__/integration/real-account-helpers.js +0 -84
- package/lib/__tests__/integration/real-account.test.js +0 -118
- package/lib/client-assertion.js +0 -50
- package/lib/client-assertion.test.js +0 -234
- package/lib/constant.js +0 -77
- package/lib/dpop.js +0 -138
- package/lib/dpop.test.js +0 -266
- package/lib/index.test.js +0 -329
- package/lib/mock.js +0 -24
- package/lib/pds-discovery.js +0 -248
- package/lib/pds-discovery.test.js +0 -281
- package/lib/pkce.js +0 -46
- package/lib/pkce.test.js +0 -117
- package/lib/test-utils.js +0 -132
- package/lib/types.js +0 -95
package/lib/pkce.test.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PKCE Tests
|
|
3
|
-
* Tests for Proof Key for Code Exchange (RFC 7636) implementation
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect } from '@jest/globals';
|
|
6
|
-
import { createHash } from 'crypto';
|
|
7
|
-
import { generateCodeVerifier, generateCodeChallenge, generatePKCECodePair, } from './pkce.js';
|
|
8
|
-
import { base64UrlEncode, sha256Base64Url } from './test-utils.js';
|
|
9
|
-
describe('PKCE Implementation', () => {
|
|
10
|
-
describe('generateCodeVerifier', () => {
|
|
11
|
-
it('should generate code verifier with correct length (43+ characters)', () => {
|
|
12
|
-
const verifier = generateCodeVerifier();
|
|
13
|
-
expect(verifier).toBeDefined();
|
|
14
|
-
expect(verifier.length).toBeGreaterThanOrEqual(43);
|
|
15
|
-
expect(typeof verifier).toBe('string');
|
|
16
|
-
});
|
|
17
|
-
it('should generate base64url-safe encoding (no +, /, or =)', () => {
|
|
18
|
-
const verifier = generateCodeVerifier();
|
|
19
|
-
expect(verifier).not.toContain('+');
|
|
20
|
-
expect(verifier).not.toContain('/');
|
|
21
|
-
expect(verifier).not.toContain('=');
|
|
22
|
-
// Should only contain base64url-safe characters
|
|
23
|
-
expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
24
|
-
});
|
|
25
|
-
it('should generate unique code verifiers each time', () => {
|
|
26
|
-
const verifiers = new Set();
|
|
27
|
-
const iterations = 100;
|
|
28
|
-
for (let i = 0; i < iterations; i++) {
|
|
29
|
-
verifiers.add(generateCodeVerifier());
|
|
30
|
-
}
|
|
31
|
-
// All verifiers should be unique
|
|
32
|
-
expect(verifiers.size).toBe(iterations);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
describe('generateCodeChallenge', () => {
|
|
36
|
-
it('should generate S256 code challenge from verifier', () => {
|
|
37
|
-
const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
|
|
38
|
-
const challenge = generateCodeChallenge(verifier);
|
|
39
|
-
// Expected challenge for the example verifier from RFC 7636
|
|
40
|
-
const expected = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';
|
|
41
|
-
expect(challenge).toBe(expected);
|
|
42
|
-
});
|
|
43
|
-
it('should produce base64url-safe encoding', () => {
|
|
44
|
-
const verifier = generateCodeVerifier();
|
|
45
|
-
const challenge = generateCodeChallenge(verifier);
|
|
46
|
-
expect(challenge).not.toContain('+');
|
|
47
|
-
expect(challenge).not.toContain('/');
|
|
48
|
-
expect(challenge).not.toContain('=');
|
|
49
|
-
expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
50
|
-
});
|
|
51
|
-
it('should generate consistent challenge for same verifier', () => {
|
|
52
|
-
const verifier = generateCodeVerifier();
|
|
53
|
-
const challenge1 = generateCodeChallenge(verifier);
|
|
54
|
-
const challenge2 = generateCodeChallenge(verifier);
|
|
55
|
-
expect(challenge1).toBe(challenge2);
|
|
56
|
-
});
|
|
57
|
-
it('should generate different challenges for different verifiers', () => {
|
|
58
|
-
const verifier1 = generateCodeVerifier();
|
|
59
|
-
const verifier2 = generateCodeVerifier();
|
|
60
|
-
const challenge1 = generateCodeChallenge(verifier1);
|
|
61
|
-
const challenge2 = generateCodeChallenge(verifier2);
|
|
62
|
-
expect(challenge1).not.toBe(challenge2);
|
|
63
|
-
});
|
|
64
|
-
it('should use SHA256 hash for S256 method', () => {
|
|
65
|
-
const verifier = 'test_verifier_12345';
|
|
66
|
-
const challenge = generateCodeChallenge(verifier);
|
|
67
|
-
// Manually compute expected hash
|
|
68
|
-
const hash = createHash('sha256');
|
|
69
|
-
hash.update(verifier);
|
|
70
|
-
const digest = hash.digest();
|
|
71
|
-
const expected = base64UrlEncode(digest);
|
|
72
|
-
expect(challenge).toBe(expected);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
describe('generatePKCECodePair', () => {
|
|
76
|
-
it('should generate valid PKCE code pair', () => {
|
|
77
|
-
const pkce = generatePKCECodePair();
|
|
78
|
-
expect(pkce.codeVerifier).toBeDefined();
|
|
79
|
-
expect(pkce.codeChallenge).toBeDefined();
|
|
80
|
-
expect(pkce.codeChallengeMethod).toBe('S256');
|
|
81
|
-
expect(pkce.codeVerifier.length).toBeGreaterThanOrEqual(43);
|
|
82
|
-
expect(pkce.codeChallenge.length).toBeGreaterThanOrEqual(43);
|
|
83
|
-
});
|
|
84
|
-
it('should generate different code pairs each time', () => {
|
|
85
|
-
const pkce1 = generatePKCECodePair();
|
|
86
|
-
const pkce2 = generatePKCECodePair();
|
|
87
|
-
expect(pkce1.codeVerifier).not.toBe(pkce2.codeVerifier);
|
|
88
|
-
expect(pkce1.codeChallenge).not.toBe(pkce2.codeChallenge);
|
|
89
|
-
});
|
|
90
|
-
it('should generate challenge that matches verifier', () => {
|
|
91
|
-
const pkce = generatePKCECodePair();
|
|
92
|
-
const expectedChallenge = generateCodeChallenge(pkce.codeVerifier);
|
|
93
|
-
expect(pkce.codeChallenge).toBe(expectedChallenge);
|
|
94
|
-
});
|
|
95
|
-
it('should use S256 challenge method', () => {
|
|
96
|
-
const pkce = generatePKCECodePair();
|
|
97
|
-
expect(pkce.codeChallengeMethod).toBe('S256');
|
|
98
|
-
// Verify it's actually S256 (SHA256 + base64url)
|
|
99
|
-
const expectedChallenge = sha256Base64Url(pkce.codeVerifier);
|
|
100
|
-
expect(pkce.codeChallenge).toBe(expectedChallenge);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('Code Verifier to Challenge Relationship', () => {
|
|
104
|
-
it('should verify challenge can be derived from verifier', () => {
|
|
105
|
-
const pkce = generatePKCECodePair();
|
|
106
|
-
const derivedChallenge = generateCodeChallenge(pkce.codeVerifier);
|
|
107
|
-
expect(derivedChallenge).toBe(pkce.codeChallenge);
|
|
108
|
-
});
|
|
109
|
-
it('should handle verifier-to-challenge round trip', () => {
|
|
110
|
-
const verifier = generateCodeVerifier();
|
|
111
|
-
const challenge = generateCodeChallenge(verifier);
|
|
112
|
-
// Challenge should be deterministic from verifier
|
|
113
|
-
const challenge2 = generateCodeChallenge(verifier);
|
|
114
|
-
expect(challenge).toBe(challenge2);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
});
|
package/lib/test-utils.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test Utilities
|
|
3
|
-
* Shared helper functions for testing
|
|
4
|
-
*/
|
|
5
|
-
import { createHash } from 'crypto';
|
|
6
|
-
import { jwtVerify } from 'jose';
|
|
7
|
-
/**
|
|
8
|
-
* Base64url encode (URL-safe base64)
|
|
9
|
-
*/
|
|
10
|
-
export function base64UrlEncode(buffer) {
|
|
11
|
-
return buffer
|
|
12
|
-
.toString('base64')
|
|
13
|
-
.replace(/\+/g, '-')
|
|
14
|
-
.replace(/\//g, '_')
|
|
15
|
-
.replace(/=/g, '');
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Base64url decode
|
|
19
|
-
*/
|
|
20
|
-
export function base64UrlDecode(str) {
|
|
21
|
-
// Add padding if needed
|
|
22
|
-
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
23
|
-
while (base64.length % 4) {
|
|
24
|
-
base64 += '=';
|
|
25
|
-
}
|
|
26
|
-
return Buffer.from(base64, 'base64');
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Verify JWT signature and return payload
|
|
30
|
-
*/
|
|
31
|
-
export async function verifyJWT(jwt, publicKey) {
|
|
32
|
-
const { payload } = await jwtVerify(jwt, publicKey);
|
|
33
|
-
return payload;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Verify DPoP JWT signature
|
|
37
|
-
*/
|
|
38
|
-
export async function verifyDPoPJWT(jwt, keyPair) {
|
|
39
|
-
return verifyJWT(jwt, keyPair.publicKey);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Generate SHA256 hash and base64url encode (same as S256 PKCE challenge)
|
|
43
|
-
*/
|
|
44
|
-
export function sha256Base64Url(input) {
|
|
45
|
-
const hash = createHash('sha256');
|
|
46
|
-
hash.update(input);
|
|
47
|
-
const digest = hash.digest();
|
|
48
|
-
return base64UrlEncode(digest);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Generate test ES256 key pair
|
|
52
|
-
*/
|
|
53
|
-
export async function generateTestKeyPair() {
|
|
54
|
-
const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
|
|
55
|
-
const keyPair = await crypto.subtle.generateKey({
|
|
56
|
-
name: 'ECDSA',
|
|
57
|
-
namedCurve: 'P-256',
|
|
58
|
-
}, true, // extractable
|
|
59
|
-
['sign', 'verify'] // Both sign and verify for testing
|
|
60
|
-
);
|
|
61
|
-
return {
|
|
62
|
-
publicKey: keyPair.publicKey,
|
|
63
|
-
privateKey: keyPair.privateKey,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Export public key as JWK
|
|
68
|
-
*/
|
|
69
|
-
export async function exportPublicKeyAsJWK(publicKey) {
|
|
70
|
-
const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
|
|
71
|
-
const jwk = await crypto.subtle.exportKey('jwk', publicKey);
|
|
72
|
-
// Remove private key fields if present
|
|
73
|
-
const { d, ...publicJwk } = jwk;
|
|
74
|
-
return publicJwk;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Create mock HTTP response headers
|
|
78
|
-
*/
|
|
79
|
-
export function createMockHeaders(headers = {}) {
|
|
80
|
-
return {
|
|
81
|
-
'content-type': 'application/json',
|
|
82
|
-
...headers,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Create mock HTTP response with DPoP nonce
|
|
87
|
-
*/
|
|
88
|
-
export function createMockResponseWithNonce(body, nonce) {
|
|
89
|
-
return {
|
|
90
|
-
statusCode: 200,
|
|
91
|
-
headers: {
|
|
92
|
-
'content-type': 'application/json',
|
|
93
|
-
'dpop-nonce': nonce,
|
|
94
|
-
},
|
|
95
|
-
body: JSON.stringify(body),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Create mock HTTP error response with use_dpop_nonce
|
|
100
|
-
*/
|
|
101
|
-
export function createMockDPoPNonceError(nonce, statusCode = 401) {
|
|
102
|
-
return {
|
|
103
|
-
statusCode,
|
|
104
|
-
headers: {
|
|
105
|
-
'content-type': 'application/json',
|
|
106
|
-
'dpop-nonce': nonce,
|
|
107
|
-
'www-authenticate': `DPoP error="use_dpop_nonce", error_description="Resource server requires nonce in DPoP proof"`,
|
|
108
|
-
},
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
error: 'use_dpop_nonce',
|
|
111
|
-
error_description: 'Resource server requires nonce in DPoP proof',
|
|
112
|
-
nonce,
|
|
113
|
-
}),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Sleep utility for testing async behavior
|
|
118
|
-
*/
|
|
119
|
-
export function sleep(ms) {
|
|
120
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Generate random string for testing
|
|
124
|
-
*/
|
|
125
|
-
export function randomString(length = 16) {
|
|
126
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
127
|
-
let result = '';
|
|
128
|
-
for (let i = 0; i < length; i++) {
|
|
129
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
130
|
-
}
|
|
131
|
-
return result;
|
|
132
|
-
}
|
package/lib/types.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
/**
|
|
3
|
-
* BlueskyConfig
|
|
4
|
-
* Configuration for AT Protocol OAuth client
|
|
5
|
-
* Requires private_key_jwt for confidential client security
|
|
6
|
-
*/
|
|
7
|
-
export const blueskyConfigGuard = z.object({
|
|
8
|
-
clientMetadataUri: z.string().url(),
|
|
9
|
-
clientId: z.string().url().optional(),
|
|
10
|
-
jwksUri: z.string().url(),
|
|
11
|
-
scope: z.string().default('atproto transition:generic'),
|
|
12
|
-
tokenEndpointAuthMethod: z.literal('private_key_jwt'),
|
|
13
|
-
}).refine((data) => {
|
|
14
|
-
// JWKS URI is required for private_key_jwt
|
|
15
|
-
if (data.tokenEndpointAuthMethod === 'private_key_jwt' && !data.jwksUri) {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
return true;
|
|
19
|
-
}, {
|
|
20
|
-
message: 'jwksUri is required when tokenEndpointAuthMethod is private_key_jwt',
|
|
21
|
-
path: ['jwksUri'],
|
|
22
|
-
});
|
|
23
|
-
/**
|
|
24
|
-
* authResponseGuard
|
|
25
|
-
* Validates query parameters from AT Protocol authorization callback
|
|
26
|
-
*/
|
|
27
|
-
export const authResponseGuard = z.object({
|
|
28
|
-
code: z.string().optional(),
|
|
29
|
-
state: z.string().optional(),
|
|
30
|
-
iss: z.string().url().optional(),
|
|
31
|
-
error: z.string().optional(),
|
|
32
|
-
error_description: z.string().optional(),
|
|
33
|
-
}).passthrough();
|
|
34
|
-
/**
|
|
35
|
-
* PARResponse
|
|
36
|
-
* Response from Pushed Authorization Request
|
|
37
|
-
*/
|
|
38
|
-
export const parResponseGuard = z.object({
|
|
39
|
-
request_uri: z.string(),
|
|
40
|
-
expires_in: z.number().optional(),
|
|
41
|
-
});
|
|
42
|
-
/**
|
|
43
|
-
* TokenResponse
|
|
44
|
-
* AT Protocol OAuth token response
|
|
45
|
-
*/
|
|
46
|
-
export const tokenResponseGuard = z.object({
|
|
47
|
-
access_token: z.string(),
|
|
48
|
-
refresh_token: z.string().optional(),
|
|
49
|
-
token_type: z.literal('Bearer'),
|
|
50
|
-
expires_in: z.number(),
|
|
51
|
-
scope: z.string(),
|
|
52
|
-
sub: z.string(),
|
|
53
|
-
did: z.string().optional(), // Explicit DID field
|
|
54
|
-
});
|
|
55
|
-
/**
|
|
56
|
-
* ProfileResponse
|
|
57
|
-
* AT Protocol profile response from PDS
|
|
58
|
-
*/
|
|
59
|
-
export const profileResponseGuard = z.object({
|
|
60
|
-
did: z.string(),
|
|
61
|
-
handle: z.string(),
|
|
62
|
-
displayName: z.string().optional(),
|
|
63
|
-
avatar: z.string().optional(),
|
|
64
|
-
email: z.string().optional(),
|
|
65
|
-
description: z.string().optional(),
|
|
66
|
-
});
|
|
67
|
-
/**
|
|
68
|
-
* AuthorizationServerMetadata
|
|
69
|
-
* OAuth authorization server metadata
|
|
70
|
-
*/
|
|
71
|
-
export const authorizationServerMetadataGuard = z.object({
|
|
72
|
-
issuer: z.string().url(),
|
|
73
|
-
pushed_authorization_request_endpoint: z.string().url(),
|
|
74
|
-
authorization_endpoint: z.string().url(),
|
|
75
|
-
token_endpoint: z.string().url(),
|
|
76
|
-
scopes_supported: z.string(),
|
|
77
|
-
});
|
|
78
|
-
/**
|
|
79
|
-
* ResourceServerMetadata
|
|
80
|
-
* OAuth protected resource metadata
|
|
81
|
-
*/
|
|
82
|
-
export const resourceServerMetadataGuard = z.object({
|
|
83
|
-
authorization_servers: z.array(z.string().url()),
|
|
84
|
-
});
|
|
85
|
-
/**
|
|
86
|
-
* DIDDocument
|
|
87
|
-
* AT Protocol DID document structure
|
|
88
|
-
*/
|
|
89
|
-
export const didDocumentGuard = z.object({
|
|
90
|
-
id: z.string(),
|
|
91
|
-
service: z.array(z.object({
|
|
92
|
-
type: z.string(),
|
|
93
|
-
serviceEndpoint: z.string().url(),
|
|
94
|
-
})).optional(),
|
|
95
|
-
});
|