harper-knowledge 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/config.yaml +17 -0
  4. package/dist/core/embeddings.d.ts +29 -0
  5. package/dist/core/embeddings.js +199 -0
  6. package/dist/core/entries.d.ts +85 -0
  7. package/dist/core/entries.js +235 -0
  8. package/dist/core/history.d.ts +30 -0
  9. package/dist/core/history.js +119 -0
  10. package/dist/core/search.d.ts +23 -0
  11. package/dist/core/search.js +306 -0
  12. package/dist/core/tags.d.ts +32 -0
  13. package/dist/core/tags.js +76 -0
  14. package/dist/core/triage.d.ts +55 -0
  15. package/dist/core/triage.js +126 -0
  16. package/dist/http-utils.d.ts +37 -0
  17. package/dist/http-utils.js +132 -0
  18. package/dist/index.d.ts +21 -0
  19. package/dist/index.js +76 -0
  20. package/dist/mcp/server.d.ts +24 -0
  21. package/dist/mcp/server.js +124 -0
  22. package/dist/mcp/tools.d.ts +13 -0
  23. package/dist/mcp/tools.js +497 -0
  24. package/dist/oauth/authorize.d.ts +27 -0
  25. package/dist/oauth/authorize.js +438 -0
  26. package/dist/oauth/github.d.ts +28 -0
  27. package/dist/oauth/github.js +62 -0
  28. package/dist/oauth/keys.d.ts +33 -0
  29. package/dist/oauth/keys.js +100 -0
  30. package/dist/oauth/metadata.d.ts +21 -0
  31. package/dist/oauth/metadata.js +55 -0
  32. package/dist/oauth/middleware.d.ts +22 -0
  33. package/dist/oauth/middleware.js +64 -0
  34. package/dist/oauth/register.d.ts +14 -0
  35. package/dist/oauth/register.js +83 -0
  36. package/dist/oauth/token.d.ts +15 -0
  37. package/dist/oauth/token.js +178 -0
  38. package/dist/oauth/validate.d.ts +30 -0
  39. package/dist/oauth/validate.js +52 -0
  40. package/dist/resources/HistoryResource.d.ts +38 -0
  41. package/dist/resources/HistoryResource.js +38 -0
  42. package/dist/resources/KnowledgeEntryResource.d.ts +64 -0
  43. package/dist/resources/KnowledgeEntryResource.js +157 -0
  44. package/dist/resources/QueryLogResource.d.ts +20 -0
  45. package/dist/resources/QueryLogResource.js +57 -0
  46. package/dist/resources/ServiceKeyResource.d.ts +51 -0
  47. package/dist/resources/ServiceKeyResource.js +132 -0
  48. package/dist/resources/TagResource.d.ts +25 -0
  49. package/dist/resources/TagResource.js +32 -0
  50. package/dist/resources/TriageResource.d.ts +51 -0
  51. package/dist/resources/TriageResource.js +107 -0
  52. package/dist/types.d.ts +317 -0
  53. package/dist/types.js +7 -0
  54. package/dist/webhooks/datadog.d.ts +26 -0
  55. package/dist/webhooks/datadog.js +120 -0
  56. package/dist/webhooks/github.d.ts +24 -0
  57. package/dist/webhooks/github.js +167 -0
  58. package/dist/webhooks/middleware.d.ts +14 -0
  59. package/dist/webhooks/middleware.js +161 -0
  60. package/dist/webhooks/types.d.ts +17 -0
  61. package/dist/webhooks/types.js +4 -0
  62. package/package.json +72 -0
  63. package/schema/knowledge.graphql +134 -0
  64. package/web/index.html +735 -0
  65. package/web/js/app.js +461 -0
  66. package/web/js/detail.js +223 -0
  67. package/web/js/editor.js +303 -0
  68. package/web/js/search.js +238 -0
  69. package/web/js/triage.js +305 -0
