@openverifiable/connector-bluesky 1.0.0 → 1.0.1
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/index.js +16 -7
- 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/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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
}
|
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.
|
|
3
|
+
"version": "1.0.1",
|
|
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": "
|
|
42
|
-
"test:coverage": "
|
|
43
|
-
"test:unit": "
|
|
44
|
-
"test: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
|
-
});
|
package/lib/client-assertion.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client Assertion Generation for private_key_jwt
|
|
3
|
-
*
|
|
4
|
-
* Generates JWT client assertion for OAuth token endpoint authentication
|
|
5
|
-
* RFC 7523: https://datatracker.ietf.org/doc/html/rfc7523
|
|
6
|
-
*/
|
|
7
|
-
import { SignJWT, importJWK } from 'jose';
|
|
8
|
-
import { randomBytes } from 'crypto';
|
|
9
|
-
/**
|
|
10
|
-
* Generate client assertion JWT
|
|
11
|
-
*
|
|
12
|
-
* Note: This requires access to the private key, which should be:
|
|
13
|
-
* 1. Stored in Logto connector config (encrypted by Logto), OR
|
|
14
|
-
* 2. Retrieved from a secure server endpoint
|
|
15
|
-
*
|
|
16
|
-
* For now, we'll support both approaches - if privateKeyJwk is in config, use it.
|
|
17
|
-
* Otherwise, the server should provide an endpoint to generate assertions.
|
|
18
|
-
*/
|
|
19
|
-
export async function generateClientAssertion(clientId, tokenEndpoint, config) {
|
|
20
|
-
// Check if private key is available in config (for server-side connectors)
|
|
21
|
-
// In production, this would come from Vault via a server endpoint
|
|
22
|
-
const privateKeyJwkStr = config.privateKeyJwk;
|
|
23
|
-
if (!privateKeyJwkStr) {
|
|
24
|
-
// Private key not in config - would need to call server endpoint
|
|
25
|
-
// For now, return null to indicate assertion generation failed
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const privateKeyJwk = JSON.parse(privateKeyJwkStr);
|
|
30
|
-
const privateKey = await importJWK(privateKeyJwk, 'ES256');
|
|
31
|
-
const now = Math.floor(Date.now() / 1000);
|
|
32
|
-
const jti = randomBytes(16).toString('base64url');
|
|
33
|
-
const jwt = new SignJWT({
|
|
34
|
-
iss: clientId,
|
|
35
|
-
sub: clientId,
|
|
36
|
-
aud: tokenEndpoint,
|
|
37
|
-
jti,
|
|
38
|
-
exp: now + 600,
|
|
39
|
-
iat: now,
|
|
40
|
-
})
|
|
41
|
-
.setProtectedHeader({
|
|
42
|
-
alg: 'ES256',
|
|
43
|
-
typ: 'JWT',
|
|
44
|
-
});
|
|
45
|
-
return await jwt.sign(privateKey);
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
throw new Error(`Failed to generate client assertion: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
49
|
-
}
|
|
50
|
-
}
|