@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.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Client Assertion Generation for private_key_jwt
3
3
  *
4
- * Generates JWT client assertion for OAuth token endpoint authentication
4
+ * Generates JWT client assertion for OAuth authentication
5
+ * Used for both PAR (Pushed Authorization Request) and token endpoint requests
5
6
  * RFC 7523: https://datatracker.ietf.org/doc/html/rfc7523
6
7
  */
7
8
  import type { BlueskyConfig } from './types.js';
@@ -14,6 +15,11 @@ import type { BlueskyConfig } from './types.js';
14
15
  *
15
16
  * For now, we'll support both approaches - if privateKeyJwk is in config, use it.
16
17
  * Otherwise, the server should provide an endpoint to generate assertions.
18
+ *
19
+ * @param clientId - The OAuth client identifier
20
+ * @param audience - The audience URL (PAR endpoint or token endpoint)
21
+ * @param config - Bluesky connector configuration
22
+ * @returns JWT client assertion string, or null if private key is not available
17
23
  */
18
- export declare function generateClientAssertion(clientId: string, tokenEndpoint: string, config: BlueskyConfig): Promise<string | null>;
24
+ export declare function generateClientAssertion(clientId: string, audience: string, config: BlueskyConfig): Promise<string | null>;
19
25
  //# sourceMappingURL=client-assertion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-assertion.d.ts","sourceRoot":"","sources":["../src/client-assertion.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmCxB"}
