loki-mode 5.7.2 → 5.7.3

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.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Authentication Middleware
3
+ *
4
+ * Provides optional token-based authentication for the API.
5
+ * By default, only allows localhost connections.
6
+ */
7
+
8
+ export interface AuthConfig {
9
+ // Allow localhost without auth (default: true)
10
+ allowLocalhost: boolean;
11
+
12
+ // API token for remote access (optional)
13
+ apiToken?: string;
14
+
15
+ // Allowed origins for CORS
16
+ allowedOrigins: string[];
17
+ }
18
+
19
+ const defaultConfig: AuthConfig = {
20
+ allowLocalhost: true,
21
+ apiToken: Deno.env.get("LOKI_API_TOKEN"),
22
+ allowedOrigins: ["http://localhost:*", "http://127.0.0.1:*"],
23
+ };
24
+
25
+ let config = { ...defaultConfig };
26
+
27
+ /**
28
+ * Configure authentication
29
+ */
30
+ export function configureAuth(newConfig: Partial<AuthConfig>): void {
31
+ config = { ...config, ...newConfig };
32
+ }
33
+
34
+ /**
35
+ * Authentication middleware
36
+ */
37
+ export function authMiddleware(
38
+ handler: (req: Request) => Promise<Response> | Response
39
+ ): (req: Request) => Promise<Response> {
40
+ return async (req: Request): Promise<Response> => {
41
+ const authResult = checkAuth(req);
42
+
43
+ if (!authResult.allowed) {
44
+ return new Response(
45
+ JSON.stringify({
46
+ error: authResult.reason,
47
+ code: "AUTH_FAILED",
48
+ }),
49
+ {
50
+ status: 401,
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ },
54
+ }
55
+ );
56
+ }
57
+
58
+ return handler(req);
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Check if request is authenticated
64
+ */
65
+ export function checkAuth(req: Request): { allowed: boolean; reason?: string } {
66
+ const url = new URL(req.url);
67
+ const host = url.hostname;
68
+
69
+ // Allow localhost connections if enabled
70
+ if (config.allowLocalhost) {
71
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
72
+ return { allowed: true };
73
+ }
74
+ }
75
+
76
+ // Check API token
77
+ if (config.apiToken) {
78
+ const authHeader = req.headers.get("Authorization");
79
+
80
+ if (authHeader) {
81
+ // Support Bearer token format
82
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
83
+ if (match && match[1] === config.apiToken) {
84
+ return { allowed: true };
85
+ }
86
+
87
+ // Support X-API-Key header
88
+ const apiKey = req.headers.get("X-API-Key");
89
+ if (apiKey === config.apiToken) {
90
+ return { allowed: true };
91
+ }
92
+ }
93
+
94
+ return {
95
+ allowed: false,
96
+ reason: "Invalid or missing API token",
97
+ };
98
+ }
99
+
100
+ // No token configured, deny remote access
101
+ return {
102
+ allowed: false,
103
+ reason: "Remote access not configured. Set LOKI_API_TOKEN to enable.",
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Generate a secure random token
109
+ */
110
+ export function generateToken(): string {
111
+ const bytes = new Uint8Array(32);
112
+ crypto.getRandomValues(bytes);
113
+ return Array.from(bytes)
114
+ .map((b) => b.toString(16).padStart(2, "0"))
115
+ .join("");
116
+ }
117
+
118
+ /**
119
+ * Get current auth config (for debugging)
120
+ */
121
+ export function getAuthConfig(): Omit<AuthConfig, "apiToken"> & {
122
+ hasToken: boolean;
123
+ } {
124
+ return {
125
+ allowLocalhost: config.allowLocalhost,
126
+ allowedOrigins: config.allowedOrigins,
127
+ hasToken: !!config.apiToken,
128
+ };
129
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * CORS Middleware
3
+ *
4
+ * Handles Cross-Origin Resource Sharing for browser clients.
5
+ */
6
+
7
+ export interface CorsConfig {
8
+ allowedOrigins: string[];
9
+ allowedMethods: string[];
10
+ allowedHeaders: string[];
11
+ exposeHeaders: string[];
12
+ maxAge: number;
13
+ credentials: boolean;
14
+ }
15
+
16
+ const defaultConfig: CorsConfig = {
17
+ allowedOrigins: ["*"],
18
+ allowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
19
+ allowedHeaders: [
20
+ "Content-Type",
21
+ "Authorization",
22
+ "X-API-Key",
23
+ "X-Request-ID",
24
+ "Accept",
25
+ "Cache-Control",
26
+ ],
27
+ exposeHeaders: ["X-Request-ID", "X-Session-ID"],
28
+ maxAge: 86400, // 24 hours
29
+ credentials: true,
30
+ };
31
+
32
+ let config = { ...defaultConfig };
33
+
34
+ /**
35
+ * Configure CORS
36
+ */
37
+ export function configureCors(newConfig: Partial<CorsConfig>): void {
38
+ config = { ...config, ...newConfig };
39
+ }
40
+
41
+ /**
42
+ * Get CORS headers for a request
43
+ */
44
+ export function getCorsHeaders(req: Request): Headers {
45
+ const headers = new Headers();
46
+ const origin = req.headers.get("Origin");
47
+
48
+ // Check if origin is allowed
49
+ const allowedOrigin = isOriginAllowed(origin);
50
+
51
+ if (allowedOrigin) {
52
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
53
+ }
54
+
55
+ headers.set(
56
+ "Access-Control-Allow-Methods",
57
+ config.allowedMethods.join(", ")
58
+ );
59
+
60
+ headers.set(
61
+ "Access-Control-Allow-Headers",
62
+ config.allowedHeaders.join(", ")
63
+ );
64
+
65
+ headers.set(
66
+ "Access-Control-Expose-Headers",
67
+ config.exposeHeaders.join(", ")
68
+ );
69
+
70
+ headers.set("Access-Control-Max-Age", config.maxAge.toString());
71
+
72
+ if (config.credentials) {
73
+ headers.set("Access-Control-Allow-Credentials", "true");
74
+ }
75
+
76
+ return headers;
77
+ }
78
+
79
+ /**
80
+ * Check if origin is allowed
81
+ */
82
+ function isOriginAllowed(origin: string | null): string | null {
83
+ if (!origin) {
84
+ return null;
85
+ }
86
+
87
+ for (const allowed of config.allowedOrigins) {
88
+ // Wildcard match all
89
+ if (allowed === "*") {
90
+ return origin;
91
+ }
92
+
93
+ // Exact match
94
+ if (allowed === origin) {
95
+ return origin;
96
+ }
97
+
98
+ // Wildcard pattern (e.g., "http://localhost:*")
99
+ if (allowed.includes("*")) {
100
+ const pattern = allowed
101
+ .replace(/\./g, "\\.")
102
+ .replace(/\*/g, ".*");
103
+ const regex = new RegExp(`^${pattern}$`);
104
+ if (regex.test(origin)) {
105
+ return origin;
106
+ }
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * CORS middleware
115
+ */
116
+ export function corsMiddleware(
117
+ handler: (req: Request) => Promise<Response> | Response
118
+ ): (req: Request) => Promise<Response> {
119
+ return async (req: Request): Promise<Response> => {
120
+ const corsHeaders = getCorsHeaders(req);
121
+
122
+ // Handle preflight requests
123
+ if (req.method === "OPTIONS") {
124
+ return new Response(null, {
125
+ status: 204,
126
+ headers: corsHeaders,
127
+ });
128
+ }
129
+
130
+ // Handle actual request
131
+ const response = await handler(req);
132
+
133
+ // Add CORS headers to response
134
+ const newHeaders = new Headers(response.headers);
135
+ for (const [key, value] of corsHeaders) {
136
+ newHeaders.set(key, value);
137
+ }
138
+
139
+ return new Response(response.body, {
140
+ status: response.status,
141
+ statusText: response.statusText,
142
+ headers: newHeaders,
143
+ });
144
+ };
145
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Error Handling Middleware
3
+ *
4
+ * Provides consistent error responses and logging.
5
+ */
6
+
7
+ import type { ApiError } from "../types/api.ts";
8
+
9
+ // Error codes
10
+ export const ErrorCodes = {
11
+ // Client errors (4xx)
12
+ BAD_REQUEST: "BAD_REQUEST",
13
+ UNAUTHORIZED: "UNAUTHORIZED",
14
+ FORBIDDEN: "FORBIDDEN",
15
+ NOT_FOUND: "NOT_FOUND",
16
+ METHOD_NOT_ALLOWED: "METHOD_NOT_ALLOWED",
17
+ CONFLICT: "CONFLICT",
18
+ VALIDATION_ERROR: "VALIDATION_ERROR",
19
+
20
+ // Server errors (5xx)
21
+ INTERNAL_ERROR: "INTERNAL_ERROR",
22
+ NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
23
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
24
+ TIMEOUT: "TIMEOUT",
25
+
26
+ // Loki-specific errors
27
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
28
+ SESSION_ALREADY_RUNNING: "SESSION_ALREADY_RUNNING",
29
+ PROVIDER_NOT_AVAILABLE: "PROVIDER_NOT_AVAILABLE",
30
+ CLI_ERROR: "CLI_ERROR",
31
+ } as const;
32
+
33
+ type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
34
+
35
+ // HTTP status codes for error codes
36
+ const errorStatusMap: Record<ErrorCode, number> = {
37
+ BAD_REQUEST: 400,
38
+ UNAUTHORIZED: 401,
39
+ FORBIDDEN: 403,
40
+ NOT_FOUND: 404,
41
+ METHOD_NOT_ALLOWED: 405,
42
+ CONFLICT: 409,
43
+ VALIDATION_ERROR: 422,
44
+ INTERNAL_ERROR: 500,
45
+ NOT_IMPLEMENTED: 501,
46
+ SERVICE_UNAVAILABLE: 503,
47
+ TIMEOUT: 504,
48
+ SESSION_NOT_FOUND: 404,
49
+ SESSION_ALREADY_RUNNING: 409,
50
+ PROVIDER_NOT_AVAILABLE: 503,
51
+ CLI_ERROR: 500,
52
+ };
53
+
54
+ /**
55
+ * Custom API error class
56
+ */
57
+ export class LokiApiError extends Error {
58
+ code: ErrorCode;
59
+ status: number;
60
+ details?: Record<string, unknown>;
61
+
62
+ constructor(
63
+ message: string,
64
+ code: ErrorCode,
65
+ details?: Record<string, unknown>
66
+ ) {
67
+ super(message);
68
+ this.name = "LokiApiError";
69
+ this.code = code;
70
+ this.status = errorStatusMap[code] || 500;
71
+ this.details = details;
72
+ }
73
+
74
+ toJSON(): ApiError {
75
+ return {
76
+ error: this.message,
77
+ code: this.code,
78
+ details: this.details,
79
+ };
80
+ }
81
+
82
+ toResponse(): Response {
83
+ return new Response(JSON.stringify(this.toJSON()), {
84
+ status: this.status,
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ },
88
+ });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Error middleware wrapper
94
+ */
95
+ export function errorMiddleware(
96
+ handler: (req: Request) => Promise<Response> | Response
97
+ ): (req: Request) => Promise<Response> {
98
+ return async (req: Request): Promise<Response> => {
99
+ try {
100
+ return await handler(req);
101
+ } catch (err) {
102
+ return handleError(err, req);
103
+ }
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Handle an error and return appropriate response
109
+ */
110
+ export function handleError(err: unknown, req?: Request): Response {
111
+ // Log error for debugging
112
+ const requestInfo = req
113
+ ? `${req.method} ${new URL(req.url).pathname}`
114
+ : "unknown request";
115
+
116
+ console.error(`Error handling ${requestInfo}:`, err);
117
+
118
+ // Handle known API errors
119
+ if (err instanceof LokiApiError) {
120
+ return err.toResponse();
121
+ }
122
+
123
+ // Handle Deno-specific errors
124
+ if (err instanceof Deno.errors.NotFound) {
125
+ return new LokiApiError("Resource not found", ErrorCodes.NOT_FOUND).toResponse();
126
+ }
127
+
128
+ if (err instanceof Deno.errors.PermissionDenied) {
129
+ return new LokiApiError(
130
+ "Permission denied",
131
+ ErrorCodes.FORBIDDEN
132
+ ).toResponse();
133
+ }
134
+
135
+ // Handle JSON parsing errors
136
+ if (err instanceof SyntaxError && err.message.includes("JSON")) {
137
+ return new LokiApiError(
138
+ "Invalid JSON in request body",
139
+ ErrorCodes.BAD_REQUEST
140
+ ).toResponse();
141
+ }
142
+
143
+ // Handle timeout errors
144
+ if (err instanceof Error && err.name === "AbortError") {
145
+ return new LokiApiError(
146
+ "Request timed out",
147
+ ErrorCodes.TIMEOUT
148
+ ).toResponse();
149
+ }
150
+
151
+ // Default to internal error
152
+ const message =
153
+ err instanceof Error ? err.message : "An unexpected error occurred";
154
+
155
+ return new LokiApiError(
156
+ message,
157
+ ErrorCodes.INTERNAL_ERROR,
158
+ Deno.env.get("LOKI_DEBUG") ? { stack: (err as Error).stack } : undefined
159
+ ).toResponse();
160
+ }
161
+
162
+ /**
163
+ * Validate request body against expected fields
164
+ */
165
+ export function validateBody<T extends Record<string, unknown>>(
166
+ body: unknown,
167
+ required: (keyof T)[],
168
+ optional: (keyof T)[] = []
169
+ ): T {
170
+ if (!body || typeof body !== "object") {
171
+ throw new LokiApiError(
172
+ "Request body must be a JSON object",
173
+ ErrorCodes.BAD_REQUEST
174
+ );
175
+ }
176
+
177
+ const obj = body as Record<string, unknown>;
178
+
179
+ // Check required fields
180
+ for (const field of required) {
181
+ if (!(field in obj)) {
182
+ throw new LokiApiError(
183
+ `Missing required field: ${String(field)}`,
184
+ ErrorCodes.VALIDATION_ERROR,
185
+ { field: String(field) }
186
+ );
187
+ }
188
+ }
189
+
190
+ // Check for unknown fields
191
+ const allowedFields = new Set([...required, ...optional]);
192
+ for (const field of Object.keys(obj)) {
193
+ if (!allowedFields.has(field)) {
194
+ throw new LokiApiError(
195
+ `Unknown field: ${field}`,
196
+ ErrorCodes.VALIDATION_ERROR,
197
+ { field }
198
+ );
199
+ }
200
+ }
201
+
202
+ return obj as T;
203
+ }
204
+
205
+ /**
206
+ * Create a simple error response
207
+ */
208
+ export function errorResponse(
209
+ message: string,
210
+ code: ErrorCode = ErrorCodes.INTERNAL_ERROR,
211
+ details?: Record<string, unknown>
212
+ ): Response {
213
+ return new LokiApiError(message, code, details).toResponse();
214
+ }
215
+
216
+ /**
217
+ * Create a success response
218
+ */
219
+ export function successResponse<T>(data: T, status = 200): Response {
220
+ return new Response(JSON.stringify(data), {
221
+ status,
222
+ headers: {
223
+ "Content-Type": "application/json",
224
+ },
225
+ });
226
+ }
package/api/mod.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Loki Mode API Module
3
+ *
4
+ * Exports for programmatic use
5
+ */
6
+
7
+ // Types
8
+ export type {
9
+ Session,
10
+ SessionStatus,
11
+ Phase,
12
+ Task,
13
+ TaskStatus,
14
+ StartSessionRequest,
15
+ StartSessionResponse,
16
+ SessionStatusResponse,
17
+ TaskSummary,
18
+ AgentSummary,
19
+ InjectInputRequest,
20
+ ApiError,
21
+ HealthResponse,
22
+ } from "./types/api.ts";
23
+
24
+ export type {
25
+ SSEEvent,
26
+ EventType,
27
+ EventFilter,
28
+ SessionEventData,
29
+ PhaseEventData,
30
+ TaskEventData,
31
+ AgentEventData,
32
+ LogEventData,
33
+ MetricsEventData,
34
+ InputRequestedEventData,
35
+ HeartbeatEventData,
36
+ AnySSEEvent,
37
+ } from "./types/events.ts";
38
+
39
+ // Services
40
+ export { eventBus } from "./services/event-bus.ts";
41
+ export { cliBridge } from "./services/cli-bridge.ts";
42
+ export { stateWatcher } from "./services/state-watcher.ts";
43
+
44
+ // Middleware
45
+ export { authMiddleware, configureAuth, generateToken } from "./middleware/auth.ts";
46
+ export { corsMiddleware, configureCors } from "./middleware/cors.ts";
47
+ export {
48
+ errorMiddleware,
49
+ LokiApiError,
50
+ ErrorCodes,
51
+ handleError,
52
+ validateBody,
53
+ errorResponse,
54
+ successResponse,
55
+ } from "./middleware/error.ts";
56
+
57
+ // Server
58
+ export { createHandler, routeRequest, parseArgs } from "./server.ts";