harper-kb 0.2.0
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/LICENSE +21 -0
- package/README.md +288 -0
- package/config.yaml +13 -0
- package/dist/core/embeddings.d.ts +31 -0
- package/dist/core/embeddings.d.ts.map +1 -0
- package/dist/core/embeddings.js +199 -0
- package/dist/core/embeddings.js.map +1 -0
- package/dist/core/entries.d.ts +101 -0
- package/dist/core/entries.d.ts.map +1 -0
- package/dist/core/entries.js +304 -0
- package/dist/core/entries.js.map +1 -0
- package/dist/core/history.d.ts +31 -0
- package/dist/core/history.d.ts.map +1 -0
- package/dist/core/history.js +119 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/knowledge-base.d.ts +49 -0
- package/dist/core/knowledge-base.d.ts.map +1 -0
- package/dist/core/knowledge-base.js +117 -0
- package/dist/core/knowledge-base.js.map +1 -0
- package/dist/core/search.d.ts +34 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +327 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/tags.d.ts +39 -0
- package/dist/core/tags.d.ts.map +1 -0
- package/dist/core/tags.js +97 -0
- package/dist/core/tags.js.map +1 -0
- package/dist/core/triage.d.ts +61 -0
- package/dist/core/triage.d.ts.map +1 -0
- package/dist/core/triage.js +136 -0
- package/dist/core/triage.js.map +1 -0
- package/dist/core/webhook-endpoints.d.ts +46 -0
- package/dist/core/webhook-endpoints.d.ts.map +1 -0
- package/dist/core/webhook-endpoints.js +85 -0
- package/dist/core/webhook-endpoints.js.map +1 -0
- package/dist/hooks.d.ts +67 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +53 -0
- package/dist/hooks.js.map +1 -0
- package/dist/http-utils.d.ts +38 -0
- package/dist/http-utils.d.ts.map +1 -0
- package/dist/http-utils.js +133 -0
- package/dist/http-utils.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/protocol.d.ts +25 -0
- package/dist/mcp/protocol.d.ts.map +1 -0
- package/dist/mcp/protocol.js +105 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +28 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +706 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/oauth/authorize.d.ts +28 -0
- package/dist/oauth/authorize.d.ts.map +1 -0
- package/dist/oauth/authorize.js +421 -0
- package/dist/oauth/authorize.js.map +1 -0
- package/dist/oauth/init.d.ts +18 -0
- package/dist/oauth/init.d.ts.map +1 -0
- package/dist/oauth/init.js +30 -0
- package/dist/oauth/init.js.map +1 -0
- package/dist/oauth/keys.d.ts +34 -0
- package/dist/oauth/keys.d.ts.map +1 -0
- package/dist/oauth/keys.js +101 -0
- package/dist/oauth/keys.js.map +1 -0
- package/dist/oauth/metadata.d.ts +23 -0
- package/dist/oauth/metadata.d.ts.map +1 -0
- package/dist/oauth/metadata.js +57 -0
- package/dist/oauth/metadata.js.map +1 -0
- package/dist/oauth/middleware.d.ts +23 -0
- package/dist/oauth/middleware.d.ts.map +1 -0
- package/dist/oauth/middleware.js +65 -0
- package/dist/oauth/middleware.js.map +1 -0
- package/dist/oauth/register.d.ts +15 -0
- package/dist/oauth/register.d.ts.map +1 -0
- package/dist/oauth/register.js +78 -0
- package/dist/oauth/register.js.map +1 -0
- package/dist/oauth/token.d.ts +16 -0
- package/dist/oauth/token.d.ts.map +1 -0
- package/dist/oauth/token.js +184 -0
- package/dist/oauth/token.js.map +1 -0
- package/dist/oauth/validate.d.ts +40 -0
- package/dist/oauth/validate.d.ts.map +1 -0
- package/dist/oauth/validate.js +61 -0
- package/dist/oauth/validate.js.map +1 -0
- package/dist/resources/HistoryResource.d.ts +41 -0
- package/dist/resources/HistoryResource.d.ts.map +1 -0
- package/dist/resources/HistoryResource.js +61 -0
- package/dist/resources/HistoryResource.js.map +1 -0
- package/dist/resources/KnowledgeBaseResource.d.ts +60 -0
- package/dist/resources/KnowledgeBaseResource.d.ts.map +1 -0
- package/dist/resources/KnowledgeBaseResource.js +118 -0
- package/dist/resources/KnowledgeBaseResource.js.map +1 -0
- package/dist/resources/KnowledgeEntryResource.d.ts +61 -0
- package/dist/resources/KnowledgeEntryResource.d.ts.map +1 -0
- package/dist/resources/KnowledgeEntryResource.js +191 -0
- package/dist/resources/KnowledgeEntryResource.js.map +1 -0
- package/dist/resources/MeResource.d.ts +31 -0
- package/dist/resources/MeResource.d.ts.map +1 -0
- package/dist/resources/MeResource.js +40 -0
- package/dist/resources/MeResource.js.map +1 -0
- package/dist/resources/QueryLogResource.d.ts +22 -0
- package/dist/resources/QueryLogResource.d.ts.map +1 -0
- package/dist/resources/QueryLogResource.js +66 -0
- package/dist/resources/QueryLogResource.js.map +1 -0
- package/dist/resources/ServiceKeyResource.d.ts +52 -0
- package/dist/resources/ServiceKeyResource.d.ts.map +1 -0
- package/dist/resources/ServiceKeyResource.js +151 -0
- package/dist/resources/ServiceKeyResource.js.map +1 -0
- package/dist/resources/TagResource.d.ts +27 -0
- package/dist/resources/TagResource.d.ts.map +1 -0
- package/dist/resources/TagResource.js +41 -0
- package/dist/resources/TagResource.js.map +1 -0
- package/dist/resources/TriageResource.d.ts +53 -0
- package/dist/resources/TriageResource.d.ts.map +1 -0
- package/dist/resources/TriageResource.js +120 -0
- package/dist/resources/TriageResource.js.map +1 -0
- package/dist/resources/WebhookEndpointResource.d.ts +63 -0
- package/dist/resources/WebhookEndpointResource.d.ts.map +1 -0
- package/dist/resources/WebhookEndpointResource.js +115 -0
- package/dist/resources/WebhookEndpointResource.js.map +1 -0
- package/dist/types.d.ts +378 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/webhooks/github.d.ts +25 -0
- package/dist/webhooks/github.d.ts.map +1 -0
- package/dist/webhooks/github.js +165 -0
- package/dist/webhooks/github.js.map +1 -0
- package/dist/webhooks/middleware.d.ts +19 -0
- package/dist/webhooks/middleware.d.ts.map +1 -0
- package/dist/webhooks/middleware.js +144 -0
- package/dist/webhooks/middleware.js.map +1 -0
- package/dist/webhooks/types.d.ts +18 -0
- package/dist/webhooks/types.d.ts.map +1 -0
- package/dist/webhooks/types.js +5 -0
- package/dist/webhooks/types.js.map +1 -0
- package/package.json +69 -0
- package/schema/knowledge.graphql +136 -0
- package/schema/oauth.graphql +45 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth RSA Key Management
|
|
3
|
+
*
|
|
4
|
+
* Generates, stores, loads, and caches an RS256 key pair for signing JWT
|
|
5
|
+
* access tokens. The key is stored in the OAuthSigningKey table so all
|
|
6
|
+
* Harper worker threads share the same key.
|
|
7
|
+
*/
|
|
8
|
+
import { generateKeyPair, exportJWK, importJWK } from 'jose';
|
|
9
|
+
const KEY_ID = 'primary';
|
|
10
|
+
let cachedPrivateKey = null;
|
|
11
|
+
let cachedPublicKeyJwk = null;
|
|
12
|
+
/**
|
|
13
|
+
* Ensure a signing key exists. Loads from DB or generates a new one.
|
|
14
|
+
* Called once during handleApplication() on each worker thread.
|
|
15
|
+
*/
|
|
16
|
+
export async function ensureSigningKey() {
|
|
17
|
+
const existing = await databases.kb.OAuthSigningKey.get(KEY_ID);
|
|
18
|
+
if (existing && existing.publicKeyJwk && existing.privateKeyJwk) {
|
|
19
|
+
cachedPublicKeyJwk = toJwk(existing.publicKeyJwk);
|
|
20
|
+
cachedPrivateKey = (await importJWK(toJwk(existing.privateKeyJwk), 'RS256'));
|
|
21
|
+
logger?.info?.('OAuth signing key loaded from database');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Generate a new RSA key pair
|
|
25
|
+
const { publicKey, privateKey } = await generateKeyPair('RS256', {
|
|
26
|
+
extractable: true,
|
|
27
|
+
});
|
|
28
|
+
const pubJwk = await exportJWK(publicKey);
|
|
29
|
+
const privJwk = await exportJWK(privateKey);
|
|
30
|
+
pubJwk.kid = KEY_ID;
|
|
31
|
+
pubJwk.use = 'sig';
|
|
32
|
+
pubJwk.alg = 'RS256';
|
|
33
|
+
try {
|
|
34
|
+
await databases.kb.OAuthSigningKey.put({
|
|
35
|
+
id: KEY_ID,
|
|
36
|
+
publicKeyJwk: JSON.stringify(pubJwk),
|
|
37
|
+
privateKeyJwk: JSON.stringify(privJwk),
|
|
38
|
+
algorithm: 'RS256',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Another worker may have created the key concurrently — reload
|
|
43
|
+
const reloaded = await databases.kb.OAuthSigningKey.get(KEY_ID);
|
|
44
|
+
if (reloaded && reloaded.publicKeyJwk && reloaded.privateKeyJwk) {
|
|
45
|
+
cachedPublicKeyJwk = toJwk(reloaded.publicKeyJwk);
|
|
46
|
+
cachedPrivateKey = (await importJWK(toJwk(reloaded.privateKeyJwk), 'RS256'));
|
|
47
|
+
logger?.info?.('OAuth signing key loaded after concurrent creation');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
throw new Error('Failed to create or load OAuth signing key');
|
|
51
|
+
}
|
|
52
|
+
cachedPrivateKey = privateKey;
|
|
53
|
+
cachedPublicKeyJwk = pubJwk;
|
|
54
|
+
logger?.info?.('OAuth signing key generated and stored');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the private key for signing JWTs.
|
|
58
|
+
* Lazily initializes if not yet loaded.
|
|
59
|
+
*/
|
|
60
|
+
export async function getPrivateKey() {
|
|
61
|
+
if (!cachedPrivateKey) {
|
|
62
|
+
await ensureSigningKey();
|
|
63
|
+
}
|
|
64
|
+
return cachedPrivateKey;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the public key JWK for token verification and JWKS endpoint.
|
|
68
|
+
* Lazily initializes if not yet loaded.
|
|
69
|
+
*/
|
|
70
|
+
export async function getPublicKeyJwk() {
|
|
71
|
+
if (!cachedPublicKeyJwk) {
|
|
72
|
+
await ensureSigningKey();
|
|
73
|
+
}
|
|
74
|
+
return cachedPublicKeyJwk;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the key ID for the JWT header.
|
|
78
|
+
*/
|
|
79
|
+
export function getKeyId() {
|
|
80
|
+
return KEY_ID;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the JWKS response body for the /oauth/jwks endpoint.
|
|
84
|
+
*/
|
|
85
|
+
export async function getJwks() {
|
|
86
|
+
return { keys: [await getPublicKeyJwk()] };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalize a value from Harper's table into a JWK object.
|
|
90
|
+
* Harper's `Any` column type may return the JWK as a string or wrapped object.
|
|
91
|
+
*/
|
|
92
|
+
function toJwk(value) {
|
|
93
|
+
if (typeof value === 'string') {
|
|
94
|
+
return JSON.parse(value);
|
|
95
|
+
}
|
|
96
|
+
if (value && typeof value === 'object') {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Cannot convert ${typeof value} to JWK`);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/oauth/keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Metadata Endpoints
|
|
3
|
+
*
|
|
4
|
+
* Serves RFC 9728 Protected Resource Metadata and RFC 8414 Authorization
|
|
5
|
+
* Server Metadata at their well-known URLs.
|
|
6
|
+
*/
|
|
7
|
+
import type { HarperRequest } from '../types.ts';
|
|
8
|
+
/**
|
|
9
|
+
* Handle GET /.well-known/oauth-protected-resource/<kbId>
|
|
10
|
+
*
|
|
11
|
+
* RFC 9728 — tells MCP clients where to find the authorization server
|
|
12
|
+
* and what scopes are supported. Each KB has its own protected resource
|
|
13
|
+
* metadata so the resource URL is scoped to that KB.
|
|
14
|
+
*/
|
|
15
|
+
export declare function handleProtectedResourceMetadata(request: HarperRequest, kbId: string): Response;
|
|
16
|
+
/**
|
|
17
|
+
* Handle GET /.well-known/oauth-authorization-server
|
|
18
|
+
*
|
|
19
|
+
* RFC 8414 — describes the authorization server's capabilities,
|
|
20
|
+
* endpoints, and supported grant types.
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleAuthServerMetadata(request: HarperRequest): Response;
|
|
23
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/oauth/metadata.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAU9F;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,GAAG,QAAQ,CAgBzE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Metadata Endpoints
|
|
3
|
+
*
|
|
4
|
+
* Serves RFC 9728 Protected Resource Metadata and RFC 8414 Authorization
|
|
5
|
+
* Server Metadata at their well-known URLs.
|
|
6
|
+
*/
|
|
7
|
+
import { getBaseUrl } from "../http-utils.js";
|
|
8
|
+
const SCOPES = ['mcp:read', 'mcp:write'];
|
|
9
|
+
/**
|
|
10
|
+
* Handle GET /.well-known/oauth-protected-resource/<kbId>
|
|
11
|
+
*
|
|
12
|
+
* RFC 9728 — tells MCP clients where to find the authorization server
|
|
13
|
+
* and what scopes are supported. Each KB has its own protected resource
|
|
14
|
+
* metadata so the resource URL is scoped to that KB.
|
|
15
|
+
*/
|
|
16
|
+
export function handleProtectedResourceMetadata(request, kbId) {
|
|
17
|
+
const baseUrl = getBaseUrl(request);
|
|
18
|
+
return jsonResponse(200, {
|
|
19
|
+
resource: `${baseUrl}/mcp/${kbId}`,
|
|
20
|
+
authorization_servers: [baseUrl],
|
|
21
|
+
scopes_supported: SCOPES,
|
|
22
|
+
bearer_methods_supported: ['header'],
|
|
23
|
+
resource_name: 'Knowledge Base MCP Server',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Handle GET /.well-known/oauth-authorization-server
|
|
28
|
+
*
|
|
29
|
+
* RFC 8414 — describes the authorization server's capabilities,
|
|
30
|
+
* endpoints, and supported grant types.
|
|
31
|
+
*/
|
|
32
|
+
export function handleAuthServerMetadata(request) {
|
|
33
|
+
const baseUrl = getBaseUrl(request);
|
|
34
|
+
return jsonResponse(200, {
|
|
35
|
+
issuer: baseUrl,
|
|
36
|
+
authorization_endpoint: `${baseUrl}/mcp-auth/authorize`,
|
|
37
|
+
token_endpoint: `${baseUrl}/mcp-auth/token`,
|
|
38
|
+
registration_endpoint: `${baseUrl}/mcp-auth/register`,
|
|
39
|
+
jwks_uri: `${baseUrl}/mcp-auth/jwks`,
|
|
40
|
+
scopes_supported: SCOPES,
|
|
41
|
+
response_types_supported: ['code'],
|
|
42
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
43
|
+
token_endpoint_auth_methods_supported: ['none'],
|
|
44
|
+
code_challenge_methods_supported: ['S256'],
|
|
45
|
+
service_documentation: `${baseUrl}/`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function jsonResponse(status, body) {
|
|
49
|
+
return new Response(JSON.stringify(body), {
|
|
50
|
+
status,
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Cache-Control': 'public, max-age=3600',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/oauth/metadata.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,MAAM,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,UAAU,+BAA+B,CAAC,OAAsB,EAAE,IAAY;IACnF,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAEpC,OAAO,YAAY,CAAC,GAAG,EAAE;QACxB,QAAQ,EAAE,GAAG,OAAO,QAAQ,IAAI,EAAE;QAClC,qBAAqB,EAAE,CAAC,OAAO,CAAC;QAChC,gBAAgB,EAAE,MAAM;QACxB,wBAAwB,EAAE,CAAC,QAAQ,CAAC;QACpC,aAAa,EAAE,2BAA2B;KAC1C,CAAC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAsB;IAC9D,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAEpC,OAAO,YAAY,CAAC,GAAG,EAAE;QACxB,MAAM,EAAE,OAAO;QACf,sBAAsB,EAAE,GAAG,OAAO,qBAAqB;QACvD,cAAc,EAAE,GAAG,OAAO,iBAAiB;QAC3C,qBAAqB,EAAE,GAAG,OAAO,oBAAoB;QACrD,QAAQ,EAAE,GAAG,OAAO,gBAAgB;QACpC,gBAAgB,EAAE,MAAM;QACxB,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,qBAAqB,EAAE,CAAC,oBAAoB,EAAE,eAAe,CAAC;QAC9D,qCAAqC,EAAE,CAAC,MAAM,CAAC;QAC/C,gCAAgC,EAAE,CAAC,MAAM,CAAC;QAC1C,qBAAqB,EAAE,GAAG,OAAO,GAAG;KACpC,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,MAAc,EAAE,IAA6B;IAClE,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACzC,MAAM;QACN,OAAO,EAAE;YACR,cAAc,EAAE,kBAAkB;YAClC,eAAe,EAAE,sBAAsB;SACvC;KACD,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Middleware
|
|
3
|
+
*
|
|
4
|
+
* Route dispatcher for all OAuth endpoints. Registered via
|
|
5
|
+
* scope.server.http() before the MCP and webhook middlewares.
|
|
6
|
+
*
|
|
7
|
+
* Routes (MCP OAuth 2.1 — separate from @harperfast/oauth's /oauth/* Resource):
|
|
8
|
+
* GET /.well-known/oauth-protected-resource/<kbId> → per-KB resource metadata
|
|
9
|
+
* GET /.well-known/oauth-authorization-server → auth server metadata
|
|
10
|
+
* POST /mcp-auth/register → DCR
|
|
11
|
+
* GET /mcp-auth/authorize → login page
|
|
12
|
+
* POST /mcp-auth/authorize → credential validation + redirect
|
|
13
|
+
* POST /mcp-auth/token → code exchange / refresh
|
|
14
|
+
* GET /mcp-auth/jwks → public key set
|
|
15
|
+
*/
|
|
16
|
+
import type { HarperRequest } from '../types.ts';
|
|
17
|
+
type MiddlewareFn = (request: HarperRequest, next: (req: HarperRequest) => Promise<unknown>) => Promise<unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Create the OAuth middleware for Harper's scope.server.http().
|
|
20
|
+
*/
|
|
21
|
+
export declare function createOAuthMiddleware(): MiddlewareFn;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/oauth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,KAAK,YAAY,GAAG,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEjH;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,YAAY,CA2CpD"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Middleware
|
|
3
|
+
*
|
|
4
|
+
* Route dispatcher for all OAuth endpoints. Registered via
|
|
5
|
+
* scope.server.http() before the MCP and webhook middlewares.
|
|
6
|
+
*
|
|
7
|
+
* Routes (MCP OAuth 2.1 — separate from @harperfast/oauth's /oauth/* Resource):
|
|
8
|
+
* GET /.well-known/oauth-protected-resource/<kbId> → per-KB resource metadata
|
|
9
|
+
* GET /.well-known/oauth-authorization-server → auth server metadata
|
|
10
|
+
* POST /mcp-auth/register → DCR
|
|
11
|
+
* GET /mcp-auth/authorize → login page
|
|
12
|
+
* POST /mcp-auth/authorize → credential validation + redirect
|
|
13
|
+
* POST /mcp-auth/token → code exchange / refresh
|
|
14
|
+
* GET /mcp-auth/jwks → public key set
|
|
15
|
+
*/
|
|
16
|
+
import { handleProtectedResourceMetadata, handleAuthServerMetadata } from "./metadata.js";
|
|
17
|
+
import { handleRegister } from "./register.js";
|
|
18
|
+
import { handleAuthorizeGet, handleAuthorizePost } from "./authorize.js";
|
|
19
|
+
import { handleToken } from "./token.js";
|
|
20
|
+
import { getJwks } from "./keys.js";
|
|
21
|
+
/**
|
|
22
|
+
* Create the OAuth middleware for Harper's scope.server.http().
|
|
23
|
+
*/
|
|
24
|
+
export function createOAuthMiddleware() {
|
|
25
|
+
return async (request, next) => {
|
|
26
|
+
const pathname = request.pathname || '';
|
|
27
|
+
const method = (request.method || 'GET').toUpperCase();
|
|
28
|
+
// Well-known metadata endpoints — protected resource metadata is per-KB
|
|
29
|
+
const prMatch = pathname.match(/^\/\.well-known\/oauth-protected-resource\/([^/]+)$/);
|
|
30
|
+
if (prMatch && method === 'GET') {
|
|
31
|
+
return handleProtectedResourceMetadata(request, prMatch[1]);
|
|
32
|
+
}
|
|
33
|
+
if (pathname === '/.well-known/oauth-authorization-server' && method === 'GET') {
|
|
34
|
+
return handleAuthServerMetadata(request);
|
|
35
|
+
}
|
|
36
|
+
// MCP OAuth endpoints (under /mcp-auth/ to avoid conflict with
|
|
37
|
+
// @harperfast/oauth's OAuthResource which owns the /oauth/* path)
|
|
38
|
+
if (pathname === '/mcp-auth/register' && method === 'POST') {
|
|
39
|
+
return handleRegister(request);
|
|
40
|
+
}
|
|
41
|
+
if (pathname === '/mcp-auth/authorize') {
|
|
42
|
+
if (method === 'GET') {
|
|
43
|
+
return handleAuthorizeGet(request);
|
|
44
|
+
}
|
|
45
|
+
if (method === 'POST') {
|
|
46
|
+
return handleAuthorizePost(request);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (pathname === '/mcp-auth/token' && method === 'POST') {
|
|
50
|
+
return handleToken(request);
|
|
51
|
+
}
|
|
52
|
+
if (pathname === '/mcp-auth/jwks' && method === 'GET') {
|
|
53
|
+
return new Response(JSON.stringify(await getJwks()), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'Cache-Control': 'public, max-age=3600',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Not a handled route — pass through
|
|
62
|
+
return next(request);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/oauth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,+BAA+B,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;GAEG;AACH,MAAM,UAAU,qBAAqB;IACpC,OAAO,KAAK,EAAE,OAAsB,EAAE,IAA8C,EAAoB,EAAE;QACzG,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAEvD,wEAAwE;QACxE,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAC;QACtF,IAAI,OAAO,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACjC,OAAO,+BAA+B,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,QAAQ,KAAK,yCAAyC,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YAChF,OAAO,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAC1C,CAAC;QAED,+DAA+D;QAC/D,kEAAkE;QAClE,IAAI,QAAQ,KAAK,oBAAoB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YAC5D,OAAO,cAAc,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,QAAQ,KAAK,qBAAqB,EAAE,CAAC;YACxC,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;gBACtB,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;YACpC,CAAC;YACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;gBACvB,OAAO,mBAAmB,CAAC,OAAO,CAAC,CAAC;YACrC,CAAC;QACF,CAAC;QACD,IAAI,QAAQ,KAAK,iBAAiB,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACzD,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;QACD,IAAI,QAAQ,KAAK,gBAAgB,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACvD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,OAAO,EAAE,CAAC,EAAE;gBACpD,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACR,cAAc,EAAE,kBAAkB;oBAClC,eAAe,EAAE,sBAAsB;iBACvC;aACD,CAAC,CAAC;QACJ,CAAC;QAED,qCAAqC;QACrC,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Dynamic Client Registration (RFC 7591)
|
|
3
|
+
*
|
|
4
|
+
* POST /oauth/register — allows MCP clients to register themselves
|
|
5
|
+
* and receive a client_id for the authorization flow.
|
|
6
|
+
*/
|
|
7
|
+
import type { HarperRequest } from '../types.ts';
|
|
8
|
+
/**
|
|
9
|
+
* Handle POST /oauth/register
|
|
10
|
+
*
|
|
11
|
+
* Accepts a registration request and stores the client in OAuthClient table.
|
|
12
|
+
* Public clients (no client_secret) are the default — PKCE provides security.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleRegister(request: HarperRequest): Promise<Response>;
|
|
15
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/oauth/register.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiE9E"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Dynamic Client Registration (RFC 7591)
|
|
3
|
+
*
|
|
4
|
+
* POST /oauth/register — allows MCP clients to register themselves
|
|
5
|
+
* and receive a client_id for the authorization flow.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { readBody } from "../http-utils.js";
|
|
9
|
+
/**
|
|
10
|
+
* Handle POST /oauth/register
|
|
11
|
+
*
|
|
12
|
+
* Accepts a registration request and stores the client in OAuthClient table.
|
|
13
|
+
* Public clients (no client_secret) are the default — PKCE provides security.
|
|
14
|
+
*/
|
|
15
|
+
export async function handleRegister(request) {
|
|
16
|
+
let body;
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readBody(request);
|
|
19
|
+
body = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return errorResponse(400, 'invalid_client_metadata', 'Invalid JSON body');
|
|
23
|
+
}
|
|
24
|
+
// Validate redirect_uris (required)
|
|
25
|
+
const redirectUris = body.redirect_uris;
|
|
26
|
+
if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
|
|
27
|
+
return errorResponse(400, 'invalid_redirect_uri', 'redirect_uris is required and must be a non-empty array');
|
|
28
|
+
}
|
|
29
|
+
// Validate each redirect URI: only localhost or https allowed
|
|
30
|
+
for (const uri of redirectUris) {
|
|
31
|
+
if (typeof uri !== 'string') {
|
|
32
|
+
return errorResponse(400, 'invalid_redirect_uri', 'Each redirect_uri must be a string');
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(uri);
|
|
36
|
+
const isLocalhost = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
|
|
37
|
+
const isHttps = parsed.protocol === 'https:';
|
|
38
|
+
if (!isLocalhost && !isHttps) {
|
|
39
|
+
return errorResponse(400, 'invalid_redirect_uri', `redirect_uri must use https:// or be localhost: ${uri}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return errorResponse(400, 'invalid_redirect_uri', `Invalid redirect_uri: ${uri}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const clientId = crypto.randomUUID();
|
|
47
|
+
const clientName = typeof body.client_name === 'string' ? body.client_name : 'Unknown Client';
|
|
48
|
+
const grantTypes = Array.isArray(body.grant_types) ? body.grant_types : ['authorization_code'];
|
|
49
|
+
const responseTypes = Array.isArray(body.response_types) ? body.response_types : ['code'];
|
|
50
|
+
const record = {
|
|
51
|
+
id: clientId,
|
|
52
|
+
clientName,
|
|
53
|
+
redirectUris,
|
|
54
|
+
grantTypes,
|
|
55
|
+
responseTypes,
|
|
56
|
+
scope: typeof body.scope === 'string' ? body.scope : 'mcp:read mcp:write',
|
|
57
|
+
};
|
|
58
|
+
await databases.kb.OAuthClient.put(record);
|
|
59
|
+
logger?.info?.(`OAuth client registered: ${clientId} (${clientName})`);
|
|
60
|
+
return new Response(JSON.stringify({
|
|
61
|
+
client_id: clientId,
|
|
62
|
+
client_name: clientName,
|
|
63
|
+
redirect_uris: redirectUris,
|
|
64
|
+
grant_types: grantTypes,
|
|
65
|
+
response_types: responseTypes,
|
|
66
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
67
|
+
}), {
|
|
68
|
+
status: 201,
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function errorResponse(status, error, description) {
|
|
73
|
+
return new Response(JSON.stringify({ error, error_description: description }), {
|
|
74
|
+
status,
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/oauth/register.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Token Endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /oauth/token — exchanges authorization codes for JWT access tokens
|
|
5
|
+
* and handles refresh token grants.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - grant_type=authorization_code (with PKCE verification)
|
|
9
|
+
* - grant_type=refresh_token (with token rotation)
|
|
10
|
+
*/
|
|
11
|
+
import type { HarperRequest } from '../types.ts';
|
|
12
|
+
/**
|
|
13
|
+
* Handle POST /oauth/token
|
|
14
|
+
*/
|
|
15
|
+
export declare function handleToken(request: HarperRequest): Promise<Response>;
|
|
16
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/oauth/token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAOjD;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8B3E"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Token Endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /oauth/token — exchanges authorization codes for JWT access tokens
|
|
5
|
+
* and handles refresh token grants.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - grant_type=authorization_code (with PKCE verification)
|
|
9
|
+
* - grant_type=refresh_token (with token rotation)
|
|
10
|
+
*/
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import { SignJWT } from 'jose';
|
|
13
|
+
import { readBody, parseFormBody, getBaseUrl } from "../http-utils.js";
|
|
14
|
+
import { getPrivateKey, getKeyId } from "./keys.js";
|
|
15
|
+
/** Access token lifetime: 1 hour */
|
|
16
|
+
const ACCESS_TOKEN_EXPIRY = '1h';
|
|
17
|
+
/**
|
|
18
|
+
* Handle POST /oauth/token
|
|
19
|
+
*/
|
|
20
|
+
export async function handleToken(request) {
|
|
21
|
+
let form;
|
|
22
|
+
try {
|
|
23
|
+
const rawBody = await readBody(request);
|
|
24
|
+
// Accept both form-urlencoded and JSON
|
|
25
|
+
const contentType = getContentType(request);
|
|
26
|
+
if (contentType.includes('json')) {
|
|
27
|
+
const json = JSON.parse(rawBody);
|
|
28
|
+
form = {};
|
|
29
|
+
for (const [k, v] of Object.entries(json)) {
|
|
30
|
+
form[k] = String(v);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
form = parseFormBody(rawBody);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return errorResponse(400, 'invalid_request', 'Invalid request body');
|
|
39
|
+
}
|
|
40
|
+
const grantType = form.grant_type;
|
|
41
|
+
if (grantType === 'authorization_code') {
|
|
42
|
+
return handleAuthorizationCodeGrant(form, request);
|
|
43
|
+
}
|
|
44
|
+
if (grantType === 'refresh_token') {
|
|
45
|
+
return handleRefreshTokenGrant(form, request);
|
|
46
|
+
}
|
|
47
|
+
return errorResponse(400, 'unsupported_grant_type', 'Unsupported grant_type');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Exchange an authorization code for tokens.
|
|
51
|
+
*/
|
|
52
|
+
async function handleAuthorizationCodeGrant(form, request) {
|
|
53
|
+
const code = form.code;
|
|
54
|
+
const redirectUri = form.redirect_uri;
|
|
55
|
+
const clientId = form.client_id;
|
|
56
|
+
const codeVerifier = form.code_verifier;
|
|
57
|
+
if (!code || !redirectUri || !clientId || !codeVerifier) {
|
|
58
|
+
return errorResponse(400, 'invalid_request', 'Missing required parameters: code, redirect_uri, client_id, code_verifier');
|
|
59
|
+
}
|
|
60
|
+
// Look up the authorization code
|
|
61
|
+
const codeRecord = await databases.kb.OAuthCode.get(code);
|
|
62
|
+
if (!codeRecord) {
|
|
63
|
+
return errorResponse(400, 'invalid_grant', 'Invalid or expired authorization code');
|
|
64
|
+
}
|
|
65
|
+
// Delete immediately — one-time use enforced before validation to prevent
|
|
66
|
+
// race conditions where two concurrent requests both read the same code
|
|
67
|
+
await databases.kb.OAuthCode.delete(code);
|
|
68
|
+
// Reject pending records (used during GitHub OAuth flow, not real auth codes)
|
|
69
|
+
if (codeRecord.type === 'pending') {
|
|
70
|
+
return errorResponse(400, 'invalid_grant', 'Invalid or expired authorization code');
|
|
71
|
+
}
|
|
72
|
+
// Validate client_id and redirect_uri match
|
|
73
|
+
if (codeRecord.clientId !== clientId) {
|
|
74
|
+
return errorResponse(400, 'invalid_grant', 'client_id does not match the authorization code');
|
|
75
|
+
}
|
|
76
|
+
if (codeRecord.redirectUri !== redirectUri) {
|
|
77
|
+
return errorResponse(400, 'invalid_grant', 'redirect_uri does not match the authorization code');
|
|
78
|
+
}
|
|
79
|
+
// PKCE verification: SHA256(code_verifier) must equal stored code_challenge
|
|
80
|
+
// Uses timing-safe comparison to prevent side-channel attacks
|
|
81
|
+
const expectedChallenge = base64urlEncode(crypto.createHash('sha256').update(codeVerifier).digest());
|
|
82
|
+
const storedChallenge = String(codeRecord.codeChallenge);
|
|
83
|
+
const expectedBuf = Buffer.from(expectedChallenge);
|
|
84
|
+
const storedBuf = Buffer.from(storedChallenge);
|
|
85
|
+
if (expectedBuf.length !== storedBuf.length || !crypto.timingSafeEqual(expectedBuf, storedBuf)) {
|
|
86
|
+
return errorResponse(400, 'invalid_grant', 'PKCE code_verifier validation failed');
|
|
87
|
+
}
|
|
88
|
+
// Issue tokens
|
|
89
|
+
const userId = codeRecord.userId;
|
|
90
|
+
const scope = codeRecord.scope;
|
|
91
|
+
const resource = codeRecord.resource || '';
|
|
92
|
+
return issueTokens(userId, clientId, scope, resource, request);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Exchange a refresh token for new tokens (with rotation).
|
|
96
|
+
*/
|
|
97
|
+
async function handleRefreshTokenGrant(form, request) {
|
|
98
|
+
const refreshToken = form.refresh_token;
|
|
99
|
+
const clientId = form.client_id;
|
|
100
|
+
if (!refreshToken || !clientId) {
|
|
101
|
+
return errorResponse(400, 'invalid_request', 'Missing required parameters: refresh_token, client_id');
|
|
102
|
+
}
|
|
103
|
+
// Look up the refresh token
|
|
104
|
+
const tokenRecord = await databases.kb.OAuthRefreshToken.get(refreshToken);
|
|
105
|
+
if (!tokenRecord) {
|
|
106
|
+
return errorResponse(400, 'invalid_grant', 'Invalid or expired refresh token');
|
|
107
|
+
}
|
|
108
|
+
if (tokenRecord.clientId !== clientId) {
|
|
109
|
+
return errorResponse(400, 'invalid_grant', 'client_id does not match the refresh token');
|
|
110
|
+
}
|
|
111
|
+
// Delete old refresh token (rotation)
|
|
112
|
+
await databases.kb.OAuthRefreshToken.delete(refreshToken);
|
|
113
|
+
const userId = tokenRecord.userId;
|
|
114
|
+
const scope = tokenRecord.scope;
|
|
115
|
+
const resource = tokenRecord.resource || '';
|
|
116
|
+
return issueTokens(userId, clientId, scope, resource, request);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Issue a JWT access token and an opaque refresh token.
|
|
120
|
+
*/
|
|
121
|
+
async function issueTokens(userId, clientId, scope, resource, request) {
|
|
122
|
+
const baseUrl = getBaseUrl(request);
|
|
123
|
+
// Use the resource URL as the audience when available (KB-scoped token),
|
|
124
|
+
// otherwise fall back to the base MCP path
|
|
125
|
+
const audience = resource || `${baseUrl}/mcp`;
|
|
126
|
+
// Sign JWT access token
|
|
127
|
+
const accessToken = await new SignJWT({
|
|
128
|
+
scope,
|
|
129
|
+
client_id: clientId,
|
|
130
|
+
})
|
|
131
|
+
.setProtectedHeader({ alg: 'RS256', kid: getKeyId() })
|
|
132
|
+
.setSubject(userId)
|
|
133
|
+
.setIssuer(baseUrl)
|
|
134
|
+
.setAudience(audience)
|
|
135
|
+
.setExpirationTime(ACCESS_TOKEN_EXPIRY)
|
|
136
|
+
.setIssuedAt()
|
|
137
|
+
.setJti(crypto.randomUUID())
|
|
138
|
+
.sign(await getPrivateKey());
|
|
139
|
+
// Generate opaque refresh token (carries resource for refresh grant)
|
|
140
|
+
const refreshToken = crypto.randomBytes(32).toString('hex');
|
|
141
|
+
await databases.kb.OAuthRefreshToken.put({
|
|
142
|
+
id: refreshToken,
|
|
143
|
+
clientId,
|
|
144
|
+
userId,
|
|
145
|
+
scope,
|
|
146
|
+
resource,
|
|
147
|
+
});
|
|
148
|
+
logger?.info?.(`OAuth tokens issued for user ${userId}, client ${clientId}`);
|
|
149
|
+
return new Response(JSON.stringify({
|
|
150
|
+
access_token: accessToken,
|
|
151
|
+
token_type: 'Bearer',
|
|
152
|
+
expires_in: 3600,
|
|
153
|
+
refresh_token: refreshToken,
|
|
154
|
+
scope,
|
|
155
|
+
}), {
|
|
156
|
+
status: 200,
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
'Cache-Control': 'no-store',
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function base64urlEncode(buffer) {
|
|
164
|
+
return buffer.toString('base64url');
|
|
165
|
+
}
|
|
166
|
+
function getContentType(request) {
|
|
167
|
+
const headers = request.headers;
|
|
168
|
+
if (!headers)
|
|
169
|
+
return '';
|
|
170
|
+
if (typeof headers.get === 'function') {
|
|
171
|
+
return headers.get('content-type') || '';
|
|
172
|
+
}
|
|
173
|
+
return (headers['content-type'] || '');
|
|
174
|
+
}
|
|
175
|
+
function errorResponse(status, error, description) {
|
|
176
|
+
return new Response(JSON.stringify({ error, error_description: description }), {
|
|
177
|
+
status,
|
|
178
|
+
headers: {
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
'Cache-Control': 'no-store',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/oauth/token.ts"],"names":[],"mappings":"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"}
|