@joshski/dust 0.1.101 → 0.1.102

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.
@@ -10,11 +10,13 @@
10
10
  * - DEBUG: Pattern for stdout debug logging (comma-separated wildcards)
11
11
  * - DUST_LOG_DIR: Override default log directory location
12
12
  * - DUST_LOG_FILE: Inherited log file path for child processes
13
+ * - DUST_LOG_FORMAT: Output format ('json' for JSON Lines, 'text' for human-readable)
13
14
  */
14
15
  export interface LoggingConfig {
15
16
  debug: string | undefined;
16
17
  logDir: string | undefined;
17
18
  logFile: string | undefined;
19
+ logFormat: 'json' | 'text' | undefined;
18
20
  }
19
21
  /**
20
22
  * Dustbucket connection configuration.
@@ -48,7 +48,7 @@ function createFileSystemEmulator(tree = {}, flatFiles) {
48
48
  creationTimes.set(path, nextCreationTime++);
49
49
  }
50
50
  return {
51
- exists: (path) => paths.has(path),
51
+ exists: paths.has.bind(paths),
52
52
  isDirectory: (path) => paths.has(path) && !files.has(path),
53
53
  readFile: async (path) => {
54
54
  if (!files.has(path)) {
@@ -46,8 +46,17 @@
46
46
  * No external dependencies.
47
47
  */
48
48
  import type { LoggingConfig } from '../env-config';
49
+ import { type LogEntry } from './match';
49
50
  import { type LogSink } from './sink';
