@autokap/core 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.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @autokap/core
2
+
3
+ Shared core library consumed by the bundled `autokap` CLI binary and the
4
+ `@autokap/mcp` server. **You probably do not need to install this package
5
+ directly** — install `@autokap/mcp` (for IDE agents) or `autokap` (for
6
+ CI / Cloud Run) and let them pull `@autokap/core` in transitively.
7
+
8
+ The package exists to keep request shaping, schema validation, secret
9
+ redaction, and the SSRF-aware config layer in a single source of truth
10
+ across the two consumers.
11
+
12
+ ## Entry points
13
+
14
+ ```ts
15
+ import { /* ... */ } from "@autokap/core"; // re-exports everything below
16
+ import { /* ... */ } from "@autokap/core/config"; // ~/.autokap/config.json, SSRF guards
17
+ import { /* ... */ } from "@autokap/core/api-client"; // apiCall(), validateApiKey(), ApiClientError
18
+ import { /* ... */ } from "@autokap/core/endpoint-helpers"; // buildEndpointAssetUrl, toCsv
19
+ import { /* ... */ } from "@autokap/core/types"; // ValidateResponse, AutokapConfig, CheckResult
20
+ import { /* ... */ } from "@autokap/core/logger"; // createStderrLogger, redactString, redactValue
21
+ ```
22
+
23
+ ## What's in here
24
+
25
+ - **Config** — atomic-write `~/.autokap/config.json` with `chmod 0600` on
26
+ POSIX. The same file is read by the CLI and the MCP server so users
27
+ authenticate once.
28
+ - **SSRF / origin guard** (`validatePublicHttpUrl`) — rejects non-http(s),
29
+ loopback, private IPv4 (including obfuscated forms — `0177.0.0.1`,
30
+ `2130706433`, hex), `.local` / `.internal`, IPv6 ULA / link-local /
31
+ IPv4-mapped / 6to4 / NAT64.
32
+ - **API client** — typed `apiCall<T>()` wrapping `fetch` with bearer auth,
33
+ retry on 429 / 5xx (idempotent methods only), `Retry-After` honoring,
34
+ error envelope unpacking, and uniform secret redaction in error bodies.
35
+ - **Logger** — stderr-only logger (stdout is reserved for the MCP
36
+ transport) plus `redactString` / `redactValue` helpers that scrub
37
+ obvious tokens (`ak_cli_…`, `ak_run_…`, bearer headers) from any string
38
+ before it lands in an error message or log line.
39
+
40
+ ## Versioning
41
+
42
+ `@autokap/core` is pinned **exactly** by `@autokap/mcp` and `autokap`.
43
+ When you bump it, bump the consumers' manifests in the same commit so
44
+ the dependency graph stays internally consistent. See the release
45
+ sequence in the root `CHANGELOG.md`.
@@ -0,0 +1,53 @@
1
+ import type { ValidateResponse } from './types.js';
2
+ export interface ValidateOptions {
3
+ apiKey: string;
4
+ apiBaseUrl: string;
5
+ signal?: AbortSignal;
6
+ timeoutMs?: number;
7
+ }
8
+ export declare function validateApiKey(options: ValidateOptions): Promise<ValidateResponse>;
9
+ export declare class ApiClientError extends Error {
10
+ readonly status: number;
11
+ readonly code?: string;
12
+ readonly params?: Record<string, unknown>;
13
+ readonly responseBody?: unknown;
14
+ constructor(message: string, status: number, options?: {
15
+ code?: string;
16
+ params?: Record<string, unknown>;
17
+ responseBody?: unknown;
18
+ cause?: unknown;
19
+ });
20
+ }
21
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
22
+ export interface ApiCallContext {
23
+ apiKey: string;
24
+ apiBaseUrl: string;
25
+ }
26
+ export interface ApiCallOptions<TBody = unknown> {
27
+ method?: HttpMethod;
28
+ body?: TBody;
29
+ searchParams?: Record<string, string | number | boolean | undefined | null>;
30
+ headers?: Record<string, string>;
31
+ signal?: AbortSignal;
32
+ timeoutMs?: number;
33
+ /** When true, returns the raw response text instead of parsing JSON. */
34
+ asText?: boolean;
35
+ /**
36
+ * Number of retry attempts after a transient failure (5xx, 429, or network
37
+ * error). Only applied to idempotent methods (GET, DELETE) by default; pass
38
+ * `idempotent: true` to force retries on POST/PATCH/PUT.
39
+ */
40
+ retries?: number;
41
+ idempotent?: boolean;
42
+ }
43
+ /**
44
+ * Generic AutoKap API helper used by every MCP tool. Handles auth, URL
45
+ * construction, error envelope unpacking (`{ error, code, params }`), retries
46
+ * on transient failures, and the 204 / empty-body cases.
47
+ *
48
+ * Non-JSON 200 responses are surfaced as ApiClientError unless `asText: true`
49
+ * — we never silently return text-as-T because that hides server contract
50
+ * drift from typed callers.
51
+ */
52
+ export declare function apiCall<TResult = unknown, TBody = unknown>(context: ApiCallContext, path: string, options?: ApiCallOptions<TBody>): Promise<TResult>;
53
+ export declare function buildApiUrl(apiBaseUrl: string, path: string, searchParams?: Record<string, string | number | boolean | undefined | null>): string;
@@ -0,0 +1,211 @@
1
+ import { validateServerOrigin } from './config.js';
2
+ import { redactString, redactValue } from './logger.js';
3
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
4
+ const DEFAULT_RETRY_ATTEMPTS = 1;
5
+ export async function validateApiKey(options) {
6
+ const { apiKey, apiBaseUrl, signal, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = options;
7
+ validateServerOrigin(apiBaseUrl);
8
+ const url = `${apiBaseUrl.replace(/\/+$/, '')}/api/cli/validate`;
9
+ let response;
10
+ try {
11
+ response = await fetch(url, {
12
+ method: 'GET',
13
+ headers: { Authorization: `Bearer ${apiKey}` },
14
+ signal: signal ?? AbortSignal.timeout(timeoutMs),
15
+ });
16
+ }
17
+ catch (err) {
18
+ throw wrapNetworkError(err, 'GET', '/api/cli/validate');
19
+ }
20
+ if (response.status === 401 || response.status === 403) {
21
+ return { valid: false };
22
+ }
23
+ if (!response.ok) {
24
+ throw new ApiClientError(`Validation endpoint returned HTTP ${response.status}`, response.status);
25
+ }
26
+ const data = (await response.json());
27
+ return data;
28
+ }
29
+ export class ApiClientError extends Error {
30
+ status;
31
+ code;
32
+ params;
33
+ responseBody;
34
+ constructor(message, status, options = {}) {
35
+ super(message, { cause: options.cause });
36
+ this.name = 'ApiClientError';
37
+ this.status = status;
38
+ this.code = options.code;
39
+ this.params = options.params;
40
+ this.responseBody = options.responseBody;
41
+ }
42
+ }
43
+ const IDEMPOTENT_METHODS = new Set(['GET', 'DELETE']);
44
+ /**
45
+ * Generic AutoKap API helper used by every MCP tool. Handles auth, URL
46
+ * construction, error envelope unpacking (`{ error, code, params }`), retries
47
+ * on transient failures, and the 204 / empty-body cases.
48
+ *
49
+ * Non-JSON 200 responses are surfaced as ApiClientError unless `asText: true`
50
+ * — we never silently return text-as-T because that hides server contract
51
+ * drift from typed callers.
52
+ */
53
+ export async function apiCall(context, path, options = {}) {
54
+ const { method = 'GET', body, searchParams, headers, signal, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, asText = false, retries = DEFAULT_RETRY_ATTEMPTS, idempotent, } = options;
55
+ validateServerOrigin(context.apiBaseUrl);
56
+ const url = buildApiUrl(context.apiBaseUrl, path, searchParams);
57
+ const finalHeaders = {
58
+ Authorization: `Bearer ${context.apiKey}`,
59
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
60
+ ...(headers ?? {}),
61
+ };
62
+ const maxAttempts = Math.max(1, (idempotent ?? IDEMPOTENT_METHODS.has(method)) ? retries + 1 : 1);
63
+ let lastError;
64
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
65
+ let response;
66
+ try {
67
+ response = await fetch(url, {
68
+ method,
69
+ headers: finalHeaders,
70
+ body: body !== undefined ? JSON.stringify(body) : undefined,
71
+ signal: signal ?? AbortSignal.timeout(timeoutMs),
72
+ });
73
+ }
74
+ catch (err) {
75
+ lastError = wrapNetworkError(err, method, path);
76
+ if (attempt < maxAttempts - 1) {
77
+ await sleep(backoffMs(attempt));
78
+ continue;
79
+ }
80
+ throw lastError;
81
+ }
82
+ if (response.status === 204) {
83
+ return null;
84
+ }
85
+ if (!response.ok) {
86
+ const apiError = await buildApiClientError(response, method, path);
87
+ // Retry on 429 / 5xx if attempts remain and the method is idempotent.
88
+ const transient = response.status === 429 || response.status >= 500;
89
+ if (transient && attempt < maxAttempts - 1) {
90
+ lastError = apiError;
91
+ await sleep(retryAfterMs(response) ?? backoffMs(attempt));
92
+ continue;
93
+ }
94
+ throw apiError;
95
+ }
96
+ if (asText) {
97
+ return (await response.text());
98
+ }
99
+ const contentLength = response.headers.get('content-length');
100
+ if (contentLength === '0') {
101
+ return null;
102
+ }
103
+ const contentType = response.headers.get('content-type') ?? '';
104
+ if (!contentType.toLowerCase().includes('json')) {
105
+ const text = await response.text();
106
+ if (text.trim().length === 0) {
107
+ return null;
108
+ }
109
+ throw new ApiClientError(`Expected JSON response from ${method} ${path}, got content-type "${contentType || '(none)'}"`, response.status, { responseBody: text.slice(0, 200) });
110
+ }
111
+ return (await response.json());
112
+ }
113
+ // Unreachable — the loop either returns or throws — but TS doesn't know that.
114
+ throw lastError ?? new ApiClientError('apiCall exhausted retries', 0);
115
+ }
116
+ async function buildApiClientError(response, method, path) {
117
+ let errorBody = undefined;
118
+ let errorMessage = `Request ${method} ${path} failed with HTTP ${response.status}`;
119
+ let errorCode;
120
+ let errorParams;
121
+ try {
122
+ const text = await response.text();
123
+ if (text.trim().length > 0) {
124
+ try {
125
+ errorBody = JSON.parse(text);
126
+ if (errorBody && typeof errorBody === 'object') {
127
+ const envelope = errorBody;
128
+ if (typeof envelope.error === 'string' && envelope.error.length > 0) {
129
+ errorMessage = envelope.error;
130
+ }
131
+ if (typeof envelope.code === 'string') {
132
+ errorCode = envelope.code;
133
+ }
134
+ if (envelope.params && typeof envelope.params === 'object') {
135
+ errorParams = envelope.params;
136
+ }
137
+ }
138
+ }
139
+ catch {
140
+ errorBody = text;
141
+ errorMessage = `${errorMessage}: ${text.slice(0, 200)}`;
142
+ }
143
+ }
144
+ }
145
+ catch {
146
+ // ignore body read failures
147
+ }
148
+ // Always redact before constructing the error envelope. The body and the
149
+ // params/message frequently land in MCP tool error responses and IDE log
150
+ // panes; a server response that echoes the bearer or includes the API key
151
+ // in a stack trace would otherwise leak straight to the agent / user.
152
+ return new ApiClientError(redactString(errorMessage), response.status, {
153
+ code: errorCode,
154
+ params: errorParams ? redactValue(errorParams) : undefined,
155
+ responseBody: errorBody !== undefined ? redactValue(errorBody) : undefined,
156
+ });
157
+ }
158
+ function wrapNetworkError(err, method, path) {
159
+ if (err instanceof ApiClientError)
160
+ return err;
161
+ const cause = err?.cause;
162
+ const causeCode = cause && typeof cause === 'object' && 'code' in cause ? cause.code : undefined;
163
+ const rawDetail = causeCode ? String(causeCode) : err.message ?? 'unknown network failure';
164
+ // Run the detail through the redactor before concatenating. Node fetch
165
+ // errors usually carry just an ENOTFOUND / ECONNREFUSED code, but a
166
+ // custom fetch polyfill (or a user-supplied URL containing inline
167
+ // credentials like `https://u:p@host/`) can echo the bearer or the
168
+ // basic-auth secret into the error message. Without redaction, those
169
+ // strings would land in the MCP tool error envelope the agent sees.
170
+ const detail = redactString(rawDetail);
171
+ return new ApiClientError(`Network failure on ${method} ${path}: ${detail}`, 0, { code: 'network_error', cause: err });
172
+ }
173
+ function sleep(ms) {
174
+ return new Promise((resolve) => setTimeout(resolve, ms));
175
+ }
176
+ function backoffMs(attempt) {
177
+ // 250ms * 2^attempt + 0–250ms jitter — keeps the second try inside the 1s
178
+ // ballpark and the third inside ~2s.
179
+ return 250 * 2 ** attempt + Math.floor(Math.random() * 250);
180
+ }
181
+ function retryAfterMs(response) {
182
+ const header = response.headers.get('retry-after');
183
+ if (!header)
184
+ return null;
185
+ const asInt = Number.parseInt(header, 10);
186
+ if (Number.isFinite(asInt) && asInt >= 0) {
187
+ return Math.min(asInt * 1000, 30_000);
188
+ }
189
+ const asDate = Date.parse(header);
190
+ if (Number.isFinite(asDate)) {
191
+ return Math.max(0, Math.min(asDate - Date.now(), 30_000));
192
+ }
193
+ return null;
194
+ }
195
+ export function buildApiUrl(apiBaseUrl, path, searchParams) {
196
+ const trimmedBase = apiBaseUrl.replace(/\/+$/, '');
197
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
198
+ const url = new URL(`${trimmedBase}${normalizedPath}`);
199
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
200
+ throw new Error(`Refusing to build API URL with unsupported scheme "${url.protocol}"; only http(s) are allowed.`);
201
+ }
202
+ if (searchParams) {
203
+ for (const [key, value] of Object.entries(searchParams)) {
204
+ if (value === undefined || value === null)
205
+ continue;
206
+ url.searchParams.set(key, String(value));
207
+ }
208
+ }
209
+ return url.toString();
210
+ }
211
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1,37 @@
1
+ import type { AutokapConfig } from './types.js';
2
+ declare const DEFAULT_API_BASE_URL = "https://autokap.app";
3
+ declare const DEFAULT_WS_URL = "wss://autokap.app/ws";
4
+ declare const LOCAL_API_BASE_URL = "http://localhost:3000";
5
+ declare const API_KEY_ENV_VAR = "AUTOKAP_API_KEY";
6
+ declare const RUN_TOKEN_ENV_VAR = "AUTOKAP_RUN_TOKEN";
7
+ declare const API_BASE_URL_ENV_VAR = "AUTOKAP_API_BASE_URL";
8
+ declare const WS_URL_ENV_VAR = "AUTOKAP_WS_URL";
9
+ declare const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = "AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN";
10
+ export declare function getConfigDir(): string;
11
+ export declare function getConfigPath(): string;
12
+ export declare function getDefaultApiBaseUrl(): string;
13
+ export declare function getDefaultWsUrl(apiBaseUrl?: string): string;
14
+ export declare function readConfig(): Promise<AutokapConfig | null>;
15
+ export declare function writeConfig(config: AutokapConfig): Promise<void>;
16
+ export declare function deleteConfig(): Promise<void>;
17
+ declare function assertAllowedApiOrigin(candidateUrl: string, baselineUrl: string, envVar?: string): void;
18
+ /**
19
+ * SSRF guard for any caller-supplied URL that the AutoKap stack will
20
+ * eventually fetch (MCP tool inputs — apiBaseUrl, proxyUrl, webhookUrl,
21
+ * loginUrl, project baseUrl, etc.).
22
+ *
23
+ * Rejects:
24
+ * - non-http(s) schemes (file://, javascript:, gopher://, data:, ...)
25
+ * - localhost / 127.0.0.0/8 / private IPv4 / link-local / IPv6 loopback
26
+ * - hostnames ending in `.local`, `.internal`
27
+ *
28
+ * When `AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN=1`, the private-host check is
29
+ * relaxed for dev workflows. The scheme check is NEVER relaxed.
30
+ *
31
+ * `validatePublicHttpUrl` is the public alias used by MCP tools that accept
32
+ * user-provided URLs; `validateServerOrigin` is the alias used by core API
33
+ * client paths.
34
+ */
35
+ export declare function validateServerOrigin(candidateUrl: string): void;
36
+ export declare function validatePublicHttpUrl(candidateUrl: string, fieldLabel?: string): void;
37
+ export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, assertAllowedApiOrigin, };
package/dist/config.js ADDED
@@ -0,0 +1,404 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import fs from 'node:fs/promises';
4
+ import crypto from 'node:crypto';
5
+ const DEFAULT_API_BASE_URL = 'https://autokap.app';
6
+ const DEFAULT_WS_URL = 'wss://autokap.app/ws';
7
+ const LOCAL_API_BASE_URL = 'http://localhost:3000';
8
+ const API_KEY_ENV_VAR = 'AUTOKAP_API_KEY';
9
+ const RUN_TOKEN_ENV_VAR = 'AUTOKAP_RUN_TOKEN';
10
+ const API_BASE_URL_ENV_VAR = 'AUTOKAP_API_BASE_URL';
11
+ const WS_URL_ENV_VAR = 'AUTOKAP_WS_URL';
12
+ const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = 'AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN';
13
+ export function getConfigDir() {
14
+ return path.join(os.homedir(), '.autokap');
15
+ }
16
+ export function getConfigPath() {
17
+ return path.join(getConfigDir(), 'config.json');
18
+ }
19
+ export function getDefaultApiBaseUrl() {
20
+ return normalizeUrl(process.env[API_BASE_URL_ENV_VAR]) ?? DEFAULT_API_BASE_URL;
21
+ }
22
+ export function getDefaultWsUrl(apiBaseUrl = getDefaultApiBaseUrl()) {
23
+ return normalizeUrl(process.env[WS_URL_ENV_VAR]) ?? deriveWsUrl(apiBaseUrl);
24
+ }
25
+ export async function readConfig() {
26
+ const envRunToken = normalizeApiKey(process.env[RUN_TOKEN_ENV_VAR]);
27
+ if (envRunToken) {
28
+ const apiBaseUrl = getDefaultApiBaseUrl();
29
+ const wsUrl = getDefaultWsUrl(apiBaseUrl);
30
+ assertAllowedApiOrigin(apiBaseUrl, DEFAULT_API_BASE_URL, API_BASE_URL_ENV_VAR);
31
+ return { apiKey: envRunToken, apiBaseUrl, wsUrl };
32
+ }
33
+ const envApiKey = normalizeApiKey(process.env[API_KEY_ENV_VAR]);
34
+ if (envApiKey) {
35
+ const apiBaseUrl = getDefaultApiBaseUrl();
36
+ const wsUrl = getDefaultWsUrl(apiBaseUrl);
37
+ assertAllowedApiOrigin(apiBaseUrl, DEFAULT_API_BASE_URL, API_BASE_URL_ENV_VAR);
38
+ return { apiKey: envApiKey, apiBaseUrl, wsUrl };
39
+ }
40
+ // Only swallow IO / parse failures. Origin-allowlist / schema errors must
41
+ // bubble up so the user sees what's wrong instead of "not authenticated".
42
+ let raw;
43
+ try {
44
+ raw = await fs.readFile(getConfigPath(), 'utf-8');
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(raw);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ if (!parsed || typeof parsed !== 'object')
57
+ return null;
58
+ const record = parsed;
59
+ if (typeof record.apiKey !== 'string' || record.apiKey.length === 0)
60
+ return null;
61
+ const storedApiBaseUrlRaw = typeof record.apiBaseUrl === 'string' ? record.apiBaseUrl : null;
62
+ const storedWsUrlRaw = typeof record.wsUrl === 'string' ? record.wsUrl : null;
63
+ const envApiBaseUrl = normalizeUrl(process.env[API_BASE_URL_ENV_VAR]);
64
+ const envWsUrl = normalizeUrl(process.env[WS_URL_ENV_VAR]);
65
+ const storedApiBaseUrl = normalizeUrl(storedApiBaseUrlRaw) ?? DEFAULT_API_BASE_URL;
66
+ const apiBaseUrl = envApiBaseUrl ?? storedApiBaseUrl;
67
+ const wsUrl = envWsUrl ??
68
+ (envApiBaseUrl
69
+ ? deriveWsUrl(apiBaseUrl)
70
+ : normalizeUrl(storedWsUrlRaw) ?? deriveWsUrl(apiBaseUrl));
71
+ assertAllowedApiOrigin(apiBaseUrl, storedApiBaseUrl, envApiBaseUrl ? API_BASE_URL_ENV_VAR : undefined);
72
+ if (envWsUrl) {
73
+ assertAllowedApiOrigin(wsUrl.replace(/^ws/, 'http'), deriveWsUrl(storedApiBaseUrl).replace(/^ws/, 'http'), WS_URL_ENV_VAR);
74
+ }
75
+ return { apiKey: record.apiKey, apiBaseUrl, wsUrl };
76
+ }
77
+ export async function writeConfig(config) {
78
+ const dir = getConfigDir();
79
+ await fs.mkdir(dir, { recursive: true });
80
+ if (process.platform !== 'win32') {
81
+ try {
82
+ await fs.chmod(dir, 0o700);
83
+ }
84
+ catch {
85
+ // best-effort — the file's 0600 below is the real safety
86
+ }
87
+ }
88
+ const configPath = getConfigPath();
89
+ const normalizedConfig = {
90
+ ...config,
91
+ apiBaseUrl: normalizeUrl(config.apiBaseUrl) ?? DEFAULT_API_BASE_URL,
92
+ wsUrl: normalizeUrl(config.wsUrl) ?? deriveWsUrl(config.apiBaseUrl),
93
+ };
94
+ // Atomic write: write to a per-process tmp file (so concurrent writes don't
95
+ // clobber each other), chmod it BEFORE the rename (so the final file is
96
+ // never world-readable on POSIX), then rename. `fs.rename` is atomic on
97
+ // POSIX for paths on the same filesystem; Windows replaces the destination
98
+ // atomically as well when the source/dest are on the same volume.
99
+ //
100
+ // `mode: 0o600` is passed to writeFile so the tmp file is created with the
101
+ // tight permission set from the very first byte — closes the small window
102
+ // where umask alone would have produced 0o644.
103
+ const tmpPath = `${configPath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
104
+ try {
105
+ await fs.writeFile(tmpPath, JSON.stringify(normalizedConfig, null, 2), {
106
+ encoding: 'utf-8',
107
+ mode: 0o600,
108
+ });
109
+ if (process.platform !== 'win32') {
110
+ // Belt-and-suspenders: enforce 0600 even if the FS ignored the mode
111
+ // argument (some filesystems / Node versions do).
112
+ await fs.chmod(tmpPath, 0o600);
113
+ }
114
+ await fs.rename(tmpPath, configPath);
115
+ }
116
+ catch (err) {
117
+ await fs.rm(tmpPath, { force: true }).catch(() => undefined);
118
+ throw err;
119
+ }
120
+ }
121
+ export async function deleteConfig() {
122
+ try {
123
+ await fs.rm(getConfigPath());
124
+ }
125
+ catch {
126
+ // ignore missing file
127
+ }
128
+ }
129
+ function normalizeApiKey(value) {
130
+ const trimmed = value?.trim();
131
+ return trimmed || null;
132
+ }
133
+ function normalizeUrl(value) {
134
+ const trimmed = value?.trim();
135
+ if (!trimmed)
136
+ return null;
137
+ return trimmed.replace(/\/+$/, '');
138
+ }
139
+ function deriveWsUrl(apiBaseUrl) {
140
+ try {
141
+ const url = new URL(apiBaseUrl);
142
+ url.protocol =
143
+ url.protocol === 'https:' ? 'wss:' : url.protocol === 'http:' ? 'ws:' : url.protocol;
144
+ const pathname = url.pathname.replace(/\/+$/, '');
145
+ url.pathname = `${pathname || ''}/ws`;
146
+ url.search = '';
147
+ url.hash = '';
148
+ return url.toString().replace(/\/+$/, '');
149
+ }
150
+ catch {
151
+ return DEFAULT_WS_URL;
152
+ }
153
+ }
154
+ function normalizeOrigin(value) {
155
+ const normalized = normalizeUrl(value);
156
+ if (!normalized)
157
+ return null;
158
+ try {
159
+ const parsed = new URL(normalized);
160
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
161
+ return null;
162
+ return parsed.origin;
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ }
168
+ function isUnsafeOverrideEnabled() {
169
+ const raw = process.env[ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR]?.trim().toLowerCase();
170
+ return raw === '1' || raw === 'true';
171
+ }
172
+ function isTrustedOrigin(origin) {
173
+ return (origin === normalizeOrigin(DEFAULT_API_BASE_URL) ||
174
+ origin === normalizeOrigin(LOCAL_API_BASE_URL));
175
+ }
176
+ function assertAllowedApiOrigin(candidateUrl, baselineUrl, envVar) {
177
+ const candidateOrigin = normalizeOrigin(candidateUrl);
178
+ const baselineOrigin = normalizeOrigin(baselineUrl);
179
+ if (!candidateOrigin || !baselineOrigin) {
180
+ throw new Error('AutoKap config contains an invalid server origin');
181
+ }
182
+ if (candidateOrigin === baselineOrigin || isTrustedOrigin(candidateOrigin)) {
183
+ return;
184
+ }
185
+ if (isUnsafeOverrideEnabled()) {
186
+ process.stderr.write(`[autokap:warn] Allowing unsafe server override from ${baselineOrigin} to ${candidateOrigin} because ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1\n`);
187
+ return;
188
+ }
189
+ throw new Error(`Refusing unsafe server override to ${candidateOrigin}. Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to allow ${envVar ?? 'this override'} explicitly.`);
190
+ }
191
+ /**
192
+ * SSRF guard for any caller-supplied URL that the AutoKap stack will
193
+ * eventually fetch (MCP tool inputs — apiBaseUrl, proxyUrl, webhookUrl,
194
+ * loginUrl, project baseUrl, etc.).
195
+ *
196
+ * Rejects:
197
+ * - non-http(s) schemes (file://, javascript:, gopher://, data:, ...)
198
+ * - localhost / 127.0.0.0/8 / private IPv4 / link-local / IPv6 loopback
199
+ * - hostnames ending in `.local`, `.internal`
200
+ *
201
+ * When `AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN=1`, the private-host check is
202
+ * relaxed for dev workflows. The scheme check is NEVER relaxed.
203
+ *
204
+ * `validatePublicHttpUrl` is the public alias used by MCP tools that accept
205
+ * user-provided URLs; `validateServerOrigin` is the alias used by core API
206
+ * client paths.
207
+ */
208
+ export function validateServerOrigin(candidateUrl) {
209
+ validatePublicHttpUrl(candidateUrl, 'server origin');
210
+ }
211
+ export function validatePublicHttpUrl(candidateUrl, fieldLabel = 'URL') {
212
+ let parsed;
213
+ try {
214
+ parsed = new URL(candidateUrl);
215
+ }
216
+ catch {
217
+ throw new Error(`Invalid ${fieldLabel}: ${candidateUrl}`);
218
+ }
219
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
220
+ throw new Error(`Refusing ${fieldLabel} with unsupported scheme "${parsed.protocol}"; only http(s) are allowed.`);
221
+ }
222
+ if (isPrivateOrInternalHost(parsed.hostname)) {
223
+ if (isUnsafeOverrideEnabled()) {
224
+ process.stderr.write(`[autokap:warn] Allowing private/internal host ${parsed.hostname} for ${fieldLabel} because ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1\n`);
225
+ return;
226
+ }
227
+ throw new Error(`Refusing ${fieldLabel} pointing at a private or link-local host (${parsed.hostname}). ` +
228
+ `Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to override (dev/test only).`);
229
+ }
230
+ }
231
+ function isPrivateOrInternalHost(hostname) {
232
+ const lower = hostname.toLowerCase();
233
+ if (lower === 'localhost')
234
+ return true;
235
+ if (lower === '0.0.0.0' || lower === '::' || lower === '[::]')
236
+ return true;
237
+ if (lower.endsWith('.local') || lower.endsWith('.internal'))
238
+ return true;
239
+ if (lower === '::1' || lower === '[::1]')
240
+ return true;
241
+ // IPv4 — dotted-decimal AND obfuscated forms (octal/hex/decimal). Node's
242
+ // fetch normalizes these via libuv's getaddrinfo, so `http://2130706433/`
243
+ // resolves to 127.0.0.1 and would otherwise slip past a dotted-only check.
244
+ const ipv4 = parseIpv4(lower);
245
+ if (ipv4) {
246
+ const [a, b] = ipv4;
247
+ if (a === 10)
248
+ return true; // 10.0.0.0/8
249
+ if (a === 127)
250
+ return true; // loopback 127.0.0.0/8
251
+ if (a === 169 && b === 254)
252
+ return true; // link-local 169.254.0.0/16
253
+ if (a === 172 && b >= 16 && b <= 31)
254
+ return true; // 172.16.0.0/12
255
+ if (a === 192 && b === 168)
256
+ return true; // 192.168.0.0/16
257
+ if (a === 100 && b >= 64 && b <= 127)
258
+ return true; // CGNAT 100.64.0.0/10
259
+ if (a === 0)
260
+ return true; // 0.0.0.0/8 (this network)
261
+ if (a >= 224)
262
+ return true; // multicast + reserved
263
+ }
264
+ // Bracketed IPv6 — loopback, ULA, link-local, IPv4-mapped, 6to4-encoded
265
+ // loopback, NAT64 well-known prefix carrying private/loopback v4.
266
+ if (lower.startsWith('[') && lower.endsWith(']')) {
267
+ return isPrivateIpv6(lower.slice(1, -1));
268
+ }
269
+ return false;
270
+ }
271
+ /**
272
+ * Parse an IPv4 literal that Node's fetch / getaddrinfo would accept,
273
+ * including the obfuscated forms attackers use to bypass naive checks:
274
+ * - dotted-decimal: 127.0.0.1
275
+ * - dotted with leading 0: 0177.0.0.1 (octal — 0177 = 127)
276
+ * - dotted with 0x prefix: 0x7f.0.0.1 (hex)
277
+ * - single 32-bit integer: 2130706433 (decimal — equals 127.0.0.1)
278
+ * - single hex integer: 0x7f000001
279
+ *
280
+ * Returns the octet tuple [a, b, c, d] when recognized, else null.
281
+ * Each octet is clamped on parse so malformed inputs like 999.0.0.0
282
+ * return null instead of allowing the SSRF guard to mis-classify them.
283
+ */
284
+ function parseIpv4(literal) {
285
+ const parts = literal.split('.');
286
+ if (parts.length === 4) {
287
+ const octets = [];
288
+ for (const part of parts) {
289
+ const n = parseObfuscatedOctet(part);
290
+ if (n === null || n < 0 || n > 255)
291
+ return null;
292
+ octets.push(n);
293
+ }
294
+ return octets;
295
+ }
296
+ if (parts.length === 1) {
297
+ const n = parseObfuscatedInt(parts[0]);
298
+ if (n === null || n < 0 || n > 0xff_ff_ff_ff)
299
+ return null;
300
+ return [
301
+ (n >>> 24) & 0xff,
302
+ (n >>> 16) & 0xff,
303
+ (n >>> 8) & 0xff,
304
+ n & 0xff,
305
+ ];
306
+ }
307
+ return null;
308
+ }
309
+ function parseObfuscatedOctet(part) {
310
+ // Reject empties / spaces / sign.
311
+ if (part.length === 0 || /[^0-9a-fx]/.test(part))
312
+ return null;
313
+ if (part.startsWith('0x') || part.startsWith('0X')) {
314
+ const hex = part.slice(2);
315
+ if (hex.length === 0 || /[^0-9a-f]/.test(hex))
316
+ return null;
317
+ return Number.parseInt(hex, 16);
318
+ }
319
+ if (part.length > 1 && part.startsWith('0')) {
320
+ if (/[^0-7]/.test(part))
321
+ return null;
322
+ return Number.parseInt(part, 8);
323
+ }
324
+ if (/[^0-9]/.test(part))
325
+ return null;
326
+ return Number.parseInt(part, 10);
327
+ }
328
+ function parseObfuscatedInt(part) {
329
+ if (part.startsWith('0x') || part.startsWith('0X')) {
330
+ const hex = part.slice(2);
331
+ if (hex.length === 0 || /[^0-9a-f]/.test(hex))
332
+ return null;
333
+ return Number.parseInt(hex, 16);
334
+ }
335
+ if (part.length > 1 && part.startsWith('0')) {
336
+ if (/[^0-7]/.test(part))
337
+ return null;
338
+ return Number.parseInt(part, 8);
339
+ }
340
+ if (/[^0-9]/.test(part))
341
+ return null;
342
+ return Number.parseInt(part, 10);
343
+ }
344
+ function isPrivateIpv6(ipv6) {
345
+ if (ipv6 === '::1')
346
+ return true;
347
+ if (ipv6.startsWith('fc') || ipv6.startsWith('fd'))
348
+ return true; // ULA fc00::/7
349
+ if (ipv6.startsWith('fe80'))
350
+ return true; // link-local
351
+ // IPv4-mapped (RFC 4291): canonicalizes to either `::ffff:a.b.c.d` or the
352
+ // hex-pair shorthand `::ffff:H:L`. Node's URL canonicalizes dotted form to
353
+ // the hex-pair form, so both must be handled.
354
+ if (ipv6.startsWith('::ffff:')) {
355
+ const tail = ipv6.slice('::ffff:'.length);
356
+ return embeddedV4IsPrivate(tail);
357
+ }
358
+ if (ipv6.startsWith('::ffff:0:')) {
359
+ const tail = ipv6.slice('::ffff:0:'.length);
360
+ return embeddedV4IsPrivate(tail);
361
+ }
362
+ // Bare hex-pair shorthand (no ::ffff prefix), e.g. `::7f00:1` = 127.0.0.1.
363
+ if (ipv6.startsWith('::') && !ipv6.startsWith(':::')) {
364
+ const tail = ipv6.slice(2);
365
+ if (/^[0-9a-f]{1,4}:[0-9a-f]{1,4}$/.test(tail)) {
366
+ return embeddedV4IsPrivate(tail);
367
+ }
368
+ }
369
+ // 6to4-encoded prefix `2002::/16` — when the embedded IPv4 is private/
370
+ // loopback, the route lands in those networks despite the public-looking
371
+ // outer prefix.
372
+ const sixToFour = ipv6.match(/^2002:([0-9a-f]{1,4}):([0-9a-f]{1,4})/);
373
+ if (sixToFour) {
374
+ return embeddedV4IsPrivate(`${sixToFour[1]}:${sixToFour[2]}`);
375
+ }
376
+ // NAT64 well-known prefix `64:ff9b::/96` — last 32 bits are the embedded v4.
377
+ const nat64 = ipv6.match(/^64:ff9b::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
378
+ if (nat64) {
379
+ return embeddedV4IsPrivate(`${nat64[1]}:${nat64[2]}`);
380
+ }
381
+ return false;
382
+ }
383
+ /**
384
+ * Parse a 32-bit IPv4 embedded in an IPv6 address. Accepts both the
385
+ * dotted-decimal form (`127.0.0.1`) and the hex-pair shorthand (`7f00:1`).
386
+ */
387
+ function embeddedV4IsPrivate(tail) {
388
+ const inner = parseIpv4(tail);
389
+ if (inner) {
390
+ return isPrivateOrInternalHost(`${inner[0]}.${inner[1]}.${inner[2]}.${inner[3]}`);
391
+ }
392
+ const colon = tail.split(':');
393
+ if (colon.length === 2) {
394
+ const hi = Number.parseInt(colon[0], 16);
395
+ const lo = Number.parseInt(colon[1], 16);
396
+ if (Number.isFinite(hi) && Number.isFinite(lo) && hi >= 0 && hi <= 0xffff && lo >= 0 && lo <= 0xffff) {
397
+ const dotted = `${(hi >>> 8) & 0xff}.${hi & 0xff}.${(lo >>> 8) & 0xff}.${lo & 0xff}`;
398
+ return isPrivateOrInternalHost(dotted);
399
+ }
400
+ }
401
+ return false;
402
+ }
403
+ export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, assertAllowedApiOrigin, };
404
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,12 @@
1
+ export declare function buildEndpointAssetUrl(apiBaseUrl: string, endpointId: string): string;
2
+ export interface EndpointUrlParams {
3
+ lang: string;
4
+ theme: string;
5
+ target: string;
6
+ w?: string;
7
+ quality?: string;
8
+ format?: string;
9
+ scale?: string;
10
+ }
11
+ export declare function buildEndpointUrlParams(assetType: string): EndpointUrlParams;
12
+ export declare function toCsv(rows: Array<Record<string, unknown>>, columns: string[]): string;
@@ -0,0 +1,33 @@
1
+ export function buildEndpointAssetUrl(apiBaseUrl, endpointId) {
2
+ // Encode the id even though today it's always a UUID — if the backend ever
3
+ // adopts a non-opaque id format (timestamp-prefixed, slug-shaped) that
4
+ // includes `/`, `?`, `#`, or `..`, raw interpolation would silently
5
+ // produce a malformed URL or a path-traversal vector.
6
+ return `${apiBaseUrl.replace(/\/+$/, '')}/api/v1/assets/${encodeURIComponent(endpointId)}`;
7
+ }
8
+ export function buildEndpointUrlParams(assetType) {
9
+ const isClip = assetType === 'clip';
10
+ return {
11
+ lang: '<lang>',
12
+ theme: '<theme>',
13
+ target: '<target>',
14
+ ...(isClip
15
+ ? { format: 'mp4|webm|gif' }
16
+ : { w: '<width>', quality: '<quality>', format: 'png|jpeg|webp', scale: '1|2|3' }),
17
+ };
18
+ }
19
+ export function toCsv(rows, columns) {
20
+ const escape = (value) => {
21
+ if (value === null || value === undefined)
22
+ return '';
23
+ const str = String(value);
24
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
25
+ return `"${str.replace(/"/g, '""')}"`;
26
+ }
27
+ return str;
28
+ };
29
+ const header = columns.map(escape).join(',');
30
+ const body = rows.map((row) => columns.map((col) => escape(row[col])).join(',')).join('\n');
31
+ return `${header}\n${body}`;
32
+ }
33
+ //# sourceMappingURL=endpoint-helpers.js.map
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './config.js';
3
+ export * from './api-client.js';
4
+ export * from './endpoint-helpers.js';
5
+ export * from './logger.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './config.js';
3
+ export * from './api-client.js';
4
+ export * from './endpoint-helpers.js';
5
+ export * from './logger.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export interface Logger {
3
+ debug: (message: string, ...args: unknown[]) => void;
4
+ info: (message: string, ...args: unknown[]) => void;
5
+ warn: (message: string, ...args: unknown[]) => void;
6
+ error: (message: string, ...args: unknown[]) => void;
7
+ }
8
+ export interface CreateLoggerOptions {
9
+ /**
10
+ * Minimum log level. Defaults to `process.env.AUTOKAP_LOG` (case-insensitive
11
+ * one of 'debug' | 'info' | 'warn' | 'error') and falls back to 'info'.
12
+ */
13
+ minLevel?: LogLevel;
14
+ }
15
+ export declare function createStderrLogger(options?: CreateLoggerOptions): Logger;
16
+ declare function redactString(input: string): string;
17
+ declare function redactValue(value: unknown, seen?: WeakSet<object>): unknown;
18
+ export { redactString, redactValue };
19
+ export declare const __test: {
20
+ redactString: typeof redactString;
21
+ redactValue: typeof redactValue;
22
+ };
package/dist/logger.js ADDED
@@ -0,0 +1,91 @@
1
+ const LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
2
+ // AutoKap API/run-token prefixes — leak detection. Pattern is intentionally
3
+ // broad: catches `ak_cli_<hex>`, `ak_run_<hex>`, and any other `ak_<scope>_<id>`
4
+ // that's at least 6 chars long so we never accidentally log a real key.
5
+ const API_KEY_PATTERN = /\bak_[a-z]+_[A-Za-z0-9_-]{6,}\b/g;
6
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._\-]{6,}/gi;
7
+ const SENSITIVE_KEY_PATTERN = /(api[_-]?key|password|token|bearer|secret|authorization)/i;
8
+ function format(level, message) {
9
+ return `[autokap:${level}] ${message}`;
10
+ }
11
+ function resolveMinLevel(explicit) {
12
+ if (explicit)
13
+ return explicit;
14
+ const env = process.env.AUTOKAP_LOG?.trim().toLowerCase();
15
+ if (env && env in LEVEL_ORDER)
16
+ return env;
17
+ return 'info';
18
+ }
19
+ export function createStderrLogger(options = {}) {
20
+ const minLevel = resolveMinLevel(options.minLevel);
21
+ const minRank = LEVEL_ORDER[minLevel];
22
+ const write = (level, message, args) => {
23
+ if (LEVEL_ORDER[level] < minRank)
24
+ return;
25
+ const line = format(level, redactString(message));
26
+ if (args.length > 0) {
27
+ process.stderr.write(`${line} ${args.map(serialize).join(' ')}\n`);
28
+ }
29
+ else {
30
+ process.stderr.write(`${line}\n`);
31
+ }
32
+ };
33
+ return {
34
+ debug: (msg, ...args) => write('debug', msg, args),
35
+ info: (msg, ...args) => write('info', msg, args),
36
+ warn: (msg, ...args) => write('warn', msg, args),
37
+ error: (msg, ...args) => write('error', msg, args),
38
+ };
39
+ }
40
+ function redactString(input) {
41
+ // Bearer first — otherwise the inner ak_… pattern matches first and the
42
+ // Bearer suffix won't recognize the masked literal as a Bearer token.
43
+ return input.replace(BEARER_PATTERN, 'Bearer ***REDACTED').replace(API_KEY_PATTERN, 'ak_***_REDACTED');
44
+ }
45
+ function redactValue(value, seen = new WeakSet()) {
46
+ if (typeof value === 'string')
47
+ return redactString(value);
48
+ if (value === null || value === undefined)
49
+ return value;
50
+ if (typeof value !== 'object')
51
+ return value;
52
+ if (seen.has(value))
53
+ return '[Circular]';
54
+ seen.add(value);
55
+ if (Array.isArray(value))
56
+ return value.map((entry) => redactValue(entry, seen));
57
+ const out = {};
58
+ for (const [key, raw] of Object.entries(value)) {
59
+ if (SENSITIVE_KEY_PATTERN.test(key)) {
60
+ out[key] = '[redacted]';
61
+ }
62
+ else {
63
+ out[key] = redactValue(raw, seen);
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ function serialize(value) {
69
+ if (value instanceof Error) {
70
+ const stack = value.stack ?? value.message;
71
+ const cause = value.cause;
72
+ const causeSuffix = cause ? ` (cause: ${redactString(String(cause))})` : '';
73
+ return `${redactString(stack)}${causeSuffix}`;
74
+ }
75
+ if (typeof value === 'string')
76
+ return redactString(value);
77
+ try {
78
+ return JSON.stringify(redactValue(value));
79
+ }
80
+ catch {
81
+ return redactString(String(value));
82
+ }
83
+ }
84
+ // Public for callers that need to redact a value before surfacing it to an
85
+ // MCP client (e.g. `errorResult.responseBody`, tool error envelopes). The
86
+ // canonical entry point — using the logger's own redaction keeps the
87
+ // secrets-on-the-wire definition in one place.
88
+ export { redactString, redactValue };
89
+ // Backwards-compat alias retained for older tests; prefer the named exports.
90
+ export const __test = { redactString, redactValue };
91
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,19 @@
1
+ export interface AutokapConfig {
2
+ apiKey: string;
3
+ apiBaseUrl: string;
4
+ wsUrl: string;
5
+ }
6
+ export interface ValidateResponse {
7
+ valid: boolean;
8
+ userId?: string;
9
+ keyType?: string;
10
+ email?: string;
11
+ name?: string;
12
+ }
13
+ export type CheckStatus = 'ok' | 'warn' | 'fail';
14
+ export interface CheckResult {
15
+ name: string;
16
+ status: CheckStatus;
17
+ message: string;
18
+ fixCommand?: string;
19
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@autokap/core",
3
+ "version": "0.1.0",
4
+ "description": "Shared core library for AutoKap CLI and MCP server",
5
+ "license": "ISC",
6
+ "author": "AutoKap",
7
+ "homepage": "https://autokap.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/AutoKap/autokap.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./config": {
25
+ "types": "./dist/config.d.ts",
26
+ "default": "./dist/config.js"
27
+ },
28
+ "./api-client": {
29
+ "types": "./dist/api-client.d.ts",
30
+ "default": "./dist/api-client.js"
31
+ },
32
+ "./endpoint-helpers": {
33
+ "types": "./dist/endpoint-helpers.d.ts",
34
+ "default": "./dist/endpoint-helpers.js"
35
+ },
36
+ "./types": {
37
+ "types": "./dist/types.d.ts",
38
+ "default": "./dist/types.js"
39
+ },
40
+ "./logger": {
41
+ "types": "./dist/logger.d.ts",
42
+ "default": "./dist/logger.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run --root ../..",
49
+ "prepublishOnly": "npm run build && node -e \"require('node:fs').accessSync('dist/index.js'); require('node:fs').accessSync('dist/index.d.ts');\""
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "files": [
55
+ "dist/**/*.js",
56
+ "dist/**/*.d.ts",
57
+ "!dist/**/*.js.map"
58
+ ],
59
+ "dependencies": {},
60
+ "devDependencies": {}
61
+ }