@@ -0,0 +1,55 @@
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
11
+ *
12
+ * RFC 9728 — tells MCP clients where to find the authorization server
13
+ * and what scopes are supported.
14
+ */
15
+ export function handleProtectedResourceMetadata(request) {
16
+ const baseUrl = getBaseUrl(request);
17
+ return jsonResponse(200, {
18
+ resource: `${baseUrl}/mcp`,
19
+ authorization_servers: [baseUrl],
20
+ scopes_supported: SCOPES,
21
+ bearer_methods_supported: ["header"],
22
+ resource_name: "Harper Knowledge Base MCP Server",
23
+ });
24
+ }
25
+ /**
26
+ * Handle GET /.well-known/oauth-authorization-server
27
+ *
28
+ * RFC 8414 — describes the authorization server's capabilities,
29
+ * endpoints, and supported grant types.
30
+ */
31
+ export function handleAuthServerMetadata(request) {
32
+ const baseUrl = getBaseUrl(request);
33
+ return jsonResponse(200, {
34
+ issuer: baseUrl,
35
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
36
+ token_endpoint: `${baseUrl}/oauth/token`,
37
+ registration_endpoint: `${baseUrl}/oauth/register`,
38
+ jwks_uri: `${baseUrl}/oauth/jwks`,
39
+ scopes_supported: SCOPES,
40
+ response_types_supported: ["code"],
41
+ grant_types_supported: ["authorization_code", "refresh_token"],
42
+ token_endpoint_auth_methods_supported: ["none"],
43
+ code_challenge_methods_supported: ["S256"],
44
+ service_documentation: "https://harper.fast/",
45
+ });
46
+ }
47
+ function jsonResponse(status, body) {
48
+ return new Response(JSON.stringify(body), {
49
+ status,
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ "Cache-Control": "public, max-age=3600",
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,22 @@
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:
8
+ * GET /.well-known/oauth-protected-resource → metadata
9
+ * GET /.well-known/oauth-authorization-server → metadata
10
+ * POST /oauth/register → DCR
11
+ * GET /oauth/authorize → login page
12
+ * POST /oauth/authorize → credential validation + redirect
13
+ * POST /oauth/token → code exchange / refresh
14
+ * GET /oauth/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 {};
@@ -0,0 +1,64 @@
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:
8
+ * GET /.well-known/oauth-protected-resource → metadata
9
+ * GET /.well-known/oauth-authorization-server → metadata
10
+ * POST /oauth/register → DCR
11
+ * GET /oauth/authorize → login page
12
+ * POST /oauth/authorize → credential validation + redirect
13
+ * POST /oauth/token → code exchange / refresh
14
+ * GET /oauth/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
29
+ if (pathname === "/.well-known/oauth-protected-resource" &&
30
+ method === "GET") {
31
+ return handleProtectedResourceMetadata(request);
32
+ }
33
+ if (pathname === "/.well-known/oauth-authorization-server" &&
34
+ method === "GET") {
35
+ return handleAuthServerMetadata(request);
36
+ }
37
+ // OAuth endpoints
38
+ if (pathname === "/oauth/register" && method === "POST") {
39
+ return handleRegister(request);
40
+ }
41
+ if (pathname === "/oauth/authorize") {
42
+ if (method === "GET") {
43
+ return handleAuthorizeGet(request);
44
+ }
45
+ if (method === "POST") {
46
+ return handleAuthorizePost(request);
47
+ }
48
+ }
49
+ if (pathname === "/oauth/token" && method === "POST") {
50
+ return handleToken(request);
51
+ }
52
+ if (pathname === "/oauth/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 an OAuth route — pass through
62
+ return next(request);
63
+ };
64
+ }
@@ -0,0 +1,14 @@
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>;
@@ -0,0 +1,83 @@
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" ||
37
+ parsed.hostname === "127.0.0.1" ||
38
+ parsed.hostname === "::1";
39
+ const isHttps = parsed.protocol === "https:";
40
+ if (!isLocalhost && !isHttps) {
41
+ return errorResponse(400, "invalid_redirect_uri", `redirect_uri must use https:// or be localhost: ${uri}`);
42
+ }
43
+ }
44
+ catch {
45
+ return errorResponse(400, "invalid_redirect_uri", `Invalid redirect_uri: ${uri}`);
46
+ }
47
+ }
48
+ const clientId = crypto.randomUUID();
49
+ const clientName = typeof body.client_name === "string" ? body.client_name : "Unknown Client";
50
+ const grantTypes = Array.isArray(body.grant_types)
51
+ ? body.grant_types
52
+ : ["authorization_code"];
53
+ const responseTypes = Array.isArray(body.response_types)
54
+ ? body.response_types
55
+ : ["code"];
56
+ const record = {
57
+ id: clientId,
58
+ clientName,
59
+ redirectUris,
60
+ grantTypes,
61
+ responseTypes,
62
+ scope: typeof body.scope === "string" ? body.scope : "mcp:read mcp:write",
63
+ };
64
+ await databases.kb.OAuthClient.put(record);
65
+ logger?.info?.(`OAuth client registered: ${clientId} (${clientName})`);
66
+ return new Response(JSON.stringify({
67
+ client_id: clientId,
68
+ client_name: clientName,
69
+ redirect_uris: redirectUris,
70
+ grant_types: grantTypes,
71
+ response_types: responseTypes,
72
+ client_id_issued_at: Math.floor(Date.now() / 1000),
73
+ }), {
74
+ status: 201,
75
+ headers: { "Content-Type": "application/json" },
76
+ });
77
+ }
78
+ function errorResponse(status, error, description) {
79
+ return new Response(JSON.stringify({ error, error_description: description }), {
80
+ status,
81
+ headers: { "Content-Type": "application/json" },
82
+ });
83
+ }
@@ -0,0 +1,15 @@
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>;
@@ -0,0 +1,178 @@
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 ||
86
+ !crypto.timingSafeEqual(expectedBuf, storedBuf)) {
87
+ return errorResponse(400, "invalid_grant", "PKCE code_verifier validation failed");
88
+ }
89
+ // Issue tokens
90
+ const userId = codeRecord.userId;
91
+ const scope = codeRecord.scope;
92
+ return issueTokens(userId, clientId, scope, 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
+ return issueTokens(userId, clientId, scope, request);
116
+ }
117
+ /**
118
+ * Issue a JWT access token and an opaque refresh token.
119
+ */
120
+ async function issueTokens(userId, clientId, scope, request) {
121
+ const baseUrl = getBaseUrl(request);
122
+ // Sign JWT access token
123
+ const accessToken = await new SignJWT({
124
+ scope,
125
+ client_id: clientId,
126
+ })
127
+ .setProtectedHeader({ alg: "RS256", kid: getKeyId() })
128
+ .setSubject(userId)
129
+ .setIssuer(baseUrl)
130
+ .setAudience(`${baseUrl}/mcp`)
131
+ .setExpirationTime(ACCESS_TOKEN_EXPIRY)
132
+ .setIssuedAt()
133
+ .setJti(crypto.randomUUID())
134
+ .sign(await getPrivateKey());
135
+ // Generate opaque refresh token
136
+ const refreshToken = crypto.randomBytes(32).toString("hex");
137
+ await databases.kb.OAuthRefreshToken.put({
138
+ id: refreshToken,
139
+ clientId,
140
+ userId,
141
+ scope,
142
+ });
143
+ logger?.info?.(`OAuth tokens issued for user ${userId}, client ${clientId}`);
144
+ return new Response(JSON.stringify({
145
+ access_token: accessToken,
146
+ token_type: "Bearer",
147
+ expires_in: 3600,
148
+ refresh_token: refreshToken,
149
+ scope,
150
+ }), {
151
+ status: 200,
152
+ headers: {
153
+ "Content-Type": "application/json",
154
+ "Cache-Control": "no-store",
155
+ },
156
+ });
157
+ }
158
+ function base64urlEncode(buffer) {
159
+ return buffer.toString("base64url");
160
+ }
161
+ function getContentType(request) {
162
+ const headers = request.headers;
163
+ if (!headers)
164
+ return "";
165
+ if (typeof headers.get === "function") {
166
+ return headers.get("content-type") || "";
167
+ }
168
+ return (headers["content-type"] || "");
169
+ }
170
+ function errorResponse(status, error, description) {
171
+ return new Response(JSON.stringify({ error, error_description: description }), {
172
+ status,
173
+ headers: {
174
+ "Content-Type": "application/json",
175
+ "Cache-Control": "no-store",
176
+ },
177
+ });
178
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * OAuth JWT Bearer Token Validation
3
+ *
4
+ * Validates JWT access tokens on incoming MCP requests.
5
+ * Verifies the signature, issuer, audience, expiration, and scope.
6
+ */
7
+ import type { HarperRequest } from "../types.ts";
8
+ export interface ValidatedCaller {
9
+ /** User identifier (from JWT sub claim, e.g. "github:octocat") */
10
+ userId: string;
11
+ /** OAuth client ID */
12
+ clientId: string;
13
+ /** Granted scopes */
14
+ scopes: string[];
15
+ }
16
+ export interface AuthResult {
17
+ /** Validated caller, or null if no valid token */
18
+ caller: ValidatedCaller | null;
19
+ /** Whether an Authorization header with Bearer token was present */
20
+ hasToken: boolean;
21
+ }
22
+ /**
23
+ * Validate the Bearer token from an MCP request.
24
+ *
25
+ * Returns { caller, hasToken } so the MCP middleware can distinguish:
26
+ * - No token → anonymous read-only access
27
+ * - Valid token → authenticated access with granted scopes
28
+ * - Invalid token → 401 (token present but verification failed)
29
+ */
30
+ export declare function validateMcpAuth(request: HarperRequest): Promise<AuthResult>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * OAuth JWT Bearer Token Validation
3
+ *
4
+ * Validates JWT access tokens on incoming MCP requests.
5
+ * Verifies the signature, issuer, audience, expiration, and scope.
6
+ */
7
+ import { jwtVerify, createLocalJWKSet } from "jose";
8
+ import { getBaseUrl, getHeader } from "../http-utils.js";
9
+ import { getJwks } from "./keys.js";
10
+ /**
11
+ * Validate the Bearer token from an MCP request.
12
+ *
13
+ * Returns { caller, hasToken } so the MCP middleware can distinguish:
14
+ * - No token → anonymous read-only access
15
+ * - Valid token → authenticated access with granted scopes
16
+ * - Invalid token → 401 (token present but verification failed)
17
+ */
18
+ export async function validateMcpAuth(request) {
19
+ const token = extractBearerToken(request);
20
+ if (!token)
21
+ return { caller: null, hasToken: false };
22
+ try {
23
+ const baseUrl = getBaseUrl(request);
24
+ const jwks = createLocalJWKSet(await getJwks());
25
+ const { payload } = await jwtVerify(token, jwks, {
26
+ issuer: baseUrl,
27
+ audience: `${baseUrl}/mcp`,
28
+ });
29
+ return {
30
+ caller: {
31
+ userId: payload.sub || "unknown",
32
+ clientId: payload.client_id || "unknown",
33
+ scopes: typeof payload.scope === "string" ? payload.scope.split(" ") : [],
34
+ },
35
+ hasToken: true,
36
+ };
37
+ }
38
+ catch (error) {
39
+ logger?.warn?.(`JWT validation failed: ${error.message}`);
40
+ return { caller: null, hasToken: true };
41
+ }
42
+ }
43
+ /**
44
+ * Extract the Bearer token from the Authorization header.
45
+ */
46
+ function extractBearerToken(request) {
47
+ const authValue = getHeader(request, "authorization");
48
+ if (!authValue)
49
+ return null;
50
+ const match = authValue.match(/^Bearer\s+(.+)$/i);
51
+ return match ? match[1] : null;
52
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * History Resource
3
+ *
4
+ * REST endpoint for knowledge entry edit history.
5
+ *
6
+ * Routes:
7
+ * GET /History/<entryId> — get edit history for a knowledge entry
8
+ */
9
+ declare const HistoryResource_base: any;
10
+ export declare class HistoryResource extends HistoryResource_base {
11
+ static loadAsInstance: boolean;
12
+ /**
13
+ * GET /History/<entryId> — get edit history for a knowledge entry.
14
+ * Public read access (same as KnowledgeEntry).
15
+ */
16
+ get(target?: any): Promise<{
17
+ status: number;
18
+ data: {
19
+ error: string;
20
+ };
21
+ entryId?: undefined;
22
+ editCount?: undefined;
23
+ edits?: undefined;
24
+ } | {
25
+ entryId: string;
26
+ editCount: number;
27
+ edits: {
28
+ id: string;
29
+ entryId: string;
30
+ editSummary?: string;
31
+ changedFields: string[];
32
+ createdAt?: Date;
33
+ }[];
34
+ status?: undefined;
35
+ data?: undefined;
36
+ }>;
37
+ }
38
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * History Resource
3
+ *
4
+ * REST endpoint for knowledge entry edit history.
5
+ *
6
+ * Routes:
7
+ * GET /History/<entryId> — get edit history for a knowledge entry
8
+ */
9
+ import { getHistory } from "../core/history.js";
10
+ function getResourceClass() {
11
+ return globalThis.Resource;
12
+ }
13
+ export class HistoryResource extends getResourceClass() {
14
+ static loadAsInstance = false;
15
+ /**
16
+ * GET /History/<entryId> — get edit history for a knowledge entry.
17
+ * Public read access (same as KnowledgeEntry).
18
+ */
19
+ async get(target) {
20
+ const entryId = this.getId();
21
+ if (!entryId) {
22
+ return {
23
+ status: 400,
24
+ data: { error: "Entry ID required: GET /History/<entryId>" },
25
+ };
26
+ }
27
+ const limitParam = target?.get?.("limit") || target?.limit;
28
+ const limit = limitParam ? parseInt(String(limitParam), 10) : 50;
29
+ const edits = await getHistory(String(entryId), limit);
30
+ // Strip sensitive fields from public responses (usernames, previous values)
31
+ const publicEdits = edits.map(({ editedBy: _u, previousSnapshot: _s, ...rest }) => rest);
32
+ return {
33
+ entryId: String(entryId),
34
+ editCount: publicEdits.length,
35
+ edits: publicEdits,
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Knowledge Entry Resource
3
+ *
4
+ * REST endpoint for knowledge base entries.
5
+ * GET is public; POST, PUT, DELETE require authentication.
6
+ *
7
+ * Routes:
8
+ * GET /Knowledge/<id> — return single entry
9
+ * GET /Knowledge/?query=.. — search entries
10
+ * POST /Knowledge/ — create new entry (auth required)
11
+ * PUT /Knowledge/<id> — update entry (auth required)
12
+ * DELETE /Knowledge/<id> — deprecate entry (team role required)
13
+ */
14
+ declare const KnowledgeEntryResource_base: any;
15
+ export declare class KnowledgeEntryResource extends KnowledgeEntryResource_base {
16
+ static loadAsInstance: boolean;
17
+ /**
18
+ * GET /Knowledge/<id> — return a single entry by ID.
19
+ * GET /Knowledge/?query=... — search the knowledge base.
20
+ * PUBLIC — no auth required.
21
+ */
22
+ get(target?: any): Promise<Omit<import("../types.ts").KnowledgeEntry, "embedding"> | Omit<import("../types.ts").SearchResult, "embedding">[] | {
23
+ status: number;
24
+ data: {
25
+ error: string;
26
+ };
27
+ error?: undefined;
28
+ } | {
29
+ error: string;
30
+ status?: undefined;
31
+ data?: undefined;
32
+ }>;
33
+ /**
34
+ * POST /Knowledge/ — create a new knowledge entry.
35
+ * AUTH REQUIRED. AI agents have their confidence forced to "ai-generated".
36
+ */
37
+ post(_target: any, data: any): Promise<import("../types.ts").KnowledgeEntry | {
38
+ status: number;
39
+ data: {
40
+ error: string;
41
+ };
42
+ }>;
43
+ /**
44
+ * PUT /Knowledge/<id> — create or update an entry.
45
+ * AUTH REQUIRED. AI agents have their confidence forced to "ai-generated".
46
+ */
47
+ put(_target: any, data: any): Promise<import("../types.ts").KnowledgeEntry | {
48
+ status: number;
49
+ data: {
50
+ error: string;
51
+ };
52
+ }>;
53
+ /**
54
+ * DELETE /Knowledge/<id> — deprecate an entry (soft delete).
55
+ * AUTH REQUIRED: team role only.
56
+ */
57
+ delete(_target?: any): Promise<true | {
58
+ status: number;
59
+ data: {
60
+ error: string;
61
+ };
62
+ }>;
63
+ }
64
+ export {};