1
+ {"version":3,"file":"client-assertion.d.ts","sourceRoot":"","sources":["../src/client-assertion.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmCxB"}
@@ -1 +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"}
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;AAupB9B;;GAEG;AACH,QAAA,MAAM,sBAAsB,EAAE,eAAe,CAAC,eAAe,CAW5D,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
package/lib/index.js CHANGED
@@ -87,7 +87,7 @@ const authorizationServerMetadataGuard = z.object({
87
87
  pushed_authorization_request_endpoint: z.string().url(),
88
88
  authorization_endpoint: z.string().url(),
89
89
  token_endpoint: z.string().url(),
90
- scopes_supported: z.string(),
90
+ scopes_supported: z.union([z.string(), z.array(z.string())]),
91
91
  });
92
92
  /**
93
93
  * ResourceServerMetadata
@@ -596,13 +596,22 @@ async function fetchAuthorizationServerMetadataFromUrl(authServerUrl) {
596
596
  }
597
597
  // Validate that atproto scope is supported
598
598
  const scopesSupported = parsed.data.scopes_supported;
599
+ let scopes;
599
600
  if (typeof scopesSupported === 'string') {
600
- const scopes = scopesSupported.split(' ');
601
- if (!scopes.includes('atproto')) {
602
- throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, {
603
- error: 'Authorization server does not support atproto scope',
604
- });
605
- }
601
+ // Handle space-separated string format (RFC 8414 format)
602
+ scopes = scopesSupported.split(' ');
603
+ }
604
+ else if (Array.isArray(scopesSupported)) {
605
+ // Handle array format (Bluesky/AT Protocol format)
606
+ scopes = scopesSupported;
607
+ }
608
+ else {
609
+ scopes = [];
610
+ }
611
+ if (!scopes.includes('atproto')) {
612
+ throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, {
613
+ error: 'Authorization server does not support atproto scope',
614
+ });
606
615
  }
607
616
  return parsed.data;
608
617
  }
@@ -610,7 +619,8 @@ async function fetchAuthorizationServerMetadataFromUrl(authServerUrl) {
610
619
  /**
611
620
  * Client Assertion Generation for private_key_jwt
612
621
  *
613
- * Generates JWT client assertion for OAuth token endpoint authentication
622
+ * Generates JWT client assertion for OAuth authentication
623
+ * Used for both PAR (Pushed Authorization Request) and token endpoint requests
614
624
  * RFC 7523: https://datatracker.ietf.org/doc/html/rfc7523
615
625
  */
616
626
  /**
@@ -622,8 +632,13 @@ async function fetchAuthorizationServerMetadataFromUrl(authServerUrl) {
622
632
  *
623
633
  * For now, we'll support both approaches - if privateKeyJwk is in config, use it.
624
634
  * Otherwise, the server should provide an endpoint to generate assertions.
635
+ *
636
+ * @param clientId - The OAuth client identifier
637
+ * @param audience - The audience URL (PAR endpoint or token endpoint)
638
+ * @param config - Bluesky connector configuration
639
+ * @returns JWT client assertion string, or null if private key is not available
625
640
  */
626
- async function generateClientAssertion(clientId, tokenEndpoint, config) {
641
+ async function generateClientAssertion(clientId, audience, config) {
627
642
  // Check if private key is available in config (for server-side connectors)
628
643
  // In production, this would come from Vault via a server endpoint
629
644
  const privateKeyJwkStr = config.privateKeyJwk;
@@ -640,7 +655,7 @@ async function generateClientAssertion(clientId, tokenEndpoint, config) {
640
655
  const jwt = new SignJWT({
641
656
  iss: clientId,
642
657
  sub: clientId,
643
- aud: tokenEndpoint,
658
+ aud: audience,
644
659
  jti,
645
660
  exp: now + 600,
646
661
  iat: now,
@@ -945,6 +960,55 @@ const getAuthorizationUri = (getConfig) => async ({ state, redirectUri, ...rest
945
960
  code_challenge_method: pkce.codeChallengeMethod,
946
961
  ...('login_hint' in rest && rest.login_hint ? { login_hint: String(rest.login_hint) } : {}),
947
962
  });
963
+ // Add client assertion for confidential clients (required for PAR)
964
+ if (validatedConfig.tokenEndpointAuthMethod === 'private_key_jwt') {
965
+ try {
966
+ const clientAssertion = await generateClientAssertion(effectiveClientId, authServerMetadata.pushed_authorization_request_endpoint, validatedConfig);
967
+ if (clientAssertion) {
968
+ parParams.append('client_assertion', clientAssertion);
969
+ parParams.append('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
970
+ }
971
+ else {
972
+ // Fallback to assertion endpoint
973
+ const assertionEndpoint = config.assertionEndpoint;
974
+ if (assertionEndpoint) {
975
+ const assertionResponse = await got.post(assertionEndpoint, {
976
+ json: {
977
+ clientId: effectiveClientId,
978
+ tokenEndpoint: authServerMetadata.pushed_authorization_request_endpoint
979
+ },
980
+ headers: {
981
+ 'X-API-Key': config.assertionApiKey || '',
982
+ },
983
+ timeout: { request: defaultTimeout },
984
+ });
985
+ const assertionData = parseJson(assertionResponse.body);
986
+ if (assertionData?.client_assertion) {
987
+ parParams.append('client_assertion', assertionData.client_assertion);
988
+ parParams.append('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
989
+ }
990
+ else {
991
+ throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, {
992
+ error: 'Client assertion required for PAR but could not be generated',
993
+ });
994
+ }
995
+ }
996
+ else {
997
+ throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, {
998
+ error: 'Client assertion required for PAR. Provide privateKeyJwk or assertionEndpoint.',
999
+ });
1000
+ }
1001
+ }
1002
+ }
1003
+ catch (error) {
1004
+ if (error instanceof ConnectorError) {
1005
+ throw error;
1006
+ }
1007
+ throw new ConnectorError(ConnectorErrorCodes.General, {
1008
+ error: `Failed to generate client assertion for PAR: ${error instanceof Error ? error.message : 'Unknown error'}`,
1009
+ });
1010
+ }
1011
+ }
948
1012
  const { requestUri, nonce } = await makePARRequest(authServerMetadata.pushed_authorization_request_endpoint, parParams, dpopKeyPair);
949
1013
  // Store PKCE verifier and DPoP key pair in state store
950
1014
  // Export DPoP keypair as JWK for storage
package/lib/types.d.ts CHANGED
@@ -141,19 +141,19 @@ export declare const authorizationServerMetadataGuard: z.ZodObject<{
141
141
  pushed_authorization_request_endpoint: z.ZodString;
142
142
  authorization_endpoint: z.ZodString;
143
143
  token_endpoint: z.ZodString;
144
- scopes_supported: z.ZodString;
144
+ scopes_supported: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
145
145
  }, "strip", z.ZodTypeAny, {
146
146
  issuer: string;
147
147
  pushed_authorization_request_endpoint: string;
148
148
  authorization_endpoint: string;
149
149
  token_endpoint: string;
150
- scopes_supported: string;
150
+ scopes_supported: string | string[];
151
151
  }, {
152
152
  issuer: string;
153
153
  pushed_authorization_request_endpoint: string;
154
154
  authorization_endpoint: string;
155
155
  token_endpoint: string;
156
- scopes_supported: string;
156
+ scopes_supported: string | string[];
157
157
  }>;
158
158
  export type AuthorizationServerMetadata = z.infer<typeof authorizationServerMetadataGuard>;
159
159
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openverifiable/connector-bluesky",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Bluesky/AT Protocol OAuth connector for LogTo with PAR, PKCE, and DPoP support",
5
5
  "author": "OpenVerifiable (https://github.com/openverifiable)",
6
6
  "homepage": "https://openverifiable.org",
@@ -33,15 +33,14 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "precommit": "lint-staged",
36
- "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json",
37
36
  "build": "rm -rf lib/ && tsc -p tsconfig.build.json --noEmit && rollup -c && exit 0",
38
37
  "format": "prettier --write 'src/**/*.{js,ts,cjs,mjs}'",
39
38
  "lint": "eslint --ext .ts src",
40
39
  "lint:report": "npm lint --format json --output-file report.json",
41
- "test": "npm build:test && NODE_OPTIONS=--experimental-vm-modules jest",
42
- "test:coverage": "npm run test --coverage --silent",
43
- "test:unit": "npm build:test && NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=integration",
44
- "test:integration": "npm build:test && NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=integration"
40
+ "test": "vitest src",
41
+ "test:coverage": "vitest src --coverage",
42
+ "test:unit": "vitest src --exclude '**/integration/**'",
43
+ "test:integration": "vitest src/__tests__/integration"
45
44
  },
46
45
  "dependencies": {
47
46
  "@logto/connector-kit": "4.6.0",
@@ -76,6 +75,8 @@
76
75
  "nock": "^13.5.4",
77
76
  "prettier": "^2.8.8",
78
77
  "rollup": "^3.29.4",
78
+ "vitest": "^3.1.1",
79
+ "@vitest/coverage-v8": "^3.1.1",
79
80
  "rollup-plugin-summary": "^2.0.0",
80
81
  "semantic-release": "^22.0.12",
81
82
  "supertest": "^6.3.4",
@@ -1,56 +0,0 @@
1
- /**
2
- * DID Document Fixtures
3
- * Valid DID documents for testing DID resolution and PDS discovery
4
- */
5
- export const didPlcDocument = {
6
- id: 'did:plc:example123456789',
7
- '@context': [
8
- 'https://www.w3.org/ns/did/v1',
9
- 'https://w3id.org/security/multikey/v1',
10
- ],
11
- service: [
12
- {
13
- id: '#atproto_pds',
14
- type: 'AtprotoPersonalDataServer',
15
- serviceEndpoint: 'https://bsky.social',
16
- },
17
- ],
18
- verificationMethod: [
19
- {
20
- id: '#atproto',
21
- type: 'Multikey',
22
- controller: 'did:plc:example123456789',
23
- publicKeyMultibase: 'z1234567890abcdef',
24
- },
25
- ],
26
- };
27
- export const didWebDocument = {
28
- id: 'did:web:example.com',
29
- '@context': [
30
- 'https://www.w3.org/ns/did/v1',
31
- 'https://w3id.org/security/multikey/v1',
32
- ],
33
- service: [
34
- {
35
- id: '#atproto_pds',
36
- type: 'AtprotoPersonalDataServer',
37
- serviceEndpoint: 'https://pds.example.com',
38
- },
39
- ],
40
- verificationMethod: [
41
- {
42
- id: '#atproto',
43
- type: 'Multikey',
44
- controller: 'did:web:example.com',
45
- publicKeyMultibase: 'zabcdef1234567890',
46
- },
47
- ],
48
- };
49
- export const didDocumentWithHandle = {
50
- ...didPlcDocument,
51
- alsoKnownAs: ['at://example.bsky.social'],
52
- };
53
- export const invalidDidDocument = {
54
- id: 'did:plc:invalid',
55
- // Missing required service field
56
- };
@@ -1,56 +0,0 @@
1
- /**
2
- * OAuth Metadata Fixtures
3
- * Authorization server and resource server metadata for testing
4
- */
5
- export const authorizationServerMetadata = {
6
- issuer: 'https://bsky.social',
7
- pushed_authorization_request_endpoint: 'https://bsky.social/oauth/par',
8
- authorization_endpoint: 'https://bsky.social/oauth/authorize',
9
- token_endpoint: 'https://bsky.social/oauth/token',
10
- scopes_supported: 'atproto transition:generic',
11
- response_types_supported: ['code'],
12
- grant_types_supported: ['authorization_code', 'refresh_token'],
13
- code_challenge_methods_supported: ['S256'],
14
- dpop_signing_alg_values_supported: ['ES256'],
15
- };
16
- export const authorizationServerMetadataWithoutAtproto = {
17
- ...authorizationServerMetadata,
18
- scopes_supported: 'openid profile email',
19
- };
20
- export const resourceServerMetadata = {
21
- resource: 'https://bsky.social',
22
- authorization_servers: ['https://bsky.social'],
23
- };
24
- export const resourceServerMetadataWithEntryway = {
25
- resource: 'https://pds.example.com',
26
- authorization_servers: ['https://entryway.example.com'],
27
- };
28
- export const clientMetadata = {
29
- client_id: 'https://app.example.com/oauth/client-metadata.json',
30
- application_type: 'web',
31
- client_name: 'Example Browser App',
32
- client_uri: 'https://app.example.com',
33
- dpop_bound_access_tokens: true,
34
- grant_types: ['authorization_code', 'refresh_token'],
35
- redirect_uris: ['https://app.example.com/oauth/callback'],
36
- response_types: ['code'],
37
- scope: 'atproto transition:generic',
38
- token_endpoint_auth_method: 'none',
39
- };
40
- export const confidentialClientMetadata = {
41
- ...clientMetadata,
42
- token_endpoint_auth_method: 'private_key_jwt',
43
- token_endpoint_auth_signing_alg: 'ES256',
44
- jwks_uri: 'https://app.example.com/.well-known/jwks.json',
45
- jwks: {
46
- keys: [
47
- {
48
- kty: 'EC',
49
- crv: 'P-256',
50
- x: 'example_x',
51
- y: 'example_y',
52
- kid: 'key-1',
53
- },
54
- ],
55
- },
56
- };
@@ -1,41 +0,0 @@
1
- /**
2
- * OAuth Error Response Fixtures
3
- * Standard OAuth error responses for testing error handling
4
- */
5
- export const useDpopNonceError = {
6
- error: 'use_dpop_nonce',
7
- error_description: 'Resource server requires nonce in DPoP proof',
8
- nonce: 'eyJ7S_zG.eyJH0-Z.HX4w-7v',
9
- };
10
- export const invalidRequestError = {
11
- error: 'invalid_request',
12
- error_description: 'The request is missing a required parameter',
13
- };
14
- export const invalidClientError = {
15
- error: 'invalid_client',
16
- error_description: 'Client authentication failed',
17
- };
18
- export const invalidGrantError = {
19
- error: 'invalid_grant',
20
- error_description: 'The provided authorization grant is invalid',
21
- };
22
- export const invalidScopeError = {
23
- error: 'invalid_scope',
24
- error_description: 'The requested scope is invalid',
25
- };
26
- export const unauthorizedClientError = {
27
- error: 'unauthorized_client',
28
- error_description: 'The client is not authorized to request an authorization code',
29
- };
30
- export const unsupportedGrantTypeError = {
31
- error: 'unsupported_grant_type',
32
- error_description: 'The authorization grant type is not supported',
33
- };
34
- export const serverError = {
35
- error: 'server_error',
36
- error_description: 'The authorization server encountered an error',
37
- };
38
- export const temporarilyUnavailableError = {
39
- error: 'temporarily_unavailable',
40
- error_description: 'The authorization server is temporarily unavailable',
41
- };
@@ -1,28 +0,0 @@
1
- /**
2
- * Profile Response Fixtures
3
- * AT Protocol profile responses from PDS for testing
4
- */
5
- export const profileResponse = {
6
- did: 'did:plc:example123456789',
7
- handle: 'example.bsky.social',
8
- displayName: 'Example User',
9
- avatar: 'https://cdn.bsky.app/avatar/example.jpg',
10
- email: 'user@example.com',
11
- description: 'This is an example user profile',
12
- createdAt: '2024-01-01T00:00:00.000Z',
13
- };
14
- export const profileResponseMinimal = {
15
- did: 'did:plc:example123456789',
16
- handle: 'example.bsky.social',
17
- };
18
- export const profileResponseWithoutEmail = {
19
- did: 'did:plc:example123456789',
20
- handle: 'example.bsky.social',
21
- displayName: 'Example User',
22
- avatar: 'https://cdn.bsky.app/avatar/example.jpg',
23
- description: 'This is an example user profile',
24
- };
25
- export const profileResponseWithDifferentDID = {
26
- ...profileResponse,
27
- did: 'did:plc:different123456',
28
- };
@@ -1,33 +0,0 @@
1
- /**
2
- * Token Response Fixtures
3
- * OAuth token exchange responses for testing
4
- */
5
- export const tokenResponse = {
6
- access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example',
7
- refresh_token: 'refresh_token_12345',
8
- token_type: 'Bearer',
9
- expires_in: 3600,
10
- scope: 'atproto transition:generic',
11
- sub: 'did:plc:example123456789',
12
- did: 'did:plc:example123456789',
13
- };
14
- export const tokenResponseWithoutRefresh = {
15
- access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example',
16
- token_type: 'Bearer',
17
- expires_in: 3600,
18
- scope: 'atproto transition:generic',
19
- sub: 'did:plc:example123456789',
20
- };
21
- export const tokenResponseWithDifferentDID = {
22
- ...tokenResponse,
23
- sub: 'did:plc:different123456',
24
- did: 'did:plc:different123456',
25
- };
26
- export const refreshTokenResponse = {
27
- access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.new_token',
28
- refresh_token: 'new_refresh_token_67890',
29
- token_type: 'Bearer',
30
- expires_in: 3600,
31
- scope: 'atproto transition:generic',
32
- sub: 'did:plc:example123456789',
33
- };
@@ -1,84 +0,0 @@
1
- /**
2
- * Real Account Test Helpers
3
- * Utilities for testing with real AT Protocol accounts
4
- *
5
- * Note: For full token verification, you may need @atproto/api
6
- * Install it as a dev dependency if needed: npm install --save-dev @atproto/api
7
- */
8
- /**
9
- * Get test account from environment variables
10
- */
11
- export function getTestAccount() {
12
- const handle = process.env.TEST_BLUESKY_HANDLE;
13
- const did = process.env.TEST_BLUESKY_DID;
14
- const pdsUrl = process.env.TEST_BLUESKY_PDS || 'https://bsky.social';
15
- if (!handle || !did) {
16
- throw new Error('TEST_BLUESKY_HANDLE and TEST_BLUESKY_DID must be set for integration tests. ' +
17
- 'Create a test account at https://bsky.app and set these environment variables.');
18
- }
19
- return { handle, did, pdsUrl };
20
- }
21
- /**
22
- * Verify access token is valid by making an authenticated request
23
- *
24
- * Note: This requires @atproto/api. For a simpler check, you can verify
25
- * the token structure or make a direct HTTP request with DPoP.
26
- */
27
- export async function verifyAccessToken(accessToken, did, pdsUrl) {
28
- try {
29
- // Simple validation: check token structure
30
- // In production, you'd make an actual API call with DPoP
31
- if (!accessToken || accessToken.length < 10) {
32
- return false;
33
- }
34
- // For full verification, you would:
35
- // 1. Create DPoP proof for the request
36
- // 2. Make authenticated request to PDS
37
- // 3. Verify response is successful
38
- // For now, just check basic structure
39
- return typeof accessToken === 'string' && accessToken.length > 0;
40
- }
41
- catch (error) {
42
- console.error('Token verification failed:', error);
43
- return false;
44
- }
45
- }
46
- /**
47
- * Create test config from environment variables
48
- */
49
- export function getTestConfig() {
50
- const clientMetadataUri = process.env.TEST_CLIENT_METADATA_URI;
51
- const clientId = process.env.TEST_CLIENT_ID || clientMetadataUri;
52
- const jwksUri = process.env.TEST_JWKS_URI;
53
- if (!clientMetadataUri) {
54
- throw new Error('TEST_CLIENT_METADATA_URI must be set. ' +
55
- 'This should be a publicly accessible URL to your OAuth client metadata JSON file.');
56
- }
57
- if (!jwksUri) {
58
- throw new Error('TEST_JWKS_URI must be set for confidential clients. ' +
59
- 'This should be a publicly accessible URL to your JWKS JSON file.');
60
- }
61
- return {
62
- clientMetadataUri,
63
- clientId: clientId,
64
- jwksUri: jwksUri,
65
- scope: process.env.TEST_SCOPE || 'atproto transition:generic',
66
- tokenEndpointAuthMethod: 'private_key_jwt',
67
- };
68
- }
69
- /**
70
- * Check if integration tests should run
71
- */
72
- export function shouldRunIntegrationTests() {
73
- return !!(process.env.TEST_BLUESKY_HANDLE &&
74
- process.env.TEST_BLUESKY_DID &&
75
- process.env.TEST_CLIENT_METADATA_URI);
76
- }
77
- /**
78
- * Get test redirect URI
79
- */
80
- export function getTestRedirectUri() {
81
- return (process.env.TEST_REDIRECT_URI ||
82
- process.env.PUBLIC_URL + '/oauth/callback' ||
83
- 'https://example.com/oauth/callback');
84
- }
@@ -1,118 +0,0 @@
1
- /**
2
- * Real Account Integration Tests
3
- *
4
- * These tests require:
5
- * 1. A real Bluesky test account
6
- * 2. Publicly accessible client metadata and JWKS
7
- * 3. Environment variables configured (see INTEGRATION_TESTING.md)
8
- *
9
- * These tests are skipped if TEST_BLUESKY_HANDLE is not set.
10
- */
11
- import { describe, it, expect, beforeAll } from '@jest/globals';
12
- import { getTestAccount, getTestConfig, verifyAccessToken, shouldRunIntegrationTests, getTestRedirectUri, } from './real-account-helpers.js';
13
- import { resolvePDS, fetchAuthorizationServerMetadata } from '../../pds-discovery.js';
14
- import createBlueskyConnector from '../../index.js';
15
- describe('Real Account Integration Tests', () => {
16
- const shouldSkip = !shouldRunIntegrationTests();
17
- let testAccount;
18
- let testConfig;
19
- beforeAll(() => {
20
- if (shouldSkip) {
21
- console.warn('\n⚠️ Skipping real account tests - TEST_BLUESKY_HANDLE not set\n' +
22
- ' See INTEGRATION_TESTING.md for setup instructions\n');
23
- return;
24
- }
25
- testAccount = getTestAccount();
26
- testConfig = getTestConfig();
27
- });
28
- describe('PDS Resolution', () => {
29
- it('should resolve real handle to DID', async () => {
30
- if (shouldSkip)
31
- return;
32
- const pdsUrl = await resolvePDS(testAccount.handle);
33
- expect(pdsUrl).toBeDefined();
34
- expect(pdsUrl).toMatch(/^https:\/\//);
35
- expect(pdsUrl).toBe(testAccount.pdsUrl);
36
- }, 30000); // Longer timeout for real network requests
37
- it('should resolve real DID to PDS', async () => {
38
- if (shouldSkip)
39
- return;
40
- const pdsUrl = await resolvePDS(testAccount.did);
41
- expect(pdsUrl).toBeDefined();
42
- expect(pdsUrl).toMatch(/^https:\/\//);
43
- expect(pdsUrl).toBe(testAccount.pdsUrl);
44
- }, 30000);
45
- it('should fetch real authorization server metadata', async () => {
46
- if (shouldSkip)
47
- return;
48
- const metadata = await fetchAuthorizationServerMetadata(testAccount.pdsUrl);
49
- expect(metadata.issuer).toBeDefined();
50
- expect(metadata.authorization_endpoint).toBeDefined();
51
- expect(metadata.token_endpoint).toBeDefined();
52
- expect(metadata.pushed_authorization_request_endpoint).toBeDefined();
53
- expect(metadata.scopes_supported).toContain('atproto');
54
- }, 30000);
55
- });
56
- describe('OAuth Flow (Manual Authorization Required)', () => {
57
- it('should generate valid authorization URL', async () => {
58
- if (shouldSkip)
59
- return;
60
- const mockConfig = jest
61
- .fn()
62
- .mockResolvedValue(testConfig);
63
- const setSession = jest.fn();
64
- const connector = await createBlueskyConnector({ getConfig: mockConfig });
65
- const authUrl = await connector.getAuthorizationUri({
66
- state: `test-state-${Date.now()}`,
67
- redirectUri: getTestRedirectUri(),
68
- connectorId: 'bluesky-web',
69
- connectorFactoryId: 'bluesky',
70
- jti: `test-jti-${Date.now()}`,
71
- headers: {},
72
- }, setSession);
73
- expect(authUrl).toBeDefined();
74
- expect(authUrl).toContain('bsky.social');
75
- expect(authUrl).toContain('request_uri');
76
- // Log URL for manual testing
77
- console.log('\n📋 Authorization URL for manual testing:');
78
- console.log(authUrl);
79
- console.log('\n⚠️ To complete the test:');
80
- console.log(' 1. Open the URL above in a browser');
81
- console.log(' 2. Complete the OAuth authorization');
82
- console.log(' 3. Copy the authorization code from the callback URL');
83
- console.log(' 4. Set TEST_AUTHORIZATION_CODE environment variable');
84
- console.log(' 5. Re-run this test to verify token exchange\n');
85
- }, 60000);
86
- });
87
- describe('Token Verification', () => {
88
- it('should verify a real access token', async () => {
89
- if (shouldSkip)
90
- return;
91
- // This test requires a pre-obtained access token
92
- const accessToken = process.env.TEST_ACCESS_TOKEN;
93
- if (!accessToken) {
94
- console.warn('\n⚠️ Skipping token verification - TEST_ACCESS_TOKEN not set\n' +
95
- ' To test token verification:\n' +
96
- ' 1. Complete OAuth flow manually\n' +
97
- ' 2. Extract access_token from session\n' +
98
- ' 3. Set TEST_ACCESS_TOKEN environment variable\n' +
99
- ' 4. Re-run this test\n');
100
- return;
101
- }
102
- const isValid = await verifyAccessToken(accessToken, testAccount.did, testAccount.pdsUrl);
103
- expect(isValid).toBe(true);
104
- }, 30000);
105
- });
106
- describe('Error Scenarios', () => {
107
- it('should handle invalid handle gracefully', async () => {
108
- if (shouldSkip)
109
- return;
110
- await expect(resolvePDS('invalid-handle-12345.bsky.social')).rejects.toThrow();
111
- }, 30000);
112
- it('should handle invalid DID gracefully', async () => {
113
- if (shouldSkip)
114
- return;
115
- await expect(resolvePDS('did:plc:invalid123456789')).rejects.toThrow();
116
- }, 30000);
117
- });
118
- });