toolcraft-openapi 0.0.139 → 0.0.141
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 +42 -0
- package/dist/composition.json +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/network-error.js +6 -0
- package/dist/runtime.d.ts +5 -2
- package/dist/runtime.js +52 -7
- package/dist/spec-cache.d.ts +41 -0
- package/dist/spec-cache.js +376 -0
- package/dist/spec-source.d.ts +26 -0
- package/dist/spec-source.js +109 -8
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -50,6 +50,45 @@ export const client = defineGeneratedClient({
|
|
|
50
50
|
Generated lower-level group and operation exports remain available when callers
|
|
51
51
|
want a curated command surface with `defineClient()`.
|
|
52
52
|
|
|
53
|
+
## Live runtime clients
|
|
54
|
+
|
|
55
|
+
`defineClientFromSpec()` materializes the same OpenAPI command model at process startup without
|
|
56
|
+
writing generated source files:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const client = await defineClientFromSpec("https://api.example.com/openapi.json", {
|
|
60
|
+
name: "internal-agent",
|
|
61
|
+
baseUrl: "https://api.example.com",
|
|
62
|
+
auth,
|
|
63
|
+
cache: {
|
|
64
|
+
onFallback: (message) => process.stderr.write(`${message}\n`)
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
HTTP sources use a cross-process disk cache by default when the built-in `fetch` is used.
|
|
70
|
+
Successful documents remain fresh for the server's `Cache-Control: max-age`, or five
|
|
71
|
+
minutes when the server does not provide one. Stale entries revalidate with `If-None-Match` when
|
|
72
|
+
an ETag is available. Fetching and reading the response have a three-second timeout; a transport
|
|
73
|
+
failure or timeout uses the last successfully materialized document. HTTP errors and invalid live
|
|
74
|
+
documents still fail visibly.
|
|
75
|
+
|
|
76
|
+
Runtime source options:
|
|
77
|
+
|
|
78
|
+
- `cache: false` - disables HTTP source caching and offline fallback.
|
|
79
|
+
- `cache.directory` - absolute cache directory override.
|
|
80
|
+
- `cache.maxAgeMs` - fallback freshness when the response does not define cache behavior.
|
|
81
|
+
- `cache.onFallback(message)` - reports when a stale document is used after a transport failure.
|
|
82
|
+
- `onTimeout({ source, timeoutMs, usingCachedDocument })` - application hook for presenting
|
|
83
|
+
network-access guidance without coupling the library to a specific network or VPN.
|
|
84
|
+
- `timeoutMs` - total HTTP fetch/body timeout. Defaults to `3000`; `0` disables it.
|
|
85
|
+
|
|
86
|
+
Supplying a custom `fetch` disables automatic caching unless `cache` is explicitly provided. Use a
|
|
87
|
+
different `cache.directory` for each identity when explicitly caching authenticated custom fetches.
|
|
88
|
+
A custom filesystem participates when it provides `realpath` and the cache write operations;
|
|
89
|
+
read-only filesystems still load the live document without persisting it. New cache entries are
|
|
90
|
+
committed only after the command tree materializes successfully.
|
|
91
|
+
|
|
53
92
|
### CI drift check
|
|
54
93
|
|
|
55
94
|
```sh
|
|
@@ -79,6 +118,9 @@ toolcraft-openapi-generate --check
|
|
|
79
118
|
- `AUTH_BACKEND` - forwarded to `auth-store` to select `file` or `keychain` storage.
|
|
80
119
|
- `TOOLCRAFT_OPENAPI_ENV` - selects a configured OpenAPI environment when using
|
|
81
120
|
`resolveOpenApiBaseUrl`.
|
|
121
|
+
- `TOOLCRAFT_OPENAPI_CACHE_DIR` - absolute directory for live HTTP OpenAPI cache entries.
|
|
122
|
+
- `TOOLCRAFT_OPENAPI_CACHE` - set to `0` or `false` to disable the default live HTTP cache.
|
|
123
|
+
- `XDG_CACHE_HOME` - cache root fallback; defaults to `~/.cache`.
|
|
82
124
|
|
|
83
125
|
## Configuration
|
|
84
126
|
|
package/dist/composition.json
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type { InspectOpenApiSourceOptions, OpenApiInspectionSource } from "./ins
|
|
|
14
14
|
export { renderOpenApiInspection } from "./render-inspection.js";
|
|
15
15
|
export { commandsFromSpec, defineClientFromSpec, resolveOpenApiBaseUrl } from "./runtime.js";
|
|
16
16
|
export type { CommandsFromSpecOptions, DefineClientFromSpecOptions, OpenApiDocumentSource } from "./runtime.js";
|
|
17
|
+
export type { OpenApiSpecCacheFileSystem, OpenApiSpecCacheOptions, OpenApiTimeoutContext } from "./spec-cache.js";
|
|
17
18
|
export type { DefineClientOptions, DefinedClient, OpenApiClientServices } from "./define-client.js";
|
|
18
19
|
export type { AuthProvider, CommandContributor, TokenSource } from "./auth/types.js";
|
|
19
20
|
export { bearerTokenAuth } from "./auth/bearer-token-auth.js";
|
package/dist/network-error.js
CHANGED
|
@@ -24,6 +24,12 @@ export function classifyNetworkError(error, url) {
|
|
|
24
24
|
return new UserError(`Temporary DNS failure for ${host}. Network may be down.`, {
|
|
25
25
|
cause: error
|
|
26
26
|
});
|
|
27
|
+
case "UND_ERR_SOCKET":
|
|
28
|
+
return new UserError(`Network connection failed: ${redactedUrl}.`, { cause: error });
|
|
29
|
+
case "UND_ERR_CONNECT_TIMEOUT":
|
|
30
|
+
case "UND_ERR_HEADERS_TIMEOUT":
|
|
31
|
+
case "UND_ERR_BODY_TIMEOUT":
|
|
32
|
+
return new UserError(`Request timed out: ${redactedUrl}.`, { cause: error });
|
|
27
33
|
}
|
|
28
34
|
if (findAbortError(error) !== null) {
|
|
29
35
|
return new UserError(`Request aborted: ${redactedUrl}.`, { cause: error });
|
package/dist/runtime.d.ts
CHANGED
|
@@ -2,12 +2,15 @@ import { type CommandNode } from "toolcraft";
|
|
|
2
2
|
import { type DefineClientOptions, type DefinedClient, type OpenApiClientServices } from "./define-client.js";
|
|
3
3
|
import { type OpenApiDocument } from "./generate.js";
|
|
4
4
|
import type { ToolcraftConfig } from "./config.js";
|
|
5
|
-
import { type
|
|
5
|
+
import { type OpenApiSpecCacheFileSystem, type OpenApiSpecCacheOptions, type OpenApiTimeoutContext } from "./spec-cache.js";
|
|
6
6
|
export type OpenApiDocumentSource = OpenApiDocument | string | URL;
|
|
7
7
|
export interface CommandsFromSpecOptions {
|
|
8
8
|
cwd?: string;
|
|
9
9
|
fetch?: typeof globalThis.fetch;
|
|
10
|
-
fs?:
|
|
10
|
+
fs?: OpenApiSpecCacheFileSystem;
|
|
11
|
+
cache?: false | OpenApiSpecCacheOptions;
|
|
12
|
+
onTimeout?: (context: OpenApiTimeoutContext) => void | Promise<void>;
|
|
13
|
+
timeoutMs?: number;
|
|
11
14
|
config?: ToolcraftConfig;
|
|
12
15
|
}
|
|
13
16
|
export type DefineClientFromSpecOptions<TServices extends object = Record<string, never>> = Omit<DefineClientOptions<TServices>, "baseUrl" | "commands"> & CommandsFromSpecOptions & {
|
package/dist/runtime.js
CHANGED
|
@@ -5,15 +5,27 @@ import { collectTagDescriptions, collectSchemaOptionEntries, collectGeneratedCom
|
|
|
5
5
|
import { groupByNoun } from "./group-by-noun.js";
|
|
6
6
|
import { prepareMultipartFileInputs, requestJson, writeBinaryResponseOutput } from "./http.js";
|
|
7
7
|
import { buildRequestShape, executePreflightBlocks } from "./interpreter.js";
|
|
8
|
+
import { DEFAULT_OPENAPI_FETCH_TIMEOUT_MS, loadCachedOpenApiSource } from "./spec-cache.js";
|
|
8
9
|
import { parseOpenApiDocument, readOpenApiSourceText } from "./spec-source.js";
|
|
9
10
|
const RUNTIME_COMMAND_SCOPE = ["cli", "mcp", "sdk"];
|
|
10
11
|
export async function commandsFromSpec(source, options = {}) {
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
const resolved = await resolveDocument(source, options);
|
|
13
|
+
const commands = createRuntimeNodes(resolved.document, options.config);
|
|
14
|
+
await resolved.commit?.();
|
|
15
|
+
return commands;
|
|
13
16
|
}
|
|
14
17
|
export async function defineClientFromSpec(spec, options) {
|
|
15
|
-
const { cwd, fetch, fs: specFs, config, baseUrl, environment, env, ...clientOptions } = options;
|
|
16
|
-
const
|
|
18
|
+
const { cwd, fetch, fs: specFs, cache, onTimeout, timeoutMs, config, baseUrl, environment, env, ...clientOptions } = options;
|
|
19
|
+
const resolved = await resolveDocument(spec, {
|
|
20
|
+
cwd,
|
|
21
|
+
fetch,
|
|
22
|
+
fs: specFs,
|
|
23
|
+
cache,
|
|
24
|
+
onTimeout,
|
|
25
|
+
timeoutMs,
|
|
26
|
+
config
|
|
27
|
+
});
|
|
28
|
+
const document = resolved.document;
|
|
17
29
|
const resolvedBaseUrl = baseUrl ??
|
|
18
30
|
resolveOpenApiBaseUrl({
|
|
19
31
|
document,
|
|
@@ -25,18 +37,51 @@ export async function defineClientFromSpec(spec, options) {
|
|
|
25
37
|
throw new UserError("defineClientFromSpec could not resolve a base URL. Pass baseUrl, configure an environment, or define OpenAPI servers[0].url.");
|
|
26
38
|
}
|
|
27
39
|
const commands = createRuntimeNodes(document, config);
|
|
28
|
-
|
|
40
|
+
const client = defineClient({ ...clientOptions, baseUrl: resolvedBaseUrl, commands });
|
|
41
|
+
await resolved.commit?.();
|
|
42
|
+
return client;
|
|
29
43
|
}
|
|
30
44
|
async function resolveDocument(source, options) {
|
|
31
45
|
if (typeof source !== "string" && !(source instanceof URL)) {
|
|
32
|
-
return source;
|
|
46
|
+
return { document: source };
|
|
47
|
+
}
|
|
48
|
+
const sourceUrl = source instanceof URL ? source : tryParseUrl(source);
|
|
49
|
+
if (sourceUrl !== null && (sourceUrl.protocol === "http:" || sourceUrl.protocol === "https:")) {
|
|
50
|
+
const loaded = await loadCachedOpenApiSource(sourceUrl, {
|
|
51
|
+
cache: options.cache ?? resolveDefaultCache(options),
|
|
52
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
53
|
+
fs: options.fs ?? fs,
|
|
54
|
+
onTimeout: options.onTimeout,
|
|
55
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_OPENAPI_FETCH_TIMEOUT_MS
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
document: loaded.document,
|
|
59
|
+
...(loaded.commit === undefined ? {} : { commit: loaded.commit })
|
|
60
|
+
};
|
|
33
61
|
}
|
|
34
62
|
const sourceText = await readOpenApiSourceText(source, {
|
|
35
63
|
cwd: options.cwd ?? process.cwd(),
|
|
36
64
|
fetch: options.fetch ?? globalThis.fetch,
|
|
37
65
|
fs: options.fs ?? fs
|
|
38
66
|
});
|
|
39
|
-
return parseOpenApiDocument(sourceText, source);
|
|
67
|
+
return { document: parseOpenApiDocument(sourceText, source) };
|
|
68
|
+
}
|
|
69
|
+
function resolveDefaultCache(options) {
|
|
70
|
+
if (options.fetch !== undefined) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const configured = Object.prototype.hasOwnProperty.call(process.env, "TOOLCRAFT_OPENAPI_CACHE")
|
|
74
|
+
? process.env.TOOLCRAFT_OPENAPI_CACHE?.trim().toLowerCase()
|
|
75
|
+
: undefined;
|
|
76
|
+
return configured === "0" || configured === "false" ? false : {};
|
|
77
|
+
}
|
|
78
|
+
function tryParseUrl(value) {
|
|
79
|
+
try {
|
|
80
|
+
return new URL(value);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
40
85
|
}
|
|
41
86
|
export function resolveOpenApiBaseUrl(options) {
|
|
42
87
|
const environments = options.environments;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type OpenApiSourceFileSystem } from "./spec-source.js";
|
|
2
|
+
import type { OpenApiDocument } from "./generate.js";
|
|
3
|
+
export declare const DEFAULT_OPENAPI_FETCH_TIMEOUT_MS = 3000;
|
|
4
|
+
export declare const DEFAULT_OPENAPI_CACHE_MAX_AGE_MS: number;
|
|
5
|
+
export interface OpenApiSpecCacheOptions {
|
|
6
|
+
directory?: string;
|
|
7
|
+
maxAgeMs?: number;
|
|
8
|
+
onFallback?: (message: string) => void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface LoadCachedOpenApiSourceOptions {
|
|
11
|
+
cache: false | OpenApiSpecCacheOptions;
|
|
12
|
+
fetch: typeof globalThis.fetch;
|
|
13
|
+
fs: OpenApiSpecCacheFileSystem;
|
|
14
|
+
onTimeout?: (context: OpenApiTimeoutContext) => void | Promise<void>;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface OpenApiTimeoutContext {
|
|
18
|
+
source: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
usingCachedDocument: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface OpenApiSpecCacheFileSystem extends OpenApiSourceFileSystem {
|
|
23
|
+
writeFile?(filePath: string, contents: string, options?: {
|
|
24
|
+
encoding?: BufferEncoding;
|
|
25
|
+
flag?: string;
|
|
26
|
+
mode?: number;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
rename?(fromPath: string, toPath: string): Promise<void>;
|
|
29
|
+
mkdir?(directoryPath: string, options?: {
|
|
30
|
+
recursive?: boolean;
|
|
31
|
+
mode?: number;
|
|
32
|
+
}): Promise<unknown>;
|
|
33
|
+
unlink?(filePath: string): Promise<void>;
|
|
34
|
+
realpath?(filePath: string): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
export interface LoadedOpenApiSource {
|
|
37
|
+
sourceText: string;
|
|
38
|
+
document: OpenApiDocument;
|
|
39
|
+
commit?: () => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare function loadCachedOpenApiSource(inputUrl: URL, options: LoadCachedOpenApiSourceOptions): Promise<LoadedOpenApiSource>;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { UserError } from "toolcraft";
|
|
5
|
+
import { hasOwnErrorCode } from "./error-codes.js";
|
|
6
|
+
import { fetchOpenApiHttpSource, OpenApiTimeoutError, OpenApiTransportError, parseOpenApiDocument } from "./spec-source.js";
|
|
7
|
+
import { redactSensitiveQueryValues } from "./redaction.js";
|
|
8
|
+
export const DEFAULT_OPENAPI_FETCH_TIMEOUT_MS = 3_000;
|
|
9
|
+
export const DEFAULT_OPENAPI_CACHE_MAX_AGE_MS = 5 * 60_000;
|
|
10
|
+
export async function loadCachedOpenApiSource(inputUrl, options) {
|
|
11
|
+
validateTimeout(options.timeoutMs);
|
|
12
|
+
if (options.cache === false) {
|
|
13
|
+
let result;
|
|
14
|
+
try {
|
|
15
|
+
result = await fetchOpenApiHttpSource(inputUrl, options.fetch, {
|
|
16
|
+
timeoutMs: options.timeoutMs
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
notifyTimeout(options, inputUrl, error, false);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (result.status === "not-modified") {
|
|
24
|
+
throw new UserError(`Failed to fetch ${JSON.stringify(inputUrl.toString())}: received 304 without a cached document.`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
sourceText: result.sourceText,
|
|
28
|
+
document: parseOpenApiDocument(result.sourceText, inputUrl)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
validateMaxAge(options.cache.maxAgeMs);
|
|
32
|
+
const directory = resolveCacheDirectory(options.cache.directory);
|
|
33
|
+
const cachePath = resolveCachePath(inputUrl, directory);
|
|
34
|
+
const cached = await readCacheEntry(cachePath, options.fs);
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const freshnessMs = cached?.entry.maxAgeMs;
|
|
37
|
+
if (cached !== null &&
|
|
38
|
+
cached.entry.validatedAt <= now &&
|
|
39
|
+
freshnessMs !== undefined &&
|
|
40
|
+
now - cached.entry.validatedAt < freshnessMs) {
|
|
41
|
+
return {
|
|
42
|
+
sourceText: cached.entry.sourceText,
|
|
43
|
+
document: cached.document
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await fetchOpenApiHttpSource(inputUrl, options.fetch, {
|
|
49
|
+
...(cached?.entry.etag === undefined ? {} : { etag: cached.entry.etag }),
|
|
50
|
+
timeoutMs: options.timeoutMs
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
notifyTimeout(options, inputUrl, error, cached !== null);
|
|
55
|
+
if (!(error instanceof OpenApiTransportError) || cached === null) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
notifyFallback(options.cache, inputUrl, error);
|
|
59
|
+
return {
|
|
60
|
+
sourceText: cached.entry.sourceText,
|
|
61
|
+
document: cached.document
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const writableFs = getWritableFileSystem(options.fs);
|
|
65
|
+
if (result.status === "not-modified") {
|
|
66
|
+
if (cached === null) {
|
|
67
|
+
throw new UserError(`Failed to fetch ${JSON.stringify(inputUrl.toString())}: received 304 without a cached document.`);
|
|
68
|
+
}
|
|
69
|
+
if (cached.entry.etag === undefined) {
|
|
70
|
+
throw new UserError(`Failed to fetch ${JSON.stringify(inputUrl.toString())}: received 304 without a cached validator.`);
|
|
71
|
+
}
|
|
72
|
+
const policy = result.cacheControl === undefined
|
|
73
|
+
? {
|
|
74
|
+
store: true,
|
|
75
|
+
maxAgeMs: Math.max(0, cached.entry.maxAgeMs - (parseDeltaSeconds(result.age ?? "") ?? 0) * 1_000)
|
|
76
|
+
}
|
|
77
|
+
: resolveCachePolicy(result.cacheControl, result.age, options.cache.maxAgeMs, cached.entry.maxAgeMs);
|
|
78
|
+
if (!policy.store) {
|
|
79
|
+
return {
|
|
80
|
+
sourceText: cached.entry.sourceText,
|
|
81
|
+
document: cached.document,
|
|
82
|
+
...(writableFs === null ? {} : { commit: () => removeCacheEntry(cachePath, writableFs) })
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const entry = {
|
|
86
|
+
...cached.entry,
|
|
87
|
+
validatedAt: now,
|
|
88
|
+
maxAgeMs: policy.maxAgeMs,
|
|
89
|
+
...(result.etag === undefined ? {} : { etag: result.etag })
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
sourceText: cached.entry.sourceText,
|
|
93
|
+
document: cached.document,
|
|
94
|
+
...(writableFs === null
|
|
95
|
+
? {}
|
|
96
|
+
: { commit: () => persistCacheEntry(cachePath, entry, writableFs) })
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const policy = resolveCachePolicy(result.cacheControl, result.age, options.cache.maxAgeMs, DEFAULT_OPENAPI_CACHE_MAX_AGE_MS);
|
|
100
|
+
const document = parseOpenApiDocument(result.sourceText, inputUrl);
|
|
101
|
+
if (!policy.store) {
|
|
102
|
+
return {
|
|
103
|
+
sourceText: result.sourceText,
|
|
104
|
+
document,
|
|
105
|
+
...(writableFs === null ? {} : { commit: () => removeCacheEntry(cachePath, writableFs) })
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const entry = {
|
|
109
|
+
version: 1,
|
|
110
|
+
sourceText: result.sourceText,
|
|
111
|
+
validatedAt: now,
|
|
112
|
+
maxAgeMs: policy.maxAgeMs,
|
|
113
|
+
...(result.etag === undefined ? {} : { etag: result.etag })
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
sourceText: result.sourceText,
|
|
117
|
+
document,
|
|
118
|
+
...(writableFs === null
|
|
119
|
+
? {}
|
|
120
|
+
: { commit: () => persistCacheEntry(cachePath, entry, writableFs) })
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function notifyTimeout(options, inputUrl, error, usingCachedDocument) {
|
|
124
|
+
if (!(error instanceof OpenApiTimeoutError) || options.onTimeout === undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
void Promise.resolve(options.onTimeout({
|
|
129
|
+
source: redactSensitiveQueryValues(inputUrl.toString()),
|
|
130
|
+
timeoutMs: error.timeoutMs,
|
|
131
|
+
usingCachedDocument
|
|
132
|
+
})).catch(() => undefined);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// A presentation hook cannot change source-loading behavior.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function resolveCachePolicy(cacheControl, age, configuredMaxAgeMs, fallbackMaxAgeMs) {
|
|
139
|
+
const directives = (cacheControl ?? "")
|
|
140
|
+
.split(",")
|
|
141
|
+
.map((directive) => directive.trim().toLowerCase());
|
|
142
|
+
if (directives.includes("no-store")) {
|
|
143
|
+
return { store: false, maxAgeMs: 0 };
|
|
144
|
+
}
|
|
145
|
+
if (directives.includes("no-cache")) {
|
|
146
|
+
return { store: true, maxAgeMs: 0 };
|
|
147
|
+
}
|
|
148
|
+
for (const directive of directives) {
|
|
149
|
+
if (!directive.startsWith("max-age=")) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const seconds = parseDeltaSeconds(directive.slice("max-age=".length));
|
|
153
|
+
if (seconds !== null) {
|
|
154
|
+
const responseAge = parseDeltaSeconds(age ?? "") ?? 0;
|
|
155
|
+
return { store: true, maxAgeMs: Math.max(0, seconds - responseAge) * 1_000 };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
store: true,
|
|
160
|
+
maxAgeMs: configuredMaxAgeMs ?? fallbackMaxAgeMs
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function parseDeltaSeconds(value) {
|
|
164
|
+
const trimmed = value.trim();
|
|
165
|
+
const unquoted = trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')
|
|
166
|
+
? trimmed.slice(1, -1)
|
|
167
|
+
: trimmed;
|
|
168
|
+
if (unquoted.length === 0) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const seconds = Number(unquoted);
|
|
172
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
173
|
+
}
|
|
174
|
+
function resolveCacheDirectory(configuredDirectory) {
|
|
175
|
+
const envDirectory = readOwnEnvValue(process.env, "TOOLCRAFT_OPENAPI_CACHE_DIR");
|
|
176
|
+
const xdgCacheHome = readOwnEnvValue(process.env, "XDG_CACHE_HOME");
|
|
177
|
+
const directory = configuredDirectory ?? envDirectory;
|
|
178
|
+
if (directory !== undefined) {
|
|
179
|
+
if (!path.isAbsolute(directory)) {
|
|
180
|
+
throw new UserError("OpenAPI cache directory must be an absolute path.");
|
|
181
|
+
}
|
|
182
|
+
return directory;
|
|
183
|
+
}
|
|
184
|
+
const cacheRoot = xdgCacheHome ?? path.join(os.homedir(), ".cache");
|
|
185
|
+
if (!path.isAbsolute(cacheRoot)) {
|
|
186
|
+
throw new UserError("XDG_CACHE_HOME must be an absolute path.");
|
|
187
|
+
}
|
|
188
|
+
return path.join(cacheRoot, "toolcraft-openapi", "specs");
|
|
189
|
+
}
|
|
190
|
+
function resolveCachePath(inputUrl, directory) {
|
|
191
|
+
const canonicalUrl = new URL(inputUrl);
|
|
192
|
+
canonicalUrl.hash = "";
|
|
193
|
+
const key = createHash("sha256").update(canonicalUrl.toString()).digest("hex");
|
|
194
|
+
return path.join(directory, `${key}.json`);
|
|
195
|
+
}
|
|
196
|
+
async function readCacheEntry(cachePath, fs) {
|
|
197
|
+
if (!hasRealpath(fs)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await assertSafeCachePath(cachePath, fs);
|
|
202
|
+
const parsed = JSON.parse(await fs.readFile(cachePath, "utf8"));
|
|
203
|
+
if (!isCacheEntry(parsed)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
entry: sanitizeCacheEntry(parsed),
|
|
208
|
+
document: parseOpenApiDocument(parsed.sourceText, cachePath)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function sanitizeCacheEntry(entry) {
|
|
216
|
+
if (entry.etag === undefined || isValidEtag(entry.etag)) {
|
|
217
|
+
return entry;
|
|
218
|
+
}
|
|
219
|
+
const withoutEtag = { ...entry };
|
|
220
|
+
delete withoutEtag.etag;
|
|
221
|
+
return withoutEtag;
|
|
222
|
+
}
|
|
223
|
+
function isValidEtag(etag) {
|
|
224
|
+
try {
|
|
225
|
+
new Headers({ "If-None-Match": etag });
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function persistCacheEntry(cachePath, entry, fs) {
|
|
233
|
+
try {
|
|
234
|
+
await assertSafeCachePath(cachePath, fs);
|
|
235
|
+
await fs.mkdir(path.dirname(cachePath), { recursive: true, mode: 0o700 });
|
|
236
|
+
await assertSafeCachePath(cachePath, fs);
|
|
237
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
238
|
+
const temporaryPath = `${cachePath}.${randomUUID()}.tmp`;
|
|
239
|
+
let temporaryCreated = false;
|
|
240
|
+
try {
|
|
241
|
+
await fs.writeFile(temporaryPath, JSON.stringify(entry), {
|
|
242
|
+
encoding: "utf8",
|
|
243
|
+
flag: "wx",
|
|
244
|
+
mode: 0o600
|
|
245
|
+
});
|
|
246
|
+
temporaryCreated = true;
|
|
247
|
+
await fs.rename(temporaryPath, cachePath);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (temporaryCreated || !hasOwnErrorCode(error, "EEXIST")) {
|
|
252
|
+
await fs.unlink(temporaryPath).catch(() => undefined);
|
|
253
|
+
}
|
|
254
|
+
if (!hasOwnErrorCode(error, "EEXIST")) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Cache writes are best-effort; the materialized live document remains usable.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function removeCacheEntry(cachePath, fs) {
|
|
265
|
+
try {
|
|
266
|
+
await assertSafeCachePath(cachePath, fs);
|
|
267
|
+
await fs.unlink(cachePath);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Cache removal is best-effort; the materialized live document remains usable.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function getWritableFileSystem(fs) {
|
|
274
|
+
if (typeof fs.writeFile !== "function" ||
|
|
275
|
+
typeof fs.rename !== "function" ||
|
|
276
|
+
typeof fs.mkdir !== "function" ||
|
|
277
|
+
typeof fs.unlink !== "function" ||
|
|
278
|
+
typeof fs.realpath !== "function") {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
return fs;
|
|
282
|
+
}
|
|
283
|
+
function hasRealpath(fs) {
|
|
284
|
+
return typeof fs.realpath === "function";
|
|
285
|
+
}
|
|
286
|
+
async function assertSafeCachePath(cachePath, fs) {
|
|
287
|
+
const directory = path.resolve(path.dirname(cachePath));
|
|
288
|
+
let existingPath = directory;
|
|
289
|
+
while (true) {
|
|
290
|
+
try {
|
|
291
|
+
if (path.resolve(await fs.realpath(existingPath)) !== existingPath) {
|
|
292
|
+
throw new UserError("OpenAPI cache path must remain inside its configured directory.");
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (!hasOwnErrorCode(error, "ENOENT")) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
const parentPath = path.dirname(existingPath);
|
|
301
|
+
if (parentPath === existingPath) {
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
existingPath = parentPath;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
if (path.resolve(await fs.realpath(directory)) !== directory) {
|
|
309
|
+
throw new UserError("OpenAPI cache path must remain inside its configured directory.");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (hasOwnErrorCode(error, "ENOENT")) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const canonicalCachePath = path.resolve(await fs.realpath(cachePath));
|
|
320
|
+
if (canonicalCachePath !== path.resolve(cachePath)) {
|
|
321
|
+
throw new UserError("OpenAPI cache path must remain inside its configured directory.");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
if (!hasOwnErrorCode(error, "ENOENT")) {
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function isCacheEntry(value) {
|
|
331
|
+
if (typeof value !== "object" || value === null) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const entry = value;
|
|
335
|
+
return (Object.hasOwn(entry, "version") &&
|
|
336
|
+
entry.version === 1 &&
|
|
337
|
+
Object.hasOwn(entry, "sourceText") &&
|
|
338
|
+
typeof entry.sourceText === "string" &&
|
|
339
|
+
Object.hasOwn(entry, "validatedAt") &&
|
|
340
|
+
typeof entry.validatedAt === "number" &&
|
|
341
|
+
Number.isFinite(entry.validatedAt) &&
|
|
342
|
+
Object.hasOwn(entry, "maxAgeMs") &&
|
|
343
|
+
typeof entry.maxAgeMs === "number" &&
|
|
344
|
+
Number.isFinite(entry.maxAgeMs) &&
|
|
345
|
+
entry.maxAgeMs >= 0 &&
|
|
346
|
+
(!Object.hasOwn(entry, "etag") || typeof entry.etag === "string"));
|
|
347
|
+
}
|
|
348
|
+
function notifyFallback(cache, inputUrl, error) {
|
|
349
|
+
if (cache.onFallback === undefined) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
353
|
+
try {
|
|
354
|
+
void Promise.resolve(cache.onFallback(`Using cached OpenAPI document for ${JSON.stringify(redactSensitiveQueryValues(inputUrl.toString()))} because refresh failed: ${message}`)).catch(() => undefined);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Cache fallback remains usable even if a presentation callback fails.
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function validateTimeout(timeoutMs) {
|
|
361
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
|
|
362
|
+
throw new UserError("OpenAPI fetch timeout must be a finite non-negative number.");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function validateMaxAge(maxAgeMs) {
|
|
366
|
+
if (maxAgeMs !== undefined && (!Number.isFinite(maxAgeMs) || maxAgeMs < 0)) {
|
|
367
|
+
throw new UserError("OpenAPI cache maxAgeMs must be a finite non-negative number.");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function readOwnEnvValue(env, key) {
|
|
371
|
+
if (!Object.prototype.hasOwnProperty.call(env, key)) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
const value = env[key]?.trim();
|
|
375
|
+
return value === undefined || value.length === 0 ? undefined : value;
|
|
376
|
+
}
|
package/dist/spec-source.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UserError } from "toolcraft";
|
|
1
2
|
import type { OpenApiDocument } from "./generate.js";
|
|
2
3
|
export interface OpenApiSourceFileSystem {
|
|
3
4
|
readFile(filePath: string, encoding: BufferEncoding): Promise<string>;
|
|
@@ -7,5 +8,30 @@ export interface OpenApiSourceServices {
|
|
|
7
8
|
fetch: typeof globalThis.fetch;
|
|
8
9
|
fs: OpenApiSourceFileSystem;
|
|
9
10
|
}
|
|
11
|
+
export interface OpenApiHttpSourceOptions {
|
|
12
|
+
etag?: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export type OpenApiHttpSourceResult = {
|
|
16
|
+
status: "modified";
|
|
17
|
+
sourceText: string;
|
|
18
|
+
etag?: string;
|
|
19
|
+
cacheControl?: string;
|
|
20
|
+
age?: string;
|
|
21
|
+
} | {
|
|
22
|
+
status: "not-modified";
|
|
23
|
+
etag?: string;
|
|
24
|
+
cacheControl?: string;
|
|
25
|
+
age?: string;
|
|
26
|
+
};
|
|
27
|
+
export declare class OpenApiHttpStatusError extends UserError {
|
|
28
|
+
}
|
|
29
|
+
export declare class OpenApiTransportError extends UserError {
|
|
30
|
+
}
|
|
31
|
+
export declare class OpenApiTimeoutError extends OpenApiTransportError {
|
|
32
|
+
readonly timeoutMs: number;
|
|
33
|
+
constructor(message: string, timeoutMs: number, options?: ErrorOptions);
|
|
34
|
+
}
|
|
10
35
|
export declare function readOpenApiSourceText(input: string | URL, services: OpenApiSourceServices): Promise<string>;
|
|
36
|
+
export declare function fetchOpenApiHttpSource(inputUrl: URL, fetch: typeof globalThis.fetch, options?: OpenApiHttpSourceOptions): Promise<OpenApiHttpSourceResult>;
|
|
11
37
|
export declare function parseOpenApiDocument(sourceText: string, input: string | URL): OpenApiDocument;
|
package/dist/spec-source.js
CHANGED
|
@@ -4,6 +4,18 @@ import { parse as parseYaml } from "yaml";
|
|
|
4
4
|
import { UserError } from "toolcraft";
|
|
5
5
|
import { renderSourceSnippet } from "toolcraft/source-snippet";
|
|
6
6
|
import { classifyNetworkError } from "./network-error.js";
|
|
7
|
+
import { redactSensitiveQueryValues } from "./redaction.js";
|
|
8
|
+
export class OpenApiHttpStatusError extends UserError {
|
|
9
|
+
}
|
|
10
|
+
export class OpenApiTransportError extends UserError {
|
|
11
|
+
}
|
|
12
|
+
export class OpenApiTimeoutError extends OpenApiTransportError {
|
|
13
|
+
timeoutMs;
|
|
14
|
+
constructor(message, timeoutMs, options) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.timeoutMs = timeoutMs;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
7
19
|
export async function readOpenApiSourceText(input, services) {
|
|
8
20
|
const inputUrl = input instanceof URL ? input : tryParseUrl(input);
|
|
9
21
|
const sourceLabel = formatSourceLabel(input);
|
|
@@ -20,29 +32,118 @@ export async function readOpenApiSourceText(input, services) {
|
|
|
20
32
|
if (inputUrl.protocol !== "http:" && inputUrl.protocol !== "https:") {
|
|
21
33
|
throw new UserError(`Unsupported OpenAPI input URL protocol ${JSON.stringify(inputUrl.protocol)}.`);
|
|
22
34
|
}
|
|
35
|
+
const result = await fetchOpenApiHttpSource(inputUrl, services.fetch);
|
|
36
|
+
if (result.status === "not-modified") {
|
|
37
|
+
throw new UserError(`Failed to fetch ${JSON.stringify(inputUrl.toString())}: received 304 without a cached document.`);
|
|
38
|
+
}
|
|
39
|
+
return result.sourceText;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof UserError) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
throw new UserError(`Failed to read OpenAPI document ${JSON.stringify(sourceLabel)}: ${getErrorMessage(error)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function fetchOpenApiHttpSource(inputUrl, fetch, options = {}) {
|
|
49
|
+
validateTimeout(options.timeoutMs);
|
|
50
|
+
const url = inputUrl.toString();
|
|
51
|
+
const timeoutMs = options.timeoutMs;
|
|
52
|
+
const controller = timeoutMs === undefined || timeoutMs === 0 ? undefined : new AbortController();
|
|
53
|
+
const requestInit = createOpenApiRequestInit(options.etag, controller?.signal);
|
|
54
|
+
const request = async () => {
|
|
23
55
|
let response;
|
|
24
56
|
try {
|
|
25
|
-
response = await
|
|
57
|
+
response = requestInit === undefined ? await fetch(url) : await fetch(url, requestInit);
|
|
26
58
|
}
|
|
27
59
|
catch (error) {
|
|
28
|
-
throw
|
|
60
|
+
throw toTransportError(error, url) ?? error;
|
|
61
|
+
}
|
|
62
|
+
if (response.status === 304) {
|
|
63
|
+
return {
|
|
64
|
+
status: "not-modified",
|
|
65
|
+
...readResponseCacheHeaders(response)
|
|
66
|
+
};
|
|
29
67
|
}
|
|
30
68
|
if (!response.ok) {
|
|
31
69
|
const contentType = response.headers.get("content-type") ?? "";
|
|
32
70
|
const text = await response.text().catch(() => "");
|
|
33
71
|
const snippet = text.length === 0 ? "" : `\n body: ${truncate(text, 500)}`;
|
|
34
|
-
throw new
|
|
72
|
+
throw new OpenApiHttpStatusError(`Failed to fetch ${JSON.stringify(url)}: ` +
|
|
35
73
|
`${response.status} ${response.statusText}` +
|
|
36
74
|
(contentType ? ` (content-type: ${contentType})` : "") +
|
|
37
75
|
snippet);
|
|
38
76
|
}
|
|
39
|
-
|
|
77
|
+
try {
|
|
78
|
+
return {
|
|
79
|
+
status: "modified",
|
|
80
|
+
sourceText: await response.text(),
|
|
81
|
+
...readResponseCacheHeaders(response)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw (toTransportError(error, url) ??
|
|
86
|
+
new OpenApiTransportError(`Failed to read the OpenAPI response body from ${JSON.stringify(redactSensitiveQueryValues(url))}.`, { cause: error }));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
if (controller === undefined || timeoutMs === undefined) {
|
|
90
|
+
return await request();
|
|
40
91
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
92
|
+
const timeoutCause = Object.assign(new Error("OpenAPI request timed out"), {
|
|
93
|
+
code: "ETIMEDOUT",
|
|
94
|
+
timeout: timeoutMs
|
|
95
|
+
});
|
|
96
|
+
const classified = classifyNetworkError(timeoutCause, url);
|
|
97
|
+
const timeoutError = new OpenApiTimeoutError(classified?.message ?? `OpenAPI request timed out after ${timeoutMs}ms.`, timeoutMs, { cause: timeoutCause });
|
|
98
|
+
let timeout;
|
|
99
|
+
try {
|
|
100
|
+
return await Promise.race([
|
|
101
|
+
request(),
|
|
102
|
+
new Promise((_resolve, reject) => {
|
|
103
|
+
timeout = setTimeout(() => {
|
|
104
|
+
reject(timeoutError);
|
|
105
|
+
controller.abort(timeoutError);
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
})
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (timeout !== undefined) {
|
|
112
|
+
clearTimeout(timeout);
|
|
44
113
|
}
|
|
45
|
-
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function toTransportError(error, url) {
|
|
117
|
+
if (error instanceof OpenApiTransportError) {
|
|
118
|
+
return error;
|
|
119
|
+
}
|
|
120
|
+
const classified = classifyNetworkError(error, url);
|
|
121
|
+
return classified === null
|
|
122
|
+
? null
|
|
123
|
+
: new OpenApiTransportError(classified.message, { cause: error });
|
|
124
|
+
}
|
|
125
|
+
function createOpenApiRequestInit(etag, signal) {
|
|
126
|
+
if (etag === undefined && signal === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...(etag === undefined ? {} : { headers: { "If-None-Match": etag } }),
|
|
131
|
+
...(signal === undefined ? {} : { signal })
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function readResponseCacheHeaders(response) {
|
|
135
|
+
const etag = response.headers.get("etag") ?? undefined;
|
|
136
|
+
const cacheControl = response.headers.get("cache-control") ?? undefined;
|
|
137
|
+
const age = response.headers.get("age") ?? undefined;
|
|
138
|
+
return {
|
|
139
|
+
...(etag === undefined ? {} : { etag }),
|
|
140
|
+
...(cacheControl === undefined ? {} : { cacheControl }),
|
|
141
|
+
...(age === undefined ? {} : { age })
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function validateTimeout(timeoutMs) {
|
|
145
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
|
|
146
|
+
throw new UserError("OpenAPI fetch timeout must be a finite non-negative number.");
|
|
46
147
|
}
|
|
47
148
|
}
|
|
48
149
|
export function parseOpenApiDocument(sourceText, input) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft-openapi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.141",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"toolcraft-openapi-generate": "dist/bin/generate.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"toolcraft": "0.0.
|
|
33
|
+
"toolcraft": "0.0.141",
|
|
34
34
|
"auth-store": "^0.0.1",
|
|
35
35
|
"fast-string-width": "^3.0.2",
|
|
36
36
|
"fast-wrap-ansi": "^0.2.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"directory": "packages/toolcraft-openapi"
|
|
47
47
|
},
|
|
48
48
|
"optionalDependencies": {
|
|
49
|
-
"toolcraft-schema": "0.0.
|
|
49
|
+
"toolcraft-schema": "0.0.141",
|
|
50
50
|
"toolcraft-design": "*",
|
|
51
51
|
"@poe-code/frontmatter": "*"
|
|
52
52
|
},
|