vector-framework 1.2.2 → 1.2.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.
- package/README.md +18 -6
- package/dist/auth/protected.d.ts +4 -4
- package/dist/auth/protected.d.ts.map +1 -1
- package/dist/auth/protected.js +10 -7
- package/dist/auth/protected.js.map +1 -1
- package/dist/cache/manager.d.ts +2 -0
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +21 -4
- package/dist/cache/manager.js.map +1 -1
- package/dist/checkpoint/artifacts/compressor.d.ts +5 -0
- package/dist/checkpoint/artifacts/compressor.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/compressor.js +24 -0
- package/dist/checkpoint/artifacts/compressor.js.map +1 -0
- package/dist/checkpoint/artifacts/decompress-worker.d.ts +2 -0
- package/dist/checkpoint/artifacts/decompress-worker.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/decompress-worker.js +31 -0
- package/dist/checkpoint/artifacts/decompress-worker.js.map +1 -0
- package/dist/checkpoint/artifacts/hasher.d.ts +2 -0
- package/dist/checkpoint/artifacts/hasher.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/hasher.js +7 -0
- package/dist/checkpoint/artifacts/hasher.js.map +1 -0
- package/dist/checkpoint/artifacts/manifest.d.ts +6 -0
- package/dist/checkpoint/artifacts/manifest.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/manifest.js +55 -0
- package/dist/checkpoint/artifacts/manifest.js.map +1 -0
- package/dist/checkpoint/artifacts/materializer.d.ts +16 -0
- package/dist/checkpoint/artifacts/materializer.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/materializer.js +168 -0
- package/dist/checkpoint/artifacts/materializer.js.map +1 -0
- package/dist/checkpoint/artifacts/packager.d.ts +12 -0
- package/dist/checkpoint/artifacts/packager.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/packager.js +82 -0
- package/dist/checkpoint/artifacts/packager.js.map +1 -0
- package/dist/checkpoint/artifacts/repository.d.ts +11 -0
- package/dist/checkpoint/artifacts/repository.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/repository.js +29 -0
- package/dist/checkpoint/artifacts/repository.js.map +1 -0
- package/dist/checkpoint/artifacts/store.d.ts +13 -0
- package/dist/checkpoint/artifacts/store.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/store.js +85 -0
- package/dist/checkpoint/artifacts/store.js.map +1 -0
- package/dist/checkpoint/artifacts/types.d.ts +21 -0
- package/dist/checkpoint/artifacts/types.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/types.js +2 -0
- package/dist/checkpoint/artifacts/types.js.map +1 -0
- package/dist/checkpoint/artifacts/worker-decompressor.d.ts +17 -0
- package/dist/checkpoint/artifacts/worker-decompressor.d.ts.map +1 -0
- package/dist/checkpoint/artifacts/worker-decompressor.js +148 -0
- package/dist/checkpoint/artifacts/worker-decompressor.js.map +1 -0
- package/dist/checkpoint/asset-store.d.ts +10 -0
- package/dist/checkpoint/asset-store.d.ts.map +1 -0
- package/dist/checkpoint/asset-store.js +46 -0
- package/dist/checkpoint/asset-store.js.map +1 -0
- package/dist/checkpoint/bundler.d.ts +15 -0
- package/dist/checkpoint/bundler.d.ts.map +1 -0
- package/dist/checkpoint/bundler.js +45 -0
- package/dist/checkpoint/bundler.js.map +1 -0
- package/dist/checkpoint/cli.d.ts +2 -0
- package/dist/checkpoint/cli.d.ts.map +1 -0
- package/dist/checkpoint/cli.js +157 -0
- package/dist/checkpoint/cli.js.map +1 -0
- package/dist/checkpoint/entrypoint-generator.d.ts +17 -0
- package/dist/checkpoint/entrypoint-generator.d.ts.map +1 -0
- package/dist/checkpoint/entrypoint-generator.js +251 -0
- package/dist/checkpoint/entrypoint-generator.js.map +1 -0
- package/dist/checkpoint/forwarder.d.ts +6 -0
- package/dist/checkpoint/forwarder.d.ts.map +1 -0
- package/dist/checkpoint/forwarder.js +74 -0
- package/dist/checkpoint/forwarder.js.map +1 -0
- package/dist/checkpoint/gateway.d.ts +11 -0
- package/dist/checkpoint/gateway.d.ts.map +1 -0
- package/dist/checkpoint/gateway.js +30 -0
- package/dist/checkpoint/gateway.js.map +1 -0
- package/dist/checkpoint/ipc.d.ts +12 -0
- package/dist/checkpoint/ipc.d.ts.map +1 -0
- package/dist/checkpoint/ipc.js +96 -0
- package/dist/checkpoint/ipc.js.map +1 -0
- package/dist/checkpoint/manager.d.ts +20 -0
- package/dist/checkpoint/manager.d.ts.map +1 -0
- package/dist/checkpoint/manager.js +214 -0
- package/dist/checkpoint/manager.js.map +1 -0
- package/dist/checkpoint/process-manager.d.ts +35 -0
- package/dist/checkpoint/process-manager.d.ts.map +1 -0
- package/dist/checkpoint/process-manager.js +203 -0
- package/dist/checkpoint/process-manager.js.map +1 -0
- package/dist/checkpoint/resolver.d.ts +25 -0
- package/dist/checkpoint/resolver.d.ts.map +1 -0
- package/dist/checkpoint/resolver.js +95 -0
- package/dist/checkpoint/resolver.js.map +1 -0
- package/dist/checkpoint/socket-path.d.ts +2 -0
- package/dist/checkpoint/socket-path.d.ts.map +1 -0
- package/dist/checkpoint/socket-path.js +51 -0
- package/dist/checkpoint/socket-path.js.map +1 -0
- package/dist/checkpoint/types.d.ts +54 -0
- package/dist/checkpoint/types.d.ts.map +1 -0
- package/dist/checkpoint/types.js +2 -0
- package/dist/checkpoint/types.js.map +1 -0
- package/dist/cli/index.js +10 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/option-resolution.d.ts +1 -1
- package/dist/cli/option-resolution.d.ts.map +1 -1
- package/dist/cli/option-resolution.js.map +1 -1
- package/dist/cli.js +3709 -328
- package/dist/core/config-loader.d.ts +1 -0
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +10 -2
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/router.d.ts +24 -3
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +398 -249
- package/dist/core/router.js.map +1 -1
- package/dist/core/server.d.ts +2 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +22 -8
- package/dist/core/server.js.map +1 -1
- package/dist/core/vector.d.ts +3 -0
- package/dist/core/vector.d.ts.map +1 -1
- package/dist/core/vector.js +51 -1
- package/dist/core/vector.js.map +1 -1
- package/dist/dev/route-scanner.d.ts.map +1 -1
- package/dist/dev/route-scanner.js +2 -1
- package/dist/dev/route-scanner.js.map +1 -1
- package/dist/http.d.ts +32 -7
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +144 -13
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +1297 -74
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1296 -73
- package/dist/middleware/manager.d.ts +3 -3
- package/dist/middleware/manager.d.ts.map +1 -1
- package/dist/middleware/manager.js +9 -8
- package/dist/middleware/manager.js.map +1 -1
- package/dist/openapi/docs-ui.d.ts.map +1 -1
- package/dist/openapi/docs-ui.js +1097 -61
- package/dist/openapi/docs-ui.js.map +1 -1
- package/dist/openapi/generator.d.ts +2 -1
- package/dist/openapi/generator.d.ts.map +1 -1
- package/dist/openapi/generator.js +240 -7
- package/dist/openapi/generator.js.map +1 -1
- package/dist/types/index.d.ts +71 -28
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +24 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +3 -2
- package/dist/utils/validation.js.map +1 -1
- package/package.json +2 -1
- package/src/auth/protected.ts +11 -8
- package/src/cache/manager.ts +23 -4
- package/src/checkpoint/artifacts/compressor.ts +30 -0
- package/src/checkpoint/artifacts/decompress-worker.ts +49 -0
- package/src/checkpoint/artifacts/hasher.ts +6 -0
- package/src/checkpoint/artifacts/manifest.ts +72 -0
- package/src/checkpoint/artifacts/materializer.ts +211 -0
- package/src/checkpoint/artifacts/packager.ts +100 -0
- package/src/checkpoint/artifacts/repository.ts +36 -0
- package/src/checkpoint/artifacts/store.ts +102 -0
- package/src/checkpoint/artifacts/types.ts +24 -0
- package/src/checkpoint/artifacts/worker-decompressor.ts +192 -0
- package/src/checkpoint/asset-store.ts +61 -0
- package/src/checkpoint/bundler.ts +64 -0
- package/src/checkpoint/cli.ts +177 -0
- package/src/checkpoint/entrypoint-generator.ts +275 -0
- package/src/checkpoint/forwarder.ts +84 -0
- package/src/checkpoint/gateway.ts +40 -0
- package/src/checkpoint/ipc.ts +107 -0
- package/src/checkpoint/manager.ts +254 -0
- package/src/checkpoint/process-manager.ts +250 -0
- package/src/checkpoint/resolver.ts +124 -0
- package/src/checkpoint/socket-path.ts +61 -0
- package/src/checkpoint/types.ts +63 -0
- package/src/cli/index.ts +11 -2
- package/src/cli/option-resolution.ts +5 -1
- package/src/core/config-loader.ts +11 -2
- package/src/core/router.ts +505 -264
- package/src/core/server.ts +36 -9
- package/src/core/vector.ts +60 -1
- package/src/dev/route-scanner.ts +2 -1
- package/src/http.ts +219 -19
- package/src/index.ts +3 -2
- package/src/middleware/manager.ts +10 -10
- package/src/openapi/docs-ui.ts +1097 -61
- package/src/openapi/generator.ts +265 -6
- package/src/types/index.ts +83 -30
- package/src/utils/validation.ts +5 -3
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,1807 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
|
+
|
|
15
|
+
// src/checkpoint/types.ts
|
|
16
|
+
var CHECKPOINT_FORMAT_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
// src/checkpoint/artifacts/compressor.ts
|
|
19
|
+
function compressBytes(input, codec = DEFAULT_ASSET_CODEC) {
|
|
20
|
+
const normalized = new Uint8Array(input);
|
|
21
|
+
switch (codec) {
|
|
22
|
+
case "none":
|
|
23
|
+
return normalized;
|
|
24
|
+
case "gzip":
|
|
25
|
+
return Bun.gzipSync(normalized);
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(`Unsupported compression codec: ${codec}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
var DEFAULT_ASSET_CODEC = "gzip";
|
|
31
|
+
|
|
32
|
+
// src/checkpoint/artifacts/hasher.ts
|
|
33
|
+
function sha256Hex(content) {
|
|
34
|
+
const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
35
|
+
const hashBuffer = Bun.SHA256.hash(bytes);
|
|
36
|
+
const hashBytes = new Uint8Array(hashBuffer.buffer, hashBuffer.byteOffset, hashBuffer.byteLength);
|
|
37
|
+
return Buffer.from(hashBytes).toString("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/checkpoint/artifacts/manifest.ts
|
|
41
|
+
import { basename, isAbsolute as isAbsolute2, relative as relative3 } from "path";
|
|
42
|
+
function normalizeLogicalPath(input) {
|
|
43
|
+
const normalizedInput = normalizeSlashes(input).trim();
|
|
44
|
+
const rel = isAbsolutePathPortable(normalizedInput) ? toPortableRelativePath(normalizedInput) : normalizedInput;
|
|
45
|
+
const cleaned = rel.split("/").filter((segment) => segment.length > 0 && segment !== "." && segment !== "..").join("/");
|
|
46
|
+
const sanitized = cleaned.replace(UNSAFE_SEGMENT_PATTERN, "_").replace(/^\/+/, "");
|
|
47
|
+
if (sanitized.length > 0) {
|
|
48
|
+
return sanitized;
|
|
49
|
+
}
|
|
50
|
+
const fallback = basename(normalizedInput).replace(UNSAFE_SEGMENT_PATTERN, "_");
|
|
51
|
+
return fallback.length > 0 ? `external/${fallback}` : "external/asset.bin";
|
|
52
|
+
}
|
|
53
|
+
function normalizeRelativePath(input) {
|
|
54
|
+
return normalizeSlashes(input).replace(/^\/+/, "");
|
|
55
|
+
}
|
|
56
|
+
function isAbsolutePathPortable(input) {
|
|
57
|
+
const normalized = normalizeSlashes(input).trim();
|
|
58
|
+
return isAbsolute2(normalized) || WINDOWS_DRIVE_ABS_PATTERN.test(normalized) || WINDOWS_UNC_ABS_PATTERN.test(input);
|
|
59
|
+
}
|
|
60
|
+
function computeAssetFingerprint(assets) {
|
|
61
|
+
const stable = assets.map((asset) => ({
|
|
62
|
+
type: asset.type,
|
|
63
|
+
logicalPath: asset.logicalPath,
|
|
64
|
+
contentHash: asset.contentHash ?? asset.hash,
|
|
65
|
+
blobHash: asset.blobHash ?? "",
|
|
66
|
+
blobPath: asset.blobPath ?? asset.storedPath,
|
|
67
|
+
codec: asset.codec ?? "none",
|
|
68
|
+
size: asset.contentSize ?? asset.size
|
|
69
|
+
})).sort((a, b) => `${a.type}:${a.logicalPath}:${a.contentHash}:${a.blobHash}`.localeCompare(`${b.type}:${b.logicalPath}:${b.contentHash}:${b.blobHash}`));
|
|
70
|
+
return JSON.stringify(stable);
|
|
71
|
+
}
|
|
72
|
+
function normalizeSlashes(value) {
|
|
73
|
+
return value.replace(/\\/g, "/");
|
|
74
|
+
}
|
|
75
|
+
function toPortableRelativePath(input) {
|
|
76
|
+
const normalized = normalizeSlashes(input);
|
|
77
|
+
if (WINDOWS_DRIVE_ABS_PATTERN.test(normalized)) {
|
|
78
|
+
return normalized.replace(/^[a-zA-Z]:/, "").replace(/^\/+/, "");
|
|
79
|
+
}
|
|
80
|
+
if (WINDOWS_UNC_ABS_PATTERN.test(input)) {
|
|
81
|
+
return normalizeSlashes(input).replace(/^\\\\[^\\]+\\[^\\]+/, "").replace(/^\/+/, "");
|
|
82
|
+
}
|
|
83
|
+
return normalizeSlashes(relative3(process.cwd(), normalized));
|
|
84
|
+
}
|
|
85
|
+
var UNSAFE_SEGMENT_PATTERN, WINDOWS_DRIVE_ABS_PATTERN, WINDOWS_UNC_ABS_PATTERN;
|
|
86
|
+
var init_manifest = __esm(() => {
|
|
87
|
+
UNSAFE_SEGMENT_PATTERN = /[^a-zA-Z0-9._/-]/g;
|
|
88
|
+
WINDOWS_DRIVE_ABS_PATTERN = /^[a-zA-Z]:[\\/]/;
|
|
89
|
+
WINDOWS_UNC_ABS_PATTERN = /^\\\\[^\\]+\\[^\\]+/;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// src/checkpoint/artifacts/store.ts
|
|
93
|
+
import { existsSync as existsSync4, promises as fs3 } from "fs";
|
|
94
|
+
import { join as join3 } from "path";
|
|
95
|
+
|
|
96
|
+
class CheckpointArtifactStore {
|
|
97
|
+
storageDir;
|
|
98
|
+
codec;
|
|
99
|
+
constructor(storageDir, options = {}) {
|
|
100
|
+
this.storageDir = storageDir;
|
|
101
|
+
this.codec = options.assetCodec ?? DEFAULT_ASSET_CODEC;
|
|
102
|
+
}
|
|
103
|
+
async addEmbedded(logicalPath, sourcePath) {
|
|
104
|
+
return this.addAsset("embedded", logicalPath, sourcePath);
|
|
105
|
+
}
|
|
106
|
+
async addSidecar(logicalPath, sourcePath) {
|
|
107
|
+
return this.addAsset("sidecar", logicalPath, sourcePath);
|
|
108
|
+
}
|
|
109
|
+
async collect(embeddedPaths, sidecarPaths) {
|
|
110
|
+
const records = [];
|
|
111
|
+
for (const sourcePath of embeddedPaths) {
|
|
112
|
+
records.push(await this.addEmbedded(sourcePath, sourcePath));
|
|
113
|
+
}
|
|
114
|
+
for (const sourcePath of sidecarPaths) {
|
|
115
|
+
records.push(await this.addSidecar(sourcePath, sourcePath));
|
|
116
|
+
}
|
|
117
|
+
return records;
|
|
118
|
+
}
|
|
119
|
+
async addAsset(type, logicalPath, sourcePath) {
|
|
120
|
+
const content = await fs3.readFile(sourcePath);
|
|
121
|
+
const contentBytes = new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
122
|
+
const contentHash = sha256Hex(contentBytes);
|
|
123
|
+
const compressed = compressBytes(contentBytes, this.codec);
|
|
124
|
+
const blobHash = sha256Hex(compressed);
|
|
125
|
+
const blobPath = normalizeRelativePath(join3(BLOB_DIR, `${blobHash}${this.codec === "gzip" ? ".gz" : ""}`));
|
|
126
|
+
const storedPath = join3(this.storageDir, blobPath);
|
|
127
|
+
await fs3.mkdir(join3(this.storageDir, BLOB_DIR), { recursive: true });
|
|
128
|
+
if (!existsSync4(storedPath)) {
|
|
129
|
+
await this.writeAtomically(storedPath, compressed);
|
|
130
|
+
} else {
|
|
131
|
+
const existing = await fs3.readFile(storedPath);
|
|
132
|
+
const existingBytes = new Uint8Array(existing.buffer, existing.byteOffset, existing.byteLength);
|
|
133
|
+
if (sha256Hex(existingBytes) !== blobHash) {
|
|
134
|
+
await this.writeAtomically(storedPath, compressed);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
type,
|
|
139
|
+
logicalPath: normalizeLogicalPath(logicalPath),
|
|
140
|
+
storedPath,
|
|
141
|
+
hash: contentHash,
|
|
142
|
+
size: content.byteLength,
|
|
143
|
+
contentHash,
|
|
144
|
+
contentSize: content.byteLength,
|
|
145
|
+
blobHash,
|
|
146
|
+
blobSize: compressed.byteLength,
|
|
147
|
+
blobPath,
|
|
148
|
+
codec: this.codec
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async writeAtomically(path, bytes) {
|
|
152
|
+
const tempPath = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
153
|
+
await fs3.writeFile(tempPath, bytes);
|
|
154
|
+
try {
|
|
155
|
+
await fs3.rename(tempPath, path);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (!isAlreadyExists(error)) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
await fs3.rm(path, { force: true });
|
|
161
|
+
await fs3.rename(tempPath, path);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isAlreadyExists(error) {
|
|
166
|
+
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const code = error.code;
|
|
170
|
+
return code === "EEXIST" || code === "EPERM";
|
|
171
|
+
}
|
|
172
|
+
var BLOB_DIR = "_assets/blobs";
|
|
173
|
+
var init_store = __esm(() => {
|
|
174
|
+
init_manifest();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// src/checkpoint/asset-store.ts
|
|
178
|
+
import { promises as fs4 } from "fs";
|
|
179
|
+
|
|
180
|
+
class AssetStore {
|
|
181
|
+
artifactStore;
|
|
182
|
+
constructor(storageDir) {
|
|
183
|
+
this.artifactStore = new CheckpointArtifactStore(storageDir);
|
|
184
|
+
}
|
|
185
|
+
async addEmbedded(logicalPath, sourcePath) {
|
|
186
|
+
const content = await fs4.readFile(sourcePath);
|
|
187
|
+
if (content.byteLength > EMBEDDED_PER_FILE_BUDGET) {
|
|
188
|
+
throw new Error(`Embedded asset "${logicalPath}" is ${formatBytes(content.byteLength)} \u2014 exceeds ${formatBytes(EMBEDDED_PER_FILE_BUDGET)} per-file budget. Use sidecar instead.`);
|
|
189
|
+
}
|
|
190
|
+
return await this.artifactStore.addEmbedded(logicalPath, sourcePath);
|
|
191
|
+
}
|
|
192
|
+
async addSidecar(logicalPath, sourcePath) {
|
|
193
|
+
return await this.artifactStore.addSidecar(logicalPath, sourcePath);
|
|
194
|
+
}
|
|
195
|
+
async collect(embeddedPaths, sidecarPaths) {
|
|
196
|
+
const records = [];
|
|
197
|
+
for (const p of embeddedPaths) {
|
|
198
|
+
records.push(await this.addEmbedded(p, p));
|
|
199
|
+
}
|
|
200
|
+
for (const p of sidecarPaths) {
|
|
201
|
+
records.push(await this.addSidecar(p, p));
|
|
202
|
+
}
|
|
203
|
+
return records;
|
|
204
|
+
}
|
|
205
|
+
validateBudgets(records) {
|
|
206
|
+
const embeddedTotal = records.filter((r) => r.type === "embedded").reduce((acc, r) => acc + (r.contentSize ?? r.size), 0);
|
|
207
|
+
if (embeddedTotal > EMBEDDED_TOTAL_BUDGET) {
|
|
208
|
+
throw new Error(`Total embedded asset size ${formatBytes(embeddedTotal)} exceeds ${formatBytes(EMBEDDED_TOTAL_BUDGET)} budget.`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function formatBytes(bytes) {
|
|
213
|
+
if (bytes < 1024)
|
|
214
|
+
return `${bytes} B`;
|
|
215
|
+
if (bytes < 1024 * 1024)
|
|
216
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
217
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
218
|
+
}
|
|
219
|
+
var EMBEDDED_PER_FILE_BUDGET, EMBEDDED_TOTAL_BUDGET;
|
|
220
|
+
var init_asset_store = __esm(() => {
|
|
221
|
+
init_store();
|
|
222
|
+
EMBEDDED_PER_FILE_BUDGET = 64 * 1024;
|
|
223
|
+
EMBEDDED_TOTAL_BUDGET = 512 * 1024;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// src/checkpoint/bundler.ts
|
|
227
|
+
import { existsSync as existsSync5 } from "fs";
|
|
228
|
+
import { join as join4 } from "path";
|
|
229
|
+
|
|
230
|
+
class CheckpointBundler {
|
|
231
|
+
async bundle(options) {
|
|
232
|
+
const outfile = options.outputFile ?? "checkpoint.js";
|
|
233
|
+
const result = await Bun.build({
|
|
234
|
+
entrypoints: [options.entrypointPath],
|
|
235
|
+
outdir: options.outputDir,
|
|
236
|
+
target: "bun",
|
|
237
|
+
format: "esm",
|
|
238
|
+
minify: true,
|
|
239
|
+
naming: { entry: outfile }
|
|
240
|
+
});
|
|
241
|
+
if (!result.success) {
|
|
242
|
+
const messages = result.logs.map((l) => l.message ?? String(l)).join(`
|
|
243
|
+
`);
|
|
244
|
+
throw new Error(`Checkpoint bundle failed:
|
|
245
|
+
${messages}`);
|
|
246
|
+
}
|
|
247
|
+
const outputPath = join4(options.outputDir, outfile);
|
|
248
|
+
if (!existsSync5(outputPath)) {
|
|
249
|
+
const actualOutput = result.outputs.find((o) => o.kind === "entry-point");
|
|
250
|
+
if (actualOutput) {
|
|
251
|
+
const actualPath = actualOutput.path;
|
|
252
|
+
if (existsSync5(actualPath)) {
|
|
253
|
+
return this.hashFile(actualPath);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`Bundle output not found at expected path: ${outputPath}`);
|
|
257
|
+
}
|
|
258
|
+
return this.hashFile(outputPath);
|
|
259
|
+
}
|
|
260
|
+
async hashFile(path) {
|
|
261
|
+
const file = Bun.file(path);
|
|
262
|
+
const content = await file.arrayBuffer();
|
|
263
|
+
const hashBuffer = Bun.SHA256.hash(new Uint8Array(content));
|
|
264
|
+
const hashBytes = new Uint8Array(hashBuffer.buffer, hashBuffer.byteOffset, hashBuffer.byteLength);
|
|
265
|
+
const hash = Buffer.from(hashBytes).toString("hex");
|
|
266
|
+
return {
|
|
267
|
+
outputPath: path,
|
|
268
|
+
hash,
|
|
269
|
+
size: content.byteLength
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
var init_bundler = () => {};
|
|
274
|
+
|
|
275
|
+
// src/checkpoint/entrypoint-generator.ts
|
|
276
|
+
import { existsSync as existsSync6, promises as fs5 } from "fs";
|
|
277
|
+
import { join as join5, relative as relative4, sep as sep2 } from "path";
|
|
278
|
+
|
|
279
|
+
class CheckpointEntrypointGenerator {
|
|
280
|
+
discoveredRoutes = [];
|
|
281
|
+
async generate(options) {
|
|
282
|
+
const routeFiles = await this.scanRouteFiles(options.routesDir);
|
|
283
|
+
const source = this.buildSource(routeFiles, options);
|
|
284
|
+
const outputPath = join5(options.outputDir, "entrypoint.ts");
|
|
285
|
+
await fs5.writeFile(outputPath, source, "utf-8");
|
|
286
|
+
return outputPath;
|
|
287
|
+
}
|
|
288
|
+
getDiscoveredRoutes() {
|
|
289
|
+
return this.discoveredRoutes;
|
|
290
|
+
}
|
|
291
|
+
async scanRouteFiles(routesDir) {
|
|
292
|
+
if (!existsSync6(routesDir)) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
const files = [];
|
|
296
|
+
await this.walkDir(routesDir, files);
|
|
297
|
+
return files;
|
|
298
|
+
}
|
|
299
|
+
async walkDir(dir, files) {
|
|
300
|
+
const entries = await fs5.readdir(dir);
|
|
301
|
+
for (const entry of entries) {
|
|
302
|
+
const fullPath = join5(dir, entry);
|
|
303
|
+
const stats = await fs5.stat(fullPath);
|
|
304
|
+
if (stats.isDirectory()) {
|
|
305
|
+
await this.walkDir(fullPath, files);
|
|
306
|
+
} else if ((entry.endsWith(".ts") || entry.endsWith(".js")) && !entry.endsWith(".test.ts") && !entry.endsWith(".test.js") && !entry.endsWith(".spec.ts") && !entry.endsWith(".spec.js") && !entry.endsWith(".d.ts")) {
|
|
307
|
+
files.push(fullPath);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
buildSource(routeFiles, options) {
|
|
312
|
+
this.discoveredRoutes = [];
|
|
313
|
+
const imports = [];
|
|
314
|
+
const registrations = [];
|
|
315
|
+
for (const [i, file] of routeFiles.entries()) {
|
|
316
|
+
const varName = `routeModule_${i}`;
|
|
317
|
+
const importSpecifier = this.toImportSpecifier(file, options.outputDir);
|
|
318
|
+
imports.push(`import * as ${varName} from ${JSON.stringify(importSpecifier)};`);
|
|
319
|
+
registrations.push(` registerModule(${varName});`);
|
|
320
|
+
const routePath = relative4(options.routesDir, file).replace(/\.(ts|js)$/, "").split(sep2).join("/");
|
|
321
|
+
this.discoveredRoutes.push({
|
|
322
|
+
method: "*",
|
|
323
|
+
path: `/${routePath}`
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return `// Checkpoint entrypoint \u2014 auto-generated for version ${options.version}
|
|
327
|
+
// DO NOT EDIT: This file is generated by Vector checkpoint publish.
|
|
328
|
+
|
|
329
|
+
${imports.join(`
|
|
330
|
+
`)}
|
|
331
|
+
|
|
332
|
+
const socketPath = process.env.VECTOR_CHECKPOINT_SOCKET ?? '${options.socketPath}';
|
|
333
|
+
const checkpointContextHeader = 'x-vector-checkpoint-context';
|
|
334
|
+
|
|
335
|
+
// Route registration helper
|
|
336
|
+
type RouteDefinition = {
|
|
337
|
+
entry: { method: string; path: string };
|
|
338
|
+
options: { method: string; path: string; [key: string]: any };
|
|
339
|
+
handler: (ctx: any) => any;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
function isRouteDefinition(value: unknown): value is RouteDefinition {
|
|
343
|
+
return (
|
|
344
|
+
value !== null &&
|
|
345
|
+
typeof value === 'object' &&
|
|
346
|
+
'entry' in value &&
|
|
347
|
+
'options' in value &&
|
|
348
|
+
'handler' in value &&
|
|
349
|
+
typeof (value as any).handler === 'function'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const routeTable: Record<string, Record<string, (req: Request) => Response | Promise<Response>>> = {};
|
|
354
|
+
|
|
355
|
+
function parseCheckpointContext(req: Request): Record<string, unknown> {
|
|
356
|
+
const encoded = req.headers.get(checkpointContextHeader);
|
|
357
|
+
if (!encoded) {
|
|
358
|
+
return {};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const json = Buffer.from(encoded, 'base64url').toString('utf-8');
|
|
363
|
+
const parsed = JSON.parse(json);
|
|
364
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
return parsed as Record<string, unknown>;
|
|
368
|
+
} catch {
|
|
369
|
+
return {};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function parseQuery(url: URL): Record<string, string | string[]> {
|
|
374
|
+
const query: Record<string, string | string[]> = {};
|
|
375
|
+
for (const [key, value] of url.searchParams) {
|
|
376
|
+
if (key in query) {
|
|
377
|
+
const existing = query[key];
|
|
378
|
+
if (Array.isArray(existing)) {
|
|
379
|
+
existing.push(value);
|
|
380
|
+
} else {
|
|
381
|
+
query[key] = [existing as string, value];
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
query[key] = value;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return query;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function parseCookies(cookieHeader: string | null): Record<string, string> {
|
|
391
|
+
const cookies: Record<string, string> = {};
|
|
392
|
+
if (!cookieHeader) {
|
|
393
|
+
return cookies;
|
|
394
|
+
}
|
|
395
|
+
for (const pair of cookieHeader.split(';')) {
|
|
396
|
+
const idx = pair.indexOf('=');
|
|
397
|
+
if (idx > 0) {
|
|
398
|
+
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return cookies;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function extractRouteParams(req: Request): Record<string, string> {
|
|
405
|
+
const nativeParams = (req as Request & { params?: Record<string, string> }).params;
|
|
406
|
+
if (nativeParams && typeof nativeParams === 'object' && !Array.isArray(nativeParams)) {
|
|
407
|
+
return nativeParams;
|
|
408
|
+
}
|
|
409
|
+
return {};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function createContext(req: Request): Record<string, unknown> {
|
|
413
|
+
const ctx: Record<string, unknown> = Object.create(null);
|
|
414
|
+
setContextField(ctx, 'request', req);
|
|
415
|
+
|
|
416
|
+
// Initialize always-present VectorContext fields with defaults derived from the request
|
|
417
|
+
setContextField(ctx, 'params', extractRouteParams(req));
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
setContextField(ctx, 'query', parseQuery(new URL(req.url)));
|
|
421
|
+
} catch {
|
|
422
|
+
setContextField(ctx, 'query', {});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setContextField(ctx, 'cookies', parseCookies(req.headers.get('cookie')));
|
|
426
|
+
setContextField(ctx, 'metadata', {});
|
|
427
|
+
|
|
428
|
+
const checkpointContext = parseCheckpointContext(req);
|
|
429
|
+
const allowedCheckpointKeys = ['metadata', 'content', 'validatedInput', 'authUser'] as const;
|
|
430
|
+
for (const key of allowedCheckpointKeys) {
|
|
431
|
+
if (!Object.prototype.hasOwnProperty.call(checkpointContext, key)) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const value = checkpointContext[key];
|
|
435
|
+
setContextField(ctx, key, value);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return ctx;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function setContextField(target: Record<string, unknown>, key: string, value: unknown) {
|
|
442
|
+
const ownDescriptor = Object.getOwnPropertyDescriptor(target as object, key);
|
|
443
|
+
if (ownDescriptor && ownDescriptor.writable === false && typeof ownDescriptor.set !== 'function') {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
target[key] = value;
|
|
449
|
+
return;
|
|
450
|
+
} catch {
|
|
451
|
+
// Fall back to own-property define when inherited Request field is readonly.
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
Object.defineProperty(target, key, {
|
|
456
|
+
value,
|
|
457
|
+
writable: true,
|
|
458
|
+
configurable: true,
|
|
459
|
+
enumerable: true,
|
|
460
|
+
});
|
|
461
|
+
} catch {
|
|
462
|
+
// Ignore non-extensible context edge cases.
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function addToRouteTable(method: string, path: string, handler: (ctx: any) => any) {
|
|
467
|
+
if (!routeTable[path]) {
|
|
468
|
+
routeTable[path] = Object.create(null);
|
|
469
|
+
}
|
|
470
|
+
routeTable[path][method.toUpperCase()] = async (req: Request) => {
|
|
471
|
+
const ctx = createContext(req);
|
|
472
|
+
const result = await handler(ctx);
|
|
473
|
+
if (result instanceof Response) return result;
|
|
474
|
+
return Response.json(result);
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function registerModule(mod: Record<string, unknown>) {
|
|
479
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
480
|
+
if (isRouteDefinition(value)) {
|
|
481
|
+
addToRouteTable(value.options.method, value.options.path, value.handler);
|
|
482
|
+
} else if (name === 'default' && typeof value === 'function') {
|
|
483
|
+
// Default export functions need path context \u2014 skip for now
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
${registrations.join(`
|
|
489
|
+
`)}
|
|
490
|
+
|
|
491
|
+
// Health check endpoint for parent process
|
|
492
|
+
routeTable['/_vector/health'] = {
|
|
493
|
+
GET: () => Response.json({ status: 'ok', version: '${options.version}' }),
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const server = Bun.serve({
|
|
497
|
+
unix: socketPath,
|
|
498
|
+
routes: routeTable,
|
|
499
|
+
fetch(req: Request) {
|
|
500
|
+
return Response.json(
|
|
501
|
+
{ error: true, message: 'Not Found', statusCode: 404 },
|
|
502
|
+
{ status: 404 }
|
|
503
|
+
);
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Signal readiness to parent process
|
|
508
|
+
process.stdout.write('READY\\n');
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
toImportSpecifier(filePath, outputDir) {
|
|
512
|
+
const normalized = relative4(outputDir, filePath).split(sep2).join("/");
|
|
513
|
+
if (normalized.startsWith(".") || normalized.startsWith("/")) {
|
|
514
|
+
return normalized;
|
|
515
|
+
}
|
|
516
|
+
return `./${normalized}`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
var init_entrypoint_generator = () => {};
|
|
520
|
+
|
|
521
|
+
// src/checkpoint/artifacts/packager.ts
|
|
522
|
+
import { promises as fs6 } from "fs";
|
|
523
|
+
import { join as join6, relative as relative5 } from "path";
|
|
524
|
+
|
|
525
|
+
class CheckpointPackager {
|
|
526
|
+
storageDir;
|
|
527
|
+
codec;
|
|
528
|
+
constructor(storageDir, codec = "gzip") {
|
|
529
|
+
this.storageDir = storageDir;
|
|
530
|
+
this.codec = codec;
|
|
531
|
+
}
|
|
532
|
+
async packageVersion(version) {
|
|
533
|
+
const versionDir = join6(this.storageDir, version);
|
|
534
|
+
const archiveRelPath = join6(ARCHIVE_DIR, `${version}${this.archiveSuffix()}`).replace(/\\/g, "/");
|
|
535
|
+
const archivePath = join6(this.storageDir, archiveRelPath);
|
|
536
|
+
await fs6.mkdir(join6(this.storageDir, ARCHIVE_DIR), { recursive: true });
|
|
537
|
+
const files = await collectFiles(versionDir);
|
|
538
|
+
const archiveBytes = await this.buildArchiveBytes(versionDir, archivePath, files);
|
|
539
|
+
return {
|
|
540
|
+
archivePath: archiveRelPath,
|
|
541
|
+
archiveHash: sha256Hex(archiveBytes),
|
|
542
|
+
archiveSize: archiveBytes.byteLength,
|
|
543
|
+
codec: this.codec
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
async buildArchiveBytes(versionDir, archivePath, files) {
|
|
547
|
+
const ArchiveCtor = Bun.Archive;
|
|
548
|
+
if (typeof ArchiveCtor === "function") {
|
|
549
|
+
const archiveEntries = Object.fromEntries(files.map((filePath) => {
|
|
550
|
+
const rel = relative5(versionDir, filePath).replace(/\\/g, "/");
|
|
551
|
+
return [rel, Bun.file(filePath)];
|
|
552
|
+
}));
|
|
553
|
+
const archive = new ArchiveCtor(archiveEntries);
|
|
554
|
+
const tarBytes = new Uint8Array(await archive.bytes());
|
|
555
|
+
const archiveBytes = this.codec === "gzip" ? Bun.gzipSync(tarBytes) : tarBytes;
|
|
556
|
+
await Bun.write(archivePath, archiveBytes);
|
|
557
|
+
return archiveBytes;
|
|
558
|
+
}
|
|
559
|
+
await this.buildArchiveWithTar(versionDir, archivePath, files);
|
|
560
|
+
const bytes = await fs6.readFile(archivePath);
|
|
561
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
562
|
+
}
|
|
563
|
+
async buildArchiveWithTar(versionDir, archivePath, files) {
|
|
564
|
+
const relFiles = files.map((filePath) => relative5(versionDir, filePath));
|
|
565
|
+
if (relFiles.length === 0) {
|
|
566
|
+
throw new Error(`Cannot package checkpoint: no files found in "${versionDir}"`);
|
|
567
|
+
}
|
|
568
|
+
const tarArgs = this.codec === "gzip" ? ["-czf", archivePath] : ["-cf", archivePath];
|
|
569
|
+
const proc = Bun.spawn(["tar", ...tarArgs, "-C", versionDir, ...relFiles], {
|
|
570
|
+
stdout: "pipe",
|
|
571
|
+
stderr: "pipe"
|
|
572
|
+
});
|
|
573
|
+
const exitCode = await proc.exited;
|
|
574
|
+
if (exitCode === 0) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const stderr = await new Response(proc.stderr).text();
|
|
578
|
+
throw new Error(`Failed to package checkpoint archive with tar (exit ${exitCode}): ${stderr.trim()}`);
|
|
579
|
+
}
|
|
580
|
+
archiveSuffix() {
|
|
581
|
+
return this.codec === "gzip" ? ".tar.gz" : ".tar";
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function collectFiles(root) {
|
|
585
|
+
const files = [];
|
|
586
|
+
await walk(root, files);
|
|
587
|
+
return files.filter((filePath) => relative5(root, filePath).replace(/\\/g, "/") !== "manifest.json");
|
|
588
|
+
}
|
|
589
|
+
async function walk(dir, files) {
|
|
590
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
591
|
+
for (const entry of entries) {
|
|
592
|
+
const fullPath = join6(dir, entry.name);
|
|
593
|
+
if (entry.isDirectory()) {
|
|
594
|
+
await walk(fullPath, files);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (entry.isFile()) {
|
|
598
|
+
files.push(fullPath);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
var ARCHIVE_DIR = "_archives";
|
|
603
|
+
var init_packager = () => {};
|
|
604
|
+
|
|
605
|
+
// src/checkpoint/socket-path.ts
|
|
606
|
+
import { createHash } from "crypto";
|
|
607
|
+
import { tmpdir } from "os";
|
|
608
|
+
import { isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
|
|
609
|
+
function toAbsolute(dir) {
|
|
610
|
+
return isAbsolute3(dir) ? dir : resolve3(dir);
|
|
611
|
+
}
|
|
612
|
+
function unixSocketWithinLimit(path) {
|
|
613
|
+
return Buffer.byteLength(path, "utf8") <= UNIX_SOCKET_PATH_MAX_BYTES;
|
|
614
|
+
}
|
|
615
|
+
function socketFileName(storageDir, version) {
|
|
616
|
+
const versionLabel = version.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-+|-+$)/g, "").slice(0, 12);
|
|
617
|
+
const digest = createHash("sha256").update(storageDir).update("\x00").update(version).digest("hex").slice(0, 16);
|
|
618
|
+
const label = versionLabel.length > 0 ? `${versionLabel}-` : "";
|
|
619
|
+
return `vector-${label}${digest}.sock`;
|
|
620
|
+
}
|
|
621
|
+
function candidateSocketRoots() {
|
|
622
|
+
const roots = new Set;
|
|
623
|
+
const override = process.env.VECTOR_CHECKPOINT_SOCKET_DIR?.trim();
|
|
624
|
+
if (override) {
|
|
625
|
+
roots.add(toAbsolute(override));
|
|
626
|
+
}
|
|
627
|
+
if (process.platform !== "win32") {
|
|
628
|
+
roots.add("/tmp");
|
|
629
|
+
roots.add("/private/tmp");
|
|
630
|
+
}
|
|
631
|
+
roots.add(toAbsolute(tmpdir()));
|
|
632
|
+
return [...roots];
|
|
633
|
+
}
|
|
634
|
+
function resolveCheckpointSocketPath(storageDir, version) {
|
|
635
|
+
const defaultPath = join7(storageDir, version, CHECKPOINT_SOCKET_FILE);
|
|
636
|
+
if (process.platform === "win32" || unixSocketWithinLimit(defaultPath)) {
|
|
637
|
+
return defaultPath;
|
|
638
|
+
}
|
|
639
|
+
const fileName = socketFileName(storageDir, version);
|
|
640
|
+
for (const root of candidateSocketRoots()) {
|
|
641
|
+
const candidate = join7(root, fileName);
|
|
642
|
+
if (unixSocketWithinLimit(candidate)) {
|
|
643
|
+
return candidate;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return join7("/tmp", fileName);
|
|
647
|
+
}
|
|
648
|
+
var CHECKPOINT_SOCKET_FILE = "run.sock", UNIX_SOCKET_PATH_MAX_BYTES = 103;
|
|
649
|
+
var init_socket_path = () => {};
|
|
650
|
+
|
|
651
|
+
// src/checkpoint/manager.ts
|
|
652
|
+
var exports_manager = {};
|
|
653
|
+
__export(exports_manager, {
|
|
654
|
+
CheckpointManager: () => CheckpointManager
|
|
655
|
+
});
|
|
656
|
+
import { existsSync as existsSync7, promises as fs7 } from "fs";
|
|
657
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
658
|
+
function inferLegacyAssetCodec(asset) {
|
|
659
|
+
if (asset.codec) {
|
|
660
|
+
return asset.codec;
|
|
661
|
+
}
|
|
662
|
+
const rawPath = (asset.blobPath ?? asset.storedPath ?? "").trim().toLowerCase();
|
|
663
|
+
return rawPath.endsWith(".gz") ? "gzip" : "none";
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
class CheckpointManager {
|
|
667
|
+
storageDir;
|
|
668
|
+
maxCheckpoints;
|
|
669
|
+
constructor(config = {}) {
|
|
670
|
+
this.storageDir = resolve4(process.cwd(), config.storageDir ?? DEFAULT_STORAGE_DIR);
|
|
671
|
+
this.maxCheckpoints = config.maxCheckpoints ?? 10;
|
|
672
|
+
}
|
|
673
|
+
getStorageDir() {
|
|
674
|
+
return this.storageDir;
|
|
675
|
+
}
|
|
676
|
+
versionDir(version) {
|
|
677
|
+
return join8(this.storageDir, version);
|
|
678
|
+
}
|
|
679
|
+
socketPath(version) {
|
|
680
|
+
return resolveCheckpointSocketPath(this.storageDir, version);
|
|
681
|
+
}
|
|
682
|
+
async ensureStorageDir() {
|
|
683
|
+
await fs7.mkdir(this.storageDir, { recursive: true });
|
|
684
|
+
}
|
|
685
|
+
async publish(options) {
|
|
686
|
+
await this.ensureStorageDir();
|
|
687
|
+
const versionDir = this.versionDir(options.version);
|
|
688
|
+
await fs7.mkdir(versionDir, { recursive: true });
|
|
689
|
+
const generator = new CheckpointEntrypointGenerator;
|
|
690
|
+
const entrypointPath = await generator.generate({
|
|
691
|
+
version: options.version,
|
|
692
|
+
outputDir: versionDir,
|
|
693
|
+
routesDir: resolve4(process.cwd(), options.routesDir),
|
|
694
|
+
socketPath: this.socketPath(options.version)
|
|
695
|
+
});
|
|
696
|
+
const bundler = new CheckpointBundler;
|
|
697
|
+
const bundleResult = await bundler.bundle({
|
|
698
|
+
entrypointPath,
|
|
699
|
+
outputDir: versionDir
|
|
700
|
+
});
|
|
701
|
+
const assetStore = new AssetStore(this.storageDir);
|
|
702
|
+
const assets = await assetStore.collect(options.embeddedAssetPaths ?? [], options.sidecarAssetPaths ?? []);
|
|
703
|
+
assetStore.validateBudgets(assets);
|
|
704
|
+
const manifest = {
|
|
705
|
+
formatVersion: CHECKPOINT_FORMAT_VERSION,
|
|
706
|
+
version: options.version,
|
|
707
|
+
createdAt: new Date().toISOString(),
|
|
708
|
+
entrypoint: "checkpoint.js",
|
|
709
|
+
routes: generator.getDiscoveredRoutes(),
|
|
710
|
+
assets,
|
|
711
|
+
bundleHash: bundleResult.hash,
|
|
712
|
+
bundleSize: bundleResult.size,
|
|
713
|
+
checkpointArchivePath: undefined,
|
|
714
|
+
checkpointArchiveHash: undefined,
|
|
715
|
+
checkpointArchiveSize: undefined,
|
|
716
|
+
checkpointArchiveCodec: undefined
|
|
717
|
+
};
|
|
718
|
+
await this.writeManifest(options.version, manifest);
|
|
719
|
+
try {
|
|
720
|
+
await fs7.unlink(entrypointPath);
|
|
721
|
+
} catch {}
|
|
722
|
+
const packager = new CheckpointPackager(this.storageDir);
|
|
723
|
+
const archive = await packager.packageVersion(options.version);
|
|
724
|
+
manifest.checkpointArchivePath = archive.archivePath;
|
|
725
|
+
manifest.checkpointArchiveHash = archive.archiveHash;
|
|
726
|
+
manifest.checkpointArchiveSize = archive.archiveSize;
|
|
727
|
+
manifest.checkpointArchiveCodec = archive.codec;
|
|
728
|
+
await this.writeManifest(options.version, manifest);
|
|
729
|
+
await this.pruneOld();
|
|
730
|
+
return manifest;
|
|
731
|
+
}
|
|
732
|
+
async readManifest(version) {
|
|
733
|
+
const manifestPath = join8(this.versionDir(version), "manifest.json");
|
|
734
|
+
const content = await fs7.readFile(manifestPath, "utf-8");
|
|
735
|
+
return this.normalizeManifest(JSON.parse(content));
|
|
736
|
+
}
|
|
737
|
+
async writeManifest(version, manifest) {
|
|
738
|
+
const manifestPath = join8(this.versionDir(version), "manifest.json");
|
|
739
|
+
await fs7.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
740
|
+
}
|
|
741
|
+
async listVersions() {
|
|
742
|
+
if (!existsSync7(this.storageDir)) {
|
|
743
|
+
return [];
|
|
744
|
+
}
|
|
745
|
+
const entries = await fs7.readdir(this.storageDir);
|
|
746
|
+
const manifests = [];
|
|
747
|
+
for (const entry of entries) {
|
|
748
|
+
const manifestPath = join8(this.storageDir, entry, "manifest.json");
|
|
749
|
+
if (existsSync7(manifestPath)) {
|
|
750
|
+
try {
|
|
751
|
+
const content = await fs7.readFile(manifestPath, "utf-8");
|
|
752
|
+
manifests.push(this.normalizeManifest(JSON.parse(content)));
|
|
753
|
+
} catch {}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
manifests.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
757
|
+
return manifests;
|
|
758
|
+
}
|
|
759
|
+
async getActive() {
|
|
760
|
+
const pointerPath = join8(this.storageDir, ACTIVE_POINTER_FILE);
|
|
761
|
+
if (!existsSync7(pointerPath)) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
const content = await fs7.readFile(pointerPath, "utf-8");
|
|
766
|
+
return JSON.parse(content);
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async setActive(version) {
|
|
772
|
+
const versionDir = this.versionDir(version);
|
|
773
|
+
if (!existsSync7(versionDir)) {
|
|
774
|
+
throw new Error(`Checkpoint version ${version} does not exist`);
|
|
775
|
+
}
|
|
776
|
+
const manifestPath = join8(versionDir, "manifest.json");
|
|
777
|
+
if (!existsSync7(manifestPath)) {
|
|
778
|
+
throw new Error(`Checkpoint version ${version} has no manifest`);
|
|
779
|
+
}
|
|
780
|
+
await this.ensureStorageDir();
|
|
781
|
+
const pointer = {
|
|
782
|
+
version,
|
|
783
|
+
activatedAt: new Date().toISOString()
|
|
784
|
+
};
|
|
785
|
+
const pointerPath = join8(this.storageDir, ACTIVE_POINTER_FILE);
|
|
786
|
+
await fs7.writeFile(pointerPath, JSON.stringify(pointer, null, 2), "utf-8");
|
|
787
|
+
}
|
|
788
|
+
async remove(version) {
|
|
789
|
+
const active = await this.getActive();
|
|
790
|
+
if (active?.version === version) {
|
|
791
|
+
throw new Error(`Cannot remove active checkpoint version ${version}. Rollback to a different version first.`);
|
|
792
|
+
}
|
|
793
|
+
const versionDir = this.versionDir(version);
|
|
794
|
+
if (!existsSync7(versionDir)) {
|
|
795
|
+
throw new Error(`Checkpoint version ${version} does not exist`);
|
|
796
|
+
}
|
|
797
|
+
await fs7.rm(versionDir, { recursive: true, force: true });
|
|
798
|
+
}
|
|
799
|
+
async pruneOld() {
|
|
800
|
+
if (this.maxCheckpoints <= 0)
|
|
801
|
+
return;
|
|
802
|
+
const manifests = await this.listVersions();
|
|
803
|
+
if (manifests.length <= this.maxCheckpoints)
|
|
804
|
+
return;
|
|
805
|
+
const active = await this.getActive();
|
|
806
|
+
const toRemove = manifests.slice(this.maxCheckpoints);
|
|
807
|
+
for (const manifest of toRemove) {
|
|
808
|
+
if (active?.version === manifest.version)
|
|
809
|
+
continue;
|
|
810
|
+
try {
|
|
811
|
+
const versionDir = this.versionDir(manifest.version);
|
|
812
|
+
await fs7.rm(versionDir, { recursive: true, force: true });
|
|
813
|
+
} catch {}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
normalizeManifest(manifest) {
|
|
817
|
+
const normalizedAssets = (manifest.assets ?? []).map((asset) => ({
|
|
818
|
+
...asset,
|
|
819
|
+
contentHash: asset.contentHash ?? asset.hash,
|
|
820
|
+
contentSize: asset.contentSize ?? asset.size,
|
|
821
|
+
blobPath: asset.blobPath ?? asset.storedPath,
|
|
822
|
+
blobHash: asset.blobHash,
|
|
823
|
+
blobSize: asset.blobSize,
|
|
824
|
+
codec: inferLegacyAssetCodec(asset)
|
|
825
|
+
}));
|
|
826
|
+
return {
|
|
827
|
+
formatVersion: manifest.formatVersion ?? CHECKPOINT_FORMAT_VERSION,
|
|
828
|
+
version: manifest.version ?? "unknown",
|
|
829
|
+
createdAt: manifest.createdAt ?? new Date(0).toISOString(),
|
|
830
|
+
entrypoint: manifest.entrypoint ?? "checkpoint.js",
|
|
831
|
+
routes: manifest.routes ?? [],
|
|
832
|
+
assets: normalizedAssets,
|
|
833
|
+
bundleHash: manifest.bundleHash ?? "",
|
|
834
|
+
bundleSize: manifest.bundleSize ?? 0,
|
|
835
|
+
checkpointArchivePath: manifest.checkpointArchivePath,
|
|
836
|
+
checkpointArchiveHash: manifest.checkpointArchiveHash,
|
|
837
|
+
checkpointArchiveSize: manifest.checkpointArchiveSize,
|
|
838
|
+
checkpointArchiveCodec: manifest.checkpointArchiveCodec
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
var DEFAULT_STORAGE_DIR = ".vector/checkpoints", ACTIVE_POINTER_FILE = "active.json";
|
|
843
|
+
var init_manager = __esm(() => {
|
|
844
|
+
init_asset_store();
|
|
845
|
+
init_bundler();
|
|
846
|
+
init_entrypoint_generator();
|
|
847
|
+
init_packager();
|
|
848
|
+
init_socket_path();
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// src/checkpoint/ipc.ts
|
|
852
|
+
function parseIpcLine(line) {
|
|
853
|
+
const trimmed = line.trim();
|
|
854
|
+
if (!trimmed)
|
|
855
|
+
return null;
|
|
856
|
+
if (trimmed === "READY")
|
|
857
|
+
return { type: "ready" };
|
|
858
|
+
try {
|
|
859
|
+
return JSON.parse(trimmed);
|
|
860
|
+
} catch {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function waitForReady(stdout, timeoutMs = 1e4) {
|
|
865
|
+
return new Promise((resolve5, reject) => {
|
|
866
|
+
let settled = false;
|
|
867
|
+
const reader = stdout.getReader();
|
|
868
|
+
const decoder = new TextDecoder;
|
|
869
|
+
let buffer = "";
|
|
870
|
+
function settle(fn) {
|
|
871
|
+
if (settled)
|
|
872
|
+
return;
|
|
873
|
+
settled = true;
|
|
874
|
+
clearTimeout(timer);
|
|
875
|
+
try {
|
|
876
|
+
reader.releaseLock();
|
|
877
|
+
} catch {}
|
|
878
|
+
fn();
|
|
879
|
+
}
|
|
880
|
+
const timer = setTimeout(() => {
|
|
881
|
+
settle(() => reject(new Error(`Checkpoint process did not become ready within ${timeoutMs}ms`)));
|
|
882
|
+
}, timeoutMs);
|
|
883
|
+
const processLine = (line) => {
|
|
884
|
+
const msg = parseIpcLine(line);
|
|
885
|
+
if (msg?.type === "ready") {
|
|
886
|
+
settle(() => resolve5());
|
|
887
|
+
return "ready";
|
|
888
|
+
}
|
|
889
|
+
if (msg?.type === "error") {
|
|
890
|
+
settle(() => reject(new Error(`Checkpoint process error: ${msg.message}`)));
|
|
891
|
+
return "error";
|
|
892
|
+
}
|
|
893
|
+
return "continue";
|
|
894
|
+
};
|
|
895
|
+
const processBufferLines = () => {
|
|
896
|
+
let lineEnd = buffer.indexOf(`
|
|
897
|
+
`);
|
|
898
|
+
while (lineEnd !== -1) {
|
|
899
|
+
const line = buffer.slice(0, lineEnd);
|
|
900
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
901
|
+
const result = processLine(line);
|
|
902
|
+
if (result !== "continue") {
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
lineEnd = buffer.indexOf(`
|
|
906
|
+
`);
|
|
907
|
+
}
|
|
908
|
+
return "continue";
|
|
909
|
+
};
|
|
910
|
+
(async () => {
|
|
911
|
+
try {
|
|
912
|
+
while (true) {
|
|
913
|
+
const { done, value } = await reader.read();
|
|
914
|
+
if (done) {
|
|
915
|
+
buffer += decoder.decode();
|
|
916
|
+
const lastLine = buffer.trim();
|
|
917
|
+
if (lastLine.length > 0) {
|
|
918
|
+
const result2 = processLine(lastLine);
|
|
919
|
+
if (result2 !== "continue") {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
settle(() => reject(new Error("Checkpoint process stdout closed before READY signal")));
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
buffer += decoder.decode(value, { stream: true });
|
|
927
|
+
if (buffer.length > MAX_PENDING_STDOUT_CHARS) {
|
|
928
|
+
settle(() => reject(new Error(`Checkpoint process stdout exceeded ${MAX_PENDING_STDOUT_CHARS} chars before READY`)));
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const result = processBufferLines();
|
|
932
|
+
if (result !== "continue") {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch (err) {
|
|
937
|
+
settle(() => reject(err));
|
|
938
|
+
}
|
|
939
|
+
})();
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
var MAX_PENDING_STDOUT_CHARS = 1048576;
|
|
943
|
+
|
|
944
|
+
// src/checkpoint/artifacts/worker-decompressor.ts
|
|
945
|
+
import { availableParallelism, cpus } from "os";
|
|
946
|
+
|
|
947
|
+
class CheckpointWorkerDecompressor {
|
|
948
|
+
workers = [];
|
|
949
|
+
idleWorkers = [];
|
|
950
|
+
queue = [];
|
|
951
|
+
activeJobsByWorker = new Map;
|
|
952
|
+
nextJobId = 1;
|
|
953
|
+
disposed = false;
|
|
954
|
+
constructor(workerCount = resolveDefaultWorkerCount()) {
|
|
955
|
+
const normalizedCount = normalizeWorkerCount(workerCount);
|
|
956
|
+
const workerUrl = resolveWorkerModuleUrl();
|
|
957
|
+
for (let i = 0;i < normalizedCount; i++) {
|
|
958
|
+
const worker = new Worker(workerUrl.href);
|
|
959
|
+
worker.onmessage = (event) => this.handleWorkerMessage(worker, event);
|
|
960
|
+
worker.onerror = (event) => this.handleWorkerError(worker, event);
|
|
961
|
+
this.workers.push(worker);
|
|
962
|
+
this.idleWorkers.push(worker);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async decompress(input, codec) {
|
|
966
|
+
if (codec === "none") {
|
|
967
|
+
return new Uint8Array(input);
|
|
968
|
+
}
|
|
969
|
+
if (this.disposed) {
|
|
970
|
+
throw new Error("Checkpoint worker decompressor is disposed");
|
|
971
|
+
}
|
|
972
|
+
const copied = new Uint8Array(input);
|
|
973
|
+
return await new Promise((resolve5, reject) => {
|
|
974
|
+
const id = this.nextJobId++;
|
|
975
|
+
this.queue.push({
|
|
976
|
+
id,
|
|
977
|
+
request: {
|
|
978
|
+
id,
|
|
979
|
+
codec,
|
|
980
|
+
input: copied.buffer
|
|
981
|
+
},
|
|
982
|
+
resolve: resolve5,
|
|
983
|
+
reject
|
|
984
|
+
});
|
|
985
|
+
this.pump();
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
async dispose() {
|
|
989
|
+
if (this.disposed) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
this.disposed = true;
|
|
993
|
+
const error = new Error("Checkpoint worker decompressor disposed");
|
|
994
|
+
this.failAll(error);
|
|
995
|
+
for (const worker of this.workers) {
|
|
996
|
+
try {
|
|
997
|
+
worker.terminate();
|
|
998
|
+
} catch {}
|
|
999
|
+
}
|
|
1000
|
+
this.workers = [];
|
|
1001
|
+
this.idleWorkers = [];
|
|
1002
|
+
this.activeJobsByWorker.clear();
|
|
1003
|
+
}
|
|
1004
|
+
pump() {
|
|
1005
|
+
while (this.idleWorkers.length > 0 && this.queue.length > 0) {
|
|
1006
|
+
const worker = this.idleWorkers.pop();
|
|
1007
|
+
const job = this.queue.shift();
|
|
1008
|
+
this.activeJobsByWorker.set(worker, job);
|
|
1009
|
+
worker.postMessage(job.request, [job.request.input]);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
handleWorkerMessage(worker, event) {
|
|
1013
|
+
const job = this.activeJobsByWorker.get(worker);
|
|
1014
|
+
this.activeJobsByWorker.delete(worker);
|
|
1015
|
+
if (!this.disposed) {
|
|
1016
|
+
this.idleWorkers.push(worker);
|
|
1017
|
+
}
|
|
1018
|
+
if (!job) {
|
|
1019
|
+
this.pump();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const message = event.data;
|
|
1023
|
+
if (message.error) {
|
|
1024
|
+
job.reject(new Error(message.error));
|
|
1025
|
+
} else if (message.output instanceof ArrayBuffer) {
|
|
1026
|
+
job.resolve(new Uint8Array(message.output));
|
|
1027
|
+
} else {
|
|
1028
|
+
job.reject(new Error("Worker returned no output"));
|
|
1029
|
+
}
|
|
1030
|
+
this.pump();
|
|
1031
|
+
}
|
|
1032
|
+
handleWorkerError(worker, event) {
|
|
1033
|
+
const job = this.activeJobsByWorker.get(worker);
|
|
1034
|
+
this.activeJobsByWorker.delete(worker);
|
|
1035
|
+
this.idleWorkers = this.idleWorkers.filter((candidate) => candidate !== worker);
|
|
1036
|
+
const message = event.message?.trim() || "Checkpoint decompression worker crashed";
|
|
1037
|
+
const error = new Error(message);
|
|
1038
|
+
if (job) {
|
|
1039
|
+
job.reject(error);
|
|
1040
|
+
}
|
|
1041
|
+
this.failAll(error);
|
|
1042
|
+
this.dispose().catch(() => {});
|
|
1043
|
+
}
|
|
1044
|
+
failAll(error) {
|
|
1045
|
+
const queued = this.queue.splice(0, this.queue.length);
|
|
1046
|
+
for (const job of queued) {
|
|
1047
|
+
job.reject(error);
|
|
1048
|
+
}
|
|
1049
|
+
for (const job of this.activeJobsByWorker.values()) {
|
|
1050
|
+
job.reject(error);
|
|
1051
|
+
}
|
|
1052
|
+
this.activeJobsByWorker.clear();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function resolveDefaultWorkerCount() {
|
|
1056
|
+
const cores = resolveCoreCount();
|
|
1057
|
+
const reserveForMainThread = Math.max(1, cores - 1);
|
|
1058
|
+
return Math.max(1, Math.min(DEFAULT_MAX_WORKERS, reserveForMainThread));
|
|
1059
|
+
}
|
|
1060
|
+
function resolveCoreCount() {
|
|
1061
|
+
try {
|
|
1062
|
+
const parallelism = availableParallelism();
|
|
1063
|
+
if (Number.isFinite(parallelism) && parallelism > 0) {
|
|
1064
|
+
return parallelism;
|
|
1065
|
+
}
|
|
1066
|
+
} catch {}
|
|
1067
|
+
const cpuCount = cpus().length;
|
|
1068
|
+
return Number.isFinite(cpuCount) && cpuCount > 0 ? cpuCount : 1;
|
|
1069
|
+
}
|
|
1070
|
+
function normalizeWorkerCount(value) {
|
|
1071
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1072
|
+
return 1;
|
|
1073
|
+
}
|
|
1074
|
+
return Math.max(1, Math.floor(value));
|
|
1075
|
+
}
|
|
1076
|
+
function resolveWorkerModuleUrl() {
|
|
1077
|
+
if (import.meta.url.endsWith(".ts")) {
|
|
1078
|
+
return new URL("./decompress-worker.ts", import.meta.url);
|
|
1079
|
+
}
|
|
1080
|
+
return new URL("./decompress-worker.js", import.meta.url);
|
|
1081
|
+
}
|
|
1082
|
+
var DEFAULT_MAX_WORKERS = 4;
|
|
1083
|
+
var init_worker_decompressor = () => {};
|
|
1084
|
+
|
|
1085
|
+
// src/checkpoint/artifacts/materializer.ts
|
|
1086
|
+
import { existsSync as existsSync8, promises as fs8 } from "fs";
|
|
1087
|
+
import { dirname as dirname2, extname, join as join9, relative as relative6 } from "path";
|
|
1088
|
+
|
|
1089
|
+
class CheckpointArtifactMaterializer {
|
|
1090
|
+
verifyChecksums;
|
|
1091
|
+
materializedDirName;
|
|
1092
|
+
lockTimeoutMs;
|
|
1093
|
+
constructor(options = {}) {
|
|
1094
|
+
this.verifyChecksums = options.verifyChecksums ?? true;
|
|
1095
|
+
this.materializedDirName = options.materializedDirName ?? DEFAULT_MATERIALIZED_DIR;
|
|
1096
|
+
this.lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
1097
|
+
}
|
|
1098
|
+
async materialize(manifest, storageDir) {
|
|
1099
|
+
const versionDir = join9(storageDir, manifest.version);
|
|
1100
|
+
const markerPath = join9(versionDir, ".assets.ready.json");
|
|
1101
|
+
const lockPath = join9(versionDir, ".assets.lock");
|
|
1102
|
+
const fingerprint = computeAssetFingerprint(manifest.assets);
|
|
1103
|
+
if (await this.isReady(markerPath, fingerprint, manifest.assets, join9(versionDir, this.materializedDirName))) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
await this.acquireLock(lockPath);
|
|
1107
|
+
try {
|
|
1108
|
+
if (await this.isReady(markerPath, fingerprint, manifest.assets, join9(versionDir, this.materializedDirName))) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const materializedRoot = join9(versionDir, this.materializedDirName);
|
|
1112
|
+
await fs8.rm(materializedRoot, { recursive: true, force: true });
|
|
1113
|
+
await fs8.mkdir(materializedRoot, { recursive: true });
|
|
1114
|
+
const decompressor = new CheckpointWorkerDecompressor;
|
|
1115
|
+
try {
|
|
1116
|
+
for (const asset of manifest.assets) {
|
|
1117
|
+
const result = await this.materializeAsset(asset, storageDir, versionDir, materializedRoot, decompressor);
|
|
1118
|
+
asset.materializedPath = result;
|
|
1119
|
+
}
|
|
1120
|
+
} finally {
|
|
1121
|
+
await decompressor.dispose();
|
|
1122
|
+
}
|
|
1123
|
+
const marker = {
|
|
1124
|
+
fingerprint,
|
|
1125
|
+
createdAt: new Date().toISOString()
|
|
1126
|
+
};
|
|
1127
|
+
await fs8.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
|
|
1128
|
+
} finally {
|
|
1129
|
+
await fs8.rm(lockPath, { recursive: true, force: true });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async materializeAsset(asset, storageDir, versionDir, root, decompressor) {
|
|
1133
|
+
const sourcePath = this.resolveSourcePath(asset, storageDir);
|
|
1134
|
+
if (!existsSync8(sourcePath)) {
|
|
1135
|
+
throw new Error(`Checkpoint asset blob not found: ${sourcePath}`);
|
|
1136
|
+
}
|
|
1137
|
+
const blob = await fs8.readFile(sourcePath);
|
|
1138
|
+
const blobBytes = new Uint8Array(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
1139
|
+
const expectedBlobHash = asset.blobHash;
|
|
1140
|
+
if (this.verifyChecksums && expectedBlobHash && sha256Hex(blobBytes) !== expectedBlobHash) {
|
|
1141
|
+
throw new Error(`Checkpoint asset blob checksum mismatch for ${asset.logicalPath}`);
|
|
1142
|
+
}
|
|
1143
|
+
const codec = asset.codec ?? (asset.blobHash ? "gzip" : "none");
|
|
1144
|
+
const contentBytes = await decompressor.decompress(blobBytes, codec);
|
|
1145
|
+
const expectedContentHash = asset.contentHash ?? asset.hash;
|
|
1146
|
+
if (this.verifyChecksums && expectedContentHash && sha256Hex(contentBytes) !== expectedContentHash) {
|
|
1147
|
+
throw new Error(`Checkpoint asset content checksum mismatch for ${asset.logicalPath}`);
|
|
1148
|
+
}
|
|
1149
|
+
const cachedFile = await this.writeDecompressedCache(asset, storageDir, contentBytes);
|
|
1150
|
+
const safeLogicalPath = normalizeLogicalPath(asset.logicalPath);
|
|
1151
|
+
const destinationPath = join9(root, safeLogicalPath);
|
|
1152
|
+
await fs8.mkdir(dirname2(destinationPath), { recursive: true });
|
|
1153
|
+
await fs8.rm(destinationPath, { force: true });
|
|
1154
|
+
await this.linkWithFallback(cachedFile, destinationPath);
|
|
1155
|
+
return normalizePath2(relative6(versionDir, destinationPath));
|
|
1156
|
+
}
|
|
1157
|
+
async writeDecompressedCache(asset, storageDir, bytes) {
|
|
1158
|
+
const hash = asset.contentHash ?? asset.hash;
|
|
1159
|
+
const extension = extname(asset.logicalPath) || ".bin";
|
|
1160
|
+
const cacheFile = join9(storageDir, "_assets/cache", `${hash}${extension}`);
|
|
1161
|
+
await fs8.mkdir(dirname2(cacheFile), { recursive: true });
|
|
1162
|
+
if (existsSync8(cacheFile)) {
|
|
1163
|
+
if (!this.verifyChecksums) {
|
|
1164
|
+
return cacheFile;
|
|
1165
|
+
}
|
|
1166
|
+
const existing = await fs8.readFile(cacheFile);
|
|
1167
|
+
const existingBytes = new Uint8Array(existing.buffer, existing.byteOffset, existing.byteLength);
|
|
1168
|
+
if (sha256Hex(existingBytes) === hash) {
|
|
1169
|
+
return cacheFile;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
await fs8.writeFile(cacheFile, bytes);
|
|
1173
|
+
return cacheFile;
|
|
1174
|
+
}
|
|
1175
|
+
resolveSourcePath(asset, storageDir) {
|
|
1176
|
+
const rawPath = asset.blobPath ?? asset.storedPath;
|
|
1177
|
+
if (isAbsolutePathPortable(rawPath)) {
|
|
1178
|
+
return rawPath;
|
|
1179
|
+
}
|
|
1180
|
+
return join9(storageDir, rawPath);
|
|
1181
|
+
}
|
|
1182
|
+
async linkWithFallback(sourcePath, destinationPath) {
|
|
1183
|
+
try {
|
|
1184
|
+
await fs8.link(sourcePath, destinationPath);
|
|
1185
|
+
return;
|
|
1186
|
+
} catch {}
|
|
1187
|
+
try {
|
|
1188
|
+
await fs8.symlink(sourcePath, destinationPath);
|
|
1189
|
+
return;
|
|
1190
|
+
} catch {}
|
|
1191
|
+
await fs8.copyFile(sourcePath, destinationPath);
|
|
1192
|
+
}
|
|
1193
|
+
async acquireLock(lockPath) {
|
|
1194
|
+
const deadline = Date.now() + this.lockTimeoutMs;
|
|
1195
|
+
while (Date.now() < deadline) {
|
|
1196
|
+
try {
|
|
1197
|
+
await fs8.mkdir(lockPath);
|
|
1198
|
+
return;
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
if (!isAlreadyExists2(error)) {
|
|
1201
|
+
throw error;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
await sleep(DEFAULT_LOCK_POLL_MS);
|
|
1205
|
+
}
|
|
1206
|
+
throw new Error(`Timed out waiting for checkpoint asset lock: ${lockPath}`);
|
|
1207
|
+
}
|
|
1208
|
+
async isReady(markerPath, fingerprint, assets, materializedRoot) {
|
|
1209
|
+
if (!existsSync8(markerPath)) {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
const marker = JSON.parse(await fs8.readFile(markerPath, "utf-8"));
|
|
1214
|
+
if (marker.fingerprint !== fingerprint) {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
for (const asset of assets) {
|
|
1218
|
+
const expectedPath = join9(materializedRoot, normalizeLogicalPath(asset.logicalPath));
|
|
1219
|
+
if (!existsSync8(expectedPath)) {
|
|
1220
|
+
return false;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return true;
|
|
1224
|
+
} catch {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function normalizePath2(path) {
|
|
1230
|
+
return path.replace(/\\/g, "/");
|
|
1231
|
+
}
|
|
1232
|
+
function isAlreadyExists2(error) {
|
|
1233
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
1234
|
+
}
|
|
1235
|
+
function sleep(ms) {
|
|
1236
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
1237
|
+
}
|
|
1238
|
+
var DEFAULT_MATERIALIZED_DIR = "_materialized", DEFAULT_LOCK_TIMEOUT_MS = 15000, DEFAULT_LOCK_POLL_MS = 50;
|
|
1239
|
+
var init_materializer = __esm(() => {
|
|
1240
|
+
init_manifest();
|
|
1241
|
+
init_worker_decompressor();
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// src/checkpoint/process-manager.ts
|
|
1245
|
+
var exports_process_manager = {};
|
|
1246
|
+
__export(exports_process_manager, {
|
|
1247
|
+
CheckpointProcessManager: () => CheckpointProcessManager
|
|
1248
|
+
});
|
|
1249
|
+
import { existsSync as existsSync9, promises as fs9, unlinkSync } from "fs";
|
|
1250
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
1251
|
+
|
|
1252
|
+
class CheckpointProcessManager {
|
|
1253
|
+
running = new Map;
|
|
1254
|
+
pending = new Map;
|
|
1255
|
+
readyTimeoutMs;
|
|
1256
|
+
idleTimeoutMs;
|
|
1257
|
+
idleTimers = new Map;
|
|
1258
|
+
lastUsedAt = new Map;
|
|
1259
|
+
materializer;
|
|
1260
|
+
constructor(options = DEFAULT_READY_TIMEOUT_MS) {
|
|
1261
|
+
if (typeof options === "number") {
|
|
1262
|
+
this.readyTimeoutMs = options;
|
|
1263
|
+
this.idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS;
|
|
1264
|
+
} else {
|
|
1265
|
+
this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
1266
|
+
this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
1267
|
+
}
|
|
1268
|
+
this.materializer = new CheckpointArtifactMaterializer;
|
|
1269
|
+
}
|
|
1270
|
+
async spawn(manifest, storageDir) {
|
|
1271
|
+
if (this.running.has(manifest.version)) {
|
|
1272
|
+
return this.running.get(manifest.version);
|
|
1273
|
+
}
|
|
1274
|
+
if (this.pending.has(manifest.version)) {
|
|
1275
|
+
return this.pending.get(manifest.version);
|
|
1276
|
+
}
|
|
1277
|
+
const promise = this.doSpawn(manifest, storageDir);
|
|
1278
|
+
this.pending.set(manifest.version, promise);
|
|
1279
|
+
try {
|
|
1280
|
+
const result = await promise;
|
|
1281
|
+
return result;
|
|
1282
|
+
} finally {
|
|
1283
|
+
this.pending.delete(manifest.version);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async doSpawn(manifest, storageDir) {
|
|
1287
|
+
const versionDir = join10(storageDir, manifest.version);
|
|
1288
|
+
const bundlePath = join10(versionDir, manifest.entrypoint);
|
|
1289
|
+
const socketPath = resolveCheckpointSocketPath(storageDir, manifest.version);
|
|
1290
|
+
if (!existsSync9(bundlePath)) {
|
|
1291
|
+
throw new Error(`Checkpoint bundle not found: ${bundlePath}`);
|
|
1292
|
+
}
|
|
1293
|
+
await this.materializer.materialize(manifest, storageDir);
|
|
1294
|
+
await fs9.mkdir(dirname3(socketPath), { recursive: true });
|
|
1295
|
+
this.tryUnlinkSocket(socketPath);
|
|
1296
|
+
const proc = Bun.spawn(["bun", "run", bundlePath], {
|
|
1297
|
+
env: {
|
|
1298
|
+
...process.env,
|
|
1299
|
+
VECTOR_CHECKPOINT_SOCKET: socketPath,
|
|
1300
|
+
VECTOR_CHECKPOINT_VERSION: manifest.version
|
|
1301
|
+
},
|
|
1302
|
+
stdout: "pipe",
|
|
1303
|
+
stderr: "inherit"
|
|
1304
|
+
});
|
|
1305
|
+
if (!proc.stdout) {
|
|
1306
|
+
proc.kill("SIGTERM");
|
|
1307
|
+
throw new Error(`Checkpoint process for ${manifest.version} did not provide stdout`);
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
await waitForReady(proc.stdout, this.readyTimeoutMs);
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
proc.kill("SIGTERM");
|
|
1313
|
+
throw err;
|
|
1314
|
+
}
|
|
1315
|
+
const spawned = {
|
|
1316
|
+
version: manifest.version,
|
|
1317
|
+
socketPath,
|
|
1318
|
+
process: proc,
|
|
1319
|
+
pid: proc.pid
|
|
1320
|
+
};
|
|
1321
|
+
this.running.set(manifest.version, spawned);
|
|
1322
|
+
this.lastUsedAt.set(manifest.version, Date.now());
|
|
1323
|
+
this.scheduleIdleCheck(manifest.version);
|
|
1324
|
+
return spawned;
|
|
1325
|
+
}
|
|
1326
|
+
markUsed(version) {
|
|
1327
|
+
if (!this.running.has(version)) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.lastUsedAt.set(version, Date.now());
|
|
1331
|
+
}
|
|
1332
|
+
async stop(version) {
|
|
1333
|
+
const snap = this.running.get(version);
|
|
1334
|
+
if (!snap)
|
|
1335
|
+
return;
|
|
1336
|
+
this.running.delete(version);
|
|
1337
|
+
this.clearIdleTimer(version);
|
|
1338
|
+
this.lastUsedAt.delete(version);
|
|
1339
|
+
snap.process.kill("SIGTERM");
|
|
1340
|
+
const exited = await Promise.race([
|
|
1341
|
+
snap.process.exited.then(() => true),
|
|
1342
|
+
new Promise((resolve5) => setTimeout(() => resolve5(false), STOP_TIMEOUT_MS))
|
|
1343
|
+
]);
|
|
1344
|
+
if (!exited) {
|
|
1345
|
+
try {
|
|
1346
|
+
snap.process.kill("SIGKILL");
|
|
1347
|
+
await snap.process.exited;
|
|
1348
|
+
} catch {}
|
|
1349
|
+
}
|
|
1350
|
+
this.tryUnlinkSocket(snap.socketPath);
|
|
1351
|
+
}
|
|
1352
|
+
async stopAll() {
|
|
1353
|
+
const versions = [...this.running.keys()];
|
|
1354
|
+
for (const version of versions) {
|
|
1355
|
+
await this.stop(version);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
isRunning(version) {
|
|
1359
|
+
return this.running.has(version);
|
|
1360
|
+
}
|
|
1361
|
+
getRunning(version) {
|
|
1362
|
+
return this.running.get(version);
|
|
1363
|
+
}
|
|
1364
|
+
async health(version) {
|
|
1365
|
+
const snap = this.running.get(version);
|
|
1366
|
+
if (!snap)
|
|
1367
|
+
return false;
|
|
1368
|
+
try {
|
|
1369
|
+
const response = await fetch("http://localhost/_vector/health", {
|
|
1370
|
+
unix: snap.socketPath,
|
|
1371
|
+
signal: AbortSignal.timeout(2000)
|
|
1372
|
+
});
|
|
1373
|
+
return response.ok;
|
|
1374
|
+
} catch {
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
getRunningVersions() {
|
|
1379
|
+
return [...this.running.keys()];
|
|
1380
|
+
}
|
|
1381
|
+
scheduleIdleCheck(version, delayMs = this.idleTimeoutMs) {
|
|
1382
|
+
this.clearIdleTimer(version);
|
|
1383
|
+
if (this.idleTimeoutMs <= 0) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const timer = setTimeout(() => {
|
|
1387
|
+
this.handleIdleCheck(version);
|
|
1388
|
+
}, Math.max(1, delayMs));
|
|
1389
|
+
if (typeof timer.unref === "function") {
|
|
1390
|
+
timer.unref();
|
|
1391
|
+
}
|
|
1392
|
+
this.idleTimers.set(version, timer);
|
|
1393
|
+
}
|
|
1394
|
+
async handleIdleCheck(version) {
|
|
1395
|
+
if (!this.running.has(version)) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const lastUsedAt = this.lastUsedAt.get(version) ?? 0;
|
|
1399
|
+
const idleForMs = Date.now() - lastUsedAt;
|
|
1400
|
+
const remainingMs = this.idleTimeoutMs - idleForMs;
|
|
1401
|
+
if (remainingMs > 0) {
|
|
1402
|
+
this.scheduleIdleCheck(version, remainingMs);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
await this.stop(version);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
console.error(`[CheckpointProcessManager] Failed to stop idle checkpoint ${version}:`, error);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
clearIdleTimer(version) {
|
|
1412
|
+
const timer = this.idleTimers.get(version);
|
|
1413
|
+
if (!timer) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
clearTimeout(timer);
|
|
1417
|
+
this.idleTimers.delete(version);
|
|
1418
|
+
}
|
|
1419
|
+
tryUnlinkSocket(socketPath) {
|
|
1420
|
+
try {
|
|
1421
|
+
if (existsSync9(socketPath)) {
|
|
1422
|
+
unlinkSync(socketPath);
|
|
1423
|
+
}
|
|
1424
|
+
} catch {}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
var DEFAULT_READY_TIMEOUT_MS = 1e4, DEFAULT_IDLE_TIMEOUT_MS, STOP_TIMEOUT_MS = 5000;
|
|
1428
|
+
var init_process_manager = __esm(() => {
|
|
1429
|
+
init_materializer();
|
|
1430
|
+
init_socket_path();
|
|
1431
|
+
DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
// src/checkpoint/resolver.ts
|
|
1435
|
+
var exports_resolver = {};
|
|
1436
|
+
__export(exports_resolver, {
|
|
1437
|
+
CheckpointResolver: () => CheckpointResolver
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
class CheckpointResolver {
|
|
1441
|
+
manager;
|
|
1442
|
+
processManager;
|
|
1443
|
+
versionHeader;
|
|
1444
|
+
cacheKeyOverride;
|
|
1445
|
+
allowFallbackVersionHeader;
|
|
1446
|
+
pendingVersionResolves = new Map;
|
|
1447
|
+
constructor(manager, processManager, options = {}) {
|
|
1448
|
+
this.manager = manager;
|
|
1449
|
+
this.processManager = processManager;
|
|
1450
|
+
this.versionHeader = normalizeHeaderName(options.versionHeader ?? DEFAULT_VERSION_HEADER);
|
|
1451
|
+
this.cacheKeyOverride = options.cacheKeyOverride === true;
|
|
1452
|
+
this.allowFallbackVersionHeader = options.versionHeader === undefined;
|
|
1453
|
+
}
|
|
1454
|
+
async resolve(request) {
|
|
1455
|
+
const requestedVersion = this.getRequestedVersion(request);
|
|
1456
|
+
if (!requestedVersion) {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
return await this.resolveVersion(requestedVersion);
|
|
1460
|
+
}
|
|
1461
|
+
getRequestedVersion(request) {
|
|
1462
|
+
return this.getRequestedHeader(request)?.value ?? null;
|
|
1463
|
+
}
|
|
1464
|
+
getCacheKeyOverrideValue(request) {
|
|
1465
|
+
if (!this.cacheKeyOverride) {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
const requestedHeader = this.getRequestedHeader(request);
|
|
1469
|
+
if (!requestedHeader) {
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
return `${requestedHeader.name}:${requestedHeader.value}`;
|
|
1473
|
+
}
|
|
1474
|
+
getRequestedHeader(request) {
|
|
1475
|
+
const primary = request.headers.get(this.versionHeader);
|
|
1476
|
+
if (primary && primary.trim().length > 0) {
|
|
1477
|
+
return { name: this.versionHeader, value: primary.trim() };
|
|
1478
|
+
}
|
|
1479
|
+
if (this.allowFallbackVersionHeader && this.versionHeader !== FALLBACK_VERSION_HEADER) {
|
|
1480
|
+
const fallback = request.headers.get(FALLBACK_VERSION_HEADER);
|
|
1481
|
+
if (fallback && fallback.trim().length > 0) {
|
|
1482
|
+
return { name: FALLBACK_VERSION_HEADER, value: fallback.trim() };
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
async resolveVersion(version) {
|
|
1488
|
+
let running = this.processManager.getRunning(version);
|
|
1489
|
+
if (!running) {
|
|
1490
|
+
const pending = this.pendingVersionResolves.get(version);
|
|
1491
|
+
if (pending) {
|
|
1492
|
+
const socketPath2 = await pending;
|
|
1493
|
+
if (!socketPath2) {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
this.processManager.markUsed(version);
|
|
1497
|
+
return socketPath2;
|
|
1498
|
+
}
|
|
1499
|
+
const pendingResolve = (async () => {
|
|
1500
|
+
try {
|
|
1501
|
+
const manifest = await this.manager.readManifest(version);
|
|
1502
|
+
const spawned = await this.processManager.spawn(manifest, this.manager.getStorageDir());
|
|
1503
|
+
return spawned.socketPath;
|
|
1504
|
+
} catch {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
})();
|
|
1508
|
+
this.pendingVersionResolves.set(version, pendingResolve);
|
|
1509
|
+
const socketPath = await pendingResolve;
|
|
1510
|
+
this.pendingVersionResolves.delete(version);
|
|
1511
|
+
if (!socketPath) {
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
this.processManager.markUsed(version);
|
|
1515
|
+
return socketPath;
|
|
1516
|
+
}
|
|
1517
|
+
this.processManager.markUsed(version);
|
|
1518
|
+
return running.socketPath;
|
|
1519
|
+
}
|
|
1520
|
+
invalidateCache() {}
|
|
1521
|
+
}
|
|
1522
|
+
function normalizeHeaderName(value) {
|
|
1523
|
+
const normalized = value.trim().toLowerCase();
|
|
1524
|
+
return normalized.length > 0 ? normalized : DEFAULT_VERSION_HEADER;
|
|
1525
|
+
}
|
|
1526
|
+
var DEFAULT_VERSION_HEADER = "x-vector-checkpoint-version", FALLBACK_VERSION_HEADER = "x-vector-checkpoint";
|
|
1527
|
+
|
|
1528
|
+
// src/checkpoint/forwarder.ts
|
|
1529
|
+
var exports_forwarder = {};
|
|
1530
|
+
__export(exports_forwarder, {
|
|
1531
|
+
CheckpointForwarder: () => CheckpointForwarder,
|
|
1532
|
+
CHECKPOINT_CONTEXT_HEADER: () => CHECKPOINT_CONTEXT_HEADER
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
class CheckpointForwarder {
|
|
1536
|
+
async forward(request, socketPath, contextPayload) {
|
|
1537
|
+
try {
|
|
1538
|
+
const encodedContext = encodeCheckpointContext(contextPayload);
|
|
1539
|
+
const headers = buildForwardHeaders(request.headers, encodedContext);
|
|
1540
|
+
const response = await fetch(request.url, {
|
|
1541
|
+
method: request.method,
|
|
1542
|
+
headers,
|
|
1543
|
+
body: request.body,
|
|
1544
|
+
unix: socketPath,
|
|
1545
|
+
duplex: request.body ? "half" : undefined
|
|
1546
|
+
});
|
|
1547
|
+
return new Response(response.body, {
|
|
1548
|
+
status: response.status,
|
|
1549
|
+
statusText: response.statusText,
|
|
1550
|
+
headers: new Headers(response.headers)
|
|
1551
|
+
});
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
console.error("[CheckpointForwarder] Forward failed:", error);
|
|
1554
|
+
return new Response(JSON.stringify({ error: true, message: "Checkpoint unavailable", statusCode: 503 }), {
|
|
1555
|
+
status: 503,
|
|
1556
|
+
headers: { "content-type": "application/json" }
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function buildForwardHeaders(source, encodedContext) {
|
|
1562
|
+
const headers = new Headers(source);
|
|
1563
|
+
stripHopByHopHeaders(headers);
|
|
1564
|
+
if (encodedContext) {
|
|
1565
|
+
headers.set(CHECKPOINT_CONTEXT_HEADER, encodedContext);
|
|
1566
|
+
}
|
|
1567
|
+
return headers;
|
|
1568
|
+
}
|
|
1569
|
+
function stripHopByHopHeaders(headers) {
|
|
1570
|
+
const connectionValue = headers.get("connection");
|
|
1571
|
+
if (connectionValue) {
|
|
1572
|
+
for (const token of connectionValue.split(",")) {
|
|
1573
|
+
const normalized = token.trim().toLowerCase();
|
|
1574
|
+
if (normalized) {
|
|
1575
|
+
headers.delete(normalized);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
headers.delete("connection");
|
|
1580
|
+
headers.delete("keep-alive");
|
|
1581
|
+
headers.delete("proxy-authenticate");
|
|
1582
|
+
headers.delete("proxy-authorization");
|
|
1583
|
+
headers.delete("te");
|
|
1584
|
+
headers.delete("trailer");
|
|
1585
|
+
headers.delete("transfer-encoding");
|
|
1586
|
+
headers.delete("upgrade");
|
|
1587
|
+
}
|
|
1588
|
+
function encodeCheckpointContext(contextPayload) {
|
|
1589
|
+
if (!contextPayload) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
const keys = Object.keys(contextPayload);
|
|
1593
|
+
if (keys.length === 0) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
try {
|
|
1597
|
+
return Buffer.from(JSON.stringify(contextPayload), "utf-8").toString("base64url");
|
|
1598
|
+
} catch {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
var CHECKPOINT_CONTEXT_HEADER = "x-vector-checkpoint-context";
|
|
1603
|
+
|
|
1604
|
+
// src/checkpoint/gateway.ts
|
|
1605
|
+
var exports_gateway = {};
|
|
1606
|
+
__export(exports_gateway, {
|
|
1607
|
+
CheckpointGateway: () => CheckpointGateway
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
class CheckpointGateway {
|
|
1611
|
+
resolver;
|
|
1612
|
+
forwarder;
|
|
1613
|
+
constructor(resolver, forwarder) {
|
|
1614
|
+
this.resolver = resolver;
|
|
1615
|
+
this.forwarder = forwarder;
|
|
1616
|
+
}
|
|
1617
|
+
getRequestedVersion(request) {
|
|
1618
|
+
return this.resolver.getRequestedVersion(request);
|
|
1619
|
+
}
|
|
1620
|
+
getCacheKeyOverrideValue(request) {
|
|
1621
|
+
return this.resolver.getCacheKeyOverrideValue(request);
|
|
1622
|
+
}
|
|
1623
|
+
async handle(request, contextPayload) {
|
|
1624
|
+
const requestedVersion = this.getRequestedVersion(request);
|
|
1625
|
+
const socketPath = await this.resolver.resolve(request);
|
|
1626
|
+
if (!socketPath) {
|
|
1627
|
+
if (requestedVersion) {
|
|
1628
|
+
return new Response(JSON.stringify({
|
|
1629
|
+
error: true,
|
|
1630
|
+
message: `Requested checkpoint version "${requestedVersion}" is unavailable`,
|
|
1631
|
+
statusCode: 503
|
|
1632
|
+
}), { status: 503, headers: { "content-type": "application/json" } });
|
|
1633
|
+
}
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
return await this.forwarder.forward(request, socketPath, contextPayload);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/checkpoint/cli.ts
|
|
1641
|
+
var exports_cli = {};
|
|
1642
|
+
__export(exports_cli, {
|
|
1643
|
+
runCheckpointCli: () => runCheckpointCli
|
|
1644
|
+
});
|
|
1645
|
+
import { parseArgs } from "util";
|
|
1646
|
+
async function runCheckpointCli(argv) {
|
|
1647
|
+
const subcommand = argv[0];
|
|
1648
|
+
switch (subcommand) {
|
|
1649
|
+
case "publish":
|
|
1650
|
+
return await cliPublish(argv.slice(1));
|
|
1651
|
+
case "list":
|
|
1652
|
+
return await cliList(argv.slice(1));
|
|
1653
|
+
case "rollback":
|
|
1654
|
+
return await cliRollback(argv.slice(1));
|
|
1655
|
+
case "remove":
|
|
1656
|
+
return await cliRemove(argv.slice(1));
|
|
1657
|
+
default:
|
|
1658
|
+
printCheckpointHelp();
|
|
1659
|
+
if (subcommand) {
|
|
1660
|
+
console.error(`
|
|
1661
|
+
Unknown checkpoint command: ${subcommand}`);
|
|
1662
|
+
}
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
async function cliPublish(args) {
|
|
1667
|
+
const { values } = parseArgs({
|
|
1668
|
+
args,
|
|
1669
|
+
options: {
|
|
1670
|
+
version: { type: "string", short: "v" },
|
|
1671
|
+
routes: { type: "string", short: "r", default: "./routes" },
|
|
1672
|
+
storage: { type: "string", short: "s" }
|
|
1673
|
+
},
|
|
1674
|
+
strict: true
|
|
1675
|
+
});
|
|
1676
|
+
if (!values.version) {
|
|
1677
|
+
console.error("Error: --version is required for publish");
|
|
1678
|
+
console.error("Usage: vector checkpoint publish --version <ver> [--routes <dir>]");
|
|
1679
|
+
process.exit(1);
|
|
1680
|
+
}
|
|
1681
|
+
const manager = new CheckpointManager(values.storage ? { storageDir: values.storage } : undefined);
|
|
1682
|
+
try {
|
|
1683
|
+
console.log(`Publishing checkpoint ${values.version}...`);
|
|
1684
|
+
const manifest = await manager.publish({
|
|
1685
|
+
version: values.version,
|
|
1686
|
+
routesDir: values.routes
|
|
1687
|
+
});
|
|
1688
|
+
console.log(`Checkpoint ${manifest.version} published successfully.`);
|
|
1689
|
+
console.log(` Bundle hash: ${manifest.bundleHash.slice(0, 12)}...`);
|
|
1690
|
+
console.log(` Bundle size: ${formatBytes2(manifest.bundleSize)}`);
|
|
1691
|
+
console.log(` Routes: ${manifest.routes.length}`);
|
|
1692
|
+
console.log(` Assets: ${manifest.assets.length}`);
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
console.error(`Failed to publish checkpoint: ${err.message}`);
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
async function cliList(args) {
|
|
1699
|
+
const { values } = parseArgs({
|
|
1700
|
+
args,
|
|
1701
|
+
options: {
|
|
1702
|
+
storage: { type: "string", short: "s" }
|
|
1703
|
+
},
|
|
1704
|
+
strict: true
|
|
1705
|
+
});
|
|
1706
|
+
const manager = new CheckpointManager(values.storage ? { storageDir: values.storage } : undefined);
|
|
1707
|
+
const manifests = await manager.listVersions();
|
|
1708
|
+
const active = await manager.getActive();
|
|
1709
|
+
if (manifests.length === 0) {
|
|
1710
|
+
console.log("No checkpoints found.");
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
console.log("");
|
|
1714
|
+
console.log(" Version Created Bundle Hash Size Status");
|
|
1715
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1716
|
+
for (const m of manifests) {
|
|
1717
|
+
const isActive = active?.version === m.version;
|
|
1718
|
+
const status = isActive ? "\u25CF active" : " ";
|
|
1719
|
+
const hash = m.bundleHash.slice(0, 12);
|
|
1720
|
+
const size = formatBytes2(m.bundleSize).padEnd(10);
|
|
1721
|
+
const created = new Date(m.createdAt).toISOString().replace("T", " ").slice(0, 19);
|
|
1722
|
+
console.log(` ${m.version.padEnd(12)} ${created} ${hash}... ${size} ${status}`);
|
|
1723
|
+
}
|
|
1724
|
+
console.log("");
|
|
1725
|
+
}
|
|
1726
|
+
async function cliRollback(args) {
|
|
1727
|
+
const { positionals, values } = parseArgs({
|
|
1728
|
+
args,
|
|
1729
|
+
options: {
|
|
1730
|
+
storage: { type: "string", short: "s" }
|
|
1731
|
+
},
|
|
1732
|
+
strict: true,
|
|
1733
|
+
allowPositionals: true
|
|
1734
|
+
});
|
|
1735
|
+
const version = positionals[0];
|
|
1736
|
+
if (!version) {
|
|
1737
|
+
console.error("Error: version argument is required");
|
|
1738
|
+
console.error("Usage: vector checkpoint rollback <version>");
|
|
1739
|
+
process.exit(1);
|
|
1740
|
+
}
|
|
1741
|
+
const manager = new CheckpointManager(values.storage ? { storageDir: values.storage } : undefined);
|
|
1742
|
+
try {
|
|
1743
|
+
await manager.setActive(version);
|
|
1744
|
+
console.log(`Active checkpoint set to ${version}.`);
|
|
1745
|
+
console.log("Note: Restart the server for the change to take effect.");
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
console.error(`Failed to rollback: ${err.message}`);
|
|
1748
|
+
process.exit(1);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
async function cliRemove(args) {
|
|
1752
|
+
const { positionals, values } = parseArgs({
|
|
1753
|
+
args,
|
|
1754
|
+
options: {
|
|
1755
|
+
storage: { type: "string", short: "s" }
|
|
1756
|
+
},
|
|
1757
|
+
strict: true,
|
|
1758
|
+
allowPositionals: true
|
|
1759
|
+
});
|
|
1760
|
+
const version = positionals[0];
|
|
1761
|
+
if (!version) {
|
|
1762
|
+
console.error("Error: version argument is required");
|
|
1763
|
+
console.error("Usage: vector checkpoint remove <version>");
|
|
1764
|
+
process.exit(1);
|
|
1765
|
+
}
|
|
1766
|
+
const manager = new CheckpointManager(values.storage ? { storageDir: values.storage } : undefined);
|
|
1767
|
+
try {
|
|
1768
|
+
await manager.remove(version);
|
|
1769
|
+
console.log(`Checkpoint ${version} removed.`);
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
console.error(`Failed to remove checkpoint: ${err.message}`);
|
|
1772
|
+
process.exit(1);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function printCheckpointHelp() {
|
|
1776
|
+
console.log(`
|
|
1777
|
+
Usage: vector checkpoint <command>
|
|
1778
|
+
|
|
1779
|
+
Commands:
|
|
1780
|
+
publish --version <ver> [--routes <dir>] Build and store a checkpoint
|
|
1781
|
+
list List all stored checkpoints
|
|
1782
|
+
rollback <version> Activate a specific checkpoint
|
|
1783
|
+
remove <version> Delete a checkpoint
|
|
1784
|
+
|
|
1785
|
+
Options:
|
|
1786
|
+
-v, --version Semver version string (e.g. 1.2.0)
|
|
1787
|
+
-r, --routes Routes directory (default: ./routes)
|
|
1788
|
+
-s, --storage Checkpoint storage dir (default: .vector/checkpoints)
|
|
1789
|
+
`);
|
|
1790
|
+
}
|
|
1791
|
+
function formatBytes2(bytes) {
|
|
1792
|
+
if (bytes < 1024)
|
|
1793
|
+
return `${bytes} B`;
|
|
1794
|
+
if (bytes < 1024 * 1024)
|
|
1795
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1796
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1797
|
+
}
|
|
1798
|
+
var init_cli = __esm(() => {
|
|
1799
|
+
init_manager();
|
|
1800
|
+
});
|
|
3
1801
|
|
|
4
1802
|
// src/cli/index.ts
|
|
5
1803
|
import { watch } from "fs";
|
|
6
|
-
import { parseArgs } from "util";
|
|
1804
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
7
1805
|
|
|
8
1806
|
// src/cli/option-resolution.ts
|
|
9
1807
|
function resolveRoutesDir(configRoutesDir, hasRoutesOption, cliRoutes) {
|
|
@@ -100,7 +1898,7 @@ class ConfigLoader {
|
|
|
100
1898
|
if (existsSync(this.configPath)) {
|
|
101
1899
|
try {
|
|
102
1900
|
const userConfigPath = toFileUrl(this.configPath);
|
|
103
|
-
const userConfig = await import(userConfigPath);
|
|
1901
|
+
const userConfig = await import(`${userConfigPath}?t=${Date.now()}`);
|
|
104
1902
|
this.config = userConfig.default || userConfig;
|
|
105
1903
|
this.configSource = "user";
|
|
106
1904
|
} catch (error) {
|
|
@@ -120,7 +1918,7 @@ class ConfigLoader {
|
|
|
120
1918
|
async buildLegacyConfig() {
|
|
121
1919
|
const config = {};
|
|
122
1920
|
if (this.config) {
|
|
123
|
-
config.port = this.config.port;
|
|
1921
|
+
config.port = this.normalizePort(this.config.port);
|
|
124
1922
|
config.hostname = this.config.hostname;
|
|
125
1923
|
config.reusePort = this.config.reusePort;
|
|
126
1924
|
config.development = this.config.development;
|
|
@@ -131,6 +1929,7 @@ class ConfigLoader {
|
|
|
131
1929
|
config.openapi = this.config.openapi;
|
|
132
1930
|
config.startup = this.config.startup;
|
|
133
1931
|
config.shutdown = this.config.shutdown;
|
|
1932
|
+
config.checkpoint = this.config.checkpoint;
|
|
134
1933
|
}
|
|
135
1934
|
config.autoDiscover = true;
|
|
136
1935
|
if (this.config?.cors) {
|
|
@@ -155,6 +1954,12 @@ class ConfigLoader {
|
|
|
155
1954
|
}
|
|
156
1955
|
return config;
|
|
157
1956
|
}
|
|
1957
|
+
normalizePort(port) {
|
|
1958
|
+
if (port === undefined) {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
return Number(port);
|
|
1962
|
+
}
|
|
158
1963
|
async loadAuthHandler() {
|
|
159
1964
|
return this.config?.auth || null;
|
|
160
1965
|
}
|
|
@@ -257,23 +2062,26 @@ class AuthManager {
|
|
|
257
2062
|
clearProtectedHandler() {
|
|
258
2063
|
this.protectedHandler = null;
|
|
259
2064
|
}
|
|
260
|
-
async authenticate(
|
|
2065
|
+
async authenticate(context) {
|
|
261
2066
|
if (!this.protectedHandler) {
|
|
262
2067
|
throw new Error("Protected handler not configured. Use vector.protected() to set authentication handler.");
|
|
263
2068
|
}
|
|
2069
|
+
if (!context || typeof context !== "object" || !context.request) {
|
|
2070
|
+
throw new Error("Authentication context is invalid: missing request");
|
|
2071
|
+
}
|
|
264
2072
|
try {
|
|
265
|
-
const authUser = await this.protectedHandler(
|
|
266
|
-
|
|
2073
|
+
const authUser = await this.protectedHandler(context);
|
|
2074
|
+
context.authUser = authUser;
|
|
267
2075
|
return authUser;
|
|
268
2076
|
} catch (error) {
|
|
269
2077
|
throw new Error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
270
2078
|
}
|
|
271
2079
|
}
|
|
272
|
-
isAuthenticated(
|
|
273
|
-
return !!
|
|
2080
|
+
isAuthenticated(context) {
|
|
2081
|
+
return !!context.authUser;
|
|
274
2082
|
}
|
|
275
|
-
getUser(
|
|
276
|
-
return
|
|
2083
|
+
getUser(context) {
|
|
2084
|
+
return context.authUser || null;
|
|
277
2085
|
}
|
|
278
2086
|
}
|
|
279
2087
|
|
|
@@ -302,10 +2110,11 @@ class CacheManager {
|
|
|
302
2110
|
const now = Date.now();
|
|
303
2111
|
const cached = this.memoryCache.get(key);
|
|
304
2112
|
if (this.isCacheValid(cached, now)) {
|
|
305
|
-
return cached.value;
|
|
2113
|
+
return this.cloneCachedValue(cached.value);
|
|
306
2114
|
}
|
|
307
2115
|
if (this.inflight.has(key)) {
|
|
308
|
-
|
|
2116
|
+
const inflightValue = await this.inflight.get(key);
|
|
2117
|
+
return this.cloneCachedValue(inflightValue);
|
|
309
2118
|
}
|
|
310
2119
|
const promise = (async () => {
|
|
311
2120
|
const value = await factory();
|
|
@@ -324,15 +2133,31 @@ class CacheManager {
|
|
|
324
2133
|
}
|
|
325
2134
|
setInMemoryCache(key, value, ttl) {
|
|
326
2135
|
const expires = Date.now() + ttl * 1000;
|
|
327
|
-
this.memoryCache.set(key, { value, expires });
|
|
2136
|
+
this.memoryCache.set(key, { value: this.cloneForStore(value), expires });
|
|
328
2137
|
this.scheduleCleanup();
|
|
329
2138
|
}
|
|
2139
|
+
cloneForStore(value) {
|
|
2140
|
+
if (value instanceof Response) {
|
|
2141
|
+
return value.clone();
|
|
2142
|
+
}
|
|
2143
|
+
return value;
|
|
2144
|
+
}
|
|
2145
|
+
cloneCachedValue(value) {
|
|
2146
|
+
if (value instanceof Response) {
|
|
2147
|
+
return value.clone();
|
|
2148
|
+
}
|
|
2149
|
+
return value;
|
|
2150
|
+
}
|
|
330
2151
|
scheduleCleanup() {
|
|
331
2152
|
if (this.cleanupInterval)
|
|
332
2153
|
return;
|
|
333
|
-
|
|
2154
|
+
const timer = setInterval(() => {
|
|
334
2155
|
this.cleanupExpired();
|
|
335
2156
|
}, 60000);
|
|
2157
|
+
if (typeof timer.unref === "function") {
|
|
2158
|
+
timer.unref();
|
|
2159
|
+
}
|
|
2160
|
+
this.cleanupInterval = timer;
|
|
336
2161
|
}
|
|
337
2162
|
cleanupExpired() {
|
|
338
2163
|
const now = Date.now();
|
|
@@ -530,7 +2355,7 @@ class RouteScanner {
|
|
|
530
2355
|
const routePath = relative2(this.routesDir, fullPath).replace(/\.(ts|js)$/, "").split(sep).join("/");
|
|
531
2356
|
try {
|
|
532
2357
|
const importPath = process.platform === "win32" ? `file:///${fullPath.replace(/\\/g, "/")}` : fullPath;
|
|
533
|
-
const module = await import(importPath);
|
|
2358
|
+
const module = await import(`${importPath}?t=${Date.now()}`);
|
|
534
2359
|
if (module.default && typeof module.default === "function") {
|
|
535
2360
|
routes.push({
|
|
536
2361
|
name: "default",
|
|
@@ -594,26 +2419,27 @@ class MiddlewareManager {
|
|
|
594
2419
|
addFinally(...handlers) {
|
|
595
2420
|
this.finallyHandlers.push(...handlers);
|
|
596
2421
|
}
|
|
597
|
-
async executeBefore(
|
|
2422
|
+
async executeBefore(context) {
|
|
598
2423
|
if (this.beforeHandlers.length === 0)
|
|
599
|
-
return
|
|
600
|
-
let currentRequest = request;
|
|
2424
|
+
return null;
|
|
601
2425
|
for (const handler of this.beforeHandlers) {
|
|
602
|
-
const result = await handler(
|
|
2426
|
+
const result = await handler(context);
|
|
603
2427
|
if (result instanceof Response) {
|
|
604
2428
|
return result;
|
|
605
2429
|
}
|
|
606
|
-
|
|
2430
|
+
if (result !== undefined) {
|
|
2431
|
+
throw new TypeError("Before middleware must return void or Response");
|
|
2432
|
+
}
|
|
607
2433
|
}
|
|
608
|
-
return
|
|
2434
|
+
return null;
|
|
609
2435
|
}
|
|
610
|
-
async executeFinally(response,
|
|
2436
|
+
async executeFinally(response, context) {
|
|
611
2437
|
if (this.finallyHandlers.length === 0)
|
|
612
2438
|
return response;
|
|
613
2439
|
let currentResponse = response;
|
|
614
2440
|
for (const handler of this.finallyHandlers) {
|
|
615
2441
|
try {
|
|
616
|
-
currentResponse = await handler(currentResponse,
|
|
2442
|
+
currentResponse = await handler(currentResponse, context);
|
|
617
2443
|
} catch (error) {
|
|
618
2444
|
console.error("After middleware error:", error);
|
|
619
2445
|
}
|
|
@@ -644,6 +2470,52 @@ function stringifyData(data) {
|
|
|
644
2470
|
throw e;
|
|
645
2471
|
}
|
|
646
2472
|
}
|
|
2473
|
+
function isJsonContentType(contentType) {
|
|
2474
|
+
const mimeType = contentType.split(";", 1)[0] ?? contentType;
|
|
2475
|
+
return mimeType.trim().toLowerCase() === CONTENT_TYPES.JSON;
|
|
2476
|
+
}
|
|
2477
|
+
function serializeCookie(cookie) {
|
|
2478
|
+
const segments = [`${cookie.name}=${cookie.value}`];
|
|
2479
|
+
if (cookie.maxAge !== undefined && Number.isFinite(cookie.maxAge)) {
|
|
2480
|
+
segments.push(`Max-Age=${Math.trunc(cookie.maxAge)}`);
|
|
2481
|
+
}
|
|
2482
|
+
if (cookie.domain) {
|
|
2483
|
+
segments.push(`Domain=${cookie.domain}`);
|
|
2484
|
+
}
|
|
2485
|
+
if (cookie.path) {
|
|
2486
|
+
segments.push(`Path=${cookie.path}`);
|
|
2487
|
+
}
|
|
2488
|
+
if (cookie.expires !== undefined) {
|
|
2489
|
+
const expiresAt = cookie.expires instanceof Date ? cookie.expires : new Date(cookie.expires);
|
|
2490
|
+
if (!Number.isNaN(expiresAt.getTime())) {
|
|
2491
|
+
segments.push(`Expires=${expiresAt.toUTCString()}`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if (cookie.httpOnly) {
|
|
2495
|
+
segments.push("HttpOnly");
|
|
2496
|
+
}
|
|
2497
|
+
if (cookie.secure) {
|
|
2498
|
+
segments.push("Secure");
|
|
2499
|
+
}
|
|
2500
|
+
if (cookie.sameSite) {
|
|
2501
|
+
segments.push(`SameSite=${cookie.sameSite}`);
|
|
2502
|
+
}
|
|
2503
|
+
if (cookie.partitioned) {
|
|
2504
|
+
segments.push("Partitioned");
|
|
2505
|
+
}
|
|
2506
|
+
if (cookie.priority) {
|
|
2507
|
+
segments.push(`Priority=${cookie.priority}`);
|
|
2508
|
+
}
|
|
2509
|
+
return segments.join("; ");
|
|
2510
|
+
}
|
|
2511
|
+
function appendSetCookieHeaders(headers, cookies) {
|
|
2512
|
+
if (!cookies || cookies.length === 0) {
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
for (const cookie of cookies) {
|
|
2516
|
+
headers.append("set-cookie", typeof cookie === "string" ? cookie : serializeCookie(cookie));
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
647
2519
|
function createErrorResponse(code, message, contentType) {
|
|
648
2520
|
const errorBody = {
|
|
649
2521
|
error: true,
|
|
@@ -698,14 +2570,34 @@ var APIError = {
|
|
|
698
2570
|
maintenance: (msg = "Service Under Maintenance", contentType) => createErrorResponse(503, msg, contentType),
|
|
699
2571
|
custom: (statusCode, msg, contentType) => createErrorResponse(statusCode, msg, contentType)
|
|
700
2572
|
};
|
|
701
|
-
function createResponse(statusCode, data,
|
|
702
|
-
const
|
|
2573
|
+
function createResponse(statusCode, data, optionsOrContentType = CONTENT_TYPES.JSON) {
|
|
2574
|
+
const options = typeof optionsOrContentType === "string" ? { contentType: optionsOrContentType } : optionsOrContentType ?? {};
|
|
2575
|
+
const headers = new Headers(options.headers);
|
|
2576
|
+
const contentType = options.contentType ?? headers.get("content-type") ?? CONTENT_TYPES.JSON;
|
|
2577
|
+
if (options.contentType || !headers.has("content-type")) {
|
|
2578
|
+
headers.set("content-type", contentType);
|
|
2579
|
+
}
|
|
2580
|
+
appendSetCookieHeaders(headers, options.cookies);
|
|
2581
|
+
const body = isJsonContentType(contentType) ? stringifyData(data) : data;
|
|
703
2582
|
return new Response(body, {
|
|
704
2583
|
status: statusCode,
|
|
705
|
-
|
|
2584
|
+
statusText: options.statusText,
|
|
2585
|
+
headers
|
|
706
2586
|
});
|
|
707
2587
|
}
|
|
708
2588
|
|
|
2589
|
+
// src/types/index.ts
|
|
2590
|
+
var AuthKind;
|
|
2591
|
+
((AuthKind2) => {
|
|
2592
|
+
AuthKind2["ApiKey"] = "ApiKey";
|
|
2593
|
+
AuthKind2["HttpBasic"] = "HttpBasic";
|
|
2594
|
+
AuthKind2["HttpBearer"] = "HttpBearer";
|
|
2595
|
+
AuthKind2["HttpDigest"] = "HttpDigest";
|
|
2596
|
+
AuthKind2["OAuth2"] = "OAuth2";
|
|
2597
|
+
AuthKind2["OpenIdConnect"] = "OpenIdConnect";
|
|
2598
|
+
AuthKind2["MutualTls"] = "MutualTls";
|
|
2599
|
+
})(AuthKind ||= {});
|
|
2600
|
+
|
|
709
2601
|
// src/utils/schema-validation.ts
|
|
710
2602
|
function isStandardRouteSchema(schema) {
|
|
711
2603
|
const standard = schema?.["~standard"];
|
|
@@ -783,6 +2675,11 @@ function createValidationErrorPayload(target, issues) {
|
|
|
783
2675
|
}
|
|
784
2676
|
|
|
785
2677
|
// src/core/router.ts
|
|
2678
|
+
var AUTH_KIND_VALUES = new Set(Object.values(AuthKind));
|
|
2679
|
+
function isAuthKindValue(value) {
|
|
2680
|
+
return typeof value === "string" && AUTH_KIND_VALUES.has(value);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
786
2683
|
class VectorRouter {
|
|
787
2684
|
middlewareManager;
|
|
788
2685
|
authManager;
|
|
@@ -794,6 +2691,7 @@ class VectorRouter {
|
|
|
794
2691
|
routeMatchers = [];
|
|
795
2692
|
corsHeadersEntries = null;
|
|
796
2693
|
corsHandler = null;
|
|
2694
|
+
checkpointGateway = null;
|
|
797
2695
|
constructor(middlewareManager, authManager, cacheManager) {
|
|
798
2696
|
this.middlewareManager = middlewareManager;
|
|
799
2697
|
this.authManager = authManager;
|
|
@@ -805,6 +2703,9 @@ class VectorRouter {
|
|
|
805
2703
|
setCorsHandler(handler) {
|
|
806
2704
|
this.corsHandler = handler;
|
|
807
2705
|
}
|
|
2706
|
+
setCheckpointGateway(gateway) {
|
|
2707
|
+
this.checkpointGateway = gateway;
|
|
2708
|
+
}
|
|
808
2709
|
setRouteBooleanDefaults(defaults) {
|
|
809
2710
|
this.routeBooleanDefaults = { ...defaults };
|
|
810
2711
|
}
|
|
@@ -820,6 +2721,9 @@ class VectorRouter {
|
|
|
820
2721
|
resolved[key] = defaults[key];
|
|
821
2722
|
}
|
|
822
2723
|
}
|
|
2724
|
+
if (resolved.auth === true && isAuthKindValue(defaults.auth)) {
|
|
2725
|
+
resolved.auth = defaults.auth;
|
|
2726
|
+
}
|
|
823
2727
|
return resolved;
|
|
824
2728
|
}
|
|
825
2729
|
route(options, handler) {
|
|
@@ -896,218 +2800,279 @@ class VectorRouter {
|
|
|
896
2800
|
} catch {
|
|
897
2801
|
return APIError.badRequest("Malformed request URL");
|
|
898
2802
|
}
|
|
899
|
-
request._parsedUrl = url;
|
|
900
2803
|
const pathname = url.pathname;
|
|
2804
|
+
const exactPathRoute = this.routeTable[pathname];
|
|
2805
|
+
if (exactPathRoute) {
|
|
2806
|
+
if (exactPathRoute instanceof Response) {
|
|
2807
|
+
return this.applyCorsResponse(exactPathRoute.clone(), request);
|
|
2808
|
+
}
|
|
2809
|
+
const exactPathMethodMap = exactPathRoute;
|
|
2810
|
+
const handler = exactPathMethodMap[request.method] ?? (request.method === "HEAD" ? exactPathMethodMap["GET"] : undefined);
|
|
2811
|
+
if (handler) {
|
|
2812
|
+
const response = await handler(request);
|
|
2813
|
+
if (response) {
|
|
2814
|
+
return response;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
901
2818
|
for (const matcher of this.routeMatchers) {
|
|
902
2819
|
const path = matcher.path;
|
|
903
|
-
const
|
|
904
|
-
if (!
|
|
2820
|
+
const routeEntry = this.routeTable[path];
|
|
2821
|
+
if (!routeEntry)
|
|
905
2822
|
continue;
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (request.method === "OPTIONS" || request.method in methodMap) {
|
|
910
|
-
const match = pathname.match(matcher.regex);
|
|
911
|
-
if (match) {
|
|
912
|
-
try {
|
|
913
|
-
request.params = match.groups ?? {};
|
|
914
|
-
} catch {}
|
|
915
|
-
const handler = methodMap[request.method] ?? methodMap["GET"];
|
|
916
|
-
if (handler) {
|
|
917
|
-
const response = await handler(request);
|
|
918
|
-
if (response)
|
|
919
|
-
return response;
|
|
920
|
-
}
|
|
2823
|
+
if (routeEntry instanceof Response) {
|
|
2824
|
+
if (pathname === path) {
|
|
2825
|
+
return this.applyCorsResponse(routeEntry.clone(), request);
|
|
921
2826
|
}
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
const methodMap = routeEntry;
|
|
2830
|
+
const handler = methodMap[request.method] ?? (request.method === "HEAD" ? methodMap["GET"] : undefined);
|
|
2831
|
+
if (!handler) {
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
const match = pathname.match(matcher.regex);
|
|
2835
|
+
if (!match) {
|
|
2836
|
+
continue;
|
|
922
2837
|
}
|
|
2838
|
+
const response = await handler(request);
|
|
2839
|
+
if (response)
|
|
2840
|
+
return response;
|
|
923
2841
|
}
|
|
924
|
-
return STATIC_RESPONSES.NOT_FOUND.clone();
|
|
2842
|
+
return this.applyCorsResponse(STATIC_RESPONSES.NOT_FOUND.clone(), request);
|
|
925
2843
|
}
|
|
926
|
-
|
|
927
|
-
if (
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
const hasEmptyParamsObject = !!request.params && typeof request.params === "object" && !Array.isArray(request.params) && Object.keys(request.params).length === 0;
|
|
931
|
-
if (options?.params !== undefined && (request.params === undefined || hasEmptyParamsObject)) {
|
|
932
|
-
try {
|
|
933
|
-
request.params = options.params;
|
|
934
|
-
} catch {}
|
|
2844
|
+
cloneMetadata(value) {
|
|
2845
|
+
if (Array.isArray(value)) {
|
|
2846
|
+
return [...value];
|
|
935
2847
|
}
|
|
936
|
-
if (
|
|
937
|
-
|
|
2848
|
+
if (value && typeof value === "object") {
|
|
2849
|
+
return { ...value };
|
|
938
2850
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
},
|
|
964
|
-
configurable: true,
|
|
965
|
-
enumerable: true
|
|
966
|
-
});
|
|
967
|
-
} catch {
|
|
968
|
-
const url = request._parsedUrl ?? new URL(request.url);
|
|
969
|
-
try {
|
|
970
|
-
request.query = VectorRouter.parseQuery(url);
|
|
971
|
-
} catch {}
|
|
2851
|
+
return value;
|
|
2852
|
+
}
|
|
2853
|
+
createContext(request, options) {
|
|
2854
|
+
const context = {
|
|
2855
|
+
request
|
|
2856
|
+
};
|
|
2857
|
+
this.setContextField(context, "metadata", options?.metadata !== undefined ? this.cloneMetadata(options.metadata) : {});
|
|
2858
|
+
this.setContextField(context, "params", options?.params ?? {});
|
|
2859
|
+
this.setContextField(context, "query", options?.query ?? {});
|
|
2860
|
+
this.setContextField(context, "cookies", options?.cookies ?? {});
|
|
2861
|
+
return context;
|
|
2862
|
+
}
|
|
2863
|
+
setContextField(context, key, value) {
|
|
2864
|
+
context[key] = value;
|
|
2865
|
+
}
|
|
2866
|
+
hasOwnContextField(context, key) {
|
|
2867
|
+
return Object.prototype.hasOwnProperty.call(context, key);
|
|
2868
|
+
}
|
|
2869
|
+
buildCheckpointContextPayload(context) {
|
|
2870
|
+
const payload = {};
|
|
2871
|
+
const allowedKeys = ["metadata", "content", "validatedInput", "authUser"];
|
|
2872
|
+
for (const key of allowedKeys) {
|
|
2873
|
+
if (!this.hasOwnContextField(context, key)) {
|
|
2874
|
+
continue;
|
|
972
2875
|
}
|
|
2876
|
+
const value = context[key];
|
|
2877
|
+
if (typeof value === "function" || typeof value === "symbol" || value === undefined) {
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
payload[key] = value;
|
|
973
2881
|
}
|
|
974
|
-
|
|
975
|
-
Object.defineProperty(request, "cookies", {
|
|
976
|
-
get() {
|
|
977
|
-
const cookieHeader = this.headers.get("cookie") ?? "";
|
|
978
|
-
const cookies = {};
|
|
979
|
-
if (cookieHeader) {
|
|
980
|
-
for (const pair of cookieHeader.split(";")) {
|
|
981
|
-
const idx = pair.indexOf("=");
|
|
982
|
-
if (idx > 0) {
|
|
983
|
-
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
Object.defineProperty(this, "cookies", {
|
|
988
|
-
value: cookies,
|
|
989
|
-
writable: true,
|
|
990
|
-
configurable: true,
|
|
991
|
-
enumerable: true
|
|
992
|
-
});
|
|
993
|
-
return cookies;
|
|
994
|
-
},
|
|
995
|
-
configurable: true,
|
|
996
|
-
enumerable: true
|
|
997
|
-
});
|
|
998
|
-
}
|
|
2882
|
+
return payload;
|
|
999
2883
|
}
|
|
1000
|
-
resolveFallbackParams(
|
|
2884
|
+
resolveFallbackParams(pathname, routeMatcher) {
|
|
1001
2885
|
if (!routeMatcher) {
|
|
1002
2886
|
return;
|
|
1003
2887
|
}
|
|
1004
|
-
const
|
|
1005
|
-
if (
|
|
2888
|
+
const matched = pathname.match(routeMatcher);
|
|
2889
|
+
if (!matched?.groups) {
|
|
1006
2890
|
return;
|
|
1007
2891
|
}
|
|
1008
|
-
|
|
2892
|
+
return matched.groups;
|
|
2893
|
+
}
|
|
2894
|
+
getRequestedCheckpointVersion(request) {
|
|
2895
|
+
if (!this.checkpointGateway) {
|
|
2896
|
+
return null;
|
|
2897
|
+
}
|
|
2898
|
+
const gateway = this.checkpointGateway;
|
|
2899
|
+
if (gateway?.getRequestedVersion) {
|
|
2900
|
+
return gateway.getRequestedVersion(request);
|
|
2901
|
+
}
|
|
2902
|
+
const primary = request.headers.get("x-vector-checkpoint-version");
|
|
2903
|
+
if (primary && primary.trim().length > 0) {
|
|
2904
|
+
return primary.trim();
|
|
2905
|
+
}
|
|
2906
|
+
const fallback = request.headers.get("x-vector-checkpoint");
|
|
2907
|
+
if (fallback && fallback.trim().length > 0) {
|
|
2908
|
+
return fallback.trim();
|
|
2909
|
+
}
|
|
2910
|
+
return null;
|
|
2911
|
+
}
|
|
2912
|
+
getCheckpointCacheKeyOverrideValue(request) {
|
|
2913
|
+
if (!this.checkpointGateway) {
|
|
2914
|
+
return null;
|
|
2915
|
+
}
|
|
2916
|
+
const gateway = this.checkpointGateway;
|
|
2917
|
+
if (gateway?.getCacheKeyOverrideValue) {
|
|
2918
|
+
return gateway.getCacheKeyOverrideValue(request);
|
|
2919
|
+
}
|
|
2920
|
+
const primary = request.headers.get("x-vector-checkpoint-version");
|
|
2921
|
+
if (primary && primary.trim().length > 0) {
|
|
2922
|
+
return `x-vector-checkpoint-version:${primary.trim()}`;
|
|
2923
|
+
}
|
|
2924
|
+
const fallback = request.headers.get("x-vector-checkpoint");
|
|
2925
|
+
if (fallback && fallback.trim().length > 0) {
|
|
2926
|
+
return `x-vector-checkpoint:${fallback.trim()}`;
|
|
2927
|
+
}
|
|
2928
|
+
return null;
|
|
2929
|
+
}
|
|
2930
|
+
applyCheckpointCacheNamespace(cacheKey, request) {
|
|
2931
|
+
const checkpointVersion = this.getRequestedCheckpointVersion(request);
|
|
2932
|
+
if (!checkpointVersion) {
|
|
2933
|
+
return cacheKey;
|
|
2934
|
+
}
|
|
2935
|
+
return `${cacheKey}:checkpoint=${checkpointVersion}`;
|
|
2936
|
+
}
|
|
2937
|
+
applyCheckpointRouteKeyOverride(cacheKey, request) {
|
|
2938
|
+
const override = this.getCheckpointCacheKeyOverrideValue(request);
|
|
2939
|
+
if (!override) {
|
|
2940
|
+
return cacheKey;
|
|
2941
|
+
}
|
|
2942
|
+
return override;
|
|
2943
|
+
}
|
|
2944
|
+
async parseRequestBodyForContext(context, request, checkpointRequested) {
|
|
2945
|
+
let parsedContent = null;
|
|
1009
2946
|
try {
|
|
1010
|
-
|
|
2947
|
+
const bodyReadRequest = checkpointRequested ? request.clone() : request;
|
|
2948
|
+
const contentType = bodyReadRequest.headers.get("content-type");
|
|
2949
|
+
if (contentType?.startsWith("application/json")) {
|
|
2950
|
+
parsedContent = await bodyReadRequest.json();
|
|
2951
|
+
} else if (contentType?.startsWith("application/x-www-form-urlencoded")) {
|
|
2952
|
+
parsedContent = Object.fromEntries(await bodyReadRequest.formData());
|
|
2953
|
+
} else if (contentType?.startsWith("multipart/form-data")) {
|
|
2954
|
+
parsedContent = await bodyReadRequest.formData();
|
|
2955
|
+
} else {
|
|
2956
|
+
parsedContent = await bodyReadRequest.text();
|
|
2957
|
+
}
|
|
1011
2958
|
} catch {
|
|
1012
|
-
|
|
2959
|
+
parsedContent = null;
|
|
1013
2960
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
2961
|
+
this.setContextField(context, "content", parsedContent);
|
|
2962
|
+
}
|
|
2963
|
+
isLikelyStreamingBodyRequest(request) {
|
|
2964
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
2965
|
+
return false;
|
|
1017
2966
|
}
|
|
1018
|
-
|
|
2967
|
+
if (!request.body) {
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
if (request.duplex === "half") {
|
|
2971
|
+
return true;
|
|
2972
|
+
}
|
|
2973
|
+
const transferEncoding = request.headers.get("transfer-encoding");
|
|
2974
|
+
if (transferEncoding) {
|
|
2975
|
+
const hasChunked = transferEncoding.split(",").some((value) => value.trim().toLowerCase() === "chunked");
|
|
2976
|
+
if (hasChunked) {
|
|
2977
|
+
return true;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
return false;
|
|
1019
2981
|
}
|
|
1020
2982
|
wrapHandler(options, handler) {
|
|
1021
2983
|
const routePath = options.path;
|
|
1022
2984
|
const routeMatcher = routePath.includes(":") ? buildRouteRegex(routePath) : null;
|
|
1023
2985
|
return async (request) => {
|
|
1024
2986
|
const vectorRequest = request;
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
2987
|
+
let pathname = "";
|
|
2988
|
+
try {
|
|
2989
|
+
pathname = new URL(request.url).pathname;
|
|
2990
|
+
} catch {}
|
|
2991
|
+
const fallbackParams = this.resolveFallbackParams(pathname, routeMatcher);
|
|
2992
|
+
const context = this.createContext(vectorRequest, {
|
|
2993
|
+
metadata: options.metadata,
|
|
2994
|
+
params: this.getRequestParams(request, fallbackParams),
|
|
2995
|
+
query: this.getRequestQuery(request),
|
|
2996
|
+
cookies: this.getRequestCookies(request)
|
|
1030
2997
|
});
|
|
1031
2998
|
try {
|
|
1032
2999
|
if (options.expose === false) {
|
|
1033
3000
|
return APIError.forbidden("Forbidden");
|
|
1034
3001
|
}
|
|
1035
|
-
const
|
|
1036
|
-
if (
|
|
1037
|
-
return
|
|
3002
|
+
const beforeResponse = await this.middlewareManager.executeBefore(context);
|
|
3003
|
+
if (beforeResponse instanceof Response) {
|
|
3004
|
+
return beforeResponse;
|
|
1038
3005
|
}
|
|
1039
|
-
const req = beforeResult;
|
|
1040
3006
|
if (options.auth) {
|
|
1041
3007
|
try {
|
|
1042
|
-
await this.authManager.authenticate(
|
|
3008
|
+
await this.authManager.authenticate(context);
|
|
1043
3009
|
} catch (error) {
|
|
1044
3010
|
return APIError.unauthorized(error instanceof Error ? error.message : "Authentication failed", options.responseContentType);
|
|
1045
3011
|
}
|
|
1046
3012
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
3013
|
+
const executeRoute = async () => {
|
|
3014
|
+
const req = context.request;
|
|
3015
|
+
const requestForRoute = req;
|
|
3016
|
+
const checkpointRequested = this.getRequestedCheckpointVersion(requestForRoute) !== null;
|
|
3017
|
+
const shouldDeferStreamingValidation = this.isLikelyStreamingBodyRequest(requestForRoute) && options.schema?.input !== undefined && options.validate !== false;
|
|
3018
|
+
if (!options.rawRequest && req.method !== "GET" && req.method !== "HEAD" && !shouldDeferStreamingValidation) {
|
|
3019
|
+
await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
|
|
3020
|
+
}
|
|
3021
|
+
if (shouldDeferStreamingValidation) {
|
|
3022
|
+
const validationWithoutBody = await this.validateInputSchema(context, options, fallbackParams, {
|
|
3023
|
+
includeBody: false,
|
|
3024
|
+
allowBodyDeferral: true
|
|
3025
|
+
});
|
|
3026
|
+
if (validationWithoutBody.response) {
|
|
3027
|
+
return validationWithoutBody.response;
|
|
3028
|
+
}
|
|
3029
|
+
if (validationWithoutBody.requiresBody) {
|
|
3030
|
+
if (!options.rawRequest && req.method !== "GET" && req.method !== "HEAD") {
|
|
3031
|
+
await this.parseRequestBodyForContext(context, requestForRoute, checkpointRequested);
|
|
3032
|
+
}
|
|
3033
|
+
const fullValidation = await this.validateInputSchema(context, options, fallbackParams);
|
|
3034
|
+
if (fullValidation.response) {
|
|
3035
|
+
return fullValidation.response;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
} else {
|
|
3039
|
+
const inputValidation = await this.validateInputSchema(context, options, fallbackParams);
|
|
3040
|
+
if (inputValidation.response) {
|
|
3041
|
+
return inputValidation.response;
|
|
1059
3042
|
}
|
|
1060
|
-
} catch {
|
|
1061
|
-
parsedContent = null;
|
|
1062
3043
|
}
|
|
1063
|
-
this.
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
3044
|
+
if (this.checkpointGateway) {
|
|
3045
|
+
const checkpointResponse = await this.checkpointGateway.handle(req, this.buildCheckpointContextPayload(context));
|
|
3046
|
+
if (checkpointResponse) {
|
|
3047
|
+
return checkpointResponse;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
return await handler(context);
|
|
3051
|
+
};
|
|
1069
3052
|
let result;
|
|
1070
3053
|
const cacheOptions = options.cache;
|
|
1071
3054
|
if (cacheOptions && typeof cacheOptions === "number" && cacheOptions > 0) {
|
|
1072
|
-
const cacheKey = this.cacheManager.generateKey(
|
|
1073
|
-
authUser:
|
|
1074
|
-
});
|
|
1075
|
-
result = await this.cacheManager.get(cacheKey, async () =>
|
|
1076
|
-
const res = await handler(req);
|
|
1077
|
-
if (res instanceof Response) {
|
|
1078
|
-
return {
|
|
1079
|
-
_isResponse: true,
|
|
1080
|
-
body: await res.text(),
|
|
1081
|
-
status: res.status,
|
|
1082
|
-
headers: Object.fromEntries(res.headers.entries())
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
return res;
|
|
1086
|
-
}, cacheOptions);
|
|
3055
|
+
const cacheKey = this.applyCheckpointCacheNamespace(this.cacheManager.generateKey(context.request, {
|
|
3056
|
+
authUser: context.authUser
|
|
3057
|
+
}), context.request);
|
|
3058
|
+
result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions);
|
|
1087
3059
|
} else if (cacheOptions && typeof cacheOptions === "object" && cacheOptions.ttl) {
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
return res;
|
|
1102
|
-
}, cacheOptions.ttl);
|
|
3060
|
+
const hasRouteCacheKey = typeof cacheOptions.key === "string" && cacheOptions.key.length > 0;
|
|
3061
|
+
let cacheKey;
|
|
3062
|
+
if (hasRouteCacheKey) {
|
|
3063
|
+
cacheKey = this.applyCheckpointRouteKeyOverride(cacheOptions.key, context.request);
|
|
3064
|
+
} else {
|
|
3065
|
+
const generatedKey = this.cacheManager.generateKey(context.request, {
|
|
3066
|
+
authUser: context.authUser
|
|
3067
|
+
});
|
|
3068
|
+
cacheKey = this.applyCheckpointCacheNamespace(generatedKey, context.request);
|
|
3069
|
+
}
|
|
3070
|
+
result = await this.cacheManager.get(cacheKey, async () => await executeRoute(), cacheOptions.ttl);
|
|
1103
3071
|
} else {
|
|
1104
|
-
result = await
|
|
3072
|
+
result = await executeRoute();
|
|
1105
3073
|
}
|
|
1106
|
-
if (result
|
|
1107
|
-
result =
|
|
1108
|
-
status: result.status,
|
|
1109
|
-
headers: result.headers
|
|
1110
|
-
});
|
|
3074
|
+
if (result instanceof Response && !!cacheOptions) {
|
|
3075
|
+
result = result.clone();
|
|
1111
3076
|
}
|
|
1112
3077
|
let response;
|
|
1113
3078
|
if (options.rawResponse || result instanceof Response) {
|
|
@@ -1115,19 +3080,8 @@ class VectorRouter {
|
|
|
1115
3080
|
} else {
|
|
1116
3081
|
response = createResponse(200, result, options.responseContentType);
|
|
1117
3082
|
}
|
|
1118
|
-
response = await this.middlewareManager.executeFinally(response,
|
|
1119
|
-
|
|
1120
|
-
if (entries) {
|
|
1121
|
-
for (const [k, v] of entries) {
|
|
1122
|
-
response.headers.set(k, v);
|
|
1123
|
-
}
|
|
1124
|
-
} else {
|
|
1125
|
-
const dynamicCors = this.corsHandler;
|
|
1126
|
-
if (dynamicCors) {
|
|
1127
|
-
response = dynamicCors(response, req);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
return response;
|
|
3083
|
+
response = await this.middlewareManager.executeFinally(response, context);
|
|
3084
|
+
return this.applyCorsResponse(response, context.request);
|
|
1131
3085
|
} catch (error) {
|
|
1132
3086
|
if (error instanceof Response) {
|
|
1133
3087
|
return error;
|
|
@@ -1144,9 +3098,11 @@ class VectorRouter {
|
|
|
1144
3098
|
const nodeEnv = typeof Bun !== "undefined" ? Bun.env.NODE_ENV : "development";
|
|
1145
3099
|
return nodeEnv !== "production";
|
|
1146
3100
|
}
|
|
1147
|
-
async buildInputValidationPayload(
|
|
1148
|
-
|
|
1149
|
-
|
|
3101
|
+
async buildInputValidationPayload(context, options, fallbackParams, validationOptions) {
|
|
3102
|
+
const request = context.request;
|
|
3103
|
+
const includeBody = validationOptions?.includeBody !== false;
|
|
3104
|
+
let body = includeBody && this.hasOwnContextField(context, "content") ? context.content : undefined;
|
|
3105
|
+
if (includeBody && options.rawRequest && request.method !== "GET" && request.method !== "HEAD") {
|
|
1150
3106
|
try {
|
|
1151
3107
|
body = await request.clone().text();
|
|
1152
3108
|
} catch {
|
|
@@ -1154,79 +3110,150 @@ class VectorRouter {
|
|
|
1154
3110
|
}
|
|
1155
3111
|
}
|
|
1156
3112
|
return {
|
|
1157
|
-
params: request
|
|
1158
|
-
query: request
|
|
3113
|
+
params: this.getRequestParams(request, fallbackParams),
|
|
3114
|
+
query: this.getRequestQuery(request),
|
|
1159
3115
|
headers: Object.fromEntries(request.headers.entries()),
|
|
1160
|
-
cookies: request
|
|
3116
|
+
cookies: this.getRequestCookies(request),
|
|
1161
3117
|
body
|
|
1162
3118
|
};
|
|
1163
3119
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
if (
|
|
3120
|
+
getRequestParams(request, fallbackParams) {
|
|
3121
|
+
const nativeParams = this.readRequestObjectField(request, "params");
|
|
3122
|
+
if (nativeParams && Object.keys(nativeParams).length > 0) {
|
|
3123
|
+
return nativeParams;
|
|
3124
|
+
}
|
|
3125
|
+
return fallbackParams ?? {};
|
|
3126
|
+
}
|
|
3127
|
+
getRequestQuery(request) {
|
|
3128
|
+
const nativeQuery = this.readRequestObjectField(request, "query");
|
|
3129
|
+
if (nativeQuery) {
|
|
3130
|
+
return nativeQuery;
|
|
3131
|
+
}
|
|
3132
|
+
try {
|
|
3133
|
+
return VectorRouter.parseQuery(new URL(request.url));
|
|
3134
|
+
} catch {
|
|
3135
|
+
return {};
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
getRequestCookies(request) {
|
|
3139
|
+
const nativeCookies = this.readRequestObjectField(request, "cookies");
|
|
3140
|
+
if (nativeCookies) {
|
|
3141
|
+
return nativeCookies;
|
|
3142
|
+
}
|
|
3143
|
+
return VectorRouter.parseCookies(request.headers.get("cookie"));
|
|
3144
|
+
}
|
|
3145
|
+
readRequestObjectField(request, key) {
|
|
3146
|
+
const value = request[key];
|
|
3147
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1167
3148
|
return;
|
|
1168
3149
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
3150
|
+
return value;
|
|
3151
|
+
}
|
|
3152
|
+
applyValidatedInput(context, validatedValue) {
|
|
3153
|
+
this.setContextField(context, "validatedInput", validatedValue);
|
|
3154
|
+
}
|
|
3155
|
+
issueHasBodyPath(issue) {
|
|
3156
|
+
if (!issue || typeof issue !== "object" || !("path" in issue)) {
|
|
3157
|
+
return false;
|
|
1174
3158
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
} catch {}
|
|
3159
|
+
const path = issue.path;
|
|
3160
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
3161
|
+
return false;
|
|
1179
3162
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
3163
|
+
const segment = path[0];
|
|
3164
|
+
if (segment && typeof segment === "object" && "key" in segment) {
|
|
3165
|
+
return segment.key === "body";
|
|
3166
|
+
}
|
|
3167
|
+
return segment === "body";
|
|
3168
|
+
}
|
|
3169
|
+
issueHasExplicitNonBodyPath(issue) {
|
|
3170
|
+
if (!issue || typeof issue !== "object" || !("path" in issue)) {
|
|
3171
|
+
return false;
|
|
1184
3172
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
3173
|
+
const path = issue.path;
|
|
3174
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
3175
|
+
return false;
|
|
3176
|
+
}
|
|
3177
|
+
const segment = path[0];
|
|
3178
|
+
if (segment && typeof segment === "object" && "key" in segment) {
|
|
3179
|
+
return segment.key !== "body";
|
|
1187
3180
|
}
|
|
3181
|
+
return segment !== "body";
|
|
1188
3182
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
} catch {
|
|
1193
|
-
return;
|
|
3183
|
+
issueHasUnknownPath(issue) {
|
|
3184
|
+
if (!issue || typeof issue !== "object" || !("path" in issue)) {
|
|
3185
|
+
return true;
|
|
1194
3186
|
}
|
|
1195
|
-
|
|
3187
|
+
const path = issue.path;
|
|
3188
|
+
if (!Array.isArray(path)) {
|
|
3189
|
+
return true;
|
|
3190
|
+
}
|
|
3191
|
+
return path.length === 0;
|
|
1196
3192
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}
|
|
3193
|
+
shouldDeferBodyValidation(issues, context, validationOptions) {
|
|
3194
|
+
if (!(validationOptions?.allowBodyDeferral === true && validationOptions?.includeBody === false)) {
|
|
3195
|
+
return false;
|
|
3196
|
+
}
|
|
3197
|
+
const request = context.request;
|
|
3198
|
+
const mayHaveRequestBody = request.method !== "GET" && request.method !== "HEAD" && request.body !== null;
|
|
3199
|
+
if (!mayHaveRequestBody || issues.length === 0) {
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
if (issues.some((issue) => this.issueHasBodyPath(issue))) {
|
|
3203
|
+
return true;
|
|
3204
|
+
}
|
|
3205
|
+
const hasExplicitNonBodyPath = issues.some((issue) => this.issueHasExplicitNonBodyPath(issue));
|
|
3206
|
+
const hasUnknownPath = issues.some((issue) => this.issueHasUnknownPath(issue));
|
|
3207
|
+
return !hasExplicitNonBodyPath && hasUnknownPath;
|
|
1201
3208
|
}
|
|
1202
|
-
async validateInputSchema(
|
|
3209
|
+
async validateInputSchema(context, options, fallbackParams, validationOptions) {
|
|
1203
3210
|
const inputSchema = options.schema?.input;
|
|
1204
3211
|
if (!inputSchema) {
|
|
1205
|
-
return null;
|
|
3212
|
+
return { response: null, requiresBody: false };
|
|
1206
3213
|
}
|
|
1207
3214
|
if (options.validate === false) {
|
|
1208
|
-
return null;
|
|
3215
|
+
return { response: null, requiresBody: false };
|
|
1209
3216
|
}
|
|
1210
3217
|
if (!isStandardRouteSchema(inputSchema)) {
|
|
1211
|
-
return
|
|
3218
|
+
return {
|
|
3219
|
+
response: APIError.internalServerError("Invalid route schema configuration", options.responseContentType),
|
|
3220
|
+
requiresBody: false
|
|
3221
|
+
};
|
|
1212
3222
|
}
|
|
1213
3223
|
const includeRawIssues = this.isDevelopmentMode();
|
|
1214
|
-
const payload = await this.buildInputValidationPayload(
|
|
3224
|
+
const payload = await this.buildInputValidationPayload(context, options, fallbackParams, {
|
|
3225
|
+
includeBody: validationOptions?.includeBody
|
|
3226
|
+
});
|
|
1215
3227
|
try {
|
|
1216
3228
|
const validation = await runStandardValidation(inputSchema, payload);
|
|
1217
3229
|
if (validation.success === false) {
|
|
3230
|
+
if (this.shouldDeferBodyValidation(validation.issues, context, validationOptions)) {
|
|
3231
|
+
return { response: null, requiresBody: true };
|
|
3232
|
+
}
|
|
1218
3233
|
const issues = normalizeValidationIssues(validation.issues, includeRawIssues);
|
|
1219
|
-
return
|
|
3234
|
+
return {
|
|
3235
|
+
response: createResponse(422, createValidationErrorPayload("input", issues), options.responseContentType),
|
|
3236
|
+
requiresBody: false
|
|
3237
|
+
};
|
|
1220
3238
|
}
|
|
1221
|
-
this.applyValidatedInput(
|
|
1222
|
-
return null;
|
|
3239
|
+
this.applyValidatedInput(context, validation.value);
|
|
3240
|
+
return { response: null, requiresBody: false };
|
|
1223
3241
|
} catch (error) {
|
|
1224
3242
|
const thrownIssues = extractThrownIssues(error);
|
|
1225
3243
|
if (thrownIssues) {
|
|
3244
|
+
if (this.shouldDeferBodyValidation(thrownIssues, context, validationOptions)) {
|
|
3245
|
+
return { response: null, requiresBody: true };
|
|
3246
|
+
}
|
|
1226
3247
|
const issues = normalizeValidationIssues(thrownIssues, includeRawIssues);
|
|
1227
|
-
return
|
|
3248
|
+
return {
|
|
3249
|
+
response: createResponse(422, createValidationErrorPayload("input", issues), options.responseContentType),
|
|
3250
|
+
requiresBody: false
|
|
3251
|
+
};
|
|
1228
3252
|
}
|
|
1229
|
-
return
|
|
3253
|
+
return {
|
|
3254
|
+
response: APIError.internalServerError(error instanceof Error ? error.message : "Validation failed", options.responseContentType),
|
|
3255
|
+
requiresBody: false
|
|
3256
|
+
};
|
|
1230
3257
|
}
|
|
1231
3258
|
}
|
|
1232
3259
|
getOrCreateMethodMap(path) {
|
|
@@ -1277,6 +3304,19 @@ class VectorRouter {
|
|
|
1277
3304
|
}
|
|
1278
3305
|
return query;
|
|
1279
3306
|
}
|
|
3307
|
+
static parseCookies(cookieHeader) {
|
|
3308
|
+
const cookies = {};
|
|
3309
|
+
if (!cookieHeader) {
|
|
3310
|
+
return cookies;
|
|
3311
|
+
}
|
|
3312
|
+
for (const pair of cookieHeader.split(";")) {
|
|
3313
|
+
const idx = pair.indexOf("=");
|
|
3314
|
+
if (idx > 0) {
|
|
3315
|
+
cookies[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return cookies;
|
|
3319
|
+
}
|
|
1280
3320
|
routeSpecificityScore(path) {
|
|
1281
3321
|
const STATIC_SEGMENT_WEIGHT = 1000;
|
|
1282
3322
|
const PARAM_SEGMENT_WEIGHT = 10;
|
|
@@ -1299,6 +3339,20 @@ class VectorRouter {
|
|
|
1299
3339
|
}
|
|
1300
3340
|
return score;
|
|
1301
3341
|
}
|
|
3342
|
+
applyCorsResponse(response, request) {
|
|
3343
|
+
const entries = this.corsHeadersEntries;
|
|
3344
|
+
if (entries) {
|
|
3345
|
+
for (const [k, v] of entries) {
|
|
3346
|
+
response.headers.set(k, v);
|
|
3347
|
+
}
|
|
3348
|
+
return response;
|
|
3349
|
+
}
|
|
3350
|
+
const dynamicCors = this.corsHandler;
|
|
3351
|
+
if (dynamicCors) {
|
|
3352
|
+
return dynamicCors(response, request);
|
|
3353
|
+
}
|
|
3354
|
+
return response;
|
|
3355
|
+
}
|
|
1302
3356
|
}
|
|
1303
3357
|
|
|
1304
3358
|
// src/core/server.ts
|
|
@@ -1525,6 +3579,124 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1525
3579
|
.dark .json-number { color: #7dc9ff; }
|
|
1526
3580
|
.dark .json-boolean { color: #93a4bf; }
|
|
1527
3581
|
.dark .json-null { color: #7c8ba3; }
|
|
3582
|
+
.param-row {
|
|
3583
|
+
--param-row-bg-rgb: 255 255 255;
|
|
3584
|
+
}
|
|
3585
|
+
.dark .param-row {
|
|
3586
|
+
--param-row-bg-rgb: 10 10 10;
|
|
3587
|
+
}
|
|
3588
|
+
.param-row-head {
|
|
3589
|
+
display: grid;
|
|
3590
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
3591
|
+
align-items: center;
|
|
3592
|
+
gap: 0.5rem;
|
|
3593
|
+
min-width: 0;
|
|
3594
|
+
width: 100%;
|
|
3595
|
+
}
|
|
3596
|
+
.param-row-main {
|
|
3597
|
+
min-width: 0;
|
|
3598
|
+
display: flex;
|
|
3599
|
+
align-items: center;
|
|
3600
|
+
gap: 0.4rem;
|
|
3601
|
+
overflow: hidden;
|
|
3602
|
+
}
|
|
3603
|
+
.param-tooltip-trigger {
|
|
3604
|
+
border: 0;
|
|
3605
|
+
margin: 0;
|
|
3606
|
+
padding: 0;
|
|
3607
|
+
background: transparent;
|
|
3608
|
+
color: inherit;
|
|
3609
|
+
cursor: pointer;
|
|
3610
|
+
font: inherit;
|
|
3611
|
+
text-align: left;
|
|
3612
|
+
min-width: 0;
|
|
3613
|
+
}
|
|
3614
|
+
.param-tooltip-trigger:focus-visible {
|
|
3615
|
+
outline: 2px solid rgba(0, 161, 255, 0.65);
|
|
3616
|
+
outline-offset: 2px;
|
|
3617
|
+
border-radius: 0.375rem;
|
|
3618
|
+
}
|
|
3619
|
+
.param-name-trigger {
|
|
3620
|
+
min-width: 0;
|
|
3621
|
+
flex: 1 1 auto;
|
|
3622
|
+
overflow: hidden;
|
|
3623
|
+
}
|
|
3624
|
+
.param-name-text {
|
|
3625
|
+
display: block;
|
|
3626
|
+
max-width: 100%;
|
|
3627
|
+
overflow: hidden;
|
|
3628
|
+
text-overflow: ellipsis;
|
|
3629
|
+
white-space: nowrap;
|
|
3630
|
+
mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 18px), transparent 100%);
|
|
3631
|
+
-webkit-mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 18px), transparent 100%);
|
|
3632
|
+
}
|
|
3633
|
+
.param-type-fade {
|
|
3634
|
+
position: relative;
|
|
3635
|
+
z-index: 1;
|
|
3636
|
+
display: block;
|
|
3637
|
+
max-width: none;
|
|
3638
|
+
overflow: visible;
|
|
3639
|
+
text-overflow: clip;
|
|
3640
|
+
padding-left: 1.25rem;
|
|
3641
|
+
white-space: nowrap;
|
|
3642
|
+
text-align: left;
|
|
3643
|
+
justify-self: end;
|
|
3644
|
+
background: linear-gradient(
|
|
3645
|
+
90deg,
|
|
3646
|
+
rgba(var(--param-row-bg-rgb), 0) 0%,
|
|
3647
|
+
rgba(var(--param-row-bg-rgb), 0.76) 36%,
|
|
3648
|
+
rgba(var(--param-row-bg-rgb), 0.94) 68%,
|
|
3649
|
+
rgba(var(--param-row-bg-rgb), 1) 100%
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
#param-value-tooltip {
|
|
3653
|
+
position: fixed;
|
|
3654
|
+
top: 0;
|
|
3655
|
+
left: 0;
|
|
3656
|
+
z-index: 70;
|
|
3657
|
+
width: min(42rem, calc(100vw - 0.75rem));
|
|
3658
|
+
border-radius: 0.5rem;
|
|
3659
|
+
border: 1px solid rgba(15, 23, 42, 0.12);
|
|
3660
|
+
background: rgba(255, 255, 255, 0.92);
|
|
3661
|
+
color: #111111;
|
|
3662
|
+
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.14);
|
|
3663
|
+
backdrop-filter: blur(12px) saturate(145%);
|
|
3664
|
+
-webkit-backdrop-filter: blur(12px) saturate(145%);
|
|
3665
|
+
padding: 0.4rem 0.6rem;
|
|
3666
|
+
opacity: 0;
|
|
3667
|
+
pointer-events: none;
|
|
3668
|
+
transform: translateY(6px) scale(0.98);
|
|
3669
|
+
transition:
|
|
3670
|
+
opacity var(--motion-fast) var(--motion-ease),
|
|
3671
|
+
transform var(--motion-fast) var(--motion-ease);
|
|
3672
|
+
}
|
|
3673
|
+
#param-value-tooltip.is-visible {
|
|
3674
|
+
opacity: 1;
|
|
3675
|
+
pointer-events: auto;
|
|
3676
|
+
transform: translateY(0) scale(1);
|
|
3677
|
+
}
|
|
3678
|
+
.dark #param-value-tooltip {
|
|
3679
|
+
border-color: rgba(148, 163, 184, 0.24);
|
|
3680
|
+
background: rgba(17, 17, 17, 0.9);
|
|
3681
|
+
color: #ededed;
|
|
3682
|
+
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.45);
|
|
3683
|
+
}
|
|
3684
|
+
#param-tooltip-line {
|
|
3685
|
+
margin: 0;
|
|
3686
|
+
font-size: 11px;
|
|
3687
|
+
line-height: 1.3;
|
|
3688
|
+
font-family: "JetBrains Mono", monospace;
|
|
3689
|
+
white-space: normal;
|
|
3690
|
+
word-break: break-word;
|
|
3691
|
+
}
|
|
3692
|
+
#param-tooltip-description {
|
|
3693
|
+
margin: 0.2rem 0 0;
|
|
3694
|
+
font-size: 11px;
|
|
3695
|
+
line-height: 1.3;
|
|
3696
|
+
opacity: 0.8;
|
|
3697
|
+
white-space: normal;
|
|
3698
|
+
word-break: break-word;
|
|
3699
|
+
}
|
|
1528
3700
|
</style>
|
|
1529
3701
|
</head>
|
|
1530
3702
|
<body class="bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans antialiased flex h-screen overflow-hidden">
|
|
@@ -1554,6 +3726,16 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1554
3726
|
/>
|
|
1555
3727
|
</div>
|
|
1556
3728
|
</div>
|
|
3729
|
+
<div id="auth-panel" class="border-b border-light-border dark:border-dark-border">
|
|
3730
|
+
<button id="auth-toggle" class="w-full flex items-center justify-between px-4 py-2.5 text-xs font-semibold uppercase tracking-wider opacity-60 hover:opacity-100 transition-opacity">
|
|
3731
|
+
<span class="flex items-center gap-1.5">
|
|
3732
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
|
3733
|
+
Auth
|
|
3734
|
+
</span>
|
|
3735
|
+
<svg id="auth-chevron" class="w-3.5 h-3.5 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
|
3736
|
+
</button>
|
|
3737
|
+
<div id="auth-fields" class="px-4 pb-3 space-y-2"></div>
|
|
3738
|
+
</div>
|
|
1557
3739
|
<nav class="flex-1 overflow-y-auto px-3 py-2 space-y-6 text-sm" id="sidebar-nav"></nav>
|
|
1558
3740
|
</aside>
|
|
1559
3741
|
|
|
@@ -1587,6 +3769,9 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1587
3769
|
<span id="endpoint-method" class="px-2.5 py-0.5 rounded-full text-xs font-mono font-medium"></span>
|
|
1588
3770
|
<h2 class="text-xl font-semibold tracking-tight" id="endpoint-title">Operation</h2>
|
|
1589
3771
|
</div>
|
|
3772
|
+
<div id="deprecated-banner" class="hidden mb-4 px-3 py-2 rounded border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 text-xs font-medium">
|
|
3773
|
+
! This operation is deprecated
|
|
3774
|
+
</div>
|
|
1590
3775
|
<p class="text-sm opacity-80 mb-8 font-mono" id="endpoint-path">/</p>
|
|
1591
3776
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
|
|
1592
3777
|
<div class="lg:col-span-5 space-y-8" id="params-column"></div>
|
|
@@ -1682,6 +3867,10 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1682
3867
|
<pre id="expand-viewer" class="hidden w-full h-[70vh] text-sm p-3 rounded border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg overflow-auto font-mono"></pre>
|
|
1683
3868
|
</div>
|
|
1684
3869
|
</div>
|
|
3870
|
+
<div id="param-value-tooltip" aria-hidden="true" role="tooltip">
|
|
3871
|
+
<p id="param-tooltip-line"></p>
|
|
3872
|
+
<p id="param-tooltip-description" class="hidden"></p>
|
|
3873
|
+
</div>
|
|
1685
3874
|
|
|
1686
3875
|
<script>
|
|
1687
3876
|
const spec = ${specJson};
|
|
@@ -1752,14 +3941,71 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1752
3941
|
return ops;
|
|
1753
3942
|
}
|
|
1754
3943
|
|
|
3944
|
+
const AUTH_STATE_KEY = "vector-docs-auth-v1";
|
|
3945
|
+
const AUTH_SELECTION_KEY = "vector-docs-auth-selection-v1";
|
|
3946
|
+
const HEADERS_STATE_KEY = "vector-docs-headers-v1";
|
|
3947
|
+
|
|
3948
|
+
function loadSavedHeaders() {
|
|
3949
|
+
try {
|
|
3950
|
+
const raw = localStorage.getItem(HEADERS_STATE_KEY);
|
|
3951
|
+
if (raw) {
|
|
3952
|
+
const parsed = JSON.parse(raw);
|
|
3953
|
+
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
|
|
3954
|
+
}
|
|
3955
|
+
} catch {}
|
|
3956
|
+
return [{ key: "", value: "" }];
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
function saveHeaders() {
|
|
3960
|
+
try { localStorage.setItem(HEADERS_STATE_KEY, JSON.stringify(requestHeaders)); } catch {}
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
function loadAuthState() {
|
|
3964
|
+
try {
|
|
3965
|
+
const raw = localStorage.getItem(AUTH_STATE_KEY);
|
|
3966
|
+
if (raw) return JSON.parse(raw);
|
|
3967
|
+
} catch {}
|
|
3968
|
+
return {};
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
function saveAuthState() {
|
|
3972
|
+
try { localStorage.setItem(AUTH_STATE_KEY, JSON.stringify(authState)); } catch {}
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function loadAuthSelectionState() {
|
|
3976
|
+
try {
|
|
3977
|
+
const raw = localStorage.getItem(AUTH_SELECTION_KEY);
|
|
3978
|
+
if (raw) {
|
|
3979
|
+
const parsed = JSON.parse(raw);
|
|
3980
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3981
|
+
return parsed;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
} catch {}
|
|
3985
|
+
return {};
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
function saveAuthSelectionState() {
|
|
3989
|
+
try { localStorage.setItem(AUTH_SELECTION_KEY, JSON.stringify(authSelectionState)); } catch {}
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
const authSchemes = (spec.components && spec.components.securitySchemes) || {};
|
|
3993
|
+
let authState = loadAuthState();
|
|
3994
|
+
let authSelectionState = loadAuthSelectionState();
|
|
3995
|
+
|
|
1755
3996
|
const operations = getOperations();
|
|
1756
3997
|
let selected = operations[0] || null;
|
|
1757
3998
|
const operationParamValues = new Map();
|
|
1758
3999
|
const operationBodyDrafts = new Map();
|
|
1759
|
-
const requestHeaders =
|
|
4000
|
+
const requestHeaders = loadSavedHeaders();
|
|
1760
4001
|
let expandModalMode = null;
|
|
1761
4002
|
let isMobileSidebarOpen = false;
|
|
1762
4003
|
let sidebarSearchQuery = "";
|
|
4004
|
+
const paramTooltipRoot = document.getElementById("param-value-tooltip");
|
|
4005
|
+
const paramTooltipLine = document.getElementById("param-tooltip-line");
|
|
4006
|
+
const paramTooltipDescription = document.getElementById("param-tooltip-description");
|
|
4007
|
+
let activeParamTooltipTrigger = null;
|
|
4008
|
+
let paramTooltipHideTimer = null;
|
|
1763
4009
|
|
|
1764
4010
|
function setMobileSidebarOpen(open) {
|
|
1765
4011
|
const sidebar = document.getElementById("docs-sidebar");
|
|
@@ -1779,6 +4025,29 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1779
4025
|
return op.method + " " + op.path;
|
|
1780
4026
|
}
|
|
1781
4027
|
|
|
4028
|
+
function getOpHash(op) {
|
|
4029
|
+
var tag = op.tag || "default";
|
|
4030
|
+
var id = (op.operation && op.operation.operationId)
|
|
4031
|
+
? op.operation.operationId
|
|
4032
|
+
: op.method.toLowerCase() + "_" + op.path.split("/").filter(Boolean).join("_").replace(/[{}]/g, "");
|
|
4033
|
+
return "#/" + encodeURIComponent(tag) + "/" + encodeURIComponent(id);
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
function findOpByHash(hash) {
|
|
4037
|
+
if (!hash || hash.length <= 1) return null;
|
|
4038
|
+
var parts = hash.slice(1).split("/").filter(Boolean);
|
|
4039
|
+
if (parts.length < 2) return null;
|
|
4040
|
+
var hashTag = decodeURIComponent(parts[0]);
|
|
4041
|
+
var hashId = decodeURIComponent(parts[1]);
|
|
4042
|
+
return operations.find(function(op) {
|
|
4043
|
+
if (op.tag !== hashTag) return false;
|
|
4044
|
+
var id = (op.operation && op.operation.operationId)
|
|
4045
|
+
? op.operation.operationId
|
|
4046
|
+
: op.method.toLowerCase() + "_" + op.path.split("/").filter(Boolean).join("_").replace(/[{}]/g, "");
|
|
4047
|
+
return id === hashId;
|
|
4048
|
+
}) || null;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
1782
4051
|
function getOperationParameterGroups(op) {
|
|
1783
4052
|
const params =
|
|
1784
4053
|
op &&
|
|
@@ -1828,7 +4097,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1828
4097
|
return resolved;
|
|
1829
4098
|
}
|
|
1830
4099
|
|
|
1831
|
-
function buildRequestPath(op, pathParams, queryParams, values) {
|
|
4100
|
+
function buildRequestPath(op, pathParams, queryParams, values, extraQuery) {
|
|
1832
4101
|
const resolvedPath = resolvePath(op.path, pathParams, values);
|
|
1833
4102
|
const query = new URLSearchParams();
|
|
1834
4103
|
|
|
@@ -1840,6 +4109,13 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1840
4109
|
query.append(param.name, String(rawValue));
|
|
1841
4110
|
}
|
|
1842
4111
|
|
|
4112
|
+
if (extraQuery) {
|
|
4113
|
+
for (const key of Object.keys(extraQuery)) {
|
|
4114
|
+
const val = extraQuery[key];
|
|
4115
|
+
if (val) query.set(key, String(val));
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
|
|
1843
4119
|
const queryString = query.toString();
|
|
1844
4120
|
return queryString ? resolvedPath + "?" + queryString : resolvedPath;
|
|
1845
4121
|
}
|
|
@@ -1979,14 +4255,25 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
1979
4255
|
|
|
1980
4256
|
const name = document.createElement("span");
|
|
1981
4257
|
name.textContent = op.name;
|
|
4258
|
+
if (op.operation && op.operation.deprecated) {
|
|
4259
|
+
name.style.textDecoration = "line-through";
|
|
4260
|
+
name.style.opacity = "0.5";
|
|
4261
|
+
}
|
|
1982
4262
|
|
|
1983
4263
|
row.appendChild(method);
|
|
1984
4264
|
row.appendChild(name);
|
|
4265
|
+
if (op.operation && op.operation.deprecated) {
|
|
4266
|
+
const badge = document.createElement("span");
|
|
4267
|
+
badge.className = "text-[9px] px-1 py-0.5 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-500 font-semibold shrink-0";
|
|
4268
|
+
badge.textContent = "deprecated";
|
|
4269
|
+
row.appendChild(badge);
|
|
4270
|
+
}
|
|
1985
4271
|
a.appendChild(row);
|
|
1986
4272
|
|
|
1987
4273
|
a.onclick = (e) => {
|
|
1988
4274
|
e.preventDefault();
|
|
1989
4275
|
selected = op;
|
|
4276
|
+
history.pushState(null, "", getOpHash(op));
|
|
1990
4277
|
renderSidebar();
|
|
1991
4278
|
renderEndpoint();
|
|
1992
4279
|
if (window.innerWidth < 768) {
|
|
@@ -2000,51 +4287,257 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2000
4287
|
}
|
|
2001
4288
|
}
|
|
2002
4289
|
|
|
4290
|
+
function hideParamTooltip() {
|
|
4291
|
+
if (!paramTooltipRoot) return;
|
|
4292
|
+
if (paramTooltipHideTimer) {
|
|
4293
|
+
window.clearTimeout(paramTooltipHideTimer);
|
|
4294
|
+
paramTooltipHideTimer = null;
|
|
4295
|
+
}
|
|
4296
|
+
paramTooltipRoot.classList.remove("is-visible");
|
|
4297
|
+
paramTooltipRoot.setAttribute("aria-hidden", "true");
|
|
4298
|
+
if (activeParamTooltipTrigger) {
|
|
4299
|
+
activeParamTooltipTrigger.setAttribute("aria-expanded", "false");
|
|
4300
|
+
}
|
|
4301
|
+
activeParamTooltipTrigger = null;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
function scheduleParamTooltipHide() {
|
|
4305
|
+
if (paramTooltipHideTimer) {
|
|
4306
|
+
window.clearTimeout(paramTooltipHideTimer);
|
|
4307
|
+
}
|
|
4308
|
+
paramTooltipHideTimer = window.setTimeout(() => {
|
|
4309
|
+
hideParamTooltip();
|
|
4310
|
+
}, 95);
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
function positionParamTooltip(trigger) {
|
|
4314
|
+
if (!paramTooltipRoot || !trigger) return;
|
|
4315
|
+
const viewportPadding = 8;
|
|
4316
|
+
const spacing = 10;
|
|
4317
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
4318
|
+
const tooltipRect = paramTooltipRoot.getBoundingClientRect();
|
|
4319
|
+
let left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
|
|
4320
|
+
left = Math.max(viewportPadding, Math.min(left, window.innerWidth - tooltipRect.width - viewportPadding));
|
|
4321
|
+
let top = triggerRect.top - tooltipRect.height - spacing;
|
|
4322
|
+
if (top < viewportPadding) {
|
|
4323
|
+
top = triggerRect.bottom + spacing;
|
|
4324
|
+
}
|
|
4325
|
+
if (top + tooltipRect.height > window.innerHeight - viewportPadding) {
|
|
4326
|
+
top = window.innerHeight - tooltipRect.height - viewportPadding;
|
|
4327
|
+
}
|
|
4328
|
+
paramTooltipRoot.style.left = Math.round(left) + "px";
|
|
4329
|
+
paramTooltipRoot.style.top = Math.round(top) + "px";
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
function showParamTooltip(trigger) {
|
|
4333
|
+
if (
|
|
4334
|
+
!paramTooltipRoot ||
|
|
4335
|
+
!paramTooltipLine ||
|
|
4336
|
+
!paramTooltipDescription ||
|
|
4337
|
+
!trigger
|
|
4338
|
+
) {
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
if (paramTooltipHideTimer) {
|
|
4342
|
+
window.clearTimeout(paramTooltipHideTimer);
|
|
4343
|
+
paramTooltipHideTimer = null;
|
|
4344
|
+
}
|
|
4345
|
+
const label = trigger.getAttribute("data-param-tooltip-label") || "Value";
|
|
4346
|
+
const value = trigger.getAttribute("data-param-tooltip-value") || "";
|
|
4347
|
+
const related = trigger.getAttribute("data-param-tooltip-related") || "";
|
|
4348
|
+
const description = trigger.getAttribute("data-param-tooltip-description") || "";
|
|
4349
|
+
if (activeParamTooltipTrigger && activeParamTooltipTrigger !== trigger) {
|
|
4350
|
+
activeParamTooltipTrigger.setAttribute("aria-expanded", "false");
|
|
4351
|
+
}
|
|
4352
|
+
activeParamTooltipTrigger = trigger;
|
|
4353
|
+
activeParamTooltipTrigger.setAttribute("aria-expanded", "true");
|
|
4354
|
+
const pathLabel = related ? " | path: " + related : "";
|
|
4355
|
+
paramTooltipLine.textContent = label + ": " + value + pathLabel;
|
|
4356
|
+
if (description.trim()) {
|
|
4357
|
+
paramTooltipDescription.textContent = description;
|
|
4358
|
+
paramTooltipDescription.classList.remove("hidden");
|
|
4359
|
+
} else {
|
|
4360
|
+
paramTooltipDescription.textContent = "";
|
|
4361
|
+
paramTooltipDescription.classList.add("hidden");
|
|
4362
|
+
}
|
|
4363
|
+
paramTooltipRoot.classList.add("is-visible");
|
|
4364
|
+
paramTooltipRoot.setAttribute("aria-hidden", "false");
|
|
4365
|
+
positionParamTooltip(trigger);
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
function registerParamTooltipTargets(scope) {
|
|
4369
|
+
if (!scope) return;
|
|
4370
|
+
const targets = scope.querySelectorAll("[data-param-tooltip-value]");
|
|
4371
|
+
for (const target of targets) {
|
|
4372
|
+
target.addEventListener("click", (event) => {
|
|
4373
|
+
event.preventDefault();
|
|
4374
|
+
if (
|
|
4375
|
+
activeParamTooltipTrigger === target &&
|
|
4376
|
+
paramTooltipRoot &&
|
|
4377
|
+
paramTooltipRoot.classList.contains("is-visible")
|
|
4378
|
+
) {
|
|
4379
|
+
hideParamTooltip();
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
showParamTooltip(target);
|
|
4383
|
+
});
|
|
4384
|
+
target.addEventListener("mouseenter", () => {
|
|
4385
|
+
showParamTooltip(target);
|
|
4386
|
+
});
|
|
4387
|
+
target.addEventListener("mouseleave", (event) => {
|
|
4388
|
+
const related = event.relatedTarget;
|
|
4389
|
+
if (paramTooltipRoot && related && paramTooltipRoot.contains(related)) return;
|
|
4390
|
+
scheduleParamTooltipHide();
|
|
4391
|
+
});
|
|
4392
|
+
target.addEventListener("focus", () => {
|
|
4393
|
+
showParamTooltip(target);
|
|
4394
|
+
});
|
|
4395
|
+
target.addEventListener("blur", (event) => {
|
|
4396
|
+
const related = event.relatedTarget;
|
|
4397
|
+
if (paramTooltipRoot && related && paramTooltipRoot.contains(related)) return;
|
|
4398
|
+
scheduleParamTooltipHide();
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
|
|
2003
4403
|
function renderParamSection(title, params) {
|
|
2004
4404
|
if (!params.length) return "";
|
|
2005
4405
|
let rows = "";
|
|
2006
4406
|
for (const p of params) {
|
|
2007
|
-
const
|
|
2008
|
-
const
|
|
2009
|
-
|
|
4407
|
+
const schema = resolveSchemaRef(p.schema || {});
|
|
4408
|
+
const typeRaw = getSchemaTypeLabel(schema);
|
|
4409
|
+
const type = escapeHtml(typeRaw);
|
|
4410
|
+
const nameRaw = p.name || "";
|
|
4411
|
+
const name = escapeHtml(nameRaw);
|
|
4412
|
+
const tooltipName = escapeHtmlAttribute(nameRaw);
|
|
4413
|
+
const tooltipType = escapeHtmlAttribute(typeRaw);
|
|
4414
|
+
const tooltipDescription = (typeof p.description === "string" && p.description.trim())
|
|
4415
|
+
? escapeHtmlAttribute(p.description.trim())
|
|
4416
|
+
: (typeof schema.description === "string" && schema.description.trim())
|
|
4417
|
+
? escapeHtmlAttribute(schema.description.trim())
|
|
4418
|
+
: "";
|
|
4419
|
+
const desc = (typeof p.description === "string" && p.description.trim())
|
|
4420
|
+
? '<p class="text-xs opacity-60 mt-0.5 leading-snug">' + renderMarkdown(p.description.trim()) + '</p>'
|
|
4421
|
+
: "";
|
|
4422
|
+
const extra = buildSchemaExtra(schema);
|
|
4423
|
+
rows +=
|
|
4424
|
+
'<div class="param-row py-2 border-b border-light-border/50 dark:border-dark-border/50">' +
|
|
4425
|
+
'<div class="param-row-head">' +
|
|
4426
|
+
'<div class="param-row-main">' +
|
|
4427
|
+
'<button type="button" class="param-tooltip-trigger param-name-trigger" data-param-tooltip-label="Parameter" data-param-tooltip-value="' +
|
|
4428
|
+
tooltipName +
|
|
4429
|
+
'" data-param-tooltip-description="' +
|
|
4430
|
+
tooltipDescription +
|
|
4431
|
+
'" aria-expanded="false">' +
|
|
4432
|
+
'<code class="text-sm font-mono param-name-text">' +
|
|
4433
|
+
name +
|
|
4434
|
+
"</code></button>" +
|
|
4435
|
+
'<span class="text-xs text-brand shrink-0">' +
|
|
4436
|
+
(p.required ? "required" : "optional") +
|
|
4437
|
+
"</span></div>" +
|
|
4438
|
+
'<button type="button" class="param-tooltip-trigger param-type-fade text-xs font-mono opacity-60" data-param-tooltip-label="Type" data-param-tooltip-value="' +
|
|
4439
|
+
tooltipType +
|
|
4440
|
+
'" data-param-tooltip-description="' +
|
|
4441
|
+
tooltipDescription +
|
|
4442
|
+
'" aria-expanded="false">' +
|
|
4443
|
+
type +
|
|
4444
|
+
"</button></div>" +
|
|
4445
|
+
desc +
|
|
4446
|
+
extra +
|
|
4447
|
+
"</div>";
|
|
2010
4448
|
}
|
|
2011
4449
|
return '<div><h3 class="text-sm font-semibold mb-3 flex items-center border-b border-light-border dark:border-dark-border pb-2">' + escapeHtml(title) + "</h3>" + rows + "</div>";
|
|
2012
4450
|
}
|
|
2013
4451
|
|
|
2014
4452
|
function getSchemaTypeLabel(schema) {
|
|
2015
|
-
|
|
2016
|
-
if (
|
|
2017
|
-
if (
|
|
2018
|
-
if (
|
|
2019
|
-
if (
|
|
2020
|
-
if (
|
|
2021
|
-
if (Array.isArray(
|
|
2022
|
-
if (Array.isArray(
|
|
4453
|
+
const resolved = resolveSchemaRef(schema);
|
|
4454
|
+
if (!resolved || typeof resolved !== "object") return "unknown";
|
|
4455
|
+
if (Array.isArray(resolved.type)) return resolved.type.join(" | ");
|
|
4456
|
+
if (resolved.type) return String(resolved.type);
|
|
4457
|
+
if (resolved.properties) return "object";
|
|
4458
|
+
if (resolved.items) return "array";
|
|
4459
|
+
if (Array.isArray(resolved.oneOf)) return "oneOf";
|
|
4460
|
+
if (Array.isArray(resolved.anyOf)) return "anyOf";
|
|
4461
|
+
if (Array.isArray(resolved.allOf)) return "allOf";
|
|
2023
4462
|
return "unknown";
|
|
2024
4463
|
}
|
|
2025
4464
|
|
|
4465
|
+
function buildSchemaExtra(schema) {
|
|
4466
|
+
const resolved = resolveSchemaRef(schema);
|
|
4467
|
+
if (!resolved || typeof resolved !== "object") return "";
|
|
4468
|
+
const chips = [];
|
|
4469
|
+
if (resolved.format) chips.push(escapeHtml(String(resolved.format)));
|
|
4470
|
+
if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
|
|
4471
|
+
const shown = resolved.enum.slice(0, 5).map(function(v) { return escapeHtml(JSON.stringify(v)); });
|
|
4472
|
+
chips.push(shown.join(" | ") + (resolved.enum.length > 5 ? " \u2026" : ""));
|
|
4473
|
+
}
|
|
4474
|
+
if (resolved.minimum !== undefined) chips.push("min: " + resolved.minimum);
|
|
4475
|
+
if (resolved.maximum !== undefined) chips.push("max: " + resolved.maximum);
|
|
4476
|
+
if (typeof resolved.exclusiveMinimum === "number") chips.push(">" + resolved.exclusiveMinimum);
|
|
4477
|
+
if (typeof resolved.exclusiveMaximum === "number") chips.push("<" + resolved.exclusiveMaximum);
|
|
4478
|
+
if (resolved.minLength !== undefined) chips.push("minLen: " + resolved.minLength);
|
|
4479
|
+
if (resolved.maxLength !== undefined) chips.push("maxLen: " + resolved.maxLength);
|
|
4480
|
+
if (resolved.minItems !== undefined) chips.push("minItems: " + resolved.minItems);
|
|
4481
|
+
if (resolved.maxItems !== undefined) chips.push("maxItems: " + resolved.maxItems);
|
|
4482
|
+
if (resolved.uniqueItems) chips.push("unique");
|
|
4483
|
+
if (resolved.pattern) chips.push("/" + escapeHtml(String(resolved.pattern)) + "/");
|
|
4484
|
+
if (!chips.length) return "";
|
|
4485
|
+
return '<div class="flex flex-wrap gap-1 mt-1.5">' +
|
|
4486
|
+
chips.map(function(c) {
|
|
4487
|
+
return '<span class="text-[10px] px-1.5 py-0.5 rounded bg-black/5 dark:bg-white/5 font-mono opacity-80">' + c + '</span>';
|
|
4488
|
+
}).join("") +
|
|
4489
|
+
'</div>';
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
function resolveSchemaRef(schema, visitedRefs) {
|
|
4493
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
4494
|
+
const ref = typeof schema.$ref === "string" ? schema.$ref : "";
|
|
4495
|
+
if (!ref || !ref.startsWith("#/components/schemas/")) {
|
|
4496
|
+
return schema;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
const seen = visitedRefs || new Set();
|
|
4500
|
+
if (seen.has(ref)) return schema;
|
|
4501
|
+
seen.add(ref);
|
|
4502
|
+
|
|
4503
|
+
const parts = ref.split("/");
|
|
4504
|
+
const schemaName = parts[parts.length - 1];
|
|
4505
|
+
const referenced = spec && spec.components && spec.components.schemas && spec.components.schemas[schemaName];
|
|
4506
|
+
if (!referenced || typeof referenced !== "object") return schema;
|
|
4507
|
+
|
|
4508
|
+
const merged = Object.assign({}, referenced, schema);
|
|
4509
|
+
delete merged.$ref;
|
|
4510
|
+
return resolveSchemaRef(merged, seen);
|
|
4511
|
+
}
|
|
4512
|
+
|
|
2026
4513
|
function buildSchemaChildren(schema) {
|
|
2027
|
-
|
|
4514
|
+
const resolved = resolveSchemaRef(schema);
|
|
4515
|
+
if (!resolved || typeof resolved !== "object") return [];
|
|
2028
4516
|
|
|
2029
4517
|
const children = [];
|
|
2030
4518
|
|
|
2031
|
-
if (
|
|
4519
|
+
if (resolved.properties && typeof resolved.properties === "object") {
|
|
2032
4520
|
const requiredSet = new Set(
|
|
2033
|
-
Array.isArray(
|
|
4521
|
+
Array.isArray(resolved.required) ? resolved.required : [],
|
|
2034
4522
|
);
|
|
2035
|
-
for (const [name, childSchema] of Object.entries(
|
|
4523
|
+
for (const [name, childSchema] of Object.entries(resolved.properties)) {
|
|
4524
|
+
const childDef = childSchema || {};
|
|
4525
|
+
const isArrayType = Array.isArray(childDef.type)
|
|
4526
|
+
? childDef.type.includes("array")
|
|
4527
|
+
: childDef.type === "array";
|
|
4528
|
+
const isArrayLike = isArrayType || childDef.items !== undefined;
|
|
2036
4529
|
children.push({
|
|
2037
|
-
name,
|
|
2038
|
-
schema:
|
|
4530
|
+
name: isArrayLike ? (name + "[]") : name,
|
|
4531
|
+
schema: childDef,
|
|
2039
4532
|
required: requiredSet.has(name),
|
|
2040
4533
|
});
|
|
2041
4534
|
}
|
|
2042
4535
|
}
|
|
2043
4536
|
|
|
2044
|
-
if (
|
|
4537
|
+
if (resolved.items) {
|
|
2045
4538
|
children.push({
|
|
2046
|
-
name:
|
|
2047
|
-
schema:
|
|
4539
|
+
name: getArrayItemNodeName(resolved.items),
|
|
4540
|
+
schema: resolved.items,
|
|
2048
4541
|
required: true,
|
|
2049
4542
|
});
|
|
2050
4543
|
}
|
|
@@ -2052,45 +4545,100 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2052
4545
|
return children;
|
|
2053
4546
|
}
|
|
2054
4547
|
|
|
2055
|
-
function
|
|
2056
|
-
|
|
2057
|
-
const
|
|
4548
|
+
function getArrayItemNodeName(itemSchema) {
|
|
4549
|
+
if (!itemSchema || typeof itemSchema !== "object") return "item";
|
|
4550
|
+
const title =
|
|
4551
|
+
typeof itemSchema.title === "string" && itemSchema.title.trim()
|
|
4552
|
+
? itemSchema.title.trim()
|
|
4553
|
+
: "";
|
|
4554
|
+
if (title) return title;
|
|
4555
|
+
|
|
4556
|
+
const ref =
|
|
4557
|
+
typeof itemSchema.$ref === "string" && itemSchema.$ref.trim()
|
|
4558
|
+
? itemSchema.$ref.trim()
|
|
4559
|
+
: "";
|
|
4560
|
+
if (ref) {
|
|
4561
|
+
const parts = ref.split("/").filter(Boolean);
|
|
4562
|
+
const last = parts[parts.length - 1];
|
|
4563
|
+
if (last) return last;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
const typeLabel = getSchemaTypeLabel(itemSchema);
|
|
4567
|
+
if (typeLabel && typeLabel !== "unknown") return typeLabel;
|
|
4568
|
+
return "type";
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
function renderSchemaFieldNode(field, depth, parentPath) {
|
|
4572
|
+
const schema = resolveSchemaRef(field.schema || {});
|
|
4573
|
+
const nameRaw = field.name || "field";
|
|
4574
|
+
const name = escapeHtml(nameRaw);
|
|
2058
4575
|
const requiredLabel = field.required ? "required" : "optional";
|
|
2059
|
-
const
|
|
4576
|
+
const typeRaw = getSchemaTypeLabel(schema);
|
|
4577
|
+
const type = escapeHtml(typeRaw);
|
|
4578
|
+
const tooltipName = escapeHtmlAttribute(nameRaw);
|
|
4579
|
+
const tooltipType = escapeHtmlAttribute(typeRaw);
|
|
4580
|
+
const fieldPath = parentPath ? (parentPath + "." + nameRaw) : nameRaw;
|
|
4581
|
+
const tooltipPath = escapeHtmlAttribute(fieldPath);
|
|
4582
|
+
const tooltipDescription = (typeof schema.description === "string" && schema.description.trim())
|
|
4583
|
+
? escapeHtmlAttribute(schema.description.trim())
|
|
4584
|
+
: "";
|
|
2060
4585
|
const children = buildSchemaChildren(schema);
|
|
2061
4586
|
const padding = depth * 14;
|
|
4587
|
+
const extra = buildSchemaExtra(schema);
|
|
2062
4588
|
|
|
2063
4589
|
if (!children.length) {
|
|
2064
4590
|
return (
|
|
2065
|
-
'<div class="py-2 border-b border-light-border/50 dark:border-dark-border/50" style="padding-left:' +
|
|
4591
|
+
'<div class="param-row py-2 border-b border-light-border/50 dark:border-dark-border/50" style="padding-left:' +
|
|
2066
4592
|
padding +
|
|
2067
|
-
'px"><div class="
|
|
4593
|
+
'px"><div class="param-row-head"><div class="param-row-main">' +
|
|
4594
|
+
'<button type="button" class="param-tooltip-trigger param-name-trigger" data-param-tooltip-label="Field" data-param-tooltip-value="' +
|
|
4595
|
+
tooltipName +
|
|
4596
|
+
'" data-param-tooltip-related="' +
|
|
4597
|
+
tooltipPath +
|
|
4598
|
+
'" data-param-tooltip-description="' +
|
|
4599
|
+
tooltipDescription +
|
|
4600
|
+
'" aria-expanded="false"><code class="text-sm font-mono param-name-text">' +
|
|
2068
4601
|
name +
|
|
2069
|
-
'</code><span class="text-xs text-brand
|
|
4602
|
+
'</code></button><span class="text-xs text-brand shrink-0">' +
|
|
2070
4603
|
requiredLabel +
|
|
2071
|
-
'</span></div><
|
|
4604
|
+
'</span></div><button type="button" class="param-tooltip-trigger param-type-fade text-xs font-mono opacity-60" data-param-tooltip-label="Type" data-param-tooltip-value="' +
|
|
4605
|
+
tooltipType +
|
|
4606
|
+
'" data-param-tooltip-description="' +
|
|
4607
|
+
tooltipDescription +
|
|
4608
|
+
'" aria-expanded="false">' +
|
|
2072
4609
|
type +
|
|
2073
|
-
"</
|
|
4610
|
+
"</button></div>" + extra + "</div>"
|
|
2074
4611
|
);
|
|
2075
4612
|
}
|
|
2076
4613
|
|
|
2077
4614
|
let nested = "";
|
|
2078
4615
|
for (const child of children) {
|
|
2079
|
-
nested += renderSchemaFieldNode(child, depth + 1);
|
|
4616
|
+
nested += renderSchemaFieldNode(child, depth + 1, fieldPath);
|
|
2080
4617
|
}
|
|
2081
4618
|
|
|
2082
4619
|
return (
|
|
2083
|
-
'<details
|
|
2084
|
-
'<summary class="list-none cursor-pointer py-2
|
|
4620
|
+
'<details open>' +
|
|
4621
|
+
'<summary class="list-none cursor-pointer py-2 border-b border-light-border/50 dark:border-dark-border/50" style="padding-left:' +
|
|
2085
4622
|
padding +
|
|
2086
|
-
'px"><div class="
|
|
4623
|
+
'px"><div class="param-row-head"><div class="param-row-main"><span class="text-xs opacity-70 shrink-0">\u25BE</span>' +
|
|
4624
|
+
'<button type="button" class="param-tooltip-trigger param-name-trigger" data-param-tooltip-label="Field" data-param-tooltip-value="' +
|
|
4625
|
+
tooltipName +
|
|
4626
|
+
'" data-param-tooltip-related="' +
|
|
4627
|
+
tooltipPath +
|
|
4628
|
+
'" data-param-tooltip-description="' +
|
|
4629
|
+
tooltipDescription +
|
|
4630
|
+
'" aria-expanded="false"><code class="text-sm font-mono param-name-text">' +
|
|
2087
4631
|
name +
|
|
2088
|
-
'</code><span class="text-xs text-brand">' +
|
|
4632
|
+
'</code></button><span class="text-xs text-brand shrink-0">' +
|
|
2089
4633
|
requiredLabel +
|
|
2090
|
-
'</span></div><
|
|
4634
|
+
'</span></div><button type="button" class="param-tooltip-trigger param-type-fade text-xs font-mono opacity-60" data-param-tooltip-label="Type" data-param-tooltip-value="' +
|
|
4635
|
+
tooltipType +
|
|
4636
|
+
'" data-param-tooltip-description="' +
|
|
4637
|
+
tooltipDescription +
|
|
4638
|
+
'" aria-expanded="false">' +
|
|
2091
4639
|
type +
|
|
2092
|
-
"</
|
|
2093
|
-
|
|
4640
|
+
"</button></div>" + extra + "</summary>" +
|
|
4641
|
+
"<div>" +
|
|
2094
4642
|
nested +
|
|
2095
4643
|
"</div></details>"
|
|
2096
4644
|
);
|
|
@@ -2103,7 +4651,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2103
4651
|
|
|
2104
4652
|
let rows = "";
|
|
2105
4653
|
for (const child of rootChildren) {
|
|
2106
|
-
rows += renderSchemaFieldNode(child, 0);
|
|
4654
|
+
rows += renderSchemaFieldNode(child, 0, "");
|
|
2107
4655
|
}
|
|
2108
4656
|
|
|
2109
4657
|
return (
|
|
@@ -2130,27 +4678,40 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2130
4678
|
const responseDef = responses[statusCode];
|
|
2131
4679
|
if (!responseDef || typeof responseDef !== "object") continue;
|
|
2132
4680
|
|
|
4681
|
+
const responseDesc = (typeof responseDef.description === "string" && responseDef.description.trim())
|
|
4682
|
+
? responseDef.description.trim()
|
|
4683
|
+
: "";
|
|
4684
|
+
|
|
2133
4685
|
const jsonSchema =
|
|
2134
4686
|
responseDef.content &&
|
|
2135
4687
|
responseDef.content["application/json"] &&
|
|
2136
4688
|
responseDef.content["application/json"].schema;
|
|
2137
4689
|
|
|
2138
|
-
if (!jsonSchema || typeof jsonSchema !== "object") continue;
|
|
2139
|
-
|
|
2140
|
-
const rootChildren = buildSchemaChildren(jsonSchema);
|
|
2141
|
-
if (!rootChildren.length) continue;
|
|
2142
|
-
|
|
2143
4690
|
let rows = "";
|
|
2144
|
-
|
|
2145
|
-
|
|
4691
|
+
if (jsonSchema && typeof jsonSchema === "object") {
|
|
4692
|
+
const rootChildren = buildSchemaChildren(jsonSchema);
|
|
4693
|
+
for (const child of rootChildren) {
|
|
4694
|
+
rows += renderSchemaFieldNode(child, 0, "");
|
|
4695
|
+
}
|
|
2146
4696
|
}
|
|
2147
4697
|
|
|
4698
|
+
if (!responseDesc && !rows) continue;
|
|
4699
|
+
|
|
4700
|
+
const descHtml = responseDesc
|
|
4701
|
+
? ' <span class="normal-case font-sans opacity-70 ml-1">\u2014 ' + escapeHtml(responseDesc) + '</span>'
|
|
4702
|
+
: "";
|
|
4703
|
+
const contentHtml = rows || '<p class="text-xs opacity-60 mt-1">No schema fields</p>';
|
|
4704
|
+
|
|
2148
4705
|
sections +=
|
|
2149
|
-
'<
|
|
4706
|
+
'<details class="mb-4">' +
|
|
4707
|
+
'<summary class="list-none cursor-pointer">' +
|
|
4708
|
+
'<h4 class="text-xs font-mono uppercase tracking-wider opacity-70 mb-2">Status ' +
|
|
2150
4709
|
escapeHtml(statusCode) +
|
|
4710
|
+
descHtml +
|
|
2151
4711
|
"</h4>" +
|
|
2152
|
-
|
|
2153
|
-
|
|
4712
|
+
"</summary>" +
|
|
4713
|
+
contentHtml +
|
|
4714
|
+
"</details>";
|
|
2154
4715
|
}
|
|
2155
4716
|
|
|
2156
4717
|
if (!sections) return "";
|
|
@@ -2242,6 +4803,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2242
4803
|
"w-full text-xs px-2.5 py-2 rounded border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg focus:outline-none focus:border-brand dark:focus:border-brand transition-colors font-mono";
|
|
2243
4804
|
keyInput.addEventListener("input", () => {
|
|
2244
4805
|
entry.key = keyInput.value;
|
|
4806
|
+
saveHeaders();
|
|
2245
4807
|
updateRequestPreview();
|
|
2246
4808
|
});
|
|
2247
4809
|
|
|
@@ -2256,6 +4818,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2256
4818
|
"w-full text-xs px-2.5 py-2 rounded border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg focus:outline-none focus:border-brand dark:focus:border-brand transition-colors font-mono";
|
|
2257
4819
|
valueInput.addEventListener("input", () => {
|
|
2258
4820
|
entry.value = valueInput.value;
|
|
4821
|
+
saveHeaders();
|
|
2259
4822
|
updateRequestPreview();
|
|
2260
4823
|
});
|
|
2261
4824
|
|
|
@@ -2269,6 +4832,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2269
4832
|
'<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 12h12"></path></svg>';
|
|
2270
4833
|
removeButton.addEventListener("click", () => {
|
|
2271
4834
|
requestHeaders.splice(index, 1);
|
|
4835
|
+
saveHeaders();
|
|
2272
4836
|
renderHeaderInputs();
|
|
2273
4837
|
updateRequestPreview();
|
|
2274
4838
|
});
|
|
@@ -2285,15 +4849,32 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2285
4849
|
return Object.keys(headers).some((key) => key.toLowerCase() === target);
|
|
2286
4850
|
}
|
|
2287
4851
|
|
|
2288
|
-
function
|
|
2289
|
-
const
|
|
4852
|
+
function buildCookieHeaderValue(cookieValues) {
|
|
4853
|
+
const entries = Object.entries(cookieValues);
|
|
4854
|
+
if (!entries.length) return "";
|
|
4855
|
+
return entries
|
|
4856
|
+
.map(([name, value]) => String(name) + "=" + encodeURIComponent(String(value)))
|
|
4857
|
+
.join("; ");
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
function getRequestHeadersObject(op) {
|
|
4861
|
+
const auth = getAuthHeaders(op);
|
|
4862
|
+
const authCookies = getAuthCookieParams(op);
|
|
4863
|
+
if (Object.keys(authCookies).length > 0) {
|
|
4864
|
+
const cookieHeader = buildCookieHeaderValue(authCookies);
|
|
4865
|
+
if (cookieHeader) {
|
|
4866
|
+
auth["Cookie"] = cookieHeader;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
const manual = {};
|
|
2290
4870
|
for (const entry of requestHeaders) {
|
|
2291
4871
|
const key = String(entry.key || "").trim();
|
|
2292
4872
|
const value = String(entry.value || "").trim();
|
|
2293
4873
|
if (!key || !value) continue;
|
|
2294
|
-
|
|
4874
|
+
manual[key] = value;
|
|
2295
4875
|
}
|
|
2296
|
-
|
|
4876
|
+
// Auth provides defaults; manual headers win on conflict
|
|
4877
|
+
return Object.assign({}, auth, manual);
|
|
2297
4878
|
}
|
|
2298
4879
|
|
|
2299
4880
|
function buildCurl(op, headers, body, requestPath) {
|
|
@@ -2330,6 +4911,33 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2330
4911
|
.replace(/>/g, ">");
|
|
2331
4912
|
}
|
|
2332
4913
|
|
|
4914
|
+
function escapeHtmlAttribute(value) {
|
|
4915
|
+
return String(value)
|
|
4916
|
+
.replace(/&/g, "&")
|
|
4917
|
+
.replace(/</g, "<")
|
|
4918
|
+
.replace(/>/g, ">")
|
|
4919
|
+
.replace(/"/g, """)
|
|
4920
|
+
.replace(/'/g, "'");
|
|
4921
|
+
}
|
|
4922
|
+
|
|
4923
|
+
function renderMarkdown(text) {
|
|
4924
|
+
if (!text || typeof text !== "string") return "";
|
|
4925
|
+
var s = escapeHtml(text);
|
|
4926
|
+
// inline code \u2014 process first to protect content inside backticks
|
|
4927
|
+
s = s.replace(/\`([^\`\\n]+)\`/g, '<code class="text-xs font-mono bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded">$1</code>');
|
|
4928
|
+
// bold **text**
|
|
4929
|
+
s = s.replace(/\\*\\*([^*\\n]+)\\*\\*/g, '<strong>$1</strong>');
|
|
4930
|
+
// italic *text*
|
|
4931
|
+
s = s.replace(/\\*([^*\\n]+)\\*/g, '<em>$1</em>');
|
|
4932
|
+
// links [text](url)
|
|
4933
|
+
s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, function(m, txt, url) {
|
|
4934
|
+
var lc = url.toLowerCase().replace(/\\s/g, "");
|
|
4935
|
+
if (lc.indexOf("javascript:") === 0 || lc.indexOf("data:") === 0 || lc.indexOf("vbscript:") === 0) return txt;
|
|
4936
|
+
return '<a href="' + url.replace(/"/g, '"') + '" target="_blank" rel="noopener noreferrer" class="text-brand hover:underline">' + txt + '</a>';
|
|
4937
|
+
});
|
|
4938
|
+
return s;
|
|
4939
|
+
}
|
|
4940
|
+
|
|
2333
4941
|
function toPrettyJson(value) {
|
|
2334
4942
|
const trimmed = (value || "").trim();
|
|
2335
4943
|
if (!trimmed) return null;
|
|
@@ -2463,10 +5071,10 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2463
5071
|
|
|
2464
5072
|
const { path, query } = getOperationParameterGroups(selected);
|
|
2465
5073
|
const values = getParameterValues(selected);
|
|
2466
|
-
const requestPath = buildRequestPath(selected, path, query, values);
|
|
5074
|
+
const requestPath = buildRequestPath(selected, path, query, values, getAuthQueryParams(selected));
|
|
2467
5075
|
const bodyInput = document.getElementById("body-input");
|
|
2468
5076
|
const body = bodyInput ? bodyInput.value.trim() : "";
|
|
2469
|
-
const headers = getRequestHeadersObject();
|
|
5077
|
+
const headers = getRequestHeadersObject(selected);
|
|
2470
5078
|
if (body && !hasHeaderName(headers, "Content-Type")) {
|
|
2471
5079
|
headers["Content-Type"] = "application/json";
|
|
2472
5080
|
}
|
|
@@ -2517,8 +5125,13 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2517
5125
|
}
|
|
2518
5126
|
setResponseContent("", "", false);
|
|
2519
5127
|
|
|
5128
|
+
const deprecatedBanner = document.getElementById("deprecated-banner");
|
|
5129
|
+
if (deprecatedBanner) {
|
|
5130
|
+
deprecatedBanner.classList.toggle("hidden", !op.deprecated);
|
|
5131
|
+
}
|
|
5132
|
+
|
|
2520
5133
|
document.getElementById("tag-title").textContent = selected.tag;
|
|
2521
|
-
document.getElementById("tag-description").
|
|
5134
|
+
document.getElementById("tag-description").innerHTML = op.description ? renderMarkdown(op.description) : "Interactive API documentation.";
|
|
2522
5135
|
const methodNode = document.getElementById("endpoint-method");
|
|
2523
5136
|
methodNode.textContent = selected.method;
|
|
2524
5137
|
methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || methodBadgeDefault);
|
|
@@ -2535,7 +5148,13 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2535
5148
|
|
|
2536
5149
|
html += renderRequestBodySchemaSection(reqSchema);
|
|
2537
5150
|
html += renderResponseSchemasSection(op.responses);
|
|
2538
|
-
document.getElementById("params-column")
|
|
5151
|
+
const paramsColumn = document.getElementById("params-column");
|
|
5152
|
+
if (paramsColumn) {
|
|
5153
|
+
hideParamTooltip();
|
|
5154
|
+
paramsColumn.innerHTML = html || '<div class="text-sm opacity-70">No parameters</div>';
|
|
5155
|
+
registerParamTooltipTargets(paramsColumn);
|
|
5156
|
+
}
|
|
5157
|
+
renderAuthPanel();
|
|
2539
5158
|
renderTryItParameterInputs(path, query);
|
|
2540
5159
|
renderHeaderInputs();
|
|
2541
5160
|
updateRequestPreview();
|
|
@@ -2546,6 +5165,24 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2546
5165
|
document.getElementById("copy-curl").addEventListener("click", async () => {
|
|
2547
5166
|
try { await navigator.clipboard.writeText(document.getElementById("curl-code").textContent || ""); } catch {}
|
|
2548
5167
|
});
|
|
5168
|
+
if (paramTooltipRoot) {
|
|
5169
|
+
paramTooltipRoot.addEventListener("mouseenter", () => {
|
|
5170
|
+
if (paramTooltipHideTimer) {
|
|
5171
|
+
window.clearTimeout(paramTooltipHideTimer);
|
|
5172
|
+
paramTooltipHideTimer = null;
|
|
5173
|
+
}
|
|
5174
|
+
});
|
|
5175
|
+
paramTooltipRoot.addEventListener("mouseleave", () => {
|
|
5176
|
+
scheduleParamTooltipHide();
|
|
5177
|
+
});
|
|
5178
|
+
}
|
|
5179
|
+
document.addEventListener("pointerdown", (event) => {
|
|
5180
|
+
if (!paramTooltipRoot) return;
|
|
5181
|
+
const target = event.target;
|
|
5182
|
+
if (target && paramTooltipRoot.contains(target)) return;
|
|
5183
|
+
if (target && target.closest && target.closest("[data-param-tooltip-value]")) return;
|
|
5184
|
+
hideParamTooltip();
|
|
5185
|
+
});
|
|
2549
5186
|
document.getElementById("sidebar-search").addEventListener("input", (event) => {
|
|
2550
5187
|
sidebarSearchQuery = event.currentTarget.value || "";
|
|
2551
5188
|
renderSidebar();
|
|
@@ -2571,7 +5208,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2571
5208
|
return;
|
|
2572
5209
|
}
|
|
2573
5210
|
|
|
2574
|
-
const requestPath = buildRequestPath(selected, path, query, values);
|
|
5211
|
+
const requestPath = buildRequestPath(selected, path, query, values, getAuthQueryParams(selected));
|
|
2575
5212
|
formatBodyJsonInput();
|
|
2576
5213
|
updateBodyJsonPresentation();
|
|
2577
5214
|
const op = selected.operation || {};
|
|
@@ -2580,7 +5217,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2580
5217
|
const bodyInput = document.getElementById("body-input");
|
|
2581
5218
|
const body =
|
|
2582
5219
|
supportsBody && bodyInput ? bodyInput.value.trim() : "";
|
|
2583
|
-
const headers = getRequestHeadersObject();
|
|
5220
|
+
const headers = getRequestHeadersObject(selected);
|
|
2584
5221
|
if (body && !hasHeaderName(headers, "Content-Type")) {
|
|
2585
5222
|
headers["Content-Type"] = "application/json";
|
|
2586
5223
|
}
|
|
@@ -2588,7 +5225,13 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2588
5225
|
setSubmitLoading(true);
|
|
2589
5226
|
try {
|
|
2590
5227
|
const requestStart = performance.now();
|
|
2591
|
-
|
|
5228
|
+
applyAuthCookies(selected);
|
|
5229
|
+
const response = await fetch(requestPath, {
|
|
5230
|
+
method: selected.method,
|
|
5231
|
+
headers,
|
|
5232
|
+
body: body || undefined,
|
|
5233
|
+
credentials: "same-origin",
|
|
5234
|
+
});
|
|
2592
5235
|
const text = await response.text();
|
|
2593
5236
|
const responseTimeMs = Math.round(performance.now() - requestStart);
|
|
2594
5237
|
const contentType = response.headers.get("content-type") || "unknown";
|
|
@@ -2664,6 +5307,7 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2664
5307
|
|
|
2665
5308
|
document.getElementById("add-header-btn").addEventListener("click", () => {
|
|
2666
5309
|
requestHeaders.push({ key: "", value: "" });
|
|
5310
|
+
saveHeaders();
|
|
2667
5311
|
renderHeaderInputs();
|
|
2668
5312
|
updateRequestPreview();
|
|
2669
5313
|
});
|
|
@@ -2777,12 +5421,21 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2777
5421
|
setMobileSidebarOpen(false);
|
|
2778
5422
|
});
|
|
2779
5423
|
window.addEventListener("resize", () => {
|
|
5424
|
+
if (activeParamTooltipTrigger) {
|
|
5425
|
+
positionParamTooltip(activeParamTooltipTrigger);
|
|
5426
|
+
}
|
|
2780
5427
|
if (window.innerWidth >= 768 && isMobileSidebarOpen) {
|
|
2781
5428
|
setMobileSidebarOpen(false);
|
|
2782
5429
|
}
|
|
2783
5430
|
});
|
|
5431
|
+
window.addEventListener("scroll", () => {
|
|
5432
|
+
if (activeParamTooltipTrigger) {
|
|
5433
|
+
positionParamTooltip(activeParamTooltipTrigger);
|
|
5434
|
+
}
|
|
5435
|
+
}, true);
|
|
2784
5436
|
document.addEventListener("keydown", (event) => {
|
|
2785
5437
|
if (event.key === "Escape") {
|
|
5438
|
+
hideParamTooltip();
|
|
2786
5439
|
if (isMobileSidebarOpen) {
|
|
2787
5440
|
setMobileSidebarOpen(false);
|
|
2788
5441
|
}
|
|
@@ -2807,7 +5460,444 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2807
5460
|
}
|
|
2808
5461
|
});
|
|
2809
5462
|
|
|
5463
|
+
function getOperationSecurityRequirements(op) {
|
|
5464
|
+
const operationSecurity = op && op.operation && Array.isArray(op.operation.security)
|
|
5465
|
+
? op.operation.security
|
|
5466
|
+
: null;
|
|
5467
|
+
if (operationSecurity) {
|
|
5468
|
+
return operationSecurity;
|
|
5469
|
+
}
|
|
5470
|
+
return Array.isArray(spec.security) ? spec.security : [];
|
|
5471
|
+
}
|
|
5472
|
+
|
|
5473
|
+
function getAuthSelectionKeyForOperation(op) {
|
|
5474
|
+
if (!op) return "";
|
|
5475
|
+
return getOperationKey(op);
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
function getAuthSchemeOptionsForOperation(op) {
|
|
5479
|
+
const requirements = getOperationSecurityRequirements(op).filter((requirement) =>
|
|
5480
|
+
requirement && typeof requirement === "object" && !Array.isArray(requirement)
|
|
5481
|
+
);
|
|
5482
|
+
if (!requirements.length) return [];
|
|
5483
|
+
|
|
5484
|
+
const seen = new Set();
|
|
5485
|
+
const options = [];
|
|
5486
|
+
for (const requirement of requirements) {
|
|
5487
|
+
for (const schemeName of Object.keys(requirement)) {
|
|
5488
|
+
if (!Object.prototype.hasOwnProperty.call(authSchemes, schemeName)) continue;
|
|
5489
|
+
if (seen.has(schemeName)) continue;
|
|
5490
|
+
seen.add(schemeName);
|
|
5491
|
+
options.push(schemeName);
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
return options;
|
|
5495
|
+
}
|
|
5496
|
+
|
|
5497
|
+
function getSelectedAuthSchemeForOperation(op) {
|
|
5498
|
+
const selectionKey = getAuthSelectionKeyForOperation(op);
|
|
5499
|
+
if (!selectionKey) return null;
|
|
5500
|
+
|
|
5501
|
+
const selectedScheme = authSelectionState[selectionKey];
|
|
5502
|
+
if (!selectedScheme || typeof selectedScheme !== "string") return null;
|
|
5503
|
+
|
|
5504
|
+
const options = Object.keys(authSchemes);
|
|
5505
|
+
if (!options.includes(selectedScheme)) {
|
|
5506
|
+
delete authSelectionState[selectionKey];
|
|
5507
|
+
saveAuthSelectionState();
|
|
5508
|
+
return null;
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
return selectedScheme;
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
function setSelectedAuthSchemeForOperation(op, schemeName) {
|
|
5515
|
+
const selectionKey = getAuthSelectionKeyForOperation(op);
|
|
5516
|
+
if (!selectionKey) return;
|
|
5517
|
+
|
|
5518
|
+
if (!schemeName) {
|
|
5519
|
+
delete authSelectionState[selectionKey];
|
|
5520
|
+
} else {
|
|
5521
|
+
authSelectionState[selectionKey] = schemeName;
|
|
5522
|
+
}
|
|
5523
|
+
saveAuthSelectionState();
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
function hasAuthStateForScheme(schemeName) {
|
|
5527
|
+
const scheme = authSchemes[schemeName];
|
|
5528
|
+
if (!scheme) return false;
|
|
5529
|
+
|
|
5530
|
+
const state = authState[schemeName] || {};
|
|
5531
|
+
const type = (scheme.type || "").toLowerCase();
|
|
5532
|
+
const httpScheme = (scheme.scheme || "").toLowerCase();
|
|
5533
|
+
|
|
5534
|
+
if (type === "http" && httpScheme === "basic") {
|
|
5535
|
+
return Boolean(state.username && state.password);
|
|
5536
|
+
}
|
|
5537
|
+
if (type === "http") {
|
|
5538
|
+
return Boolean(state.token);
|
|
5539
|
+
}
|
|
5540
|
+
if (type === "apikey") {
|
|
5541
|
+
return Boolean(state.value);
|
|
5542
|
+
}
|
|
5543
|
+
if (type === "oauth2" || type === "openidconnect") {
|
|
5544
|
+
return Boolean(state.token);
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
return false;
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
function chooseOperationSecurityRequirement(op) {
|
|
5551
|
+
const requirements = getOperationSecurityRequirements(op).filter((requirement) =>
|
|
5552
|
+
requirement && typeof requirement === "object" && !Array.isArray(requirement)
|
|
5553
|
+
);
|
|
5554
|
+
if (!requirements.length) return null;
|
|
5555
|
+
|
|
5556
|
+
const selectedScheme = getSelectedAuthSchemeForOperation(op);
|
|
5557
|
+
if (selectedScheme) {
|
|
5558
|
+
const selectedRequirement = requirements.find((requirement) =>
|
|
5559
|
+
Object.prototype.hasOwnProperty.call(requirement, selectedScheme)
|
|
5560
|
+
);
|
|
5561
|
+
if (selectedRequirement) return selectedRequirement;
|
|
5562
|
+
}
|
|
5563
|
+
|
|
5564
|
+
let bestRequirement = null;
|
|
5565
|
+
let bestScore = -1;
|
|
5566
|
+
|
|
5567
|
+
for (const requirement of requirements) {
|
|
5568
|
+
const schemeNames = Object.keys(requirement).filter((schemeName) =>
|
|
5569
|
+
Object.prototype.hasOwnProperty.call(authSchemes, schemeName)
|
|
5570
|
+
);
|
|
5571
|
+
if (!schemeNames.length) continue;
|
|
5572
|
+
|
|
5573
|
+
const providedCount = schemeNames.filter((schemeName) => hasAuthStateForScheme(schemeName)).length;
|
|
5574
|
+
const isComplete = providedCount === schemeNames.length;
|
|
5575
|
+
const score = isComplete ? 1000 + providedCount : providedCount;
|
|
5576
|
+
|
|
5577
|
+
if (score > bestScore) {
|
|
5578
|
+
bestScore = score;
|
|
5579
|
+
bestRequirement = requirement;
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
|
|
5583
|
+
return bestRequirement || requirements[0];
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
function getAuthSchemeNamesForOperation(op) {
|
|
5587
|
+
const schemeNames = Object.keys(authSchemes);
|
|
5588
|
+
if (!schemeNames.length) return [];
|
|
5589
|
+
|
|
5590
|
+
const selectedScheme = getSelectedAuthSchemeForOperation(op);
|
|
5591
|
+
if (selectedScheme) {
|
|
5592
|
+
const requirement = chooseOperationSecurityRequirement(op);
|
|
5593
|
+
if (requirement && Object.prototype.hasOwnProperty.call(requirement, selectedScheme)) {
|
|
5594
|
+
return Object.keys(requirement).filter((schemeName) =>
|
|
5595
|
+
Object.prototype.hasOwnProperty.call(authSchemes, schemeName)
|
|
5596
|
+
);
|
|
5597
|
+
}
|
|
5598
|
+
return [selectedScheme];
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
const requirement = chooseOperationSecurityRequirement(op);
|
|
5602
|
+
if (!requirement) return [];
|
|
5603
|
+
|
|
5604
|
+
return Object.keys(requirement).filter((schemeName) =>
|
|
5605
|
+
Object.prototype.hasOwnProperty.call(authSchemes, schemeName)
|
|
5606
|
+
);
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
function getAuthHeaders(op) {
|
|
5610
|
+
const headers = {};
|
|
5611
|
+
const schemeNames = getAuthSchemeNamesForOperation(op);
|
|
5612
|
+
const allSchemeNames = Object.keys(authSchemes);
|
|
5613
|
+
|
|
5614
|
+
if (!allSchemeNames.length) {
|
|
5615
|
+
const state = authState["__default__"] || {};
|
|
5616
|
+
if (state.token) headers["Authorization"] = "Bearer " + state.token;
|
|
5617
|
+
return headers;
|
|
5618
|
+
}
|
|
5619
|
+
if (!schemeNames.length) return headers;
|
|
5620
|
+
|
|
5621
|
+
for (const schemeName of schemeNames) {
|
|
5622
|
+
const scheme = authSchemes[schemeName];
|
|
5623
|
+
const state = authState[schemeName] || {};
|
|
5624
|
+
const type = (scheme.type || "").toLowerCase();
|
|
5625
|
+
const httpScheme = (scheme.scheme || "").toLowerCase();
|
|
5626
|
+
|
|
5627
|
+
if (type === "http" && httpScheme === "basic") {
|
|
5628
|
+
if (state.username && state.password) {
|
|
5629
|
+
try {
|
|
5630
|
+
headers["Authorization"] = "Basic " + btoa(state.username + ":" + state.password);
|
|
5631
|
+
} catch {}
|
|
5632
|
+
}
|
|
5633
|
+
} else if (type === "http") {
|
|
5634
|
+
if (state.token) headers["Authorization"] = "Bearer " + state.token;
|
|
5635
|
+
} else if (type === "apikey" && (scheme.in || "").toLowerCase() === "header") {
|
|
5636
|
+
if (state.value && scheme.name) headers[scheme.name] = state.value;
|
|
5637
|
+
} else if (type === "oauth2" || type === "openidconnect") {
|
|
5638
|
+
if (state.token) headers["Authorization"] = "Bearer " + state.token;
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
return headers;
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
function getAuthQueryParams(op) {
|
|
5645
|
+
const params = {};
|
|
5646
|
+
for (const schemeName of getAuthSchemeNamesForOperation(op)) {
|
|
5647
|
+
const scheme = authSchemes[schemeName];
|
|
5648
|
+
if ((scheme.type || "").toLowerCase() === "apikey" && (scheme.in || "").toLowerCase() === "query") {
|
|
5649
|
+
const state = authState[schemeName] || {};
|
|
5650
|
+
if (state.value && scheme.name) params[scheme.name] = state.value;
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
return params;
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
function getAuthCookieParams(op) {
|
|
5657
|
+
const cookies = {};
|
|
5658
|
+
for (const schemeName of getAuthSchemeNamesForOperation(op)) {
|
|
5659
|
+
const scheme = authSchemes[schemeName];
|
|
5660
|
+
if ((scheme.type || "").toLowerCase() !== "apikey") continue;
|
|
5661
|
+
if ((scheme.in || "").toLowerCase() !== "cookie") continue;
|
|
5662
|
+
const state = authState[schemeName] || {};
|
|
5663
|
+
if (state.value && scheme.name) {
|
|
5664
|
+
cookies[scheme.name] = state.value;
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
return cookies;
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
function applyAuthCookies(op) {
|
|
5671
|
+
const cookies = getAuthCookieParams(op);
|
|
5672
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
5673
|
+
try {
|
|
5674
|
+
document.cookie = encodeURIComponent(String(name)) + "=" + encodeURIComponent(String(value)) + "; path=/";
|
|
5675
|
+
} catch {}
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
function renderAuthPanel() {
|
|
5680
|
+
const fields = document.getElementById("auth-fields");
|
|
5681
|
+
if (!fields) return;
|
|
5682
|
+
fields.innerHTML = "";
|
|
5683
|
+
|
|
5684
|
+
const schemeNames = Object.keys(authSchemes);
|
|
5685
|
+
const op = selected;
|
|
5686
|
+
const operationSchemeOptions = op ? getAuthSchemeOptionsForOperation(op) : [];
|
|
5687
|
+
const availableSchemeOptions = Object.keys(authSchemes);
|
|
5688
|
+
const selectedScheme = op ? getSelectedAuthSchemeForOperation(op) : null;
|
|
5689
|
+
|
|
5690
|
+
function getAuthSchemeDisplayLabel(schemeName) {
|
|
5691
|
+
const scheme = authSchemes[schemeName] || {};
|
|
5692
|
+
const type = (scheme.type || "").toLowerCase();
|
|
5693
|
+
const httpScheme = (scheme.scheme || "").toLowerCase();
|
|
5694
|
+
const location = (scheme.in || "").toLowerCase();
|
|
5695
|
+
|
|
5696
|
+
if (type === "http" && httpScheme === "basic") return "HTTP Basic";
|
|
5697
|
+
if (type === "http" && httpScheme === "bearer") return "HTTP Bearer";
|
|
5698
|
+
if (type === "http" && httpScheme === "digest") return "HTTP Digest";
|
|
5699
|
+
if (type === "http") return "HTTP " + (httpScheme || "Token");
|
|
5700
|
+
if (type === "apikey") return "API Key" + (location ? " (" + location + ")" : "");
|
|
5701
|
+
if (type === "oauth2") return "OAuth 2.0";
|
|
5702
|
+
if (type === "openidconnect") return "OpenID Connect";
|
|
5703
|
+
if (type === "mutualtls") return "Mutual TLS";
|
|
5704
|
+
return schemeName;
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
function makeInput(placeholder, value, onInput, type) {
|
|
5708
|
+
const inp = document.createElement("input");
|
|
5709
|
+
inp.type = type || "text";
|
|
5710
|
+
inp.value = value || "";
|
|
5711
|
+
inp.placeholder = placeholder;
|
|
5712
|
+
inp.className = "w-full text-xs px-2.5 py-2 rounded-md border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg focus:outline-none focus:border-brand dark:focus:border-brand transition-colors font-mono";
|
|
5713
|
+
inp.addEventListener("input", onInput);
|
|
5714
|
+
return inp;
|
|
5715
|
+
}
|
|
5716
|
+
|
|
5717
|
+
function makeLabel(text, small) {
|
|
5718
|
+
const el = document.createElement("p");
|
|
5719
|
+
el.className = small
|
|
5720
|
+
? "text-[10px] font-medium uppercase tracking-wider opacity-55"
|
|
5721
|
+
: "text-[10px] font-semibold uppercase tracking-wider opacity-55";
|
|
5722
|
+
el.textContent = text;
|
|
5723
|
+
return el;
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
function makeField(label, input) {
|
|
5727
|
+
const wrapper = document.createElement("div");
|
|
5728
|
+
wrapper.className = "space-y-1";
|
|
5729
|
+
wrapper.appendChild(makeLabel(label, true));
|
|
5730
|
+
wrapper.appendChild(input);
|
|
5731
|
+
return wrapper;
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
function makeSchemeCard(title, subtitle) {
|
|
5735
|
+
const card = document.createElement("div");
|
|
5736
|
+
card.className = "space-y-2 rounded-md border border-light-border dark:border-dark-border bg-light-bg/40 dark:bg-dark-bg/40 p-2.5";
|
|
5737
|
+
|
|
5738
|
+
const titleRow = document.createElement("div");
|
|
5739
|
+
titleRow.className = "flex items-center justify-between gap-2";
|
|
5740
|
+
|
|
5741
|
+
const heading = document.createElement("p");
|
|
5742
|
+
heading.className = "text-[11px] font-semibold tracking-wide";
|
|
5743
|
+
heading.textContent = title;
|
|
5744
|
+
titleRow.appendChild(heading);
|
|
5745
|
+
|
|
5746
|
+
if (subtitle) {
|
|
5747
|
+
const note = document.createElement("span");
|
|
5748
|
+
note.className = "text-[10px] opacity-60 font-mono";
|
|
5749
|
+
note.textContent = subtitle;
|
|
5750
|
+
titleRow.appendChild(note);
|
|
5751
|
+
}
|
|
5752
|
+
|
|
5753
|
+
card.appendChild(titleRow);
|
|
5754
|
+
return card;
|
|
5755
|
+
}
|
|
5756
|
+
|
|
5757
|
+
if (!schemeNames.length) {
|
|
5758
|
+
if (!authState["__default__"]) authState["__default__"] = {};
|
|
5759
|
+
const defaultCard = makeSchemeCard("Default Auth", "bearer");
|
|
5760
|
+
defaultCard.appendChild(makeField("Token", makeInput("Enter token\u2026", authState["__default__"].token, function(e) {
|
|
5761
|
+
authState["__default__"].token = e.target.value;
|
|
5762
|
+
saveAuthState();
|
|
5763
|
+
updateRequestPreview();
|
|
5764
|
+
})));
|
|
5765
|
+
fields.appendChild(defaultCard);
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5769
|
+
if (op && availableSchemeOptions.length > 0) {
|
|
5770
|
+
const selectorWrap = document.createElement("div");
|
|
5771
|
+
selectorWrap.className = "space-y-1";
|
|
5772
|
+
selectorWrap.appendChild(makeLabel("Auth Type"));
|
|
5773
|
+
const select = document.createElement("select");
|
|
5774
|
+
select.className = "w-full text-xs px-2.5 py-2 rounded-md border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg focus:outline-none focus:border-brand dark:focus:border-brand transition-colors font-mono";
|
|
5775
|
+
|
|
5776
|
+
const autoOption = document.createElement("option");
|
|
5777
|
+
autoOption.value = "";
|
|
5778
|
+
autoOption.textContent = "Auto";
|
|
5779
|
+
select.appendChild(autoOption);
|
|
5780
|
+
|
|
5781
|
+
for (const schemeName of availableSchemeOptions) {
|
|
5782
|
+
const option = document.createElement("option");
|
|
5783
|
+
option.value = schemeName;
|
|
5784
|
+
const isOperationScheme = operationSchemeOptions.includes(schemeName);
|
|
5785
|
+
const label = getAuthSchemeDisplayLabel(schemeName);
|
|
5786
|
+
option.textContent = isOperationScheme
|
|
5787
|
+
? label
|
|
5788
|
+
: (label + " \u2022 override");
|
|
5789
|
+
select.appendChild(option);
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5792
|
+
select.value = selectedScheme || "";
|
|
5793
|
+
select.addEventListener("change", function(e) {
|
|
5794
|
+
setSelectedAuthSchemeForOperation(op, e.target.value || "");
|
|
5795
|
+
renderAuthPanel();
|
|
5796
|
+
updateRequestPreview();
|
|
5797
|
+
});
|
|
5798
|
+
selectorWrap.appendChild(select);
|
|
5799
|
+
fields.appendChild(selectorWrap);
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5802
|
+
const schemesToRender = selectedScheme
|
|
5803
|
+
? [selectedScheme]
|
|
5804
|
+
: (operationSchemeOptions.length ? operationSchemeOptions : schemeNames);
|
|
5805
|
+
|
|
5806
|
+
for (const schemeName of schemesToRender) {
|
|
5807
|
+
const scheme = authSchemes[schemeName];
|
|
5808
|
+
if (!authState[schemeName]) authState[schemeName] = {};
|
|
5809
|
+
const state = authState[schemeName];
|
|
5810
|
+
const type = (scheme.type || "").toLowerCase();
|
|
5811
|
+
const httpScheme = (scheme.scheme || "").toLowerCase();
|
|
5812
|
+
const card = makeSchemeCard(getAuthSchemeDisplayLabel(schemeName), schemeName);
|
|
5813
|
+
|
|
5814
|
+
if (type === "http" && httpScheme === "basic") {
|
|
5815
|
+
card.appendChild(makeField("Username", makeInput("Username", state.username, function(e) {
|
|
5816
|
+
authState[schemeName].username = e.target.value;
|
|
5817
|
+
saveAuthState();
|
|
5818
|
+
updateRequestPreview();
|
|
5819
|
+
})));
|
|
5820
|
+
card.appendChild(makeField("Password", makeInput("Password", state.password, function(e) {
|
|
5821
|
+
authState[schemeName].password = e.target.value;
|
|
5822
|
+
saveAuthState();
|
|
5823
|
+
updateRequestPreview();
|
|
5824
|
+
}, "password")));
|
|
5825
|
+
} else if (type === "apikey") {
|
|
5826
|
+
const paramName = scheme.name || "key";
|
|
5827
|
+
const location = (scheme.in || "header").toLowerCase();
|
|
5828
|
+
card.appendChild(makeField("API Key", makeInput(paramName + " (" + location + ")", state.value, function(e) {
|
|
5829
|
+
authState[schemeName].value = e.target.value;
|
|
5830
|
+
saveAuthState();
|
|
5831
|
+
updateRequestPreview();
|
|
5832
|
+
})));
|
|
5833
|
+
} else if (type === "oauth2") {
|
|
5834
|
+
card.appendChild(makeField("Access Token", makeInput("OAuth2 access token\u2026", state.token, function(e) {
|
|
5835
|
+
authState[schemeName].token = e.target.value;
|
|
5836
|
+
saveAuthState();
|
|
5837
|
+
updateRequestPreview();
|
|
5838
|
+
})));
|
|
5839
|
+
} else if (type === "openidconnect") {
|
|
5840
|
+
card.appendChild(makeField("ID Token / Access Token", makeInput("OpenID Connect token\u2026", state.token, function(e) {
|
|
5841
|
+
authState[schemeName].token = e.target.value;
|
|
5842
|
+
saveAuthState();
|
|
5843
|
+
updateRequestPreview();
|
|
5844
|
+
})));
|
|
5845
|
+
} else if (type === "http" && httpScheme === "digest") {
|
|
5846
|
+
card.appendChild(makeField("Digest Credential", makeInput("Digest token\u2026", state.token, function(e) {
|
|
5847
|
+
authState[schemeName].token = e.target.value;
|
|
5848
|
+
saveAuthState();
|
|
5849
|
+
updateRequestPreview();
|
|
5850
|
+
})));
|
|
5851
|
+
} else if (type === "http" && httpScheme === "bearer") {
|
|
5852
|
+
card.appendChild(makeField("Bearer Token", makeInput("Bearer token\u2026", state.token, function(e) {
|
|
5853
|
+
authState[schemeName].token = e.target.value;
|
|
5854
|
+
saveAuthState();
|
|
5855
|
+
updateRequestPreview();
|
|
5856
|
+
})));
|
|
5857
|
+
} else if (type === "mutualtls") {
|
|
5858
|
+
const hint = document.createElement("p");
|
|
5859
|
+
hint.className = "text-xs opacity-70 leading-relaxed";
|
|
5860
|
+
hint.textContent = "Configured by your client certificate. No token input required.";
|
|
5861
|
+
card.appendChild(hint);
|
|
5862
|
+
} else {
|
|
5863
|
+
card.appendChild(makeField("Token", makeInput("Token\u2026", state.token, function(e) {
|
|
5864
|
+
authState[schemeName].token = e.target.value;
|
|
5865
|
+
saveAuthState();
|
|
5866
|
+
updateRequestPreview();
|
|
5867
|
+
})));
|
|
5868
|
+
}
|
|
5869
|
+
fields.appendChild(card);
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
let authPanelOpen = true;
|
|
5874
|
+
document.getElementById("auth-toggle").addEventListener("click", function() {
|
|
5875
|
+
authPanelOpen = !authPanelOpen;
|
|
5876
|
+
const fieldsEl = document.getElementById("auth-fields");
|
|
5877
|
+
const chevron = document.getElementById("auth-chevron");
|
|
5878
|
+
if (fieldsEl) fieldsEl.classList.toggle("hidden", !authPanelOpen);
|
|
5879
|
+
if (chevron) chevron.style.transform = authPanelOpen ? "" : "rotate(-90deg)";
|
|
5880
|
+
});
|
|
5881
|
+
|
|
5882
|
+
// Restore selected operation from URL hash, or set hash for the default selection
|
|
5883
|
+
var initMatch = findOpByHash(window.location.hash);
|
|
5884
|
+
if (initMatch) {
|
|
5885
|
+
selected = initMatch;
|
|
5886
|
+
} else if (selected) {
|
|
5887
|
+
history.replaceState(null, "", getOpHash(selected));
|
|
5888
|
+
}
|
|
5889
|
+
|
|
5890
|
+
window.addEventListener("popstate", function() {
|
|
5891
|
+
var match = findOpByHash(window.location.hash);
|
|
5892
|
+
if (match) {
|
|
5893
|
+
selected = match;
|
|
5894
|
+
renderSidebar();
|
|
5895
|
+
renderEndpoint();
|
|
5896
|
+
}
|
|
5897
|
+
});
|
|
5898
|
+
|
|
2810
5899
|
setMobileSidebarOpen(false);
|
|
5900
|
+
renderAuthPanel();
|
|
2811
5901
|
renderSidebar();
|
|
2812
5902
|
renderEndpoint();
|
|
2813
5903
|
</script>
|
|
@@ -2816,6 +5906,106 @@ function renderOpenAPIDocsHtml(spec, openapiPath, tailwindScriptPath, logoDarkPa
|
|
|
2816
5906
|
}
|
|
2817
5907
|
|
|
2818
5908
|
// src/openapi/generator.ts
|
|
5909
|
+
var AUTH_KIND_VALUES2 = new Set(Object.values(AuthKind));
|
|
5910
|
+
var DEFAULT_SECURITY_SCHEME_NAMES = {
|
|
5911
|
+
["ApiKey" /* ApiKey */]: "apiKeyAuth",
|
|
5912
|
+
["HttpBasic" /* HttpBasic */]: "basicAuth",
|
|
5913
|
+
["HttpBearer" /* HttpBearer */]: "bearerAuth",
|
|
5914
|
+
["HttpDigest" /* HttpDigest */]: "digestAuth",
|
|
5915
|
+
["OAuth2" /* OAuth2 */]: "oauth2Auth",
|
|
5916
|
+
["OpenIdConnect" /* OpenIdConnect */]: "openIdConnectAuth",
|
|
5917
|
+
["MutualTls" /* MutualTls */]: "mutualTlsAuth"
|
|
5918
|
+
};
|
|
5919
|
+
function isAuthKind(value) {
|
|
5920
|
+
return typeof value === "string" && AUTH_KIND_VALUES2.has(value);
|
|
5921
|
+
}
|
|
5922
|
+
function resolveRouteAuthKind(routeAuth, defaultAuthKind) {
|
|
5923
|
+
if (routeAuth === undefined || routeAuth === false || routeAuth === null) {
|
|
5924
|
+
return null;
|
|
5925
|
+
}
|
|
5926
|
+
if (routeAuth === true) {
|
|
5927
|
+
return defaultAuthKind;
|
|
5928
|
+
}
|
|
5929
|
+
if (isAuthKind(routeAuth)) {
|
|
5930
|
+
return routeAuth;
|
|
5931
|
+
}
|
|
5932
|
+
return defaultAuthKind;
|
|
5933
|
+
}
|
|
5934
|
+
function resolveSecuritySchemeName(kind, authOptions) {
|
|
5935
|
+
const configuredName = authOptions?.securitySchemeNames?.[kind];
|
|
5936
|
+
if (typeof configuredName === "string" && configuredName.trim().length > 0) {
|
|
5937
|
+
return configuredName.trim();
|
|
5938
|
+
}
|
|
5939
|
+
return DEFAULT_SECURITY_SCHEME_NAMES[kind];
|
|
5940
|
+
}
|
|
5941
|
+
function toOpenApiSecurityScheme(kind) {
|
|
5942
|
+
switch (kind) {
|
|
5943
|
+
case "ApiKey" /* ApiKey */:
|
|
5944
|
+
return {
|
|
5945
|
+
type: "apiKey" /* ApiKey */,
|
|
5946
|
+
name: "X-API-Key",
|
|
5947
|
+
in: "header"
|
|
5948
|
+
};
|
|
5949
|
+
case "HttpBasic" /* HttpBasic */:
|
|
5950
|
+
return {
|
|
5951
|
+
type: "http" /* Http */,
|
|
5952
|
+
scheme: "basic" /* Basic */
|
|
5953
|
+
};
|
|
5954
|
+
case "HttpBearer" /* HttpBearer */:
|
|
5955
|
+
return {
|
|
5956
|
+
type: "http" /* Http */,
|
|
5957
|
+
scheme: "bearer" /* Bearer */,
|
|
5958
|
+
bearerFormat: "JWT"
|
|
5959
|
+
};
|
|
5960
|
+
case "HttpDigest" /* HttpDigest */:
|
|
5961
|
+
return {
|
|
5962
|
+
type: "http" /* Http */,
|
|
5963
|
+
scheme: "digest" /* Digest */
|
|
5964
|
+
};
|
|
5965
|
+
case "OAuth2" /* OAuth2 */:
|
|
5966
|
+
return {
|
|
5967
|
+
type: "oauth2" /* OAuth2 */,
|
|
5968
|
+
flows: {
|
|
5969
|
+
authorizationCode: {
|
|
5970
|
+
authorizationUrl: "https://example.com/oauth/authorize",
|
|
5971
|
+
tokenUrl: "https://example.com/oauth/token",
|
|
5972
|
+
scopes: {}
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
};
|
|
5976
|
+
case "OpenIdConnect" /* OpenIdConnect */:
|
|
5977
|
+
return {
|
|
5978
|
+
type: "openIdConnect" /* OpenIdConnect */,
|
|
5979
|
+
openIdConnectUrl: "https://example.com/.well-known/openid-configuration"
|
|
5980
|
+
};
|
|
5981
|
+
case "MutualTls" /* MutualTls */:
|
|
5982
|
+
return {
|
|
5983
|
+
type: "mutualTLS" /* MutualTls */
|
|
5984
|
+
};
|
|
5985
|
+
default: {
|
|
5986
|
+
const exhaustiveCheck = kind;
|
|
5987
|
+
return exhaustiveCheck;
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
function resolveSecurityScheme(kind, authOptions) {
|
|
5992
|
+
const defaultScheme = toOpenApiSecurityScheme(kind);
|
|
5993
|
+
const override = authOptions?.securitySchemes?.[kind];
|
|
5994
|
+
if (!override) {
|
|
5995
|
+
return defaultScheme;
|
|
5996
|
+
}
|
|
5997
|
+
const merged = {
|
|
5998
|
+
...defaultScheme,
|
|
5999
|
+
...override
|
|
6000
|
+
};
|
|
6001
|
+
if (isRecord(defaultScheme.flows) && isRecord(override.flows)) {
|
|
6002
|
+
merged.flows = {
|
|
6003
|
+
...defaultScheme.flows,
|
|
6004
|
+
...override.flows
|
|
6005
|
+
};
|
|
6006
|
+
}
|
|
6007
|
+
return merged;
|
|
6008
|
+
}
|
|
2819
6009
|
function isJSONSchemaCapable(schema) {
|
|
2820
6010
|
const standard = schema?.["~standard"];
|
|
2821
6011
|
const converter = standard?.jsonSchema;
|
|
@@ -2887,15 +6077,88 @@ function isNoBodyResponseStatus(status) {
|
|
|
2887
6077
|
return numericStatus >= 100 && numericStatus < 200 || numericStatus === 204 || numericStatus === 205 || numericStatus === 304;
|
|
2888
6078
|
}
|
|
2889
6079
|
function getResponseDescription(status) {
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
6080
|
+
const knownDescriptions = {
|
|
6081
|
+
"100": "Continue",
|
|
6082
|
+
"101": "Switching Protocols",
|
|
6083
|
+
"102": "Processing",
|
|
6084
|
+
"103": "Early Hints",
|
|
6085
|
+
"200": "OK",
|
|
6086
|
+
"201": "Created",
|
|
6087
|
+
"202": "Accepted",
|
|
6088
|
+
"203": "Non-Authoritative Information",
|
|
6089
|
+
"204": "No Content",
|
|
6090
|
+
"205": "Reset Content",
|
|
6091
|
+
"206": "Partial Content",
|
|
6092
|
+
"207": "Multi-Status",
|
|
6093
|
+
"208": "Already Reported",
|
|
6094
|
+
"226": "IM Used",
|
|
6095
|
+
"300": "Multiple Choices",
|
|
6096
|
+
"301": "Moved Permanently",
|
|
6097
|
+
"302": "Found",
|
|
6098
|
+
"303": "See Other",
|
|
6099
|
+
"304": "Not Modified",
|
|
6100
|
+
"305": "Use Proxy",
|
|
6101
|
+
"307": "Temporary Redirect",
|
|
6102
|
+
"308": "Permanent Redirect",
|
|
6103
|
+
"400": "Bad Request",
|
|
6104
|
+
"401": "Unauthorized",
|
|
6105
|
+
"402": "Payment Required",
|
|
6106
|
+
"403": "Forbidden",
|
|
6107
|
+
"404": "Not Found",
|
|
6108
|
+
"405": "Method Not Allowed",
|
|
6109
|
+
"406": "Not Acceptable",
|
|
6110
|
+
"407": "Proxy Authentication Required",
|
|
6111
|
+
"408": "Request Timeout",
|
|
6112
|
+
"409": "Conflict",
|
|
6113
|
+
"410": "Gone",
|
|
6114
|
+
"411": "Length Required",
|
|
6115
|
+
"412": "Precondition Failed",
|
|
6116
|
+
"413": "Payload Too Large",
|
|
6117
|
+
"414": "URI Too Long",
|
|
6118
|
+
"415": "Unsupported Media Type",
|
|
6119
|
+
"416": "Range Not Satisfiable",
|
|
6120
|
+
"417": "Expectation Failed",
|
|
6121
|
+
"418": "I'm a teapot",
|
|
6122
|
+
"421": "Misdirected Request",
|
|
6123
|
+
"422": "Unprocessable Content",
|
|
6124
|
+
"423": "Locked",
|
|
6125
|
+
"424": "Failed Dependency",
|
|
6126
|
+
"425": "Too Early",
|
|
6127
|
+
"426": "Upgrade Required",
|
|
6128
|
+
"428": "Precondition Required",
|
|
6129
|
+
"429": "Too Many Requests",
|
|
6130
|
+
"431": "Request Header Fields Too Large",
|
|
6131
|
+
"451": "Unavailable For Legal Reasons",
|
|
6132
|
+
"500": "Internal Server Error",
|
|
6133
|
+
"501": "Not Implemented",
|
|
6134
|
+
"502": "Bad Gateway",
|
|
6135
|
+
"503": "Service Unavailable",
|
|
6136
|
+
"504": "Gateway Timeout",
|
|
6137
|
+
"505": "HTTP Version Not Supported",
|
|
6138
|
+
"506": "Variant Also Negotiates",
|
|
6139
|
+
"507": "Insufficient Storage",
|
|
6140
|
+
"508": "Loop Detected",
|
|
6141
|
+
"510": "Not Extended",
|
|
6142
|
+
"511": "Network Authentication Required"
|
|
6143
|
+
};
|
|
6144
|
+
if (knownDescriptions[status]) {
|
|
6145
|
+
return knownDescriptions[status];
|
|
6146
|
+
}
|
|
2896
6147
|
const numericStatus = Number(status);
|
|
2897
6148
|
if (Number.isInteger(numericStatus) && numericStatus >= 100 && numericStatus < 200) {
|
|
2898
|
-
return "Informational";
|
|
6149
|
+
return "Informational Response";
|
|
6150
|
+
}
|
|
6151
|
+
if (Number.isInteger(numericStatus) && numericStatus >= 200 && numericStatus < 300) {
|
|
6152
|
+
return "Successful Response";
|
|
6153
|
+
}
|
|
6154
|
+
if (Number.isInteger(numericStatus) && numericStatus >= 300 && numericStatus < 400) {
|
|
6155
|
+
return "Redirection";
|
|
6156
|
+
}
|
|
6157
|
+
if (Number.isInteger(numericStatus) && numericStatus >= 400 && numericStatus < 500) {
|
|
6158
|
+
return "Client Error";
|
|
6159
|
+
}
|
|
6160
|
+
if (Number.isInteger(numericStatus) && numericStatus >= 500 && numericStatus < 600) {
|
|
6161
|
+
return "Server Error";
|
|
2899
6162
|
}
|
|
2900
6163
|
return "OK";
|
|
2901
6164
|
}
|
|
@@ -3330,6 +6593,8 @@ function addOutputSchemasToOperation(operation, routePath, routeSchema, target,
|
|
|
3330
6593
|
function generateOpenAPIDocument(routes, options) {
|
|
3331
6594
|
const warnings = [];
|
|
3332
6595
|
const paths = {};
|
|
6596
|
+
const defaultAuthKind = "HttpBearer" /* HttpBearer */;
|
|
6597
|
+
const usedAuthKinds = new Set;
|
|
3333
6598
|
for (const route of routes) {
|
|
3334
6599
|
if (route.options.expose === false)
|
|
3335
6600
|
continue;
|
|
@@ -3343,12 +6608,50 @@ function generateOpenAPIDocument(routes, options) {
|
|
|
3343
6608
|
operationId: createOperationId(method, openapiPath),
|
|
3344
6609
|
tags: [route.options.schema?.tag || inferTagFromPath(route.path)]
|
|
3345
6610
|
};
|
|
6611
|
+
if (typeof route.options.schema?.summary === "string" && route.options.schema.summary.trim()) {
|
|
6612
|
+
operation.summary = route.options.schema.summary.trim();
|
|
6613
|
+
}
|
|
6614
|
+
const routeSchemaDescription = typeof route.options.schema?.description === "string" && route.options.schema.description.trim() ? route.options.schema.description.trim() : typeof route.options.schema?.descrition === "string" && route.options.schema.descrition.trim() ? route.options.schema.descrition.trim() : undefined;
|
|
6615
|
+
if (routeSchemaDescription) {
|
|
6616
|
+
operation.description = routeSchemaDescription;
|
|
6617
|
+
}
|
|
6618
|
+
if (route.options.deprecated === true) {
|
|
6619
|
+
operation.deprecated = true;
|
|
6620
|
+
}
|
|
6621
|
+
const routeAuthKind = resolveRouteAuthKind(route.options.auth, defaultAuthKind);
|
|
6622
|
+
if (routeAuthKind) {
|
|
6623
|
+
usedAuthKinds.add(routeAuthKind);
|
|
6624
|
+
const securitySchemeName = resolveSecuritySchemeName(routeAuthKind, options.auth);
|
|
6625
|
+
operation.security = [{ [securitySchemeName]: [] }];
|
|
6626
|
+
}
|
|
3346
6627
|
const inputJSONSchema = convertInputSchema(route.path, route.options.schema?.input, options.target, warnings);
|
|
3347
6628
|
if (inputJSONSchema) {
|
|
6629
|
+
if (!operation.summary && typeof inputJSONSchema.title === "string" && inputJSONSchema.title.trim()) {
|
|
6630
|
+
operation.summary = inputJSONSchema.title.trim();
|
|
6631
|
+
}
|
|
6632
|
+
if (!operation.description && typeof inputJSONSchema.description === "string" && inputJSONSchema.description.trim()) {
|
|
6633
|
+
operation.description = inputJSONSchema.description.trim();
|
|
6634
|
+
}
|
|
3348
6635
|
addStructuredInputToOperation(operation, inputJSONSchema);
|
|
3349
6636
|
}
|
|
3350
6637
|
addMissingPathParameters(operation, route.path);
|
|
3351
6638
|
addOutputSchemasToOperation(operation, route.path, route.options.schema || {}, options.target, warnings);
|
|
6639
|
+
if (!operation.summary || !operation.description) {
|
|
6640
|
+
const responseEntries = Object.values(operation.responses || {});
|
|
6641
|
+
for (const response of responseEntries) {
|
|
6642
|
+
const responseSchema = response?.content?.["application/json"]?.schema;
|
|
6643
|
+
if (!responseSchema || typeof responseSchema !== "object")
|
|
6644
|
+
continue;
|
|
6645
|
+
if (!operation.summary && typeof responseSchema.title === "string" && responseSchema.title.trim()) {
|
|
6646
|
+
operation.summary = responseSchema.title.trim();
|
|
6647
|
+
}
|
|
6648
|
+
if (!operation.description && typeof responseSchema.description === "string" && responseSchema.description.trim()) {
|
|
6649
|
+
operation.description = responseSchema.description.trim();
|
|
6650
|
+
}
|
|
6651
|
+
if (operation.summary && operation.description)
|
|
6652
|
+
break;
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
3352
6655
|
paths[openapiPath] ||= {};
|
|
3353
6656
|
paths[openapiPath][method] = operation;
|
|
3354
6657
|
}
|
|
@@ -3362,6 +6665,16 @@ function generateOpenAPIDocument(routes, options) {
|
|
|
3362
6665
|
},
|
|
3363
6666
|
paths
|
|
3364
6667
|
};
|
|
6668
|
+
if (usedAuthKinds.size > 0) {
|
|
6669
|
+
const securitySchemes = {};
|
|
6670
|
+
for (const authKind of usedAuthKinds) {
|
|
6671
|
+
const name = resolveSecuritySchemeName(authKind, options.auth);
|
|
6672
|
+
securitySchemes[name] = resolveSecurityScheme(authKind, options.auth);
|
|
6673
|
+
}
|
|
6674
|
+
document.components = {
|
|
6675
|
+
securitySchemes
|
|
6676
|
+
};
|
|
6677
|
+
}
|
|
3365
6678
|
return {
|
|
3366
6679
|
document,
|
|
3367
6680
|
warnings
|
|
@@ -3498,6 +6811,17 @@ var OPENAPI_FAVICON_ASSETS = [
|
|
|
3498
6811
|
var DOCS_HTML_CACHE_CONTROL = "public, max-age=0, must-revalidate";
|
|
3499
6812
|
var DOCS_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
3500
6813
|
var DOCS_ASSET_ERROR_CACHE_CONTROL = "no-store";
|
|
6814
|
+
var DEFAULT_PORT = 3000;
|
|
6815
|
+
function normalizePort(port) {
|
|
6816
|
+
if (port === undefined) {
|
|
6817
|
+
return DEFAULT_PORT;
|
|
6818
|
+
}
|
|
6819
|
+
const normalized = Number(port);
|
|
6820
|
+
if (!Number.isInteger(normalized) || normalized < 0 || normalized > 65535) {
|
|
6821
|
+
throw new Error(`Invalid port: ${String(port)}. Port must be an integer between 0 and 65535.`);
|
|
6822
|
+
}
|
|
6823
|
+
return normalized;
|
|
6824
|
+
}
|
|
3501
6825
|
function escapeRegex(value) {
|
|
3502
6826
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3503
6827
|
}
|
|
@@ -3559,6 +6883,9 @@ class VectorServer {
|
|
|
3559
6883
|
this.router.setCorsHandler(this.corsHeadersEntries ? null : this.corsHandler.corsify);
|
|
3560
6884
|
}
|
|
3561
6885
|
}
|
|
6886
|
+
setCheckpointGateway(gateway) {
|
|
6887
|
+
this.router.setCheckpointGateway(gateway);
|
|
6888
|
+
}
|
|
3562
6889
|
normalizeOpenAPIConfig(openapi, development) {
|
|
3563
6890
|
const isDev = development !== false && true;
|
|
3564
6891
|
const defaultEnabled = isDev;
|
|
@@ -3590,7 +6917,8 @@ class VectorServer {
|
|
|
3590
6917
|
path: openapiObject.path || "/openapi.json",
|
|
3591
6918
|
target: openapiObject.target || "openapi-3.0",
|
|
3592
6919
|
docs,
|
|
3593
|
-
info: openapiObject.info
|
|
6920
|
+
info: openapiObject.info,
|
|
6921
|
+
auth: openapiObject.auth
|
|
3594
6922
|
};
|
|
3595
6923
|
}
|
|
3596
6924
|
isDocsReservedPath(path) {
|
|
@@ -3612,7 +6940,8 @@ class VectorServer {
|
|
|
3612
6940
|
const routes = this.router.getRouteDefinitions().filter((route) => !this.isDocsReservedPath(route.path));
|
|
3613
6941
|
const result = generateOpenAPIDocument(routes, {
|
|
3614
6942
|
target: this.openapiConfig.target,
|
|
3615
|
-
info: this.openapiConfig.info
|
|
6943
|
+
info: this.openapiConfig.info,
|
|
6944
|
+
auth: this.openapiConfig.auth
|
|
3616
6945
|
});
|
|
3617
6946
|
if (!this.openapiWarningsLogged && result.warnings.length > 0) {
|
|
3618
6947
|
if (this.shouldLogOpenAPIConversionWarnings()) {
|
|
@@ -3832,10 +7161,10 @@ class VectorServer {
|
|
|
3832
7161
|
return response;
|
|
3833
7162
|
}
|
|
3834
7163
|
async start() {
|
|
3835
|
-
const port = this.config.port
|
|
7164
|
+
const port = normalizePort(this.config.port);
|
|
3836
7165
|
const hostname = this.config.hostname || "localhost";
|
|
3837
7166
|
this.validateReservedOpenAPIPaths();
|
|
3838
|
-
const
|
|
7167
|
+
const appFetch = async (request) => {
|
|
3839
7168
|
try {
|
|
3840
7169
|
if (this.corsHandler && request.method === "OPTIONS") {
|
|
3841
7170
|
return this.corsHandler.preflight(request);
|
|
@@ -3844,7 +7173,7 @@ class VectorServer {
|
|
|
3844
7173
|
if (openapiResponse) {
|
|
3845
7174
|
return this.applyCors(openapiResponse, request);
|
|
3846
7175
|
}
|
|
3847
|
-
return this.
|
|
7176
|
+
return await this.router.handle(request);
|
|
3848
7177
|
} catch (error) {
|
|
3849
7178
|
console.error("Server error:", error);
|
|
3850
7179
|
return this.applyCors(new Response("Internal Server Error", { status: 500 }), request);
|
|
@@ -3855,8 +7184,7 @@ class VectorServer {
|
|
|
3855
7184
|
port,
|
|
3856
7185
|
hostname,
|
|
3857
7186
|
reusePort: this.config.reusePort !== false,
|
|
3858
|
-
|
|
3859
|
-
fetch: fallbackFetch,
|
|
7187
|
+
fetch: appFetch,
|
|
3860
7188
|
idleTimeout: this.config.idleTimeout ?? 60,
|
|
3861
7189
|
error: (error, request) => {
|
|
3862
7190
|
console.error("[ERROR] Server error:", error);
|
|
@@ -3895,7 +7223,7 @@ class VectorServer {
|
|
|
3895
7223
|
return this.server;
|
|
3896
7224
|
}
|
|
3897
7225
|
getPort() {
|
|
3898
|
-
return this.server?.port ?? this.config.port
|
|
7226
|
+
return this.server?.port ?? normalizePort(this.config.port);
|
|
3899
7227
|
}
|
|
3900
7228
|
getHostname() {
|
|
3901
7229
|
return this.server?.hostname || this.config.hostname || "localhost";
|
|
@@ -3921,6 +7249,7 @@ class Vector {
|
|
|
3921
7249
|
_protectedHandler = null;
|
|
3922
7250
|
_cacheHandler = null;
|
|
3923
7251
|
shutdownPromise = null;
|
|
7252
|
+
checkpointProcessManager = null;
|
|
3924
7253
|
constructor() {
|
|
3925
7254
|
this.middlewareManager = new MiddlewareManager;
|
|
3926
7255
|
this.authManager = new AuthManager;
|
|
@@ -3959,6 +7288,10 @@ class Vector {
|
|
|
3959
7288
|
this.router.route(options, handler);
|
|
3960
7289
|
}
|
|
3961
7290
|
async startServer(config) {
|
|
7291
|
+
if (this.checkpointProcessManager) {
|
|
7292
|
+
await this.checkpointProcessManager.stopAll();
|
|
7293
|
+
this.checkpointProcessManager = null;
|
|
7294
|
+
}
|
|
3962
7295
|
this.config = { ...this.config, ...config };
|
|
3963
7296
|
const routeDefaults = { ...this.config.defaults?.route };
|
|
3964
7297
|
this.router.setRouteBooleanDefaults(routeDefaults);
|
|
@@ -3981,6 +7314,9 @@ class Vector {
|
|
|
3981
7314
|
}
|
|
3982
7315
|
this.server = new VectorServer(this.router, this.config);
|
|
3983
7316
|
const bunServer = await this.server.start();
|
|
7317
|
+
if (this.config.checkpoint && this.config.checkpoint.enabled !== false) {
|
|
7318
|
+
await this.enableCheckpointGateway(this.config.checkpoint);
|
|
7319
|
+
}
|
|
3984
7320
|
return bunServer;
|
|
3985
7321
|
}
|
|
3986
7322
|
async discoverRoutes() {
|
|
@@ -3999,7 +7335,7 @@ class Vector {
|
|
|
3999
7335
|
for (const route of routes) {
|
|
4000
7336
|
try {
|
|
4001
7337
|
const importPath = toFileUrl(route.path);
|
|
4002
|
-
const module = await import(importPath);
|
|
7338
|
+
const module = await import(`${importPath}?t=${Date.now()}`);
|
|
4003
7339
|
const exported = route.name === "default" ? module.default : module[route.name];
|
|
4004
7340
|
if (exported) {
|
|
4005
7341
|
if (this.isRouteDefinition(exported)) {
|
|
@@ -4052,6 +7388,41 @@ class Vector {
|
|
|
4052
7388
|
return value !== null && typeof value === "object" && "entry" in value && "options" in value && "handler" in value && typeof value.handler === "function";
|
|
4053
7389
|
}
|
|
4054
7390
|
logRouteLoaded(_) {}
|
|
7391
|
+
async enableCheckpointGateway(checkpointConfig) {
|
|
7392
|
+
if (!this.server) {
|
|
7393
|
+
throw new Error("Cannot enable checkpoint gateway before server is started. Call startServer() first.");
|
|
7394
|
+
}
|
|
7395
|
+
const { CheckpointManager: CheckpointManager2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
|
|
7396
|
+
const { CheckpointProcessManager: CheckpointProcessManager2 } = await Promise.resolve().then(() => (init_process_manager(), exports_process_manager));
|
|
7397
|
+
const { CheckpointResolver: CheckpointResolver2 } = await Promise.resolve().then(() => exports_resolver);
|
|
7398
|
+
const { CheckpointForwarder: CheckpointForwarder2 } = await Promise.resolve().then(() => exports_forwarder);
|
|
7399
|
+
const { CheckpointGateway: CheckpointGateway2 } = await Promise.resolve().then(() => exports_gateway);
|
|
7400
|
+
if (this.checkpointProcessManager) {
|
|
7401
|
+
await this.checkpointProcessManager.stopAll();
|
|
7402
|
+
this.checkpointProcessManager = null;
|
|
7403
|
+
}
|
|
7404
|
+
const manager = new CheckpointManager2(checkpointConfig);
|
|
7405
|
+
const processManager = new CheckpointProcessManager2({
|
|
7406
|
+
idleTimeoutMs: checkpointConfig?.idleTimeoutMs
|
|
7407
|
+
});
|
|
7408
|
+
this.checkpointProcessManager = processManager;
|
|
7409
|
+
const resolver = new CheckpointResolver2(manager, processManager, {
|
|
7410
|
+
versionHeader: checkpointConfig?.versionHeader,
|
|
7411
|
+
cacheKeyOverride: checkpointConfig?.cacheKeyOverride
|
|
7412
|
+
});
|
|
7413
|
+
const forwarder = new CheckpointForwarder2;
|
|
7414
|
+
const gateway = new CheckpointGateway2(resolver, forwarder);
|
|
7415
|
+
this.server.setCheckpointGateway(gateway);
|
|
7416
|
+
const active = await manager.getActive();
|
|
7417
|
+
if (active) {
|
|
7418
|
+
try {
|
|
7419
|
+
const manifest = await manager.readManifest(active.version);
|
|
7420
|
+
await processManager.spawn(manifest, manager.getStorageDir());
|
|
7421
|
+
} catch (err) {
|
|
7422
|
+
console.error(`[Checkpoint] Failed to start active checkpoint ${active.version}:`, err);
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
4055
7426
|
stop() {
|
|
4056
7427
|
if (this.server) {
|
|
4057
7428
|
this.server.stop();
|
|
@@ -4064,6 +7435,10 @@ class Vector {
|
|
|
4064
7435
|
}
|
|
4065
7436
|
this.shutdownPromise = (async () => {
|
|
4066
7437
|
this.stop();
|
|
7438
|
+
if (this.checkpointProcessManager) {
|
|
7439
|
+
await this.checkpointProcessManager.stopAll();
|
|
7440
|
+
this.checkpointProcessManager = null;
|
|
7441
|
+
}
|
|
4067
7442
|
if (typeof this.config.shutdown === "function") {
|
|
4068
7443
|
await this.config.shutdown();
|
|
4069
7444
|
}
|
|
@@ -4130,7 +7505,12 @@ async function startVector(options = {}) {
|
|
|
4130
7505
|
|
|
4131
7506
|
// src/cli/index.ts
|
|
4132
7507
|
var args = typeof Bun !== "undefined" ? Bun.argv.slice(2) : process.argv.slice(2);
|
|
4133
|
-
|
|
7508
|
+
if (args[0] === "checkpoint") {
|
|
7509
|
+
const { runCheckpointCli: runCheckpointCli2 } = await Promise.resolve().then(() => (init_cli(), exports_cli));
|
|
7510
|
+
await runCheckpointCli2(args.slice(1));
|
|
7511
|
+
process.exit(0);
|
|
7512
|
+
}
|
|
7513
|
+
var { values, positionals } = parseArgs2({
|
|
4134
7514
|
args,
|
|
4135
7515
|
options: {
|
|
4136
7516
|
port: {
|
|
@@ -4252,7 +7632,7 @@ Listening on ${cyan}http://${config.hostname}:${config.port}${reset}
|
|
|
4252
7632
|
if (app) {
|
|
4253
7633
|
app.stop();
|
|
4254
7634
|
}
|
|
4255
|
-
await new Promise((
|
|
7635
|
+
await new Promise((resolve5) => setTimeout(resolve5, 100));
|
|
4256
7636
|
try {
|
|
4257
7637
|
const result2 = await startServer();
|
|
4258
7638
|
server = result2.server;
|
|
@@ -4299,8 +7679,9 @@ switch (command) {
|
|
|
4299
7679
|
Usage: vector [command] [options]
|
|
4300
7680
|
|
|
4301
7681
|
Commands:
|
|
4302
|
-
dev
|
|
4303
|
-
start
|
|
7682
|
+
dev Start development server (default)
|
|
7683
|
+
start Start production server
|
|
7684
|
+
checkpoint Manage versioned checkpoints (publish, list, rollback, remove)
|
|
4304
7685
|
|
|
4305
7686
|
Options:
|
|
4306
7687
|
-p, --port <port> Port to listen on (default: 3000)
|