@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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client Assertion Generation for private_key_jwt
|
|
3
3
|
*
|
|
4
|
-
* Generates JWT client assertion for OAuth
|
|
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,
|
|
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
|
|
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"}
|
package/lib/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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,
|
|
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:
|
|
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.
|
|
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": "
|
|
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
|
-
});
|