@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/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
- });