@kya-os/mcp-i-core 1.3.7-canary.0 → 1.3.7-canary.clientinfo.20251126041014
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test$colon$coverage.log +4239 -0
- package/.turbo/turbo-test.log +2973 -0
- package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
- package/Composer 3.md +615 -0
- package/GPT-5.md +1169 -0
- package/OPUS-plan.md +352 -0
- package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
- package/PHASE_3_SUMMARY.md +317 -0
- package/PHASE_4.1.3_SUMMARY.md +428 -0
- package/PHASE_4.1_COMPLETE.md +525 -0
- package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
- package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
- package/TEST_PLAN.md +571 -0
- package/coverage/coverage-final.json +57 -0
- package/dist/__tests__/utils/mock-providers.d.ts +1 -2
- package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
- package/dist/__tests__/utils/mock-providers.js.map +1 -1
- package/dist/cache/oauth-config-cache.d.ts +69 -0
- package/dist/cache/oauth-config-cache.d.ts.map +1 -0
- package/dist/cache/oauth-config-cache.js +76 -0
- package/dist/cache/oauth-config-cache.js.map +1 -0
- package/dist/identity/idp-token-resolver.d.ts +53 -0
- package/dist/identity/idp-token-resolver.d.ts.map +1 -0
- package/dist/identity/idp-token-resolver.js +108 -0
- package/dist/identity/idp-token-resolver.js.map +1 -0
- package/dist/identity/idp-token-storage.interface.d.ts +42 -0
- package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
- package/dist/identity/idp-token-storage.interface.js +12 -0
- package/dist/identity/idp-token-storage.interface.js.map +1 -0
- package/dist/identity/user-did-manager.d.ts +39 -1
- package/dist/identity/user-did-manager.d.ts.map +1 -1
- package/dist/identity/user-did-manager.js +69 -3
- package/dist/identity/user-did-manager.js.map +1 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/audit-logger.d.ts +37 -0
- package/dist/runtime/audit-logger.d.ts.map +1 -0
- package/dist/runtime/audit-logger.js +9 -0
- package/dist/runtime/audit-logger.js.map +1 -0
- package/dist/runtime/base.d.ts +19 -2
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +227 -11
- package/dist/runtime/base.js.map +1 -1
- package/dist/services/access-control.service.d.ts.map +1 -1
- package/dist/services/access-control.service.js +199 -15
- package/dist/services/access-control.service.js.map +1 -1
- package/dist/services/authorization/authorization-registry.d.ts +29 -0
- package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
- package/dist/services/authorization/authorization-registry.js +57 -0
- package/dist/services/authorization/authorization-registry.js.map +1 -0
- package/dist/services/authorization/types.d.ts +53 -0
- package/dist/services/authorization/types.d.ts.map +1 -0
- package/dist/services/authorization/types.js +10 -0
- package/dist/services/authorization/types.js.map +1 -0
- package/dist/services/batch-delegation.service.d.ts +53 -0
- package/dist/services/batch-delegation.service.d.ts.map +1 -0
- package/dist/services/batch-delegation.service.js +95 -0
- package/dist/services/batch-delegation.service.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +4 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/oauth-config.service.d.ts +53 -0
- package/dist/services/oauth-config.service.d.ts.map +1 -0
- package/dist/services/oauth-config.service.js +141 -0
- package/dist/services/oauth-config.service.js.map +1 -0
- package/dist/services/oauth-provider-registry.d.ts +88 -0
- package/dist/services/oauth-provider-registry.d.ts.map +1 -0
- package/dist/services/oauth-provider-registry.js +128 -0
- package/dist/services/oauth-provider-registry.js.map +1 -0
- package/dist/services/oauth-service.d.ts +77 -0
- package/dist/services/oauth-service.d.ts.map +1 -0
- package/dist/services/oauth-service.js +373 -0
- package/dist/services/oauth-service.js.map +1 -0
- package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
- package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
- package/dist/services/oauth-token-retrieval.service.js +150 -0
- package/dist/services/oauth-token-retrieval.service.js.map +1 -0
- package/dist/services/provider-resolver.d.ts +48 -0
- package/dist/services/provider-resolver.d.ts.map +1 -0
- package/dist/services/provider-resolver.js +121 -0
- package/dist/services/provider-resolver.js.map +1 -0
- package/dist/services/provider-validator.d.ts +55 -0
- package/dist/services/provider-validator.d.ts.map +1 -0
- package/dist/services/provider-validator.js +135 -0
- package/dist/services/provider-validator.js.map +1 -0
- package/dist/services/session-registration.service.d.ts +80 -0
- package/dist/services/session-registration.service.d.ts.map +1 -0
- package/dist/services/session-registration.service.js +228 -0
- package/dist/services/session-registration.service.js.map +1 -0
- package/dist/services/tool-context-builder.d.ts +57 -0
- package/dist/services/tool-context-builder.d.ts.map +1 -0
- package/dist/services/tool-context-builder.js +125 -0
- package/dist/services/tool-context-builder.js.map +1 -0
- package/dist/services/tool-protection.service.d.ts +27 -0
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +194 -4
- package/dist/services/tool-protection.service.js.map +1 -1
- package/dist/types/oauth-required-error.d.ts +40 -0
- package/dist/types/oauth-required-error.d.ts.map +1 -0
- package/dist/types/oauth-required-error.js +40 -0
- package/dist/types/oauth-required-error.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +33 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +40 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/docs/API_REFERENCE.md +1362 -0
- package/docs/COMPLIANCE_MATRIX.md +691 -0
- package/docs/STATUSLIST2021_GUIDE.md +696 -0
- package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
- package/package.json +23 -54
- package/scripts/audit-compliance.ts +724 -0
- package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
- package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
- package/src/__tests__/delegation-e2e.test.ts +690 -0
- package/src/__tests__/identity/user-did-manager.test.ts +213 -0
- package/src/__tests__/index.test.ts +56 -0
- package/src/__tests__/integration/full-flow.test.ts +776 -0
- package/src/__tests__/integration.test.ts +281 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +319 -0
- package/src/__tests__/regression/phase2-regression.test.ts +429 -0
- package/src/__tests__/runtime/audit-logger.test.ts +154 -0
- package/src/__tests__/runtime/base-extensions.test.ts +593 -0
- package/src/__tests__/runtime/base.test.ts +869 -0
- package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
- package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
- package/src/__tests__/runtime/route-interception.test.ts +686 -0
- package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
- package/src/__tests__/services/agentshield-integration.test.ts +784 -0
- package/src/__tests__/services/cache-busting.test.ts +125 -0
- package/src/__tests__/services/oauth-service-pkce.test.ts +556 -0
- package/src/__tests__/services/provider-resolver-edge-cases.test.ts +591 -0
- package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
- package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
- package/src/__tests__/utils/mock-providers.ts +340 -0
- package/src/cache/oauth-config-cache.d.ts +69 -0
- package/src/cache/oauth-config-cache.d.ts.map +1 -0
- package/src/cache/oauth-config-cache.js.map +1 -0
- package/src/cache/oauth-config-cache.ts +123 -0
- package/src/cache/tool-protection-cache.ts +171 -0
- package/src/compliance/EXAMPLE.md +412 -0
- package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
- package/src/compliance/index.ts +8 -0
- package/src/compliance/schema-registry.ts +460 -0
- package/src/compliance/schema-verifier.ts +708 -0
- package/src/config/__tests__/remote-config.spec.ts +268 -0
- package/src/config/remote-config.ts +174 -0
- package/src/config.ts +309 -0
- package/src/delegation/__tests__/audience-validator.test.ts +112 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
- package/src/delegation/__tests__/utils.test.ts +152 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
- package/src/delegation/audience-validator.ts +52 -0
- package/src/delegation/bitstring.ts +278 -0
- package/src/delegation/cascading-revocation.ts +370 -0
- package/src/delegation/delegation-graph.ts +299 -0
- package/src/delegation/index.ts +14 -0
- package/src/delegation/statuslist-manager.ts +353 -0
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
- package/src/delegation/storage/index.ts +9 -0
- package/src/delegation/storage/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
- package/src/delegation/utils.ts +42 -0
- package/src/delegation/vc-issuer.ts +232 -0
- package/src/delegation/vc-verifier.ts +568 -0
- package/src/identity/idp-token-resolver.ts +147 -0
- package/src/identity/idp-token-storage.interface.ts +59 -0
- package/src/identity/user-did-manager.ts +370 -0
- package/src/index.ts +271 -0
- package/src/providers/base.d.ts +91 -0
- package/src/providers/base.d.ts.map +1 -0
- package/src/providers/base.js.map +1 -0
- package/src/providers/base.ts +96 -0
- package/src/providers/memory.ts +142 -0
- package/src/runtime/audit-logger.ts +39 -0
- package/src/runtime/base.ts +1329 -0
- package/src/services/__tests__/access-control.integration.test.ts +443 -0
- package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
- package/src/services/__tests__/access-control.service.test.ts +970 -0
- package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
- package/src/services/__tests__/crypto.service.test.ts +531 -0
- package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
- package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
- package/src/services/__tests__/proof-verifier.test.ts +489 -0
- package/src/services/__tests__/provider-resolution.integration.test.ts +202 -0
- package/src/services/__tests__/provider-resolver.test.ts +213 -0
- package/src/services/__tests__/storage.service.test.ts +358 -0
- package/src/services/access-control.service.ts +990 -0
- package/src/services/authorization/authorization-registry.ts +66 -0
- package/src/services/authorization/types.ts +71 -0
- package/src/services/batch-delegation.service.ts +137 -0
- package/src/services/crypto.service.ts +302 -0
- package/src/services/errors.ts +76 -0
- package/src/services/index.ts +18 -0
- package/src/services/oauth-config.service.d.ts +53 -0
- package/src/services/oauth-config.service.d.ts.map +1 -0
- package/src/services/oauth-config.service.js.map +1 -0
- package/src/services/oauth-config.service.ts +192 -0
- package/src/services/oauth-provider-registry.d.ts +57 -0
- package/src/services/oauth-provider-registry.d.ts.map +1 -0
- package/src/services/oauth-provider-registry.js.map +1 -0
- package/src/services/oauth-provider-registry.ts +141 -0
- package/src/services/oauth-service.ts +544 -0
- package/src/services/oauth-token-retrieval.service.ts +245 -0
- package/src/services/proof-verifier.ts +478 -0
- package/src/services/provider-resolver.d.ts +48 -0
- package/src/services/provider-resolver.d.ts.map +1 -0
- package/src/services/provider-resolver.js.map +1 -0
- package/src/services/provider-resolver.ts +146 -0
- package/src/services/provider-validator.ts +170 -0
- package/src/services/session-registration.service.ts +317 -0
- package/src/services/storage.service.ts +566 -0
- package/src/services/tool-context-builder.ts +172 -0
- package/src/services/tool-protection.service.ts +982 -0
- package/src/types/oauth-required-error.ts +63 -0
- package/src/types/tool-protection.ts +155 -0
- package/src/utils/__tests__/did-helpers.test.ts +101 -0
- package/src/utils/base64.ts +148 -0
- package/src/utils/cors.ts +83 -0
- package/src/utils/did-helpers.ts +150 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/storage-keys.ts +278 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +56 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { ToolProtectionService } from "../../services/tool-protection.service";
|
|
3
|
+
import { InMemoryToolProtectionCache } from "../../cache/tool-protection-cache";
|
|
4
|
+
|
|
5
|
+
describe("Cache Busting in clearAndRefresh", () => {
|
|
6
|
+
let cache: InMemoryToolProtectionCache;
|
|
7
|
+
let service: ToolProtectionService;
|
|
8
|
+
const agentDid = "did:key:test-agent";
|
|
9
|
+
const projectId = "test-project-123";
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
cache = new InMemoryToolProtectionCache();
|
|
13
|
+
service = new ToolProtectionService(
|
|
14
|
+
{
|
|
15
|
+
apiUrl: "https://test.agentshield.com",
|
|
16
|
+
apiKey: "test-api-key",
|
|
17
|
+
projectId,
|
|
18
|
+
cacheTtl: 300000,
|
|
19
|
+
debug: true,
|
|
20
|
+
},
|
|
21
|
+
cache
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("clearAndRefresh should call fetchFromApi with bypassCDNCache: true", async () => {
|
|
26
|
+
// Spy on the private fetchFromApi method
|
|
27
|
+
const fetchSpy = vi.spyOn(service as any, "fetchFromApi").mockResolvedValue({
|
|
28
|
+
success: true,
|
|
29
|
+
data: {
|
|
30
|
+
toolProtections: {
|
|
31
|
+
greet: { requiresDelegation: true, requiredScopes: ["greet:execute"] },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await service.clearAndRefresh(agentDid);
|
|
37
|
+
|
|
38
|
+
// Verify fetchFromApi was called with bypassCDNCache: true
|
|
39
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
40
|
+
expect(fetchSpy).toHaveBeenCalledWith(agentDid, { bypassCDNCache: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("fetchFromApi should add cache-busting query param when bypassCDNCache is true", async () => {
|
|
44
|
+
// Mock global fetch
|
|
45
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
46
|
+
ok: true,
|
|
47
|
+
json: () => Promise.resolve({ success: true, data: { toolProtections: {} } }),
|
|
48
|
+
});
|
|
49
|
+
global.fetch = fetchMock;
|
|
50
|
+
|
|
51
|
+
// Call fetchFromApi directly with bypassCDNCache: true
|
|
52
|
+
await (service as any).fetchFromApi(agentDid, { bypassCDNCache: true });
|
|
53
|
+
|
|
54
|
+
// Verify the URL contains cache-busting timestamp
|
|
55
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
56
|
+
expect(calledUrl).toContain("?_t=");
|
|
57
|
+
expect(calledUrl).toMatch(/\?_t=\d+$/); // Ends with ?_t=<timestamp>
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("fetchFromApi should add Cache-Control headers when bypassCDNCache is true", async () => {
|
|
61
|
+
// Mock global fetch
|
|
62
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
63
|
+
ok: true,
|
|
64
|
+
json: () => Promise.resolve({ success: true, data: { toolProtections: {} } }),
|
|
65
|
+
});
|
|
66
|
+
global.fetch = fetchMock;
|
|
67
|
+
|
|
68
|
+
// Call fetchFromApi directly with bypassCDNCache: true
|
|
69
|
+
await (service as any).fetchFromApi(agentDid, { bypassCDNCache: true });
|
|
70
|
+
|
|
71
|
+
// Verify the headers contain cache-control
|
|
72
|
+
const calledOptions = fetchMock.mock.calls[0][1] as RequestInit;
|
|
73
|
+
const headers = calledOptions.headers as Record<string, string>;
|
|
74
|
+
|
|
75
|
+
expect(headers["Cache-Control"]).toBe("no-cache, no-store, must-revalidate");
|
|
76
|
+
expect(headers["Pragma"]).toBe("no-cache");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("fetchFromApi should NOT add cache-busting when bypassCDNCache is false/undefined", async () => {
|
|
80
|
+
// Mock global fetch
|
|
81
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
82
|
+
ok: true,
|
|
83
|
+
json: () => Promise.resolve({ success: true, data: { toolProtections: {} } }),
|
|
84
|
+
});
|
|
85
|
+
global.fetch = fetchMock;
|
|
86
|
+
|
|
87
|
+
// Call fetchFromApi without bypassCDNCache
|
|
88
|
+
await (service as any).fetchFromApi(agentDid);
|
|
89
|
+
|
|
90
|
+
// Verify the URL does NOT contain cache-busting timestamp
|
|
91
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
92
|
+
expect(calledUrl).not.toContain("?_t=");
|
|
93
|
+
|
|
94
|
+
// Verify no cache-control header
|
|
95
|
+
const calledOptions = fetchMock.mock.calls[0][1] as RequestInit;
|
|
96
|
+
const headers = calledOptions.headers as Record<string, string>;
|
|
97
|
+
expect(headers["Cache-Control"]).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("cache should be empty after clearAndRefresh deletes it", async () => {
|
|
101
|
+
const cacheKey = `config:tool-protections:${projectId}`;
|
|
102
|
+
|
|
103
|
+
// Pre-populate cache
|
|
104
|
+
await cache.set(cacheKey, { toolProtections: { old: { requiresDelegation: false, requiredScopes: [] } } }, 300000);
|
|
105
|
+
|
|
106
|
+
// Verify cache has data
|
|
107
|
+
const before = await cache.get(cacheKey);
|
|
108
|
+
expect(before).not.toBeNull();
|
|
109
|
+
|
|
110
|
+
// Mock fetchFromApi to avoid network call
|
|
111
|
+
vi.spyOn(service as any, "fetchFromApi").mockResolvedValue({
|
|
112
|
+
success: true,
|
|
113
|
+
data: { toolProtections: {} },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Clear and refresh
|
|
117
|
+
await service.clearAndRefresh(agentDid);
|
|
118
|
+
|
|
119
|
+
// Note: clearAndRefresh deletes then re-populates with fresh data
|
|
120
|
+
// But since our mock returns empty toolProtections, cache should have empty config
|
|
121
|
+
const after = await cache.get(cacheKey);
|
|
122
|
+
expect(after).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OAuthService PKCE Token Exchange
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* 1. Successful PKCE token exchange
|
|
6
|
+
* 2. Handling of GitHub-style 200-status errors
|
|
7
|
+
* 3. Handling of standard 4xx errors
|
|
8
|
+
* 4. Token response validation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
12
|
+
import { OAuthService } from "../../services/oauth-service";
|
|
13
|
+
import type { OAuthConfigService } from "../../services/oauth-config.service";
|
|
14
|
+
import type { FetchProvider } from "../../providers/types";
|
|
15
|
+
import type { OAuthProvider } from "@kya-os/contracts/config";
|
|
16
|
+
|
|
17
|
+
describe("OAuthService PKCE Token Exchange", () => {
|
|
18
|
+
let mockConfigService: OAuthConfigService;
|
|
19
|
+
let mockFetchProvider: FetchProvider;
|
|
20
|
+
let oauthService: OAuthService;
|
|
21
|
+
|
|
22
|
+
const mockGitHubProvider: OAuthProvider = {
|
|
23
|
+
clientId: "test-client-id",
|
|
24
|
+
clientSecret: null,
|
|
25
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
26
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
27
|
+
supportsPKCE: true,
|
|
28
|
+
requiresClientSecret: false,
|
|
29
|
+
scopes: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mockOAuthConfig = {
|
|
33
|
+
providers: {
|
|
34
|
+
github: mockGitHubProvider,
|
|
35
|
+
},
|
|
36
|
+
configuredProvider: "github",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
mockConfigService = {
|
|
41
|
+
getOAuthConfig: vi.fn().mockResolvedValue(mockOAuthConfig),
|
|
42
|
+
} as unknown as OAuthConfigService;
|
|
43
|
+
|
|
44
|
+
mockFetchProvider = {
|
|
45
|
+
fetch: vi.fn(),
|
|
46
|
+
resolveDID: vi.fn(),
|
|
47
|
+
fetchStatusList: vi.fn(),
|
|
48
|
+
fetchDelegationChain: vi.fn(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
oauthService = new OAuthService({
|
|
52
|
+
configService: mockConfigService,
|
|
53
|
+
fetchProvider: mockFetchProvider,
|
|
54
|
+
agentShieldApiUrl: "https://test.agentshield.com",
|
|
55
|
+
agentShieldApiKey: "test-api-key",
|
|
56
|
+
projectId: "test-project",
|
|
57
|
+
logger: vi.fn(),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("Successful Token Exchange", () => {
|
|
62
|
+
it("should exchange code for access_token successfully", async () => {
|
|
63
|
+
const mockTokenResponse = {
|
|
64
|
+
access_token: "gho_test_access_token_123",
|
|
65
|
+
token_type: "bearer",
|
|
66
|
+
scope: "greet:execute",
|
|
67
|
+
expires_in: 3600,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
71
|
+
ok: true,
|
|
72
|
+
json: () => Promise.resolve(mockTokenResponse),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await oauthService.exchangeToken(
|
|
76
|
+
"github",
|
|
77
|
+
"test-auth-code",
|
|
78
|
+
"test-code-verifier",
|
|
79
|
+
"https://test.workers.dev/oauth/callback"
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result.access_token).toBe("gho_test_access_token_123");
|
|
83
|
+
expect(result.token_type).toBe("bearer");
|
|
84
|
+
expect(result.scope).toBe("greet:execute");
|
|
85
|
+
expect(result.expires_in).toBe(3600);
|
|
86
|
+
expect(result.expires_at).toBeGreaterThan(Date.now());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should handle missing expires_in with default value", async () => {
|
|
90
|
+
const mockTokenResponse = {
|
|
91
|
+
access_token: "gho_test_access_token_123",
|
|
92
|
+
token_type: "bearer",
|
|
93
|
+
// No expires_in - should default to 3600
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
97
|
+
ok: true,
|
|
98
|
+
json: () => Promise.resolve(mockTokenResponse),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await oauthService.exchangeToken(
|
|
102
|
+
"github",
|
|
103
|
+
"test-auth-code",
|
|
104
|
+
"test-code-verifier",
|
|
105
|
+
"https://test.workers.dev/oauth/callback"
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(result.expires_in).toBe(3600);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should handle refresh_token when provided", async () => {
|
|
112
|
+
const mockTokenResponse = {
|
|
113
|
+
access_token: "gho_test_access_token_123",
|
|
114
|
+
refresh_token: "ghr_test_refresh_token_456",
|
|
115
|
+
token_type: "bearer",
|
|
116
|
+
expires_in: 3600,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
120
|
+
ok: true,
|
|
121
|
+
json: () => Promise.resolve(mockTokenResponse),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = await oauthService.exchangeToken(
|
|
125
|
+
"github",
|
|
126
|
+
"test-auth-code",
|
|
127
|
+
"test-code-verifier",
|
|
128
|
+
"https://test.workers.dev/oauth/callback"
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result.refresh_token).toBe("ghr_test_refresh_token_456");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("GitHub 200-Status Error Handling", () => {
|
|
136
|
+
it("should handle bad_verification_code error (200 status)", async () => {
|
|
137
|
+
const mockErrorResponse = {
|
|
138
|
+
error: "bad_verification_code",
|
|
139
|
+
error_description: "The code passed is incorrect or expired.",
|
|
140
|
+
error_uri:
|
|
141
|
+
"https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#bad-verification-code",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
145
|
+
ok: true, // GitHub returns 200 even for errors!
|
|
146
|
+
json: () => Promise.resolve(mockErrorResponse),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await expect(
|
|
150
|
+
oauthService.exchangeToken(
|
|
151
|
+
"github",
|
|
152
|
+
"expired-code",
|
|
153
|
+
"test-code-verifier",
|
|
154
|
+
"https://test.workers.dev/oauth/callback"
|
|
155
|
+
)
|
|
156
|
+
).rejects.toThrow("The code passed is incorrect or expired.");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should handle incorrect_client_credentials error (200 status)", async () => {
|
|
160
|
+
const mockErrorResponse = {
|
|
161
|
+
error: "incorrect_client_credentials",
|
|
162
|
+
error_description:
|
|
163
|
+
"The client_id and/or client_secret passed are incorrect.",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
167
|
+
ok: true,
|
|
168
|
+
json: () => Promise.resolve(mockErrorResponse),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
oauthService.exchangeToken(
|
|
173
|
+
"github",
|
|
174
|
+
"test-code",
|
|
175
|
+
"test-code-verifier",
|
|
176
|
+
"https://test.workers.dev/oauth/callback"
|
|
177
|
+
)
|
|
178
|
+
).rejects.toThrow(
|
|
179
|
+
"The client_id and/or client_secret passed are incorrect."
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should handle redirect_uri_mismatch error (200 status)", async () => {
|
|
184
|
+
const mockErrorResponse = {
|
|
185
|
+
error: "redirect_uri_mismatch",
|
|
186
|
+
error_description:
|
|
187
|
+
"The redirect_uri MUST match the registered callback URL for this application.",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
191
|
+
ok: true,
|
|
192
|
+
json: () => Promise.resolve(mockErrorResponse),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
oauthService.exchangeToken(
|
|
197
|
+
"github",
|
|
198
|
+
"test-code",
|
|
199
|
+
"test-code-verifier",
|
|
200
|
+
"https://wrong-redirect.workers.dev/oauth/callback"
|
|
201
|
+
)
|
|
202
|
+
).rejects.toThrow("The redirect_uri MUST match the registered callback");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should fallback to error code when error_description is missing", async () => {
|
|
206
|
+
const mockErrorResponse = {
|
|
207
|
+
error: "access_denied",
|
|
208
|
+
// No error_description
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
212
|
+
ok: true,
|
|
213
|
+
json: () => Promise.resolve(mockErrorResponse),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await expect(
|
|
217
|
+
oauthService.exchangeToken(
|
|
218
|
+
"github",
|
|
219
|
+
"test-code",
|
|
220
|
+
"test-code-verifier",
|
|
221
|
+
"https://test.workers.dev/oauth/callback"
|
|
222
|
+
)
|
|
223
|
+
).rejects.toThrow("access_denied");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("Standard HTTP Error Handling", () => {
|
|
228
|
+
it("should handle 400 Bad Request error", async () => {
|
|
229
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
230
|
+
ok: false,
|
|
231
|
+
status: 400,
|
|
232
|
+
text: () =>
|
|
233
|
+
Promise.resolve(
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
error: "invalid_request",
|
|
236
|
+
error_description: "Missing required parameter",
|
|
237
|
+
})
|
|
238
|
+
),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await expect(
|
|
242
|
+
oauthService.exchangeToken(
|
|
243
|
+
"github",
|
|
244
|
+
"test-code",
|
|
245
|
+
"test-code-verifier",
|
|
246
|
+
"https://test.workers.dev/oauth/callback"
|
|
247
|
+
)
|
|
248
|
+
).rejects.toThrow("Missing required parameter");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should handle 401 Unauthorized error", async () => {
|
|
252
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
253
|
+
ok: false,
|
|
254
|
+
status: 401,
|
|
255
|
+
text: () =>
|
|
256
|
+
Promise.resolve(
|
|
257
|
+
JSON.stringify({
|
|
258
|
+
error: "invalid_client",
|
|
259
|
+
error_description: "Client authentication failed",
|
|
260
|
+
})
|
|
261
|
+
),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
oauthService.exchangeToken(
|
|
266
|
+
"github",
|
|
267
|
+
"test-code",
|
|
268
|
+
"test-code-verifier",
|
|
269
|
+
"https://test.workers.dev/oauth/callback"
|
|
270
|
+
)
|
|
271
|
+
).rejects.toThrow("Client authentication failed");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should handle non-JSON error response", async () => {
|
|
275
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
276
|
+
ok: false,
|
|
277
|
+
status: 500,
|
|
278
|
+
text: () => Promise.resolve("Internal Server Error"),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await expect(
|
|
282
|
+
oauthService.exchangeToken(
|
|
283
|
+
"github",
|
|
284
|
+
"test-code",
|
|
285
|
+
"test-code-verifier",
|
|
286
|
+
"https://test.workers.dev/oauth/callback"
|
|
287
|
+
)
|
|
288
|
+
).rejects.toThrow("Internal Server Error");
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("Token Response Validation", () => {
|
|
293
|
+
it("should throw error when access_token is missing from valid response", async () => {
|
|
294
|
+
const mockInvalidResponse = {
|
|
295
|
+
token_type: "bearer",
|
|
296
|
+
scope: "greet:execute",
|
|
297
|
+
// Missing access_token
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
301
|
+
ok: true,
|
|
302
|
+
json: () => Promise.resolve(mockInvalidResponse),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await expect(
|
|
306
|
+
oauthService.exchangeToken(
|
|
307
|
+
"github",
|
|
308
|
+
"test-code",
|
|
309
|
+
"test-code-verifier",
|
|
310
|
+
"https://test.workers.dev/oauth/callback"
|
|
311
|
+
)
|
|
312
|
+
).rejects.toThrow("Token response missing access_token");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should throw error when access_token is empty string", async () => {
|
|
316
|
+
const mockInvalidResponse = {
|
|
317
|
+
access_token: "",
|
|
318
|
+
token_type: "bearer",
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
322
|
+
ok: true,
|
|
323
|
+
json: () => Promise.resolve(mockInvalidResponse),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await expect(
|
|
327
|
+
oauthService.exchangeToken(
|
|
328
|
+
"github",
|
|
329
|
+
"test-code",
|
|
330
|
+
"test-code-verifier",
|
|
331
|
+
"https://test.workers.dev/oauth/callback"
|
|
332
|
+
)
|
|
333
|
+
).rejects.toThrow("Token response missing access_token");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("PKCE Request Format", () => {
|
|
338
|
+
it("should send correct PKCE parameters", async () => {
|
|
339
|
+
const mockTokenResponse = {
|
|
340
|
+
access_token: "gho_test_access_token_123",
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
344
|
+
ok: true,
|
|
345
|
+
json: () => Promise.resolve(mockTokenResponse),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await oauthService.exchangeToken(
|
|
349
|
+
"github",
|
|
350
|
+
"test-auth-code",
|
|
351
|
+
"test-code-verifier-12345",
|
|
352
|
+
"https://test.workers.dev/oauth/callback"
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(mockFetchProvider.fetch).toHaveBeenCalledWith(
|
|
356
|
+
"https://github.com/login/oauth/access_token",
|
|
357
|
+
expect.objectContaining({
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: expect.objectContaining({
|
|
360
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
361
|
+
Accept: "application/json",
|
|
362
|
+
}),
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Verify request body contains correct PKCE parameters
|
|
367
|
+
const callArgs = (mockFetchProvider.fetch as ReturnType<typeof vi.fn>)
|
|
368
|
+
.mock.calls[0];
|
|
369
|
+
const body = callArgs[1].body as string;
|
|
370
|
+
|
|
371
|
+
expect(body).toContain("grant_type=authorization_code");
|
|
372
|
+
expect(body).toContain("code=test-auth-code");
|
|
373
|
+
expect(body).toContain("code_verifier=test-code-verifier-12345");
|
|
374
|
+
expect(body).toContain("client_id=test-client-id");
|
|
375
|
+
expect(body).toContain(
|
|
376
|
+
"redirect_uri=https%3A%2F%2Ftest.workers.dev%2Foauth%2Fcallback"
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe("Provider Not Found", () => {
|
|
382
|
+
it("should throw error for unconfigured provider", async () => {
|
|
383
|
+
await expect(
|
|
384
|
+
oauthService.exchangeToken(
|
|
385
|
+
"unknown-provider",
|
|
386
|
+
"test-code",
|
|
387
|
+
"test-code-verifier",
|
|
388
|
+
"https://test.workers.dev/oauth/callback"
|
|
389
|
+
)
|
|
390
|
+
).rejects.toThrow('Provider "unknown-provider" not configured');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("PKCE Requirement", () => {
|
|
395
|
+
it("should throw error when codeVerifier is missing for PKCE provider", async () => {
|
|
396
|
+
await expect(
|
|
397
|
+
oauthService.exchangeToken(
|
|
398
|
+
"github",
|
|
399
|
+
"test-code",
|
|
400
|
+
undefined, // Missing code_verifier
|
|
401
|
+
"https://test.workers.dev/oauth/callback"
|
|
402
|
+
)
|
|
403
|
+
).rejects.toThrow('Provider "github" requires PKCE code_verifier');
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe("Client Secret Handling in PKCE", () => {
|
|
408
|
+
it("should include client_secret when provider has one configured (GitHub OAuth Apps)", async () => {
|
|
409
|
+
// GitHub OAuth Apps require client_secret even with PKCE
|
|
410
|
+
const mockGitHubOAuthAppProvider: OAuthProvider = {
|
|
411
|
+
clientId: "github-oauth-app-client-id",
|
|
412
|
+
clientSecret: "github-oauth-app-client-secret",
|
|
413
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
414
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
415
|
+
supportsPKCE: true,
|
|
416
|
+
requiresClientSecret: true,
|
|
417
|
+
scopes: [],
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const mockConfigWithSecret = {
|
|
421
|
+
providers: {
|
|
422
|
+
github: mockGitHubOAuthAppProvider,
|
|
423
|
+
},
|
|
424
|
+
configuredProvider: "github",
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
(mockConfigService.getOAuthConfig as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
428
|
+
mockConfigWithSecret
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const mockTokenResponse = {
|
|
432
|
+
access_token: "gho_test_access_token_123",
|
|
433
|
+
token_type: "bearer",
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
437
|
+
ok: true,
|
|
438
|
+
json: () => Promise.resolve(mockTokenResponse),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await oauthService.exchangeToken(
|
|
442
|
+
"github",
|
|
443
|
+
"test-auth-code",
|
|
444
|
+
"test-code-verifier",
|
|
445
|
+
"https://test.workers.dev/oauth/callback"
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
// Verify client_secret is included in request body
|
|
449
|
+
const callArgs = (mockFetchProvider.fetch as ReturnType<typeof vi.fn>)
|
|
450
|
+
.mock.calls[0];
|
|
451
|
+
const body = callArgs[1].body as string;
|
|
452
|
+
|
|
453
|
+
expect(body).toContain("client_id=github-oauth-app-client-id");
|
|
454
|
+
expect(body).toContain("client_secret=github-oauth-app-client-secret");
|
|
455
|
+
expect(body).toContain("code_verifier=test-code-verifier");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("should NOT include client_secret when provider has null clientSecret (GitHub Apps)", async () => {
|
|
459
|
+
// GitHub Apps (not OAuth Apps) can use PKCE without client_secret
|
|
460
|
+
const mockGitHubAppProvider: OAuthProvider = {
|
|
461
|
+
clientId: "github-app-client-id",
|
|
462
|
+
clientSecret: null, // No secret for GitHub Apps with PKCE
|
|
463
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
464
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
465
|
+
supportsPKCE: true,
|
|
466
|
+
requiresClientSecret: false,
|
|
467
|
+
scopes: [],
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const mockConfigWithoutSecret = {
|
|
471
|
+
providers: {
|
|
472
|
+
github: mockGitHubAppProvider,
|
|
473
|
+
},
|
|
474
|
+
configuredProvider: "github",
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
(mockConfigService.getOAuthConfig as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
478
|
+
mockConfigWithoutSecret
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const mockTokenResponse = {
|
|
482
|
+
access_token: "gho_test_access_token_123",
|
|
483
|
+
token_type: "bearer",
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
487
|
+
ok: true,
|
|
488
|
+
json: () => Promise.resolve(mockTokenResponse),
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
await oauthService.exchangeToken(
|
|
492
|
+
"github",
|
|
493
|
+
"test-auth-code",
|
|
494
|
+
"test-code-verifier",
|
|
495
|
+
"https://test.workers.dev/oauth/callback"
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Verify client_secret is NOT included in request body
|
|
499
|
+
const callArgs = (mockFetchProvider.fetch as ReturnType<typeof vi.fn>)
|
|
500
|
+
.mock.calls[0];
|
|
501
|
+
const body = callArgs[1].body as string;
|
|
502
|
+
|
|
503
|
+
expect(body).toContain("client_id=github-app-client-id");
|
|
504
|
+
expect(body).not.toContain("client_secret");
|
|
505
|
+
expect(body).toContain("code_verifier=test-code-verifier");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("should NOT include client_secret when it is empty string", async () => {
|
|
509
|
+
const mockProviderWithEmptySecret: OAuthProvider = {
|
|
510
|
+
clientId: "test-client-id",
|
|
511
|
+
clientSecret: "", // Empty string should be treated as no secret
|
|
512
|
+
authorizationUrl: "https://example.com/oauth/authorize",
|
|
513
|
+
tokenUrl: "https://example.com/oauth/token",
|
|
514
|
+
supportsPKCE: true,
|
|
515
|
+
requiresClientSecret: false,
|
|
516
|
+
scopes: [],
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const mockConfigWithEmptySecret = {
|
|
520
|
+
providers: {
|
|
521
|
+
github: mockProviderWithEmptySecret,
|
|
522
|
+
},
|
|
523
|
+
configuredProvider: "github",
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
(mockConfigService.getOAuthConfig as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
527
|
+
mockConfigWithEmptySecret
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const mockTokenResponse = {
|
|
531
|
+
access_token: "test_access_token",
|
|
532
|
+
token_type: "bearer",
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
(mockFetchProvider.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
536
|
+
ok: true,
|
|
537
|
+
json: () => Promise.resolve(mockTokenResponse),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await oauthService.exchangeToken(
|
|
541
|
+
"github",
|
|
542
|
+
"test-auth-code",
|
|
543
|
+
"test-code-verifier",
|
|
544
|
+
"https://test.workers.dev/oauth/callback"
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// Verify client_secret is NOT included when empty
|
|
548
|
+
const callArgs = (mockFetchProvider.fetch as ReturnType<typeof vi.fn>)
|
|
549
|
+
.mock.calls[0];
|
|
550
|
+
const body = callArgs[1].body as string;
|
|
551
|
+
|
|
552
|
+
expect(body).not.toContain("client_secret");
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|