50
- export type LogFn = (...messages: unknown[]) => void;
51
+ export type { LogEntry };
52
+ /**
53
+ * Optional context object for structured logging.
54
+ * Fields are passed through to JSON output as-is.
55
+ */
56
+ export interface LogContext {
57
+ [key: string]: unknown;
58
+ }
59
+ export type LogFn = (message: string, context?: LogContext) => void;
51
60
  export interface LoggerOptions {
52
61
  /**
53
62
  * Per-logger file routing override.
@@ -4,6 +4,18 @@
4
4
  * Parses a DEBUG-style string (comma-separated, `*` wildcards)
5
5
  * and tests logger names against it. No side effects.
6
6
  */
7
+ /**
8
+ * Structured log entry for JSON output.
9
+ * Required fields: ts, logger, level, msg
10
+ * Additional context fields are passed through as-is.
11
+ */
12
+ export interface LogEntry {
13
+ ts: string;
14
+ logger: string;
15
+ level: 'info';
16
+ msg: string;
17
+ [key: string]: unknown;
18
+ }
7
19
  /**
8
20
  * Parse a DEBUG expression string into an array of RegExp matchers.
9
21
  * Returns an empty array when the input is empty or undefined.
@@ -14,6 +26,14 @@ export declare function parsePatterns(debug: string | undefined): RegExp[];
14
26
  */
15
27
  export declare function matchesAny(name: string, patterns: RegExp[]): boolean;
16
28
  /**
17
- * Format a log line with ISO timestamp and logger name.
29
+ * Format a log line with ISO timestamp and logger name (text format).
18
30
  */
19
31
  export declare function formatLine(name: string, messages: unknown[]): string;
32
+ /**
33
+ * Format a log entry as a JSON line (JSON Lines format).
34
+ */
35
+ export declare function formatJsonLine(entry: LogEntry): string;
36
+ /**
37
+ * Create a LogEntry from logger name, message, and optional context.
38
+ */
39
+ export declare function createLogEntry(name: string, message: string, context?: Record<string, unknown>): LogEntry;
package/dist/logging.js CHANGED
@@ -20,6 +20,19 @@ function formatLine(name, messages) {
20
20
  return `${new Date().toISOString()} [${name}] ${text}
21
21
  `;
22
22
  }
23
+ function formatJsonLine(entry) {
24
+ return JSON.stringify(entry) + `
25
+ `;
26
+ }
27
+ function createLogEntry(name, message, context) {
28
+ return {
29
+ ts: new Date().toISOString(),
30
+ logger: name,
31
+ level: "info",
32
+ msg: message,
33
+ ...context
34
+ };
35
+ }
23
36
 
24
37
  // lib/logging/sink.ts
25
38
  import { appendFileSync, mkdirSync } from "node:fs";
@@ -101,9 +114,10 @@ function createLoggingService(options) {
101
114
  } else if (typeof loggerOptions?.file === "string") {
102
115
  perLoggerSink = getOrCreateFileSink(loggerOptions.file);
103
116
  }
104
- return (...messages) => {
117
+ const useJsonFormat = config.logFormat === "json";
118
+ return (message, context) => {
105
119
  init();
106
- const line = formatLine(name, messages);
120
+ const line = useJsonFormat ? formatJsonLine(createLogEntry(name, message, context)) : formatLine(name, [message, ...context ? [context] : []]);
107
121
  if (perLoggerSink !== undefined) {
108
122
  if (perLoggerSink !== null) {
109
123
  perLoggerSink.write(line);
@@ -126,7 +140,8 @@ var defaultService = createLoggingService({
126
140
  config: {
127
141
  debug: process.env.DEBUG,
128
142
  logDir: process.env.DUST_LOG_DIR,
129
- logFile: process.env.DUST_LOG_FILE
143
+ logFile: process.env.DUST_LOG_FILE,
144
+ logFormat: process.env.DUST_LOG_FORMAT === "json" ? "json" : process.env.DUST_LOG_FORMAT === "text" ? "text" : undefined
130
145
  }
131
146
  });
132
147
  var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
@@ -21,6 +21,7 @@
21
21
  * → Returns response to container
22
22
  * ```
23
23
  */
24
+ import { type HelperTokenState } from './helper-token';
24
25
  export interface ClaudeApiProxyDependencies {
25
26
  homedir: () => string;
26
27
  readFileSync: (path: string, encoding: 'utf-8') => string;
@@ -99,6 +100,31 @@ export interface ErrorResponse {
99
100
  * Build a 401 response for when no OAuth token is available.
100
101
  */
101
102
  export declare function buildNoTokenResponse(): ErrorResponse;
103
+ /**
104
+ * Build a 401 response for when the helper token is invalid or expired.
105
+ */
106
+ export declare function buildInvalidHelperTokenResponse(): ErrorResponse;
107
+ /**
108
+ * Extract the helper token from incoming request headers.
109
+ * Checks both Authorization header (Bearer token) and x-api-key header.
110
+ */
111
+ export declare function extractHelperToken(headers: Record<string, string | string[] | undefined>): string | null;
112
+ /**
113
+ * Validate the incoming helper token against the issued token.
114
+ */
115
+ export declare function validateHelperToken(incomingToken: string | null, state: HelperTokenState, now?: number): boolean;
116
+ /**
117
+ * Get the current helper token, rotating if needed.
118
+ * Returns the new state and the token string.
119
+ */
120
+ export declare function getOrRefreshHelperToken(state: HelperTokenState, now?: number): {
121
+ state: HelperTokenState;
122
+ token: string;
123
+ };
124
+ /**
125
+ * Build a success response containing the helper token.
126
+ */
127
+ export declare function buildTokenResponse(token: string): ProxyResponse;
102
128
  /**
103
129
  * Build a 502 response for when the upstream request fails.
104
130
  */
@@ -106,11 +132,18 @@ export declare function buildUpstreamErrorResponse(error: unknown): ErrorRespons
106
132
  /**
107
133
  * Handle a proxy request and return a platform-agnostic response.
108
134
  * This is the pure core of the proxy logic, separated from HTTP plumbing.
135
+ *
136
+ * When helperTokenState is provided, incoming requests must include a valid
137
+ * helper token in the Authorization or x-api-key header.
109
138
  */
110
- export declare function handleProxyRequest(request: ProxyRequest, dependencies: ClaudeApiProxyDependencies): Promise<ProxyResponse>;
139
+ export declare function handleProxyRequest(request: ProxyRequest, dependencies: ClaudeApiProxyDependencies, helperTokenState?: HelperTokenState, now?: number): Promise<ProxyResponse>;
111
140
  /**
112
141
  * Creates a Claude API proxy server.
113
142
  * The server accepts HTTP requests and forwards them to the Anthropic API
114
143
  * with the OAuth token injected.
144
+ *
145
+ * The server maintains helper token state and provides a `/token` endpoint
146
+ * that returns the current helper token. All other requests must include
147
+ * a valid helper token in the Authorization or x-api-key header.
115
148
  */
116
149
  export declare function createClaudeApiProxyServer(dependencies?: ClaudeApiProxyDependencies): Promise<ClaudeApiProxyServer>;
@@ -20,6 +20,8 @@
20
20
  import type { spawn as nodeSpawn } from 'node:child_process';
21
21
  export interface GitCredentialProxyDependencies {
22
22
  spawn: typeof nodeSpawn;
23
+ /** Real user HOME directory — used when the process HOME has been overridden */
24
+ userHome?: string;
23
25
  }
24
26
  export interface GitCredentials {
25
27
  username: string;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Helper Token Module
3
+ *
4
+ * Pure functional module for generating and validating short-TTL helper tokens.
5
+ * These tokens let containerized Claude Code authenticate with the host OAuth gateway.
6
+ *
7
+ * When running Claude Code in Docker containers, the real OAuth token should never
8
+ * enter the container environment. Instead, the container fetches a synthetic
9
+ * "helper token" from the host gateway. The gateway validates this helper token
10
+ * before injecting the real OAuth token upstream.
11
+ */
12
+ /**
13
+ * Time-to-live for helper tokens in milliseconds.
14
+ * Tokens expire after this duration and must be regenerated.
15
+ */
16
+ export declare const HELPER_TOKEN_TTL_MS = 60000;
17
+ /**
18
+ * Represents a generated helper token with its issue time.
19
+ */
20
+ export interface HelperToken {
21
+ token: string;
22
+ issuedAt: number;
23
+ }
24
+ /**
25
+ * State object tracking the current helper token.
26
+ */
27
+ export interface HelperTokenState {
28
+ current: HelperToken | null;
29
+ }
30
+ /**
31
+ * Generate a synthetic helper token that mimics the Claude API key format.
32
+ * The token is cryptographically random and follows the pattern: sk-ant-api03-...
33
+ *
34
+ * @param now - Current timestamp in milliseconds (defaults to Date.now())
35
+ * @returns A HelperToken containing the token string and issue time
36
+ */
37
+ export declare function generateHelperToken(now?: number): HelperToken;
38
+ /**
39
+ * Check if a token matches the issued token and is within its TTL.
40
+ *
41
+ * @param token - The token string to validate
42
+ * @param issued - The HelperToken that was issued
43
+ * @param now - Current timestamp in milliseconds (defaults to Date.now())
44
+ * @param ttlMs - Time-to-live in milliseconds (defaults to HELPER_TOKEN_TTL_MS)
45
+ * @returns true if the token is valid, false otherwise
46
+ */
47
+ export declare function isHelperTokenValid(token: string, issued: HelperToken, now?: number, ttlMs?: number): boolean;
48
+ /**
49
+ * Create a new helper token state object.
50
+ * The state starts with no current token.
51
+ *
52
+ * @returns A new HelperTokenState with null current token
53
+ */
54
+ export declare function createHelperTokenState(): HelperTokenState;
55
+ /**
56
+ * Rotate the helper token state by generating a new token.
57
+ * Returns a new state object with the new token (immutable).
58
+ *
59
+ * @param state - The current helper token state
60
+ * @param now - Current timestamp in milliseconds (defaults to Date.now())
61
+ * @returns A new HelperTokenState with a fresh token
62
+ */
63
+ export declare function rotateHelperToken(state: HelperTokenState, now?: number): HelperTokenState;
64
+ /**
65
+ * Check if the current helper token in state is valid.
66
+ * Returns false if there is no current token.
67
+ *
68
+ * @param state - The helper token state to check
69
+ * @param now - Current timestamp in milliseconds (defaults to Date.now())
70
+ * @param ttlMs - Time-to-live in milliseconds (defaults to HELPER_TOKEN_TTL_MS)
71
+ * @returns true if the current token exists and is within TTL
72
+ */
73
+ export declare function isCurrentTokenValid(state: HelperTokenState, now?: number, ttlMs?: number): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.101",
3
+ "version": "0.1.102",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {